1 /**
2  * This module contains instance factory facilities
3  *
4  * Authors:
5  *  Mike Bierlee, m.bierlee@lostmoment.com
6  * Copyright: 2014-2025 Mike Bierlee
7  * License:
8  *  This software is licensed under the terms of the MIT license.
9  *  The full terms of the license can be found in the LICENSE file.
10  */
11 
12 module poodinis.factory;
13 
14 import poodinis.container : DependencyContainer;
15 import poodinis.imports : createImportsString;
16 
17 import std.typecons : Flag;
18 import std.exception : enforce;
19 import std.traits : Parameters, isBuiltinType, fullyQualifiedName;
20 import std.string : format;
21 
22 debug {
23     import std.stdio : writeln;
24 }
25 
26 alias CreatesSingleton = Flag!"CreatesSingleton";
27 alias InstanceFactoryMethod = Object delegate();
28 alias InstanceEventHandler = void delegate(Object instance);
29 
30 class InstanceCreationException : Exception {
31     this(string message, string file = __FILE__, size_t line = __LINE__) {
32         super(message, file, line);
33     }
34 }
35 
36 struct InstanceFactoryParameters {
37     TypeInfo_Class instanceType;
38     CreatesSingleton createsSingleton = CreatesSingleton.yes;
39     Object existingInstance;
40     InstanceFactoryMethod factoryMethod;
41 }
42 
43 class InstanceFactory {
44     private Object instance = null;
45     private InstanceFactoryParameters _factoryParameters;
46     private InstanceEventHandler _constructionHandler;
47 
48     this() {
49         factoryParameters = InstanceFactoryParameters();
50     }
51 
52     void factoryParameters(InstanceFactoryParameters factoryParameters) {
53         if (factoryParameters.factoryMethod is null) {
54             factoryParameters.factoryMethod = &this.createInstance;
55         }
56 
57         if (factoryParameters.existingInstance !is null) {
58             factoryParameters.createsSingleton = CreatesSingleton.yes;
59             this.instance = factoryParameters.existingInstance;
60         }
61 
62         _factoryParameters = factoryParameters;
63     }
64 
65     InstanceFactoryParameters factoryParameters() {
66         return _factoryParameters;
67     }
68 
69     Object getInstance() {
70         if (_factoryParameters.createsSingleton && instance !is null) {
71             debug (poodinisVerbose) {
72                 printDebugUseExistingInstance();
73             }
74 
75             return instance;
76         }
77 
78         debug (poodinisVerbose) {
79             printDebugCreateNewInstance();
80         }
81 
82         instance = _factoryParameters.factoryMethod();
83         if (_constructionHandler !is null) {
84             _constructionHandler(instance);
85         }
86 
87         return instance;
88     }
89 
90     void onConstructed(InstanceEventHandler handler) {
91         _constructionHandler = handler;
92     }
93 
94     private void printDebugUseExistingInstance() {
95         debug {
96             if (_factoryParameters.instanceType !is null) {
97                 writeln(format("DEBUG: Existing instance returned of type %s",
98                         _factoryParameters.instanceType.toString()));
99             } else {
100                 writeln("DEBUG: Existing instance returned from custom factory method");
101             }
102         }
103     }
104 
105     private void printDebugCreateNewInstance() {
106         debug {
107             if (_factoryParameters.instanceType !is null) {
108                 writeln(format("DEBUG: Creating new instance of type %s",
109                         _factoryParameters.instanceType.toString()));
110             } else {
111                 writeln("DEBUG: Creating new instance from custom factory method");
112             }
113         }
114     }
115 
116     protected Object createInstance() {
117         enforce!InstanceCreationException(_factoryParameters.instanceType,
118             "Instance type is not defined, cannot create instance without knowing its type.");
119         return _factoryParameters.instanceType.create();
120     }
121 }
122 
123 class ConstructorInjectingInstanceFactory(InstanceType) : InstanceFactory {
124     private shared DependencyContainer container;
125     private bool isBeingInjected = false;
126 
127     this(shared DependencyContainer container) {
128         this.container = container;
129     }
130 
131     private static string createArgumentList(Params...)() {
132         string argumentList = "";
133         foreach (param; Params) {
134             if (argumentList.length > 0) {
135                 argumentList ~= ",";
136             }
137 
138             argumentList ~= "container.resolve!(" ~ param.stringof ~ ")";
139         }
140         return argumentList;
141     }
142 
143     private static string createImportList(Params...)() {
144         string importList = "";
145         foreach (param; Params) {
146             importList ~= createImportsString!param;
147         }
148         return importList;
149     }
150 
151     private static bool parametersAreValid(Params...)() {
152         bool isValid = true;
153         foreach (param; Params) {
154             if (isBuiltinType!param || is(param == struct)) {
155                 isValid = false;
156                 break;
157             }
158         }
159 
160         return isValid;
161     }
162 
163     protected override Object createInstance() {
164         enforce!InstanceCreationException(container,
165             "A dependency container is not defined. Cannot perform constructor injection without one.");
166         enforce!InstanceCreationException(!isBeingInjected,
167             format("%s is already being created and injected; possible circular dependencies in constructors?",
168                 InstanceType.stringof));
169 
170         Object instance = null;
171         static if (__traits(compiles, __traits(getOverloads, InstanceType, `__ctor`))) {
172             foreach (ctor; __traits(getOverloads, InstanceType, `__ctor`)) {
173                 static if (parametersAreValid!(Parameters!ctor)) {
174                     isBeingInjected = true;
175                     mixin(createImportsString!InstanceType ~ createImportList!(
176                             Parameters!ctor) ~ `
177                         instance = new `
178                             ~ fullyQualifiedName!InstanceType ~ `(` ~ createArgumentList!(
179                                 Parameters!ctor) ~ `);
180                     `);
181                     isBeingInjected = false;
182                     break;
183                 }
184             }
185         }
186 
187         if (instance is null) {
188             instance = typeid(InstanceType).create();
189         }
190 
191         enforce!InstanceCreationException(instance !is null,
192             "Unable to create instance of type" ~ InstanceType.stringof
193                 ~ ", does it have injectable constructors?");
194 
195         return instance;
196     }
197 }
198 
199 version (unittest)  :  //
200 
201 import poodinis;
202 import poodinis.testclasses;
203 import std.exception;
204 
205 // Test instance factory with singletons
206 unittest {
207     auto factory = new InstanceFactory();
208     factory.factoryParameters = InstanceFactoryParameters(typeid(TestImplementation),
209         CreatesSingleton.yes);
210     auto instanceOne = factory.getInstance();
211     auto instanceTwo = factory.getInstance();
212 
213     assert(instanceOne !is null, "Created factory instance is null");
214     assert(instanceOne is instanceTwo, "Created factory instance is not the same");
215 }
216 
217 // Test instance factory with new instances
218 unittest {
219     auto factory = new InstanceFactory();
220     factory.factoryParameters = InstanceFactoryParameters(typeid(TestImplementation),
221         CreatesSingleton.no);
222     auto instanceOne = factory.getInstance();
223     auto instanceTwo = factory.getInstance();
224 
225     assert(instanceOne !is null, "Created factory instance is null");
226     assert(instanceOne !is instanceTwo, "Created factory instance is the same");
227 }
228 
229 // Test instance factory with existing instances
230 unittest {
231     auto existingInstance = new TestImplementation();
232     auto factory = new InstanceFactory();
233     factory.factoryParameters = InstanceFactoryParameters(typeid(TestImplementation),
234         CreatesSingleton.yes, existingInstance);
235     auto instanceOne = factory.getInstance();
236     auto instanceTwo = factory.getInstance();
237 
238     assert(instanceOne is existingInstance,
239         "Created factory instance is not the existing instance");
240     assert(instanceTwo is existingInstance,
241         "Created factory instance is not the existing instance when called again");
242 }
243 
244 // Test instance factory with existing instances when setting singleton flag to "no"
245 unittest {
246     auto existingInstance = new TestImplementation();
247     auto factory = new InstanceFactory();
248     factory.factoryParameters = InstanceFactoryParameters(typeid(TestImplementation),
249         CreatesSingleton.no, existingInstance);
250     auto instance = factory.getInstance();
251 
252     assert(instance is existingInstance,
253         "Created factory instance is not the existing instance");
254 }
255 
256 // Test creating instance using custom factory method
257 unittest {
258     Object factoryMethod() {
259         auto instance = new TestImplementation();
260         instance.someContent = "Ducks!";
261         return instance;
262     }
263 
264     auto factory = new InstanceFactory();
265     factory.factoryParameters = InstanceFactoryParameters(null,
266         CreatesSingleton.yes, null, &factoryMethod);
267     auto instance = cast(TestImplementation) factory.getInstance();
268 
269     assert(instance !is null,
270         "No instance was created by factory or could not be cast to expected type");
271     assert(instance.someContent == "Ducks!");
272 }
273 
274 // Test injecting constructor of class
275 unittest {
276     auto container = new shared DependencyContainer();
277     container.register!TestImplementation;
278 
279     auto factory = new ConstructorInjectingInstanceFactory!ClassWithConstructor(container);
280     auto instance = cast(ClassWithConstructor) factory.getInstance();
281 
282     assert(instance !is null);
283     assert(instance.testImplementation is container.resolve!TestImplementation);
284 }
285 
286 // Test injecting constructor of class with multiple constructors injects the first candidate
287 unittest {
288     auto container = new shared DependencyContainer();
289     container.register!SomeOtherClassThen;
290     container.register!TestImplementation;
291 
292     auto factory = new ConstructorInjectingInstanceFactory!ClassWithMultipleConstructors(
293         container);
294     auto instance = cast(ClassWithMultipleConstructors) factory.getInstance();
295 
296     assert(instance !is null);
297     assert(instance.someOtherClassThen is container.resolve!SomeOtherClassThen);
298     assert(instance.testImplementation is null);
299 }
300 
301 // Test injecting constructor of class with multiple constructor parameters
302 unittest {
303     auto container = new shared DependencyContainer();
304     container.register!SomeOtherClassThen;
305     container.register!TestImplementation;
306 
307     auto factory = new ConstructorInjectingInstanceFactory!ClassWithConstructorWithMultipleParameters(
308         container);
309     auto instance = cast(ClassWithConstructorWithMultipleParameters) factory.getInstance();
310 
311     assert(instance !is null);
312     assert(instance.someOtherClassThen is container.resolve!SomeOtherClassThen);
313     assert(instance.testImplementation is container.resolve!TestImplementation);
314 }
315 
316 // Test injecting constructor of class with primitive constructor parameters
317 unittest {
318     auto container = new shared DependencyContainer();
319     container.register!SomeOtherClassThen;
320 
321     auto factory = new ConstructorInjectingInstanceFactory!ClassWithPrimitiveConstructor(
322         container);
323     auto instance = cast(ClassWithPrimitiveConstructor) factory.getInstance();
324 
325     assert(instance !is null);
326     assert(instance.someOtherClassThen is container.resolve!SomeOtherClassThen);
327 }
328 
329 // Test injecting constructor of class with struct constructor parameters
330 unittest {
331     auto container = new shared DependencyContainer();
332     container.register!SomeOtherClassThen;
333 
334     auto factory = new ConstructorInjectingInstanceFactory!ClassWithStructConstructor(container);
335     auto instance = cast(ClassWithStructConstructor) factory.getInstance();
336 
337     assert(instance !is null);
338     assert(instance.someOtherClassThen is container.resolve!SomeOtherClassThen);
339 }
340 
341 // Test injecting constructor of class with empty constructor will skip injection
342 unittest {
343     auto container = new shared DependencyContainer();
344 
345     auto factory = new ConstructorInjectingInstanceFactory!ClassWithEmptyConstructor(container);
346     auto instance = cast(ClassWithEmptyConstructor) factory.getInstance();
347 
348     assert(instance !is null);
349     assert(instance.someOtherClassThen is null);
350 }
351 
352 // Test injecting constructor of class with no candidates fails
353 unittest {
354     auto container = new shared DependencyContainer();
355 
356     auto factory = new ConstructorInjectingInstanceFactory!ClassWithNonInjectableConstructor(
357         container);
358 
359     assertThrown!InstanceCreationException(factory.getInstance());
360 }