##// END OF EJS Templates
Added test: Optional dependency with child container...
cin -
r158:7e27596a76a8 default
parent child
Show More
@@ -1,169 +1,169
1 1 import { TraceSource } from "../log/TraceSource";
2 2 import { argumentNotEmptyString } from "../safe";
3 3 import { Descriptor, ContainerServiceMap, ContainerKeys, TypeOfService, ILifetime, ServiceContainer } from "./interfaces";
4 4 import { MapOf } from "../interfaces";
5 5
6 6 const trace = TraceSource.get("@implab/core/di/ActivationContext");
7 7
8 8 export interface ActivationContextInfo {
9 9 name: string;
10 10
11 11 service: string;
12 12
13 13 }
14 14
15 15 let nextId = 1;
16 16
17 17 /** This class is created once per `Container.resolve` method call and used to
18 18 * cache dependencies and to track created instances. The activation context
19 19 * tracks services with `context` activation type.
20 20 */
21 21 export class ActivationContext<S extends object> {
22 22 _cache: MapOf<any>;
23 23
24 24 _services: ContainerServiceMap<S>;
25 25
26 26 _visited: MapOf<any>;
27 27
28 28 _name: string;
29 29
30 30 _service: Descriptor<S, any>;
31 31
32 32 _container: ServiceContainer<S>;
33 33
34 34 _parent: ActivationContext<S> | undefined;
35 35
36 36 /** Creates a new activation context with the specified parameters.
37 37 * @param container the container which starts the activation process
38 38 * @param services the initial service registrations
39 39 * @param name the name of the service being activated, this parameter is
40 40 * used for the debug purpose.
41 41 * @param service the service to activate, this parameter is used for the
42 42 * debug purpose.
43 43 */
44 44 constructor(container: ServiceContainer<S>, services: ContainerServiceMap<S>, name: string, service: Descriptor<S, any>) {
45 45 this._name = name;
46 46 this._service = service;
47 47 this._visited = {};
48 48 this._cache = {};
49 49 this._services = services;
50 50 this._container = container;
51 51 }
52 52
53 53 /** the name of the current resolving dependency */
54 54 getName() {
55 55 return this._name;
56 56 }
57 57
58 58 /** Returns the container for which 'resolve' method was called */
59 59 getContainer() {
60 60 return this._container;
61 61 }
62 62
63 63 /** Resolves the specified dependency in the current context
64 64 * @param name The name of the dependency being resolved
65 65 */
66 66 resolve<K extends ContainerKeys<S>>(name: K): TypeOfService<S, K>;
67 67 /** Resolves the specified dependency with the specified default value if
68 68 * the dependency is missing.
69 69 *
70 70 * @param name The name of the dependency being resolved
71 71 * @param def A default value to return in case of the specified dependency
72 72 * is missing.
73 73 */
74 74 resolve<K extends ContainerKeys<S>, T>(name: K, def: T): TypeOfService<S, K> | T;
75 75 /** Resolves the specified dependency and returns undefined in case if the
76 76 * dependency is missing.
77 77 *
78 78 * @param name The name of the dependency being resolved
79 79 */
80 80 resolve<K extends ContainerKeys<S>>(name: K, def: undefined): TypeOfService<S, K> | undefined;
81 81 resolve<K extends ContainerKeys<S>, T>(name: K, def?: T): TypeOfService<S, K> | T | undefined {
82 82 const d = this._services[name];
83 83
84 84 if (d !== undefined) {
85 85 return this.activate(d, name.toString());
86 86 } else {
87 87 if (arguments.length > 1)
88 88 return def;
89 89 else
90 90 throw new Error(`Service ${name} not found`);
91 91 }
92 92 }
93 93
94 94 /**
95 95 * registers services local to the the activation context
96 96 *
97 97 * @name{string} the name of the service
98 98 * @service{string} the service descriptor to register
99 99 */
100 100 register<K extends keyof S>(name: K, service: Descriptor<S, S[K]>) {
101 101 argumentNotEmptyString(name, "name");
102 102
103 103 this._services[name] = service as any;
104 104 }
105 105
106 106 createLifetime(): ILifetime {
107 107 const id = nextId++;
108 108 const me = this;
109 109 return {
110 110 initialize() {
111 111 },
112 112 has() {
113 113 return id in me._cache;
114 114 },
115 115 get() {
116 116 return me._cache[id];
117 117 },
118 118 store(item: any) {
119 119 me._cache[id] = item;
120 120 }
121 121 };
122 122 }
123 123
124 124 activate<T>(d: Descriptor<S, T>, name: string) {
125 125 if (trace.isLogEnabled())
126 trace.log(`enter ${name} ${d}`);
126 trace.log("enter {0} {1}", name, d);
127 127
128 128 const ctx = this.enter(d, name);
129 129 const v = d.activate(ctx);
130 130
131 131 if (trace.isLogEnabled())
132 132 trace.log(`leave ${name}`);
133 133
134 134 return v;
135 135 }
136 136
137 137 visit(id: string) {
138 138 const count = this._visited[id] || 0;
139 139 this._visited[id] = count + 1;
140 140 return count;
141 141 }
142 142
143 143 getStack(): ActivationContextInfo[] {
144 144 const stack = [{
145 145 name: this._name,
146 146 service: this._service.toString()
147 147 }];
148 148
149 149 return this._parent ?
150 150 stack.concat(this._parent.getStack()) :
151 151 stack;
152 152 }
153 153
154 154 private enter(service: Descriptor<S, any>, name: string): this {
155 155 const clone = Object.create(this);
156 156 clone._name = name;
157 157 clone._services = Object.create(this._services);
158 158 clone._parent = this;
159 159 clone._service = service;
160 160 return clone;
161 161 }
162 162
163 163 /** Creates a clone for the current context, used to protect it from modifications */
164 164 clone(): this {
165 165 const clone = Object.create(this);
166 166 clone._services = Object.create(this._services);
167 167 return clone;
168 168 }
169 169 }
@@ -1,199 +1,218
1 1 import { IDestroyable, MapOf } from "../interfaces";
2 2 import { argumentNotNull, isDestroyable, argumentNotEmptyString, isRemovable } from "../safe";
3 3 import { ILifetime, ServiceContainer } from "./interfaces";
4 4 import { ActivationContext } from "./ActivationContext";
5 5
6 6 function safeCall(item: () => void) {
7 7 try {
8 8 item();
9 9 } catch {
10 10 // silence!
11 11 }
12 12 }
13 13
14 14 const emptyLifetime: ILifetime = Object.freeze({
15 15 has() {
16 16 return false;
17 17 },
18 18
19 19 initialize() {
20 20
21 21 },
22 22
23 23 get() {
24 24 throw new Error("The specified item isn't registered with this lifetime manager");
25 25 },
26 26
27 27 store() {
28 28 // does nothing
29 },
30
31 toString() {
32 return `[object EmptyLifetime]`;
29 33 }
30 34
31 35 });
32 36
33 37 const unknownLifetime: ILifetime = Object.freeze({
34 38 has() {
35 39 return false;
36 40 },
37 41 initialize() {
38 42 throw new Error("Can't call initialize on the unknown lifetime object");
39 43 },
40 44 get() {
41 45 throw new Error("The lifetime object isn't initialized");
42 46 },
43 47 store() {
44 48 throw new Error("Can't store a value in the unknown lifetime object");
49 },
50 toString() {
51 return `[object UnknownLifetime]`;
45 52 }
46 53 });
47 54
48 55 let nextId = 0;
49 56
50 57 const singletons: any = {};
51 58
52 59 export class LifetimeManager implements IDestroyable {
53 60 private _cleanup: (() => void)[] = [];
54 61 private _cache: MapOf<any> = {};
55 62 private _destroyed = false;
56 63
57 64 private _pending: MapOf<boolean> = {};
58 65
59 66 create(): ILifetime {
60 67 const self = this;
61 68 const id = ++nextId;
62 69 return {
63 70 has() {
64 71 return (id in self._cache);
65 72 },
66 73
67 74 get() {
68 75 const t = self._cache[id];
69 76 if (t === undefined)
70 77 throw new Error(`The item with with the key ${id} isn't found`);
71 78 return t;
72 79 },
73 80
74 81 initialize() {
75 82 if (self._pending[id])
76 83 throw Error(`Cyclic reference detected: the item with the key ${id} is already activating.`);
77 84 self._pending[id] = true;
78 85 },
79 86
80 87 store(item: any, cleanup?: (item: any) => void) {
81 88 argumentNotNull(id, "id");
82 89 argumentNotNull(item, "item");
83 90
84 91 if (this.has())
85 92 throw new Error(`The item with with the key ${id} already registered with this lifetime manager`);
86 93 delete self._pending[id];
87 94
88 95 self._cache[id] = item;
89 96
90 97 if (self._destroyed)
91 98 throw new Error("Lifetime manager is destroyed");
92 99 if (cleanup) {
93 100 self._cleanup.push(() => cleanup(item));
94 101 } else if (isDestroyable(item)) {
95 102 self._cleanup.push(() => item.destroy());
96 103 } else if (isRemovable(item)) {
97 104 self._cleanup.push(() => item.remove());
98 105 }
99 106 }
100 107 };
101 108 }
102 109
103 110 destroy() {
104 111 if (!this._destroyed) {
105 112 this._destroyed = true;
106 113 this._cleanup.forEach(safeCall);
107 114 this._cleanup.length = 0;
108 115 }
109 116 }
110 117
111 118 static empty(): ILifetime {
112 119 return emptyLifetime;
113 120 }
114 121
115 static hierarchyLifetime(): ILifetime {
122 static hierarchyLifetime() {
116 123 let _lifetime = unknownLifetime;
117 124 return {
118 125 initialize(context: ActivationContext<any>) {
119 126 if (_lifetime !== unknownLifetime)
120 127 throw new Error("Cyclic reference activation detected");
121 128
122 129 _lifetime = context.getContainer().getLifetimeManager().create();
123 130 },
124 131 get() {
125 132 return _lifetime.get();
126 133 },
127 134 has() {
128 135 return _lifetime.has();
129 136 },
130 137 store(item: any, cleanup?: (item: any) => void) {
131 138 return _lifetime.store(item, cleanup);
139 },
140 toString() {
141 return `[object HierarchyLifetime, has=${this.has()}]`;
132 142 }
133 143 };
134 144 }
135 145
136 static contextLifetime(): ILifetime {
146 static contextLifetime() {
137 147 let _lifetime = unknownLifetime;
138 148 return {
139 149 initialize(context: ActivationContext<any>) {
140 150 if (_lifetime !== unknownLifetime)
141 151 throw new Error("Cyclic reference detected");
142 152 _lifetime = context.createLifetime();
143 153 },
144 154 get() {
145 155 return _lifetime.get();
146 156 },
147 157 has() {
148 158 return _lifetime.has();
149 159 },
150 160 store(item: any) {
151 161 _lifetime.store(item);
162 },
163 toString() {
164 return `[object ContextLifetime, has=${this.has()}]`;
152 165 }
153 166 };
154 167 }
155 168
156 static singletonLifetime(typeId: string): ILifetime {
169 static singletonLifetime(typeId: string) {
157 170 argumentNotEmptyString(typeId, "typeId");
158 171 let pending = false;
159 172 return {
160 173 has() {
161 174 return typeId in singletons;
162 175 },
163 176 get() {
164 177 if (!this.has())
165 178 throw new Error(`The instance ${typeId} doesn't exists`);
166 179 return singletons[typeId];
167 180 },
168 181 initialize() {
169 182 if (pending)
170 183 throw new Error("Cyclic reference detected");
171 184 pending = true;
172 185 },
173 186 store(item: any) {
174 187 singletons[typeId] = item;
175 188 pending = false;
189 },
190 toString() {
191 return `[object SingletonLifetime, has=${this.has()}, typeId=${typeId}]`;
176 192 }
177 193 };
178 194 }
179 195
180 196 static containerLifetime(container: ServiceContainer<any>) {
181 197 let _lifetime = unknownLifetime;
182 198 return {
183 199 initialize(context: ActivationContext<any>) {
184 200 if (_lifetime !== unknownLifetime)
185 201 throw new Error("Cyclic reference detected");
186 202 _lifetime = container.getLifetimeManager().create();
187 203 },
188 204 get() {
189 205 return _lifetime.get();
190 206 },
191 207 has() {
192 208 return _lifetime.has();
193 209 },
194 210 store(item: any) {
195 211 _lifetime.store(item);
212 },
213 toString() {
214 return `[object ContainerLifetime, has=${_lifetime.has()}]`
196 215 }
197 216 };
198 217 }
199 218 }
@@ -1,63 +1,66
1 1 import { Descriptor, PartialServiceMap, ILifetime, ContainerKeys } from "../interfaces";
2 2 import { ActivationContext } from "../ActivationContext";
3 3 import { each } from "../../safe";
4 4 import { DependencyOptions, LazyDependencyOptions, Resolver } from "./interfaces";
5 5
6 6 export interface DescriptorImplArgs<S extends object, T> {
7 7 lifetime: ILifetime;
8 8
9 9 factory: (resolve: Resolver<S>) => T;
10 10
11 11 cleanup?: (item: T) => void;
12 12
13 13 overrides?: PartialServiceMap<S>;
14 14 }
15 15
16 16 export class DescriptorImpl<S extends object, T> implements Descriptor<S, T> {
17 17
18 18 private readonly _overrides?: PartialServiceMap<S>;
19 19
20 20 private readonly _lifetime: ILifetime;
21 21
22 22 private readonly _factory: (resolve: Resolver<S>) => T;
23 23
24 24 private readonly _cleanup?: (item: T) => void;
25 25
26 26 constructor(args: DescriptorImplArgs<S, T>) {
27 27 this._lifetime = args.lifetime;
28 28 this._factory = args.factory;
29 29 if (args.cleanup)
30 30 this._cleanup = args.cleanup;
31 31 if (args.overrides)
32 32 this._overrides = args.overrides;
33 33 }
34 34
35 35 activate(context: ActivationContext<S>): T {
36 36
37 37 if (this._lifetime.has())
38 38 return this._lifetime.get();
39 39
40 40 this._lifetime.initialize(context);
41 41
42 42 if (this._overrides)
43 43 each(this._overrides, (v, k) => context.register(k, v));
44 44
45 45 const resolve = (name: ContainerKeys<S>, opts?: DependencyOptions | LazyDependencyOptions) => {
46 46 if (opts && "lazy" in opts && opts.lazy) {
47 47 const c2 = context.clone();
48 48 return () => {
49 49 return opts.optional ? c2.resolve(name, opts.default) : c2.resolve(name);
50 50 };
51 51 } else {
52 52 return opts && opts.optional ? context.resolve(name, opts.default) : context.resolve(name);
53 53 }
54 54 };
55 55
56 56 const instance = this._factory.call(undefined, resolve);
57 57
58 58 this._lifetime.store(instance, this._cleanup);
59 59
60 60 return instance;
61 61 }
62 62
63 toString() {
64 return `[object DescriptorImpl, lifetime=${this._lifetime}]`;
65 }
63 66 }
@@ -1,106 +1,125
1 1 import { test } from "./TestTraits";
2 2 import { Container } from "../di/Container";
3 3 import { ReferenceDescriptor } from "../di/ReferenceDescriptor";
4 4 import { AggregateDescriptor } from "../di/AggregateDescriptor";
5 5 import { ValueDescriptor } from "../di/ValueDescriptor";
6 6 import { Foo } from "../mock/Foo";
7 7 import { Bar } from "../mock/Bar";
8 8 import { isNull } from "../safe";
9 import { Box } from "../mock/Box";
9 10
10 11 test("Container register/resolve tests", async t => {
11 12 const container = new Container<{
12 13 "bla-bla": string;
13 14 "connection": string;
14 15 "dbParams": {
15 16 timeout: number;
16 17 connection: string;
17 18 }
18 19 }>();
19 20
20 21 const connection1 = "db://localhost";
21 22
22 23 t.throws(
23 24 () => container.register("bla-bla", "bla-bla" as any),
24 25 "Do not allow to register anything other than descriptors"
25 26 );
26 27
27 28 t.doesNotThrow(
28 29 () => container.register("connection", new ValueDescriptor(connection1)),
29 30 "register ValueDescriptor"
30 31 );
31 32
32 33 t.equals(container.resolve("connection"), connection1, "resolve string value");
33 34
34 35 t.doesNotThrow(
35 36 () => container.register(
36 37 "dbParams",
37 38 new AggregateDescriptor({
38 39 timeout: 10,
39 40 connection: new ReferenceDescriptor({ name: "connection" })
40 41 })
41 42 ),
42 43 "register AggregateDescriptor"
43 44 );
44 45
45 46 const dbParams = container.resolve("dbParams");
46 47 t.equals(dbParams.connection, connection1, "should get string value 'dbParams.connection'");
47 48 });
48 49
49 50 test("Container configure/resolve tests", async t => {
50 51
51 52 const container = new Container<{
52 53 foo: Foo;
53 54 box: Bar;
54 55 bar: Bar;
55 56 db: any;
56 57 }>();
57 58
58 59 await container.configure({
59 60 foo: {
60 61 $type: Foo
61 62 },
62 63
63 64 box: {
64 65 $type: Bar,
65 66 params: {
66 67 $dependency: "foo"
67 68 }
68 69 },
69 70
70 71 bar: {
71 72 $type: Bar,
72 73 params: [{
73 74 db: {
74 75 provider: {
75 76 $dependency: "db"
76 77 }
77 78 }
78 79 }]
79 80 }
80 81 });
81 82 t.pass("should configure from js object");
82 83
83 84 const f1 = container.resolve("foo");
84 85
85 86 t.assert(!isNull(f1), "foo should be not null");
86 87
87 88 t.throws(() => container.resolve("bar"), "should not resolve dependency 'db'");
88 89
89 90 });
90 91
91 92 test("Load configuration from module", async t => {
92 93 const container = new Container();
93 94
94 95 await container.configure("../mock/config1", { contextRequire: require });
95 96 t.pass("The configuration should load");
96 97
97 98 const f1 = container.resolve("foo");
98 99
99 100 t.assert(!isNull(f1), "foo should be not null");
100 101
101 102 const b1 = container.resolve("bar") as Bar;
102 103
103 104 t.assert(!isNull(b1), "bar should not be null");
104 105 t.assert(!isNull(b1._v), "bar.foo should not be null");
105 106
106 107 });
108
109 test("Optional dependency with child container", async t => {
110 const container = new Container<{
111 foo?: Foo;
112 box: Box<Foo>;
113 }>();
114 await container.fluent({
115 box: it => it.factory($ => new Box($("foo")))
116 });
117
118 const child = await container.createChildContainer()
119 .fluent({
120 foo: it => it.factory(() => new Foo())
121 })
122
123 const box = child.resolve("box");
124 t.assert(!isNull(box.getValue()), "'foo' dependency is declared in child container");
125 }); No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now