1 /**
2     This package holds function for easy verification of external tools'
3     existence.
4 
5     Copyright: © 2019 Arne Ludwig <arne.ludwig@posteo.de>
6     License: Subject to the terms of the MIT license, as written in the
7              included LICENSE file.
8     Authors: Arne Ludwig <arne.ludwig@posteo.de>
9 */
10 module dalicious.dependency;
11 
12 
13 /**
14     This struct can be used as a decorator to mark an external dependency of
15     any symbol.
16 */
17 struct ExternalDependency
18 {
19     /// Name of the executable
20     string executable;
21     /// Name of the package that provides the executable.
22     string package_;
23     /// URL to the homepage of the package.
24     string url;
25 
26     /// Get a human-readable string describing this dependency.
27     string toString() const pure nothrow
28     {
29         if (package_ is null && url is null)
30             return executable;
31         else if (url is null)
32             return executable ~ " (part of `" ~ package_ ~ "`)";
33         else if (package_ is null)
34             return executable ~ " (see " ~ url ~ ")";
35         else
36             return executable ~ " (part of `" ~ package_ ~ "`; see " ~ url ~ ")";
37     }
38 }
39 
40 // not for public use
41 static enum isExternalDependency(alias value) = is(typeof(value) == ExternalDependency);
42 
43 unittest
44 {
45     static assert(isExternalDependency!(ExternalDependency("someTool")));
46     static assert(!isExternalDependency!"someTool");
47 }
48 
49 import std.meta : Filter, staticMap;
50 import std.traits : getUDAs;
51 
52 // not for public use
53 static enum ExternalDependency[] fromSymbol(alias symbol) = [Filter!(
54     isExternalDependency,
55     getUDAs!(symbol, ExternalDependency),
56 )];
57 
58 unittest
59 {
60     @ExternalDependency("someTool")
61     void callSomeTool(in string parameter)
62     {
63         // calls `someTool`
64     }
65 
66     static assert(fromSymbol!callSomeTool == [ExternalDependency("someTool")]);
67 }
68 
69 /**
70     Generates an array of external dependencies of `Modules`.
71 
72     Params:
73         Modules = List of symbols to be checked.
74 */
75 ExternalDependency[] externalDependencies(Modules...)()
76 {
77     static assert(Modules.length > 0, "missing Modules");
78 
79     import std.array : array;
80     import std.algorithm :
81         joiner,
82         sort,
83         uniq;
84     import std.meta : staticMap;
85     import std.traits : getSymbolsByUDA;
86 
87     alias _getSymbolsByUDA(alias Module) = getSymbolsByUDA!(Module, ExternalDependency);
88     alias byExecutableLt = (a, b) => a.executable < b.executable;
89     alias byExecutableEq = (a, b) => a.executable == b.executable;
90 
91     ExternalDependency[][] deps = [staticMap!(
92         fromSymbol,
93         staticMap!(
94             _getSymbolsByUDA,
95             Modules,
96         ),
97     )];
98 
99     return deps.joiner.array.sort!byExecutableLt.release.uniq!byExecutableEq.array;
100 }
101 
102 ///
103 unittest
104 {
105     struct Caller
106     {
107         @ExternalDependency("someTool")
108         void callSomeTool(in string parameter)
109         {
110             // calls `someTool`
111         }
112 
113         @ExternalDependency("otherTool")
114         void callOtherTool(in string parameter)
115         {
116             // calls `otherTool`
117         }
118     }
119 
120     static assert(externalDependencies!Caller == [
121         ExternalDependency("otherTool"),
122         ExternalDependency("someTool"),
123     ]);
124 }
125 
126 unittest
127 {
128     static assert(!is(externalDependencies));
129 }
130 
131 /**
132     Generates an array of external dependencies of `Modules`.
133 
134     Params:
135         Modules = List of symbols to be checked.
136 */
137 version(Posix) void enforceExternalDepencenciesAvailable(Modules...)()
138 {
139     import dalicious.process : isExecutable;
140     import std.array : array;
141     import std.algorithm : filter;
142     import std.exception : enforce;
143     import std.format : format;
144     import std.process : execute;
145     import std.range : enumerate;
146     import std.string : lineSplitter;
147 
148     enum modulesDeps = externalDependencies!Modules;
149 
150     static if (modulesDeps.length > 0)
151     {
152         auto missingExternalTools = modulesDeps
153             .filter!(extDep => !isExecutable(extDep.executable))
154             .array;
155 
156         if (missingExternalTools.length > 0)
157             throw new ExternalDependencyMissing(missingExternalTools);
158     }
159     else
160     {
161         pragma(msg, "Info: your program has no external dependencies but checks for their availability.");
162     }
163 }
164 
165 /// ditto
166 version(Posix) deprecated alias enforceExternalToolsAvailable = enforceExternalDepencenciesAvailable;
167 
168 ///
169 unittest
170 {
171     import std.exception : assertThrown;
172 
173     struct Caller
174     {
175         @ExternalDependency("/this/is/missing")
176         @ExternalDependency("this_too_is_missing", "mypack", "http://example.com/")
177         void makeCall() { }
178     }
179 
180     assertThrown!ExternalDependencyMissing(enforceExternalDepencenciesAvailable!Caller());
181     // Error message:
182     //
183     //     missing external tools:
184     //     - /this/is/missing
185     //     - this_too_is_missing (part of `mypack`; see http://example.com/)
186     //
187     //     Check your PATH and/or install the required software.
188 }
189 
190 
191 unittest
192 {
193     import std.exception : assertThrown;
194 
195     enum dependencies = [
196         ExternalDependency("/bin/sh"),
197         ExternalDependency("sh"),
198         ExternalDependency("this_too_is_missing", "mypack", "http://example.com/"),
199     ];
200 
201     struct Caller
202     {
203         @(dependencies[0])
204         @(dependencies[1])
205         void makeCall() { }
206     }
207 
208     try
209     {
210         enforceExternalDepencenciesAvailable!Caller();
211     }
212     catch (ExternalDependencyMissing e)
213     {
214         assert(e.missingExternalTools[0] == dependencies[$ - 1]);
215     }
216 }
217 
218 
219 unittest
220 {
221     import std.exception : assertThrown;
222     import std.algorithm : map;
223     import std.array : array;
224     import std.format : format;
225     import std.range : iota;
226 
227     enum dependencies = iota(100)
228         .map!(i => ExternalDependency(format!"this_is_missing_%d"(i)))
229         .array;
230 
231     struct Caller
232     {
233         mixin(format!q{
234             %-(@(dependencies[%d])%)])
235             void makeCall() { }
236         }(iota(dependencies.length)));
237     }
238 
239     try
240     {
241         enforceExternalDepencenciesAvailable!Caller();
242     }
243     catch (ExternalDependencyMissing e)
244     {
245         assert(e.missingExternalTools.length == dependencies.length);
246     }
247 }
248 
249 
250 /// Thrown if one or more external dependencies are missing.
251 class ExternalDependencyMissing : Exception
252 {
253     static enum errorMessage = "missing external tools:\n%-(- %s\n%)\n\nCheck your PATH and/or install the required software.";
254 
255     /// List of missing external depencencies.
256     const(ExternalDependency[]) missingExternalTools;
257 
258     /**
259         Params:
260             missingExternalTools = List of missing external tools
261             file                 = The file where the exception occurred.
262             line                 = The line number where the exception
263                                    occurred.
264             next                 = The previous exception in the chain of
265                                    exceptions, if any.
266     */
267     this(const(ExternalDependency[]) missingExternalTools, string file = __FILE__, size_t line = __LINE__,
268          Throwable next = null) pure
269     {
270         import std.format : format;
271 
272         super(format!errorMessage(missingExternalTools), file, line, next);
273         this.missingExternalTools = missingExternalTools;
274     }
275 }