1 /**
2 Copyright: 2018 Mark Fisher
3 
4 License:
5 Permission is hereby granted, free of charge, to any person obtaining a copy of
6 this software and associated documentation files (the "Software"), to deal in
7 the Software without restriction, including without limitation the rights to
8 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9 of the Software, and to permit persons to whom the Software is furnished to do
10 so, subject to the following conditions:
11 
12 The above copyright notice and this permission notice shall be included in all
13 copies or substantial portions of the Software.
14 
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 SOFTWARE.
22 **/
23 /**
24  * A wrapper that creates injection tools for your project.
25  * The settings are passed as template parameters to the
26  * class LocalInjector which you instantiate using the
27  * static method newInjector.
28  * Only one instance of LocalInjector is created;
29  * subsequent invocations of newInjector will return
30  * the existing instance, and the parameters will be ignored.
31  * The parameters to newInjector are parsed:
32  * Class parameters are passed to the
33  * wrapped container using the "scan" method.
34  * Tuple parameters
35  * are parsed and converted into properties which
36  * are loaded at runtime from "dxx.json".
37  * TODO allow for the property filenames to be overridden.
38  **/
39 module dxx.util.injector;
40 
41 private import std.experimental.logger;
42 private import std.stdio;
43 private import std.process : environment;
44 private import std.typecons;
45 private import std.variant;
46 private import std..string : indexOf;
47 private import std.json;
48 
49 private import aermicioi.aedi;
50 //private import aermicioi.aedi_property_reader;
51 
52 private import dxx.constants;
53 private import dxx.util.ini;
54 private import dxx.util.config;
55 
56 alias component = aermicioi.aedi.component;
57 alias autowired = aermicioi.aedi.autowired;
58 alias localInjector = InjectionContainer.getInstance;
59 
60 static auto resolveInjector(alias T,Arg...)(Arg arg,InjectionContainer i=InjectionContainer.getInstance) {
61     return i.resolve!T(arg);
62 }
63 
64 static T getInjectorProperty(T)(string k,InjectionContainer i=InjectionContainer.getInstance) {
65     return i.resolve!T(k);
66 }
67 
68 static void setInjectorProperty(T)(string k,T t,InjectionContainer i=InjectionContainer.getInstance) {
69     i.register!T(t,k);
70 }
71 
72 static auto newInjector(alias T,V...)(AggregateContainer c = null) {
73     synchronized(InjectionContainer.classinfo) {
74       if(InjectionContainer.getInstance is null) {
75         new LocalInjector!(T,V)(c);
76       }
77     }
78     return InjectionContainer.getInstance;
79 }
80 
81 static void terminateInjector() {
82     synchronized(InjectionContainer.classinfo) {
83         if(InjectionContainer.getInstance) {
84             InjectionContainer.getInstance.terminate;
85         }
86     }
87 }
88 
89 abstract class InjectionContainer {
90 
91     private static __gshared InjectionContainer INSTANCE;
92     static bool instantiated = false;
93 
94     static auto ref getInstance() {
95         //assert(INSTANCE !is null);
96         return INSTANCE;
97     }
98 
99     @property
100     AggregateContainer _container;
101 
102     void services(T)(T parent) {
103         auto s = singleton;
104         auto p = prototype;
105 
106         configureSingleton(s);
107         scanPrototype(p);
108 
109         parent.set(s,"singleton");
110         parent.set(p,"prototype");
111 
112     }
113     abstract void scanPrototype(PrototypeContainer);
114     abstract void configureSingleton(SingletonContainer);
115 
116     this(AggregateContainer _c) {
117         synchronized(InjectionContainer.classinfo) {
118             if(!instantiated) {
119                 if(INSTANCE is null) {
120                     services(_c);
121                     _container = _c;
122                     INSTANCE = this;
123                 }
124                 instantiated = true;
125             }
126         }
127     }
128 
129     auto resolve(T,Arg ...)(Arg arg) {
130         return _container.locate!T(arg);
131     }
132 
133     void register(T...)(const(string) arg) {
134         _container.configure("prototype").register!T(arg);
135     }
136     void register(T...)() {
137         _container.configure("prototype").register!T();
138     }
139     void register(T)(ref T t,const(string) arg) {
140         _container.configure("singleton").register!T(t,arg);
141     }
142 
143     void terminate() {
144       synchronized(InjectionContainer.classinfo) {
145           if(instantiated) {
146             debug(Injector) {
147               sharedLog.info("terminating");
148             }
149             _container.terminate();
150           }
151           if(INSTANCE is this) {
152             INSTANCE = null;
153             instantiated = false;
154           }
155       }
156     }
157     auto instantiate() {
158         debug(Injector) {
159             sharedLog.info("instantiating");
160         }
161         return _container.instantiate;
162     }
163     //abstract void load(T : DocumentContainer!X, X...)(T container);
164 }
165 
166 
167 final class LocalInjector(C...) : InjectionContainer {
168     this(AggregateContainer c = null) {
169         if(c is null) c = aggregate(config, "parameters");
170         super(c);
171     }
172     override void scanPrototype(PrototypeContainer p) {
173         static foreach(c;C) {
174             static if(isTuple!c is false) {
175                 debug(Injector) {
176                     pragma(msg,"scanning prototype: ");
177                     pragma(msg,c);
178                     sharedLog.info("Scanning prototype ",typeid(c));
179                 }
180                 p.scan!c;
181             }
182         }
183     }
184     override void configureSingleton(SingletonContainer) {
185     }
186 
187     auto config() {
188         debug(Injector) {
189             sharedLog.info("Injector config");
190         }
191         /*version(DXX_Developer) {
192           auto cont = container(
193             argument,
194             env,
195             //json("./dxx-dev.json"),
196             json("./resources/dxx-dev.json"),
197             json(RTConstants.constants.appDir ~ "/../../resources/dxx-dev.json"),
198             //json("./dxx.json"),
199             //json("./resource/dxx.json"),
200             //json(RTConstants.constants.appDir ~ "/../dxx.json")
201              );
202         } else {
203           auto cont = container(
204             argument,
205             env,
206             json("./resources/dxx.json"),
207             json(RTConstants.constants.appDir ~ "/../resources/dxx.json")
208             //json("/etc/aedi-example/config.json"),
209             //configFiles
210              );
211 
212         }*/
213         /*auto cont = container(
214           prototype
215         );
216         auto __j = loadJson("resources/dxx.json");
217 
218         foreach (c; cont) {
219             load(c,__j);
220         }
221         return cont;*/
222         auto cont = values;
223         version(DXX_Developer) {
224           //auto __j = loadJson("resources/dxx-dev.json");
225           load(cont,loadJson("resources/dxx-dev.json"));
226         } else version(Unittest) {
227           //auto __j = loadJson("resources/dxx-ut.json");
228           load(cont,loadJson("resources/dxx-ut.json"));
229         } else {
230           //auto __j = loadJson("resources/dxx.json");
231           load(cont,loadJson("resources/dxx.json"));
232           load(cont,loadJson("dxx.json"));
233         }
234         // parse the environment
235         // parse the command line
236         load(cont,cast(string[])runtimeConstants.argsApp.dup);
237         return cont;
238     }
239     auto loadJson(string pathOrData) {
240       import std.file : exists, readText;
241       if (pathOrData.exists) {
242         debug(trace) trace("Loading json from ", pathOrData);
243         pathOrData = pathOrData.readText();
244       }
245       try {
246         return parseJSON(pathOrData);
247       } catch(Exception e) {
248         return parseJSON("{}");
249       }
250     }
251     string[] toStringArray(const(JSONValue)[] ar) {
252       string[] res;
253       foreach(v;ar) {
254         debug(Injector) {
255             sharedLog.trace("array ",v);
256         }
257         res ~= v.str;
258       }
259       return res;
260     }
261     Variant readValue(const(JSONValue) j,string name) {
262         auto v = LocalConfig.get(name);
263         if(v != null) return v;
264         //return null;
265         // scan env
266         // scan args
267         // scan properties
268         auto inx = name.indexOf('.');
269         if(inx != -1) {
270           string n = name[0..inx];
271           if(const(JSONValue)* x = n in j) {
272             return readValue(*x,name[inx+1..$]);
273           }
274         }
275         if(const (JSONValue)* val = name in j) {
276           debug(Injector) {
277             sharedLog.trace(name," = ",*val);
278           }
279           /*static if(is(_T == int)) {
280             return Variant(val.integer);
281           } else if(is(_T == uint)) {
282             return Variant(val.integer);
283           } else if(is(_T == bool)) {
284             return Variant(val.boolean);
285           } else if (is(_T == immutable(char)[])) {
286             return Variant(val.str);
287           } else if (is(_T == string[])) {
288             string[] vals;
289             vals = toStringArray(val.array);
290             return Variant(vals);
291           }*/
292           switch(val.type) {
293             case(JSONType.integer):
294               return Variant(val.integer);
295             case(JSONType.true_):
296             case(JSONType.false_):
297               return Variant(val.boolean);
298             case(JSONType..string):
299               return Variant(val.str);
300             case(JSONType.array):
301               string[] vals;
302               vals = toStringArray(val.array);
303               return Variant(vals);
304             default:
305               return Variant(null);
306           }
307 
308         }
309         /*} else {
310           static if(is(_T == int)) {
311             return Variant(-1);
312           } else if(is(_T == bool)) {
313             return Variant(false);
314           } else if (is(_T == string)) {
315             return Variant("");
316           } else if (is(_T == string[])) {
317             string[] x = [];
318             return Variant(x);
319           }
320         }*/
321         return Variant(null);
322     }
323     //void load(T : DocumentContainer!X, X...)(T container) {
324     void load(T)(T container,const(JSONValue) __j) {
325       load(container,x=>readValue(__j,x));
326     }
327     void load(T)(T container,string[] cmd) {
328       //load(container,x=>readValue(__j,x));
329     }
330     void load(T)(T container,Variant delegate (string) getVal) {
331             with (container.configure) {
332             static foreach(c;C) {
333                 static if(isTuple!c) {
334                     template _reg(alias n,alias T) {
335                       void _reg() {
336                           static foreach (name ; T.fieldNames) {
337                             {
338                               enum fieldName = n ~ name;
339                               mixin("alias _f = c." ~ fieldName~";");
340                               alias fieldType = typeof(_f);
341                               debug(Injector) {
342                                   import std.conv;
343                                   sharedLog.trace("field: " ~ typeid(fieldType).to!string ~ " " ~ fieldName);
344                               }
345                               static if(isTuple!fieldType) {
346                                 _reg!(fieldName ~ ".",fieldType)();
347                               } else {
348                                 //auto v = readValue(__j,fieldName);
349                                 auto v = getVal(fieldName);
350                                 if(v.peek!fieldType !is null) {
351                                   debug(Injector) {
352                                     sharedLog.trace(fieldName," = ",v.get!fieldType);
353                                   }
354                                   register!fieldType(v.get!fieldType,fieldName);
355                                 }
356                               }
357                             }
358                           }
359                       }
360                     }
361                     _reg!("",c)();
362                 }
363                 // Scan properties from the .ini file.
364                 // Make them all strings.
365                 iterateValuesF!(DXXConfig.keys)( (string fqn,string k,string v) {
366                     register!string(k);
367                 } );
368             }
369         }
370     }
371 }
372 
373 unittest {
374     class MyClass {
375     }
376 
377     @component
378     class MyModule {
379         @component
380         public MyClass getMyClass() {
381             return new MyClass;
382         }
383     }
384     debug {
385         sharedLog.info("Starting component unittest.");
386     }
387     terminateInjector;
388     auto injector = newInjector!MyModule;
389     assert(injector !is null);
390     auto my = injector.resolve!MyClass;
391     assert(my !is null);
392 }
393 
394 unittest {
395     alias param = Tuple!(
396         string,"name",
397         long,"age",
398         string[string],"properties"
399     );
400     debug {
401         sharedLog.info("Starting injector tuple parameters unittest.");
402     }
403     terminateInjector;
404     auto injector = newInjector!param;
405     assert(injector !is null);
406     auto name = injector.resolve!string("name");
407     auto age = injector.resolve!long("age");
408 }
409 
410 unittest {
411     struct Param {
412       string name;
413       long age;
414       string[string] properties;
415     }
416     alias param = Tuple!(Param,"param");
417     debug {
418         sharedLog.info("Starting injector struct parameters unittest.");
419     }
420     terminateInjector;
421     auto injector = newInjector!param;
422     assert(injector !is null);
423     auto name = injector.resolve!string("param.name");
424     auto age = injector.resolve!long("param.age");
425 }