1 /**
2  * Contains the implementation of application context setup.
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.context;
15 
16 import poodinis.container : DependencyContainer;
17 import poodinis.registration : Registration, existingInstance;
18 import poodinis.factory : CreatesSingleton, InstanceFactoryParameters;
19 import poodinis.autowire : autowire;
20 
21 import std.traits : hasUDA, ReturnType;
22 
23 class ApplicationContext {
24     void registerDependencies(shared(DependencyContainer) container) {
25     }
26 }
27 
28 /**
29 * A component annotation is used for specifying which factory methods produce components in
30 * an application context.
31 */
32 struct Component {
33 }
34 
35 /**
36 * This annotation allows you to specify by which super type the component should be registered. This
37 * enables you to use type-qualified alternatives for dependencies.
38 */
39 struct RegisterByType(Type) {
40     Type type;
41 }
42 
43 /**
44 * Components with the prototype registration will be scoped as dependencies which will create
45 * new instances every time they are resolved. The factory method will be called repeatedly.
46 */
47 struct Prototype {
48 }
49 
50 /**
51 * Register dependencies through an application context.
52 *
53 * An application context allows you to fine-tune dependency set-up and instantiation.
54 * It is mostly used for dependencies which come from an external library or when you don't
55 * want to use annotations to set-up dependencies in your classes.
56 */
57 void registerContext(Context : ApplicationContext)(shared(DependencyContainer) container) {
58     auto context = new Context();
59     context.registerDependencies(container);
60     context.registerContextComponents(container);
61     container.register!(ApplicationContext, Context)().existingInstance(context);
62     autowire(container, context);
63 }
64 
65 void registerContextComponents(ApplicationContextType : ApplicationContext)(
66     ApplicationContextType context, shared(DependencyContainer) container) {
67     foreach (memberName; __traits(allMembers, ApplicationContextType)) {
68         foreach (overload; __traits(getOverloads, ApplicationContextType, memberName)) {
69             static if (__traits(getProtection, overload) == "public" && hasUDA!(overload, Component)) {
70                 auto factoryMethod = &__traits(getMember, context, memberName);
71                 Registration registration = null;
72                 auto createsSingleton = CreatesSingleton.yes;
73 
74                 foreach (attribute; __traits(getAttributes, overload)) {
75                     static if (is(attribute == RegisterByType!T, T)) {
76                         registration = container.register!(typeof(attribute.type),
77                             ReturnType!factoryMethod);
78                     } else static if (__traits(isSame, attribute, Prototype)) {
79                         createsSingleton = CreatesSingleton.no;
80                     }
81                 }
82 
83                 if (registration is null) {
84                     registration = container.register!(ReturnType!factoryMethod);
85                 }
86 
87                 registration.instanceFactory.factoryParameters = InstanceFactoryParameters(
88                     registration.instanceType,
89                     createsSingleton,
90                     null,
91                     factoryMethod
92                 );
93             }
94         }
95     }
96 }
97 
98 version (unittest)  :  //
99 
100 import poodinis;
101 import poodinis.testclasses;
102 import std.exception;
103 
104 //Test register component registrations from context
105 unittest {
106     auto container = new shared DependencyContainer();
107     auto context = new TestContext();
108     context.registerContextComponents(container);
109     auto bananaInstance = container.resolve!Banana;
110 
111     assert(bananaInstance.color == "Yellow");
112 }
113 
114 //Test non-annotated methods are not registered
115 unittest {
116     auto container = new shared DependencyContainer();
117     auto context = new TestContext();
118     context.registerContextComponents(container);
119     assertThrown!ResolveException(container.resolve!Apple);
120 }
121 
122 //Test register component by base type
123 unittest {
124     auto container = new shared DependencyContainer();
125     auto context = new TestContext();
126     context.registerContextComponents(container);
127     auto instance = container.resolve!Fruit;
128     assert(instance.getShape() == "Pear shaped");
129 }
130 
131 //Test register components with multiple candidates
132 unittest {
133     auto container = new shared DependencyContainer();
134     auto context = new TestContext();
135     context.registerContextComponents(container);
136 
137     auto rabbit = container.resolve!(Animal, Rabbit);
138     assert(rabbit.getYell() == "Squeeeeeel");
139 
140     auto wolf = container.resolve!(Animal, Wolf);
141     assert(wolf.getYell() == "Wooooooooooo");
142 }
143 
144 //Test register component as prototype
145 unittest {
146     auto container = new shared DependencyContainer();
147     auto context = new TestContext();
148     context.registerContextComponents(container);
149 
150     auto firstInstance = container.resolve!PieChart;
151     auto secondInstance = container.resolve!PieChart;
152 
153     assert(firstInstance !is null && secondInstance !is null);
154     assert(firstInstance !is secondInstance);
155 }
156 
157 // Test setting up simple dependencies through application context
158 unittest {
159     auto container = new shared DependencyContainer();
160     container.registerContext!SimpleContext;
161     auto instance = container.resolve!CakeChart;
162 
163     assert(instance !is null);
164 }
165 
166 // Test resolving dependency from registered application context
167 unittest {
168     auto container = new shared DependencyContainer();
169     container.registerContext!SimpleContext;
170     auto instance = container.resolve!Apple;
171 
172     assert(instance !is null);
173 }
174 
175 // Test autowiring application context
176 unittest {
177     auto container = new shared DependencyContainer();
178     container.register!Apple;
179     container.registerContext!AutowiredTestContext;
180     auto instance = container.resolve!ClassWrapper;
181 
182     assert(instance !is null);
183     assert(instance.someClass !is null);
184 }
185 
186 // Test autowiring application context with dependencies registered in same context
187 unittest {
188     auto container = new shared DependencyContainer();
189     container.registerContext!ComplexAutowiredTestContext;
190     auto instance = container.resolve!ClassWrapperWrapper;
191     auto wrapper = container.resolve!ClassWrapper;
192     auto someClass = container.resolve!Apple;
193 
194     assert(instance !is null);
195     assert(instance.wrapper is wrapper);
196     assert(instance.wrapper.someClass is someClass);
197 }
198 
199 // Test resolving registered context
200 unittest {
201     auto container = new shared DependencyContainer();
202     container.registerContext!TestContext;
203     container.resolve!ApplicationContext;
204 }