1 /**
2  * This module contains objects for defining and scoping dependency registrations.
3  *
4  * Part of the Poodinis Dependency Injection framework.
5  *
6  * Authors:
7  *  Mike Bierlee, m.bierlee@lostmoment.com
8  * Copyright: 2014-2025 Mike Bierlee
9  * License:
10  *  This software is licensed under the terms of the MIT license.
11  *  The full terms of the license can be found in the LICENSE file.
12  */
13 
14 module poodinis.registration;
15 
16 import poodinis.container : DependencyContainer;
17 import poodinis.factory : InstanceFactory, InstanceEventHandler,
18     InstanceCreationException, InstanceFactoryParameters, CreatesSingleton;
19 
20 class Registration {
21     private TypeInfo _registeredType = null;
22     private TypeInfo_Class _instanceType = null;
23     private Registration linkedRegistration;
24     private shared(DependencyContainer) _originatingContainer;
25     private InstanceFactory _instanceFactory;
26     private void delegate() _preDestructor;
27 
28     TypeInfo registeredType() {
29         return _registeredType;
30     }
31 
32     TypeInfo_Class instanceType() {
33         return _instanceType;
34     }
35 
36     shared(DependencyContainer) originatingContainer() {
37         return _originatingContainer;
38     }
39 
40     InstanceFactory instanceFactory() {
41         return _instanceFactory;
42     }
43 
44     void delegate() preDestructor() {
45         return _preDestructor;
46     }
47 
48     protected void preDestructor(void delegate() preDestructor) {
49         _preDestructor = preDestructor;
50     }
51 
52     this(TypeInfo registeredType, TypeInfo_Class instanceType,
53         InstanceFactory instanceFactory, shared(DependencyContainer) originatingContainer) {
54         this._registeredType = registeredType;
55         this._instanceType = instanceType;
56         this._originatingContainer = originatingContainer;
57         this._instanceFactory = instanceFactory;
58     }
59 
60     Object getInstance(InstantiationContext context = new InstantiationContext()) {
61         if (linkedRegistration !is null) {
62             return linkedRegistration.getInstance(context);
63         }
64 
65         if (instanceFactory is null) {
66             throw new InstanceCreationException(
67                 "No instance factory defined for registration of type " ~ registeredType.toString());
68         }
69 
70         return instanceFactory.getInstance();
71     }
72 
73     Registration linkTo(Registration registration) {
74         this.linkedRegistration = registration;
75         return this;
76     }
77 
78     Registration onConstructed(InstanceEventHandler handler) {
79         if (instanceFactory !is null)
80             instanceFactory.onConstructed(handler);
81         return this;
82     }
83 }
84 
85 private InstanceFactoryParameters copyFactoryParameters(Registration registration) {
86     return registration.instanceFactory.factoryParameters;
87 }
88 
89 private void setFactoryParameters(Registration registration, InstanceFactoryParameters newParameters) {
90     registration.instanceFactory.factoryParameters = newParameters;
91 }
92 
93 /**
94  * Sets the registration's instance factory type the same as the registration's.
95  *
96  * This is not a registration scope. Typically used by Poodinis internally only.
97  */
98 Registration initializeFactoryType(Registration registration) {
99     auto params = registration.copyFactoryParameters();
100     params.instanceType = registration.instanceType;
101     registration.setFactoryParameters(params);
102     return registration;
103 }
104 
105 /**
106  * Scopes registrations to return the same instance every time a given registration is resolved.
107  *
108  * Effectively makes the given registration a singleton.
109  */
110 Registration singleInstance(Registration registration) {
111     auto params = registration.copyFactoryParameters();
112     params.createsSingleton = CreatesSingleton.yes;
113     registration.setFactoryParameters(params);
114     return registration;
115 }
116 
117 /**
118  * Scopes registrations to return a new instance every time the given registration is resolved.
119  */
120 Registration newInstance(Registration registration) {
121     auto params = registration.copyFactoryParameters();
122     params.createsSingleton = CreatesSingleton.no;
123     params.existingInstance = null;
124     registration.setFactoryParameters(params);
125     return registration;
126 }
127 
128 /**
129  * Scopes registrations to return the given instance every time the given registration is resolved.
130  */
131 Registration existingInstance(Registration registration, Object instance) {
132     auto params = registration.copyFactoryParameters();
133     params.createsSingleton = CreatesSingleton.yes;
134     params.existingInstance = instance;
135     registration.setFactoryParameters(params);
136     return registration;
137 }
138 
139 /**
140  * Scopes registrations to create new instances using the given initializer delegate.
141  */
142 Registration initializedBy(T)(Registration registration, T delegate() initializer)
143         if (is(T == class) || is(T == interface)) {
144     auto params = registration.copyFactoryParameters();
145     params.createsSingleton = CreatesSingleton.no;
146     params.factoryMethod = () => cast(Object) initializer();
147     registration.setFactoryParameters(params);
148     return registration;
149 }
150 
151 /**
152  * Scopes registrations to create a new instance using the given initializer delegate. On subsequent resolves the same instance is returned.
153  */
154 Registration initializedOnceBy(T : Object)(Registration registration, T delegate() initializer) {
155     auto params = registration.copyFactoryParameters();
156     params.createsSingleton = CreatesSingleton.yes;
157     params.factoryMethod = () => cast(Object) initializer();
158     registration.setFactoryParameters(params);
159     return registration;
160 }
161 
162 string toConcreteTypeListString(Registration[] registrations) {
163     auto concreteTypeListString = "";
164     foreach (registration; registrations) {
165         if (concreteTypeListString.length > 0) {
166             concreteTypeListString ~= ", ";
167         }
168         concreteTypeListString ~= registration.instanceType.toString();
169     }
170     return concreteTypeListString;
171 }
172 
173 class InstantiationContext {
174 }
175 
176 version (unittest)  :  //
177 
178 import poodinis;
179 import poodinis.testclasses;
180 import std.exception;
181 
182 // Test getting instance without scope defined throws exception
183 unittest {
184     Registration registration = new Registration(typeid(TestType), null, null, null);
185     assertThrown!(InstanceCreationException)(registration.getInstance(), null);
186 }
187 
188 // Test set single instance scope using scope setter
189 unittest {
190     Registration registration = new Registration(null, typeid(TestType),
191         new InstanceFactory(), null).initializeFactoryType();
192     auto chainedRegistration = registration.singleInstance();
193     auto instance1 = registration.getInstance();
194     auto instance2 = registration.getInstance();
195     assert(instance1 is instance2,
196         "Registration with single instance scope did not return the same instance");
197     assert(registration is chainedRegistration,
198         "Registration returned by scope setting is not the same as the registration being set");
199 }
200 
201 // Test set new instance scope using scope setter
202 unittest {
203     Registration registration = new Registration(null, typeid(TestType),
204         new InstanceFactory(), null).initializeFactoryType();
205     auto chainedRegistration = registration.newInstance();
206     auto instance1 = registration.getInstance();
207     auto instance2 = registration.getInstance();
208     assert(instance1 !is instance2,
209         "Registration with new instance scope did not return a different instance");
210     assert(registration is chainedRegistration,
211         "Registration returned by scope setting is not the same as the registration being set");
212 }
213 
214 // Test set existing instance scope using scope setter
215 unittest {
216     Registration registration = new Registration(null, null, new InstanceFactory(), null);
217     auto expectedInstance = new TestType();
218     auto chainedRegistration = registration.existingInstance(expectedInstance);
219     auto actualInstance = registration.getInstance();
220     assert(expectedInstance is actualInstance,
221         "Registration with existing instance scope did not return the same instance");
222     assert(registration is chainedRegistration,
223         "Registration returned by scope setting is not the same as the registration being set");
224 }
225 
226 // Test linking registrations
227 unittest {
228     Registration firstRegistration = new Registration(typeid(TestInterface),
229         typeid(TestImplementation), new InstanceFactory(), null).initializeFactoryType()
230         .singleInstance();
231     Registration secondRegistration = new Registration(typeid(TestImplementation),
232         typeid(TestImplementation), new InstanceFactory(), null).initializeFactoryType()
233         .singleInstance().linkTo(firstRegistration);
234 
235     auto firstInstance = firstRegistration.getInstance();
236     auto secondInstance = secondRegistration.getInstance();
237 
238     assert(firstInstance is secondInstance);
239 }
240 
241 // Test custom factory method via initializedBy
242 unittest {
243     Registration registration = new Registration(typeid(TestInterface),
244         typeid(TestImplementation), new InstanceFactory(), null);
245 
246     registration.initializedBy({
247         auto instance = new TestImplementation();
248         instance.someContent = "createdbyinitializer";
249         return instance;
250     });
251 
252     TestImplementation instanceOne = cast(TestImplementation) registration.getInstance();
253     TestImplementation instanceTwo = cast(TestImplementation) registration.getInstance();
254     assert(instanceOne.someContent == "createdbyinitializer");
255     assert(instanceTwo.someContent == "createdbyinitializer");
256     assert(instanceOne !is instanceTwo);
257 }
258 
259 // Test custom factory method via initializedOnceBy
260 unittest {
261     Registration registration = new Registration(typeid(TestInterface),
262         typeid(TestImplementation), new InstanceFactory(), null);
263 
264     registration.initializedOnceBy({
265         auto instance = new TestImplementation();
266         instance.someContent = "createdbyinitializer";
267         return instance;
268     });
269 
270     TestImplementation instanceOne = cast(TestImplementation) registration.getInstance();
271     TestImplementation instanceTwo = cast(TestImplementation) registration.getInstance();
272     assert(instanceOne.someContent == "createdbyinitializer");
273     assert(instanceTwo.someContent == "createdbyinitializer");
274     assert(instanceOne is instanceTwo);
275 }
276 
277 // Test chaining single/new instance scope to initializedBy will not overwrite the factory method.
278 unittest {
279     Registration registration = new Registration(typeid(TestInterface),
280         typeid(TestImplementation), new InstanceFactory(), null);
281 
282     registration.initializedBy({
283         auto instance = new TestImplementation();
284         instance.someContent = "createdbyinitializer";
285         return instance;
286     });
287 
288     registration.singleInstance();
289 
290     TestImplementation instanceOne = cast(TestImplementation) registration.getInstance();
291     TestImplementation instanceTwo = cast(TestImplementation) registration.getInstance();
292     assert(instanceOne.someContent == "createdbyinitializer");
293     assert(instanceTwo.someContent == "createdbyinitializer");
294     assert(instanceOne is instanceTwo);
295 }