ditox 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils.test.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  import {createContainer, optional, ResolverError, token} from './ditox';
2
2
  import {
3
3
  bindMultiValue,
4
- getProps,
5
- getValues,
6
4
  injectable,
7
- injectableProps,
8
- resolveProps,
5
+ injectableClass,
6
+ resolveValue,
9
7
  resolveValues,
8
+ tryResolveValue,
9
+ tryResolveValues,
10
10
  } from './utils';
11
11
 
12
12
  const NUMBER = token<number>('number');
13
13
  const STRING = token<string>('string');
14
14
 
15
- describe('bindMultiValue', () => {
15
+ describe('bindMultiValue()', () => {
16
16
  const NUMBERS = token<Array<number>>('numbers');
17
17
 
18
18
  it('should append a value to an array declared by a token', () => {
@@ -53,13 +53,53 @@ describe('bindMultiValue', () => {
53
53
  });
54
54
  });
55
55
 
56
- describe('getValues', () => {
56
+ describe('tryResolveValue()', () => {
57
+ it('should return an object with values by the tokens', () => {
58
+ const container = createContainer();
59
+ container.bindValue(NUMBER, 1);
60
+ container.bindValue(STRING, 'abc');
61
+
62
+ const props: {a: number; b: string} = tryResolveValue(container, {
63
+ a: NUMBER,
64
+ b: STRING,
65
+ });
66
+ expect(props).toEqual({a: 1, b: 'abc'});
67
+ });
68
+
69
+ it('should return "undefined" item in case a value is not provided', () => {
70
+ const container = createContainer();
71
+ container.bindValue(NUMBER, 1);
72
+
73
+ const props = tryResolveValue(container, {a: NUMBER, b: STRING});
74
+ expect(props).toEqual({a: 1, b: undefined});
75
+ });
76
+
77
+ it('should return values of optional tokens in case they are not provided', () => {
78
+ const container = createContainer();
79
+ container.bindValue(NUMBER, 1);
80
+
81
+ const OPTIONAL_STRING = optional(STRING, 'value');
82
+
83
+ const props = tryResolveValue(container, {
84
+ a: NUMBER,
85
+ b: STRING,
86
+ c: OPTIONAL_STRING,
87
+ });
88
+ expect(props).toEqual({a: 1, b: undefined, c: 'value'});
89
+ });
90
+ });
91
+
92
+ describe('tryResolveValues()', () => {
57
93
  it('should return values for the tokens', () => {
58
94
  const container = createContainer();
59
95
  container.bindValue(NUMBER, 1);
60
96
  container.bindValue(STRING, 'abc');
61
97
 
62
- const values: [number, string] = getValues(container, NUMBER, STRING);
98
+ const values: [number, string] = tryResolveValues(
99
+ container,
100
+ NUMBER,
101
+ STRING,
102
+ );
63
103
  expect(values).toEqual([1, 'abc']);
64
104
  });
65
105
 
@@ -67,7 +107,7 @@ describe('getValues', () => {
67
107
  const container = createContainer();
68
108
  container.bindValue(NUMBER, 1);
69
109
 
70
- const values = getValues(container, NUMBER, STRING);
110
+ const values = tryResolveValues(container, NUMBER, STRING);
71
111
  expect(values).toEqual([1, undefined]);
72
112
  });
73
113
 
@@ -77,7 +117,7 @@ describe('getValues', () => {
77
117
 
78
118
  const OPTIONAL_STRING = optional(STRING, 'value');
79
119
 
80
- const values = getValues(container, NUMBER, STRING, OPTIONAL_STRING);
120
+ const values = tryResolveValues(container, NUMBER, STRING, OPTIONAL_STRING);
81
121
  expect(values).toEqual([1, undefined, 'value']);
82
122
  });
83
123
 
@@ -85,7 +125,7 @@ describe('getValues', () => {
85
125
  const container = createContainer();
86
126
  container.bindValue(NUMBER, 1);
87
127
 
88
- const values = getValues(
128
+ const values = tryResolveValues(
89
129
  container,
90
130
  NUMBER,
91
131
  NUMBER,
@@ -102,7 +142,43 @@ describe('getValues', () => {
102
142
  });
103
143
  });
104
144
 
105
- describe('resolveValues', () => {
145
+ describe('resolveValue()', () => {
146
+ it('should an object with resolved values from the container for the specified token props', () => {
147
+ const container = createContainer();
148
+ container.bindValue(NUMBER, 1);
149
+ container.bindValue(STRING, 'abc');
150
+
151
+ const props: {a: number; b: string} = resolveValue(container, {
152
+ a: NUMBER,
153
+ b: STRING,
154
+ });
155
+ expect(props).toEqual({a: 1, b: 'abc'});
156
+ });
157
+
158
+ it('should throw an error in case a value is not provided', () => {
159
+ const container = createContainer();
160
+ container.bindValue(NUMBER, 1);
161
+
162
+ expect(() => {
163
+ resolveValue(container, {a: NUMBER, b: STRING});
164
+ }).toThrowError(
165
+ new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
166
+ );
167
+ });
168
+
169
+ it('should resolve values of optional tokens in case they are not provided', () => {
170
+ const container = createContainer();
171
+ container.bindValue(NUMBER, 1);
172
+
173
+ const props = resolveValue(container, {
174
+ a: NUMBER,
175
+ b: optional(STRING, 'value'),
176
+ });
177
+ expect(props).toEqual({a: 1, b: 'value'});
178
+ });
179
+ });
180
+
181
+ describe('resolveValues()', () => {
106
182
  it('should resolve tokens from the container', () => {
107
183
  const container = createContainer();
108
184
  container.bindValue(NUMBER, 1);
@@ -127,12 +203,30 @@ describe('resolveValues', () => {
127
203
  const container = createContainer();
128
204
  container.bindValue(NUMBER, 1);
129
205
 
130
- const values = getValues(container, NUMBER, optional(STRING, 'value'));
206
+ const values = resolveValues(container, NUMBER, optional(STRING, 'value'));
131
207
  expect(values).toEqual([1, 'value']);
132
208
  });
209
+
210
+ it('should resolve a dictionary of tokens', () => {
211
+ const container = createContainer();
212
+ container.bindValue(NUMBER, 1);
213
+
214
+ const values = resolveValue(container, {
215
+ a: NUMBER,
216
+ b: optional(STRING, 'value'),
217
+ });
218
+ expect(values).toEqual({
219
+ a: 1,
220
+ b: 'value',
221
+ });
222
+ });
133
223
  });
134
224
 
135
225
  describe('injectable()', () => {
226
+ function join({a, b}: {a: number; b: string}): string {
227
+ return `${a}/${b}`;
228
+ }
229
+
136
230
  it('should inject values from Container as arguments of the factory', () => {
137
231
  const container = createContainer();
138
232
  container.bindValue(NUMBER, 1);
@@ -174,83 +268,104 @@ describe('injectable()', () => {
174
268
 
175
269
  expect(decoratedFactory(container)).toBe('1value');
176
270
  });
177
- });
178
271
 
179
- describe('getProps', () => {
180
- it('should return an object with values by the tokens', () => {
272
+ it('should inject values from Container as an object for the first argument of the factory', () => {
181
273
  const container = createContainer();
182
274
  container.bindValue(NUMBER, 1);
183
- container.bindValue(STRING, 'abc');
275
+ container.bindValue(STRING, '2');
184
276
 
185
- const props: {a?: number; b?: string} = getProps(container, {
186
- a: NUMBER,
187
- b: STRING,
188
- });
189
- expect(props).toEqual({a: 1, b: 'abc'});
277
+ const decoratedFactory = injectable(join, {a: NUMBER, b: STRING});
278
+
279
+ expect(decoratedFactory(container)).toBe('1/2');
190
280
  });
191
281
 
192
- it('should return "undefined" item in case a value is not provided', () => {
282
+ it('should throw ResolverError error in case a value is not provided', () => {
193
283
  const container = createContainer();
194
284
  container.bindValue(NUMBER, 1);
195
285
 
196
- const props = getProps(container, {a: NUMBER, b: STRING});
197
- expect(props).toEqual({a: 1, b: undefined});
286
+ const decoratedFactory = injectable(join, {a: NUMBER, b: STRING});
287
+
288
+ expect(() => decoratedFactory(container)).toThrowError(
289
+ new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
290
+ );
198
291
  });
199
292
 
200
- it('should return values of optional tokens in case they are not provided', () => {
293
+ it('should inject values of optional tokens in case values are not provided', () => {
201
294
  const container = createContainer();
202
295
  container.bindValue(NUMBER, 1);
203
296
 
204
- const OPTIONAL_STRING = optional(STRING, 'value');
205
-
206
- const props = getProps(container, {
297
+ const decoratedFactory = injectable(join, {
207
298
  a: NUMBER,
208
- b: STRING,
209
- c: OPTIONAL_STRING,
299
+ b: optional(STRING, 'value'),
210
300
  });
211
- expect(props).toEqual({a: 1, b: undefined, c: 'value'});
301
+
302
+ expect(decoratedFactory(container)).toBe('1/value');
212
303
  });
213
304
  });
214
305
 
215
- describe('resolveProps', () => {
216
- it('should an object with resolved values from the container for the specified token props', () => {
306
+ describe('injectableClass()', () => {
307
+ class TestClass {
308
+ a: number;
309
+ b: string;
310
+
311
+ constructor(a: number, b: string) {
312
+ this.a = a;
313
+ this.b = b;
314
+ }
315
+
316
+ concat(): string {
317
+ return this.a + this.b;
318
+ }
319
+ }
320
+
321
+ it('should inject values from Container as arguments of the constructor', () => {
217
322
  const container = createContainer();
218
323
  container.bindValue(NUMBER, 1);
219
- container.bindValue(STRING, 'abc');
324
+ container.bindValue(STRING, '2');
220
325
 
221
- const props: {a: number; b: string} = resolveProps(container, {
222
- a: NUMBER,
223
- b: STRING,
224
- });
225
- expect(props).toEqual({a: 1, b: 'abc'});
326
+ const factory = injectableClass(TestClass, NUMBER, STRING);
327
+
328
+ expect(factory(container).concat()).toBe('12');
226
329
  });
227
330
 
228
- it('should throw an error in case a value is not provided', () => {
331
+ it('should throw ResolverError error in case a value is not provided', () => {
229
332
  const container = createContainer();
230
333
  container.bindValue(NUMBER, 1);
231
334
 
232
- expect(() => {
233
- resolveProps(container, {a: NUMBER, b: STRING});
234
- }).toThrowError(
335
+ const factory = injectableClass(TestClass, NUMBER, STRING);
336
+
337
+ expect(() => factory(container)).toThrowError(
235
338
  new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
236
339
  );
237
340
  });
238
341
 
239
- it('should resolve values of optional tokens in case they are not provided', () => {
342
+ it('should inject values of optional tokens in case values are not provided', () => {
240
343
  const container = createContainer();
241
344
  container.bindValue(NUMBER, 1);
242
345
 
243
- const props = resolveProps(container, {
244
- a: NUMBER,
245
- b: optional(STRING, 'value'),
246
- });
247
- expect(props).toEqual({a: 1, b: 'value'});
346
+ const factory = injectableClass(
347
+ TestClass,
348
+ NUMBER,
349
+ optional(STRING, 'value'),
350
+ );
351
+
352
+ expect(factory(container).concat()).toBe('1value');
248
353
  });
249
354
  });
250
355
 
251
- describe('injectableProps()', () => {
252
- function join({a, b}: {a: number; b: string}): string {
253
- return `${a}/${b}`;
356
+ describe('injectableClass()', () => {
357
+ class TestClass {
358
+ a: number;
359
+ b: string;
360
+
361
+ constructor(props: {a: number; b: string}) {
362
+ this.a = props.a;
363
+ this.b = props.b;
364
+ }
365
+
366
+ join(): string {
367
+ return `${this.a}/${this.b}`;
368
+ }
254
369
  }
255
370
 
256
371
  it('should inject values from Container as an object for the first argument of the factory', () => {
@@ -258,18 +373,18 @@ describe('injectableProps()', () => {
258
373
  container.bindValue(NUMBER, 1);
259
374
  container.bindValue(STRING, '2');
260
375
 
261
- const decoratedFactory = injectableProps(join, {a: NUMBER, b: STRING});
376
+ const factory = injectableClass(TestClass, {a: NUMBER, b: STRING});
262
377
 
263
- expect(decoratedFactory(container)).toBe('1/2');
378
+ expect(factory(container).join()).toBe('1/2');
264
379
  });
265
380
 
266
381
  it('should throw ResolverError error in case a value is not provided', () => {
267
382
  const container = createContainer();
268
383
  container.bindValue(NUMBER, 1);
269
384
 
270
- const decoratedFactory = injectableProps(join, {a: NUMBER, b: STRING});
385
+ const factory = injectableClass(TestClass, {a: NUMBER, b: STRING});
271
386
 
272
- expect(() => decoratedFactory(container)).toThrowError(
387
+ expect(() => factory(container)).toThrowError(
273
388
  new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
274
389
  );
275
390
  });
@@ -278,11 +393,11 @@ describe('injectableProps()', () => {
278
393
  const container = createContainer();
279
394
  container.bindValue(NUMBER, 1);
280
395
 
281
- const decoratedFactory = injectableProps(join, {
396
+ const factory = injectableClass(TestClass, {
282
397
  a: NUMBER,
283
398
  b: optional(STRING, 'value'),
284
399
  });
285
400
 
286
- expect(decoratedFactory(container)).toBe('1/value');
401
+ expect(factory(container).join()).toBe('1/value');
287
402
  });
288
403
  });
package/src/utils.ts CHANGED
@@ -5,6 +5,19 @@ type TokenProps<Props extends ValuesProps> = {
5
5
  [K in keyof Props]: Token<Props[K]>;
6
6
  };
7
7
 
8
+ /**
9
+ * Checks if a value is the token
10
+ */
11
+ export function isToken<T>(value: unknown): value is Token<T> {
12
+ return (
13
+ value !== undefined &&
14
+ value !== null &&
15
+ typeof value === 'object' &&
16
+ 'symbol' in value &&
17
+ typeof (value as any).symbol === 'symbol'
18
+ );
19
+ }
20
+
8
21
  /**
9
22
  * Rebinds the array by the token with added new value.
10
23
  * @param container - Dependency container.
@@ -22,41 +35,154 @@ export function bindMultiValue<T>(
22
35
  }
23
36
 
24
37
  /**
25
- * Returns an array of resolved values by the specified token.
38
+ * Tries to resolve a value by the provided token.
39
+ *
40
+ * If an argument is an object which has tokens as its properties,
41
+ * then returns an object containing resolved values as properties.
42
+
26
43
  * If a token is not found, then `undefined` value is used.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const value = tryResolveValue(container, tokenA);
48
+ * console.log(value); // 1
49
+ *
50
+ * const props = tryResolveValue(container, {a: tokenA, b: tokenB});
51
+ * console.log(props); // {a: 1, b: 2}
52
+ * ```
27
53
  */
28
- export function getValues<
29
- Tokens extends Token<unknown>[],
54
+ export function tryResolveValue<
55
+ Tokens extends Token<unknown> | {[key: string]: Token<unknown>},
56
+ Values extends Tokens extends Token<infer V>
57
+ ? V | undefined
58
+ : Tokens extends TokenProps<infer Props>
59
+ ? Partial<Props>
60
+ : never,
61
+ >(container: Container, token: Tokens): Values {
62
+ if (isToken(token)) {
63
+ return container.get(token) as Values;
64
+ }
65
+
66
+ const obj: any = {};
67
+ Object.keys(token).forEach((key) => (obj[key] = container.get(token[key])));
68
+ return obj;
69
+ }
70
+
71
+ /**
72
+ * Returns an array of resolved values or objects with resolved values.
73
+ *
74
+ * If an item of the array is an object which has tokens as its properties,
75
+ * then returns an object containing resolved values as properties.
76
+
77
+ * If a token is not found, then `undefined` value is used.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const items1 = tryResolveValues(container, tokenA);
82
+ * console.log(items1); // [1]
83
+ *
84
+ * const items2 = tryResolveValues(container, tokenA, {a: tokenA, b: tokenB});
85
+ * console.log(items2); // [1, {a: 1, b: 2}]
86
+ * ```
87
+ */
88
+ export function tryResolveValues<
89
+ Tokens extends (Token<unknown> | {[key: string]: Token<unknown>})[],
30
90
  Values extends {
31
- [K in keyof Tokens]: Tokens[K] extends Token<infer V> ? V : never;
91
+ [K in keyof Tokens]: Tokens[K] extends Token<infer V>
92
+ ? V | undefined
93
+ : Tokens[K] extends TokenProps<infer Props>
94
+ ? Partial<Props>
95
+ : never;
32
96
  },
33
97
  >(container: Container, ...tokens: Tokens): Values {
34
- return tokens.map(container.get) as Values;
98
+ return tokens.map((item) => tryResolveValue(container, item)) as Values;
35
99
  }
36
100
 
37
101
  /**
38
- * Returns an array of resolved values by the specified token.
102
+ * Resolves a value by the provided token.
103
+ *
104
+ * If an argument is an object which has tokens as its properties,
105
+ * then returns an object containing resolved values as properties.
106
+
107
+ * If a value is not found by the token, then `ResolverError` is thrown.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * const value = resolveValue(container, tokenA);
112
+ * console.log(value); // 1
113
+ *
114
+ * const props = resolveValue(container, {a: tokenA, b: tokenB});
115
+ * console.log(props); // {a: 1, b: 2}
116
+ * ```
117
+ */
118
+ export function resolveValue<
119
+ Tokens extends Token<unknown> | {[key: string]: Token<unknown>},
120
+ Values extends Tokens extends Token<infer V>
121
+ ? V
122
+ : Tokens extends TokenProps<infer Props>
123
+ ? Props
124
+ : never,
125
+ >(container: Container, token: Tokens): Values {
126
+ if (isToken(token)) {
127
+ return container.resolve(token) as Values;
128
+ }
129
+
130
+ const obj: any = {};
131
+ Object.keys(token).forEach(
132
+ (key) => (obj[key] = container.resolve(token[key])),
133
+ );
134
+ return obj;
135
+ }
136
+
137
+ /**
138
+ * Returns an array of resolved values or objects with resolved values.
139
+ *
140
+ * If an item of the array is an object which has tokens as its properties,
141
+ * then returns an object containing resolved values as properties.
142
+
39
143
  * If a token is not found, then `ResolverError` is thrown.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const items1 = resolveValues(container, tokenA);
148
+ * console.log(items1); // [1]
149
+ *
150
+ * const items2 = resolveValues(container, tokenA, {a: tokenA, b: tokenB});
151
+ * console.log(items2); // [1, {a: 1, b: 2}]
152
+ * ```
40
153
  */
41
154
  export function resolveValues<
42
- Tokens extends Token<unknown>[],
155
+ Tokens extends (Token<unknown> | {[key: string]: Token<unknown>})[],
43
156
  Values extends {
44
- [K in keyof Tokens]: Tokens[K] extends Token<infer V> ? V : never;
157
+ [K in keyof Tokens]: Tokens[K] extends Token<infer V>
158
+ ? V
159
+ : Tokens[K] extends TokenProps<infer Props>
160
+ ? Props
161
+ : never;
45
162
  },
46
163
  >(container: Container, ...tokens: Tokens): Values {
47
- return tokens.map(container.resolve) as Values;
164
+ return tokens.map((item) => resolveValue(container, item)) as Values;
48
165
  }
49
166
 
50
167
  /**
51
- * Decorates a factory by passing resolved tokens as factory arguments.
168
+ * Decorates a factory by passing resolved values as factory arguments.
169
+ *
170
+ * If an argument is an object which has tokens as its properties,
171
+ * then returns an object containing resolved values as properties.
172
+ *
52
173
  * @param factory - A factory.
53
174
  * @param tokens - Tokens which correspond to factory arguments.
175
+ *
54
176
  * @return Decorated factory which takes a dependency container as a single argument.
55
177
  */
56
178
  export function injectable<
57
- Tokens extends Token<unknown>[],
179
+ Tokens extends (Token<unknown> | {[key: string]: Token<unknown>})[],
58
180
  Values extends {
59
- [K in keyof Tokens]: Tokens[K] extends Token<infer V> ? V : never;
181
+ [K in keyof Tokens]: Tokens[K] extends Token<infer V>
182
+ ? V
183
+ : Tokens[K] extends TokenProps<infer Props>
184
+ ? Props
185
+ : never;
60
186
  },
61
187
  Result,
62
188
  >(
@@ -71,62 +197,34 @@ export function injectable<
71
197
  }
72
198
 
73
199
  /**
74
- * Returns an object with resolved properties which are specified by token properties.
75
- * If a token is not found, then `undefined` value is used.
200
+ * Decorates a class by passing resolved values as arguments to its constructor.
76
201
  *
77
- * @example
78
- * ```ts
79
- * const props = getProps(container, {a: tokenA, b: tokenB});
80
- * console.log(props); // {a: 1, b: 2}
81
- * ```
82
- */
83
- export function getProps<Props extends ValuesProps>(
84
- container: Container,
85
- tokens: TokenProps<Props>,
86
- ): Partial<Props> {
87
- const obj: any = {...tokens};
88
- Object.keys(obj).forEach((key) => (obj[key] = container.get(obj[key])));
89
- return obj;
90
- }
91
-
92
- /**
93
- * Returns an object with resolved properties which are specified by token properties.
94
- * If a token is not found, then `ResolverError` is thrown.
202
+ * If an argument is an object which has tokens as its properties,
203
+ * then returns an object containing resolved values as properties.
95
204
  *
96
- * @example
97
- * ```ts
98
- * const props = resolveProps(container, {a: tokenA, b: tokenB});
99
- * console.log(props); // {a: 1, b: 2}
100
- * ```
101
- */
102
- export function resolveProps<Props extends ValuesProps>(
103
- container: Container,
104
- tokens: TokenProps<Props>,
105
- ): Props {
106
- const obj: any = {...tokens};
107
- Object.keys(obj).forEach((key) => (obj[key] = container.resolve(obj[key])));
108
- return obj;
109
- }
110
-
111
- /**
112
- * Decorates a factory by passing a resolved object with tokens as the first argument.
113
- * @param factory - A factory.
114
- * @param tokens - Object with tokens.
115
- * @return Decorated factory which takes a dependency container as a single argument.
205
+ * @param constructor - Constructor of a class
206
+ * @param tokens - Tokens which correspond to constructor arguments
116
207
  *
117
- * @example
118
- * ```ts
119
- * const factory = ({a, b}: {a: number, b: number}) => a + b;
120
- * const decoratedFactory = injectableProps(factory, {a: tokenA, b: tokenB});
121
- * const result = decoratedFactory(container);
122
- * ```
208
+ * @return A factory function which takes a dependency container as a single argument
209
+ * and returns a new created class.
123
210
  */
124
- export function injectableProps<Props extends ValuesProps, Result>(
125
- factory: (props: Props) => Result,
126
- tokens: TokenProps<Props>,
211
+ export function injectableClass<
212
+ Tokens extends (Token<unknown> | {[key: string]: Token<unknown>})[],
213
+ Values extends {
214
+ [K in keyof Tokens]: Tokens[K] extends Token<infer V>
215
+ ? V
216
+ : Tokens[K] extends TokenProps<infer Props>
217
+ ? Props
218
+ : never;
219
+ },
220
+ Result,
221
+ >(
222
+ this: unknown,
223
+ constructor: new (...params: Values) => Result,
224
+ ...tokens: Tokens
127
225
  ): (container: Container) => Result {
128
- return (container) => {
129
- const values: Props = resolveProps(container, tokens);
130
- return factory(values);
131
- };
226
+ return injectable<Tokens, Values, Result>(
227
+ (...values) => new constructor(...values),
228
+ ...tokens,
229
+ );
132
230
  }