Skip to content
This repository was archived by the owner on Apr 21, 2024. It is now read-only.

Commit 77c3a43

Browse files
authored
Merge pull request #7 from Coder-Spirit/dependency-groups
Dependency groups
2 parents 116c674 + e4eab17 commit 77c3a43

File tree

6 files changed

+334
-18
lines changed

6 files changed

+334
-18
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,26 @@ const container = createContainer()
8282
.register('fn', func(printNameAndAge, 'someName', 'someAge'))
8383
// And constructors too
8484
.register('Person', constructor(Person, 'someAge', 'someName'))
85+
// We can "define groups" by using `:` as an infix, the group's name will be
86+
// the first part of the string before `:`.
87+
// Groups can be used in all "register" methods.
88+
.registerValue('group1:a', 1) // group == 'group1'
89+
.registerValue('group1:b', 2)
90+
.registerValue('group2:a', 3) // group == 'group2'
91+
.registerValue('group2:b', 4)
8592
93+
// We can resolve registered functions
8694
const print = container.resolve('fn')
8795
print() // Prints "Timmy is aged 5"
8896

97+
// We can resolve registered constructors
8998
const person = container.resolve('Person')
9099
console.print(person.age) // Prints "5"
91100
console.print(person.name) // Prints "Timmy"
101+
102+
// We can resolve registered "groups"
103+
container.resolveGroup('group1') // ~ [1, 2], not necessarily in the same order
104+
container.resolveGroup('group2') // ~ [3, 4], not necessarily in the same order
92105
```
93106

94107
It is also possible to register and resolve asynchronous factories and
@@ -103,6 +116,9 @@ If you are curious, just try out:
103116

104117
- First-class support for Deno.
105118
- First-class support for asynchronous dependency resolution.
119+
- Stricter types for dependencies re-registration.
120+
- Groups registration and resolution: very useful when we need to resolve all
121+
dependencies belonging to a same category.
106122
- The container interface has been split into `ReaderContainer` and
107123
`WriterContainer`, making it easier to use precise types.
108124
- More extense documentation.

lambda-ioc/README.md

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,67 @@ import { ... } from 'https://deno.land/x/lambda_ioc@[VERSION]/lambda-ioc/deno/in
5858
## Example
5959

6060
```ts
61-
import { createContainer } from '@coderspirit/lambda-ioc'
61+
import {
62+
constructor,
63+
createContainer,
64+
func
65+
} from '@coderspirit/lambda-ioc'
6266

6367
function printNameAndAge(name: string, age: number) {
6468
console.log(`${name} is aged ${age}`)
6569
}
70+
71+
class Person {
72+
constructor(
73+
public readonly age: number,
74+
public readonly name: string
75+
) {}
76+
}
6677
6778
const container = createContainer()
68-
.register('someAge', value(5))
69-
.register('someName', value('Timmy'))
79+
.registerValue('someAge', 5)
80+
.registerValue('someName', 'Timmy')
81+
// We can register functions
7082
.register('fn', func(printNameAndAge, 'someName', 'someAge'))
83+
// And constructors too
84+
.register('Person', constructor(Person, 'someAge', 'someName'))
85+
// We can "define groups" by using `:` as an infix, the group's name will be
86+
// the first part of the string before `:`.
87+
// Groups can be used in all "register" methods.
88+
.registerValue('group1:a', 1) // group == 'group1'
89+
.registerValue('group1:b', 2)
90+
.registerValue('group2:a', 3) // group == 'group2'
91+
.registerValue('group2:b', 4)
7192
93+
// We can resolve registered functions
7294
const print = container.resolve('fn')
7395
print() // Prints "Timmy is aged 5"
96+
97+
// We can resolve registered constructors
98+
const person = container.resolve('Person')
99+
console.print(person.age) // Prints "5"
100+
console.print(person.name) // Prints "Timmy"
101+
102+
// We can resolve registered "groups"
103+
container.resolveGroup('group1') // ~ [1, 2], not necessarily in the same order
104+
container.resolveGroup('group2') // ~ [3, 4], not necessarily in the same order
74105
```
106+
107+
It is also possible to register and resolve asynchronous factories and
108+
dependencies. They are not documented yet because some "helpers" are missing,
109+
and therefore it's a bit more annoying to take advantage of that feature.
110+
111+
If you are curious, just try out:
112+
- `registerAsync`
113+
- `resolveAsync`
114+
115+
## Differences respect to Diddly
116+
117+
- First-class support for Deno.
118+
- First-class support for asynchronous dependency resolution.
119+
- Stricter types for dependencies re-registration.
120+
- Groups registration and resolution: very useful when we need to resolve all
121+
dependencies belonging to a same category.
122+
- The container interface has been split into `ReaderContainer` and
123+
`WriterContainer`, making it easier to use precise types.
124+
- More extense documentation.

lambda-ioc/deno/container.ts

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
export type ContainerKey = string | symbol
44

5-
/**
6-
* Represents a dependency factory: a function that, given an IoC container, it
7-
* is able to instantiate a specific dependency.
8-
*/
5+
type ExtractPrefix<S extends ContainerKey> =
6+
S extends `${infer Prefix}:${string}` ? Prefix : never
7+
8+
type ExtractPrefixedValues<
9+
Prefix extends string,
10+
Struct extends Record<ContainerKey, unknown>,
11+
BaseKeys extends keyof Struct = keyof Struct,
12+
> = BaseKeys extends `${Prefix}:${infer U}` ? Struct[`${Prefix}:${U}`] : never
913

1014
export interface SyncDependencyFactory<
1115
T,
@@ -21,12 +25,13 @@ export interface AsyncDependencyFactory<
2125
Record<ContainerKey, unknown>
2226
>,
2327
> {
24-
// It might seem counterintuitive that we allow T as return type, but the
25-
// factory might also "become async" because of its dependencies, not just
26-
// because of its return type.
2728
(container: TContainer): Promise<T>
2829
}
2930

31+
/**
32+
* Represents a dependency factory: a function that, given an IoC container, it
33+
* is able to instantiate a specific dependency.
34+
*/
3035
export interface DependencyFactory<
3136
T,
3237
TContainer extends ReadableContainer<
@@ -63,6 +68,33 @@ export interface ReadableContainer<
6368
): Promise<TAsyncDependencies[TName]>
6469
}
6570

71+
export interface RedableGroupContainer<
72+
TSyncDependencies extends Record<ContainerKey, unknown>,
73+
TAsyncDependencies extends Record<ContainerKey, unknown>,
74+
> {
75+
resolveGroup<
76+
GroupName extends keyof TSyncDependencies extends ContainerKey
77+
? ExtractPrefix<keyof TSyncDependencies>
78+
: never,
79+
>(
80+
groupName: GroupName,
81+
): keyof TSyncDependencies extends ContainerKey
82+
? ExtractPrefixedValues<GroupName, TSyncDependencies>[]
83+
: never
84+
85+
resolveGroupAsync<
86+
GroupName extends keyof TAsyncDependencies extends ContainerKey
87+
? ExtractPrefix<keyof TAsyncDependencies>
88+
: never,
89+
>(
90+
groupName: GroupName,
91+
): Promise<
92+
keyof TAsyncDependencies extends ContainerKey
93+
? ExtractPrefixedValues<GroupName, TAsyncDependencies>[]
94+
: never
95+
>
96+
}
97+
6698
/**
6799
* Represents a write-only version of a type-safe IoC container with
68100
* "auto-wired" dependencies resolution.
@@ -173,6 +205,7 @@ export interface Container<
173205
TSyncDependencies extends Record<ContainerKey, unknown>,
174206
TAsyncDependencies extends Record<ContainerKey, unknown>,
175207
> extends ReadableContainer<TSyncDependencies, TAsyncDependencies>,
208+
RedableGroupContainer<TSyncDependencies, TAsyncDependencies>,
176209
WritableContainer<TSyncDependencies, TAsyncDependencies> {}
177210

178211
/**
@@ -365,5 +398,45 @@ function __createContainer<
365398
] as AsyncDependencyFactory<TAsyncDependencies[TName], typeof this>
366399
)(this)
367400
},
401+
402+
resolveGroup<GroupName extends string>(
403+
groupName: GroupName,
404+
): keyof TSyncDependencies extends ContainerKey
405+
? ExtractPrefixedValues<GroupName, TSyncDependencies>[]
406+
: never {
407+
return (
408+
Object.entries(syncDependencies)
409+
.filter(([key]) => {
410+
return key.startsWith(`${groupName}:`)
411+
})
412+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
413+
.map(([_key, value]) => {
414+
return value(this)
415+
}) as keyof TSyncDependencies extends ContainerKey
416+
? ExtractPrefixedValues<GroupName, TSyncDependencies>[]
417+
: never
418+
)
419+
},
420+
421+
async resolveGroupAsync<GroupName extends string>(
422+
groupName: GroupName,
423+
): Promise<
424+
keyof TAsyncDependencies extends ContainerKey
425+
? ExtractPrefixedValues<GroupName, TAsyncDependencies>[]
426+
: never
427+
> {
428+
return (await Promise.all(
429+
Object.entries(asyncDependencies)
430+
.filter(([key]) => {
431+
return key.startsWith(`${groupName}:`)
432+
})
433+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
434+
.map(([_key, value]) => {
435+
return value(this)
436+
}),
437+
)) as keyof TAsyncDependencies extends ContainerKey
438+
? ExtractPrefixedValues<GroupName, TAsyncDependencies>[]
439+
: never
440+
},
368441
}
369442
}

lambda-ioc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@coderspirit/lambda-ioc",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"main": "./dist/cjs/index.js",
55
"module": "./dist/esm/index.js",
66
"types": "./dist/cjs/index.d.ts",

lambda-ioc/src/__tests__/container.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,46 @@ describe('container', () => {
7979
expect(await c1.resolveAsync('ab')).toBe(15)
8080
expect(await c2.resolveAsync('ab')).toBe(21)
8181
})
82+
83+
it('can register groups by relying on prefixes', () => {
84+
const container = createContainer()
85+
.register('a', () => 10)
86+
.register('b', () => 20)
87+
.register('g1:a', () => 30)
88+
.register('g1:b', () => 40)
89+
.register('g2:a', () => 50)
90+
.register('g2:b', () => 60)
91+
92+
const g1 = container.resolveGroup('g1')
93+
expect(g1).toHaveLength(2)
94+
expect(g1).toContain(30)
95+
expect(g1).toContain(40)
96+
97+
const g2 = container.resolveGroup('g2')
98+
expect(g2).toHaveLength(2)
99+
expect(g2).toContain(50)
100+
expect(g2).toContain(60)
101+
})
102+
103+
it('can register asynchronous groups by relying on prefixes', async () => {
104+
const container = createContainer()
105+
.register('a', () => 10)
106+
.register('b', () => 20)
107+
.registerAsync('g1:a', () => Promise.resolve(30))
108+
.registerAsync('g1:b', () => Promise.resolve(40))
109+
.registerAsync('g2:a', () => Promise.resolve(50))
110+
.registerAsync('g2:b', () => Promise.resolve(60))
111+
112+
const g1 = await container.resolveGroupAsync('g1')
113+
expect(g1).toHaveLength(2)
114+
expect(g1).toContain(30)
115+
expect(g1).toContain(40)
116+
117+
const g2 = await container.resolveGroupAsync('g2')
118+
expect(g2).toHaveLength(2)
119+
expect(g2).toContain(50)
120+
expect(g2).toContain(60)
121+
})
82122
})
83123

84124
// Type tests
@@ -195,4 +235,68 @@ describe('@types/container', () => {
195235
const c_can_only_resolve_b: C_resolveAsync_Parameters_is_b = true
196236
expect(c_can_only_resolve_b).toBe(true)
197237
})
238+
239+
it('only resolves the sync registered groups', () => {
240+
type C = Container<
241+
{
242+
a: number
243+
b: number
244+
'g1:a': number
245+
'g1:b': string
246+
'g2:a': string
247+
'g2:b': boolean
248+
},
249+
{}
250+
>
251+
252+
type C_resolveGroup_Parameters = Parameters<C['resolveGroup']>
253+
type C_resolveGroup_Parameters_extends_g1g2 =
254+
C_resolveGroup_Parameters extends ['g1' | 'g2'] ? true : false
255+
type g1g2_extends_C_resolveGroup_Parameters = [
256+
'g1' | 'g2',
257+
] extends C_resolveGroup_Parameters
258+
? true
259+
: false
260+
type C_resolveGroup_Parameters_is_g1g2 =
261+
C_resolveGroup_Parameters_extends_g1g2 extends true
262+
? g1g2_extends_C_resolveGroup_Parameters extends true
263+
? true
264+
: false
265+
: false
266+
const c_can_only_resolveGroup_g1g2: C_resolveGroup_Parameters_is_g1g2 = true
267+
expect(c_can_only_resolveGroup_g1g2).toBe(true)
268+
})
269+
270+
it('only resolves the async registered groups', () => {
271+
type C = Container<
272+
{
273+
a: number
274+
b: number
275+
},
276+
{
277+
'g1:a': number
278+
'g1:b': string
279+
'g2:a': string
280+
'g2:b': boolean
281+
}
282+
>
283+
284+
type C_resolveGroupAsync_Parameters = Parameters<C['resolveGroupAsync']>
285+
type C_resolveGroupAsync_Parameters_extends_g1g2 =
286+
C_resolveGroupAsync_Parameters extends ['g1' | 'g2'] ? true : false
287+
type g1g2_extends_C_resolveGroupAsync_Parameters = [
288+
'g1' | 'g2',
289+
] extends C_resolveGroupAsync_Parameters
290+
? true
291+
: false
292+
type C_resolveGroupAsync_Parameters_is_g1g2 =
293+
C_resolveGroupAsync_Parameters_extends_g1g2 extends true
294+
? g1g2_extends_C_resolveGroupAsync_Parameters extends true
295+
? true
296+
: false
297+
: false
298+
const c_can_only_resolveGroupAsync_g1g2: C_resolveGroupAsync_Parameters_is_g1g2 =
299+
true
300+
expect(c_can_only_resolveGroupAsync_g1g2).toBe(true)
301+
})
198302
})

0 commit comments

Comments
 (0)