1 /**
2  * This module contains instance factory facilities
3  *
4  * Authors:
5  *  Mike Bierlee, m.bierlee@lostmoment.com
6  * Copyright: 2014-2023 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     public @property 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     public @property InstanceFactoryParameters factoryParameters() {
66         return _factoryParameters;
67     }
68 
69     public 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 }