@navios/di 0.4.1 → 0.5.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/README.md +211 -1
- package/coverage/clover.xml +1912 -1277
- package/coverage/coverage-final.json +37 -28
- package/coverage/docs/examples/basic-usage.mts.html +1 -1
- package/coverage/docs/examples/factory-pattern.mts.html +1 -1
- package/coverage/docs/examples/index.html +1 -1
- package/coverage/docs/examples/injection-tokens.mts.html +1 -1
- package/coverage/docs/examples/request-scope-example.mts.html +1 -1
- package/coverage/docs/examples/service-lifecycle.mts.html +1 -1
- package/coverage/index.html +71 -41
- package/coverage/lib/_tsup-dts-rollup.d.mts.html +682 -43
- package/coverage/lib/index.d.mts.html +7 -4
- package/coverage/lib/index.html +5 -5
- package/coverage/lib/testing/index.d.mts.html +91 -0
- package/coverage/lib/testing/index.html +116 -0
- package/coverage/src/base-instance-holder-manager.mts.html +589 -0
- package/coverage/src/container.mts.html +257 -74
- package/coverage/src/decorators/factory.decorator.mts.html +1 -1
- package/coverage/src/decorators/index.html +1 -1
- package/coverage/src/decorators/index.mts.html +1 -1
- package/coverage/src/decorators/injectable.decorator.mts.html +20 -20
- package/coverage/src/enums/index.html +1 -1
- package/coverage/src/enums/index.mts.html +1 -1
- package/coverage/src/enums/injectable-scope.enum.mts.html +1 -1
- package/coverage/src/enums/injectable-type.enum.mts.html +1 -1
- package/coverage/src/errors/di-error.mts.html +292 -0
- package/coverage/src/errors/errors.enum.mts.html +30 -21
- package/coverage/src/errors/factory-not-found.mts.html +31 -22
- package/coverage/src/errors/factory-token-not-resolved.mts.html +29 -26
- package/coverage/src/errors/index.html +56 -41
- package/coverage/src/errors/index.mts.html +15 -9
- package/coverage/src/errors/instance-destroying.mts.html +31 -22
- package/coverage/src/errors/instance-expired.mts.html +31 -22
- package/coverage/src/errors/instance-not-found.mts.html +31 -22
- package/coverage/src/errors/unknown-error.mts.html +31 -43
- package/coverage/src/event-emitter.mts.html +14 -14
- package/coverage/src/factory-context.mts.html +1 -1
- package/coverage/src/index.html +121 -46
- package/coverage/src/index.mts.html +7 -4
- package/coverage/src/injection-token.mts.html +28 -28
- package/coverage/src/injector.mts.html +1 -1
- package/coverage/src/instance-resolver.mts.html +1762 -0
- package/coverage/src/interfaces/factory.interface.mts.html +1 -1
- package/coverage/src/interfaces/index.html +1 -1
- package/coverage/src/interfaces/index.mts.html +1 -1
- package/coverage/src/interfaces/on-service-destroy.interface.mts.html +1 -1
- package/coverage/src/interfaces/on-service-init.interface.mts.html +1 -1
- package/coverage/src/registry.mts.html +28 -28
- package/coverage/src/request-context-holder.mts.html +183 -102
- package/coverage/src/request-context-manager.mts.html +532 -0
- package/coverage/src/service-instantiator.mts.html +49 -49
- package/coverage/src/service-invalidator.mts.html +1372 -0
- package/coverage/src/service-locator-event-bus.mts.html +48 -48
- package/coverage/src/service-locator-instance-holder.mts.html +2 -14
- package/coverage/src/service-locator-manager.mts.html +71 -335
- package/coverage/src/service-locator.mts.html +240 -2328
- package/coverage/src/symbols/index.html +1 -1
- package/coverage/src/symbols/index.mts.html +1 -1
- package/coverage/src/symbols/injectable-token.mts.html +1 -1
- package/coverage/src/testing/index.html +131 -0
- package/coverage/src/testing/index.mts.html +88 -0
- package/coverage/src/testing/test-container.mts.html +445 -0
- package/coverage/src/token-processor.mts.html +607 -0
- package/coverage/src/utils/defer.mts.html +28 -214
- package/coverage/src/utils/get-injectable-token.mts.html +7 -7
- package/coverage/src/utils/get-injectors.mts.html +99 -99
- package/coverage/src/utils/index.html +15 -15
- package/coverage/src/utils/index.mts.html +4 -7
- package/coverage/src/utils/types.mts.html +1 -1
- package/docs/injectable.md +51 -11
- package/docs/scopes.md +63 -29
- package/lib/_tsup-dts-rollup.d.mts +447 -212
- package/lib/_tsup-dts-rollup.d.ts +447 -212
- package/lib/chunk-44F3LXW5.mjs +2043 -0
- package/lib/chunk-44F3LXW5.mjs.map +1 -0
- package/lib/index.d.mts +6 -4
- package/lib/index.d.ts +6 -4
- package/lib/index.js +1199 -773
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +4 -1599
- package/lib/index.mjs.map +1 -1
- package/lib/testing/index.d.mts +2 -0
- package/lib/testing/index.d.ts +2 -0
- package/lib/testing/index.js +2060 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/index.mjs +73 -0
- package/lib/testing/index.mjs.map +1 -0
- package/package.json +11 -1
- package/src/__tests__/container.spec.mts +47 -13
- package/src/__tests__/errors.spec.mts +53 -27
- package/src/__tests__/injectable.spec.mts +73 -0
- package/src/__tests__/request-scope.spec.mts +0 -2
- package/src/__tests__/service-locator-manager.spec.mts +12 -82
- package/src/__tests__/service-locator.spec.mts +1009 -1
- package/src/__type-tests__/inject.spec-d.mts +30 -7
- package/src/__type-tests__/injectable.spec-d.mts +76 -37
- package/src/base-instance-holder-manager.mts +2 -9
- package/src/container.mts +70 -10
- package/src/decorators/injectable.decorator.mts +29 -5
- package/src/errors/di-error.mts +69 -0
- package/src/errors/index.mts +9 -7
- package/src/injection-token.mts +1 -0
- package/src/injector.mts +2 -0
- package/src/instance-resolver.mts +559 -0
- package/src/request-context-holder.mts +0 -2
- package/src/request-context-manager.mts +149 -0
- package/src/service-invalidator.mts +429 -0
- package/src/service-locator-instance-holder.mts +0 -4
- package/src/service-locator-manager.mts +10 -40
- package/src/service-locator.mts +86 -782
- package/src/testing/README.md +80 -0
- package/src/testing/__tests__/test-container.spec.mts +173 -0
- package/src/testing/index.mts +1 -0
- package/src/testing/test-container.mts +120 -0
- package/src/token-processor.mts +174 -0
- package/src/utils/get-injectors.mts +161 -24
- package/src/utils/index.mts +0 -1
- package/src/utils/types.mts +12 -8
- package/tsup.config.mts +1 -1
- package/src/__tests__/defer.spec.mts +0 -166
- package/src/errors/errors.enum.mts +0 -8
- package/src/errors/factory-not-found.mts +0 -8
- package/src/errors/factory-token-not-resolved.mts +0 -10
- package/src/errors/instance-destroying.mts +0 -8
- package/src/errors/instance-expired.mts +0 -8
- package/src/errors/instance-not-found.mts +0 -8
- package/src/errors/unknown-error.mts +0 -15
- package/src/utils/defer.mts +0 -73
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Testing Infrastructure
|
|
2
|
+
|
|
3
|
+
The `@navios/di/testing` package provides a `TestContainer` class that extends the base `Container` with additional methods useful for testing.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **TestContainer**: A specialized container for testing with simplified binding methods
|
|
8
|
+
- **bind().toValue()**: Bind tokens to specific values (useful for mocks)
|
|
9
|
+
- **bind().toClass()**: Bind tokens to class constructors
|
|
10
|
+
- **clear()**: Clear all instances and bindings from the container
|
|
11
|
+
- **Convenience methods**: `bindValue()`, `bindClass()`, `createChild()`
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Injectable, InjectionToken } from '@navios/di'
|
|
17
|
+
import { TestContainer } from '@navios/di/testing'
|
|
18
|
+
|
|
19
|
+
// Create a test container
|
|
20
|
+
const container = new TestContainer()
|
|
21
|
+
|
|
22
|
+
// Create injection tokens
|
|
23
|
+
const API_URL_TOKEN = InjectionToken.create<string>('api-url')
|
|
24
|
+
const HTTP_CLIENT_TOKEN = InjectionToken.create<HttpClient>('http-client')
|
|
25
|
+
|
|
26
|
+
// Mock implementations
|
|
27
|
+
class MockHttpClient implements HttpClient {
|
|
28
|
+
async get(url: string) {
|
|
29
|
+
return { data: 'mocked response' }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Bind values for testing
|
|
34
|
+
container.bindValue(API_URL_TOKEN, 'https://test-api.com')
|
|
35
|
+
container.bindClass(HTTP_CLIENT_TOKEN, MockHttpClient)
|
|
36
|
+
|
|
37
|
+
// Or use the fluent API
|
|
38
|
+
container.bind(API_URL_TOKEN).toValue('https://test-api.com')
|
|
39
|
+
|
|
40
|
+
container.bind(HTTP_CLIENT_TOKEN).toClass(MockHttpClient)
|
|
41
|
+
|
|
42
|
+
// Bind a class to itself
|
|
43
|
+
@Injectable()
|
|
44
|
+
class UserService {
|
|
45
|
+
constructor(
|
|
46
|
+
@Inject(API_URL_TOKEN) private apiUrl: string,
|
|
47
|
+
@Inject(HTTP_CLIENT_TOKEN) private httpClient: HttpClient,
|
|
48
|
+
) {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
container.bindSelf(UserService)
|
|
52
|
+
|
|
53
|
+
// Clear the container between tests
|
|
54
|
+
container.clear()
|
|
55
|
+
|
|
56
|
+
// Create isolated child containers
|
|
57
|
+
const childContainer = container.createChild()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API Reference
|
|
61
|
+
|
|
62
|
+
### TestContainer
|
|
63
|
+
|
|
64
|
+
#### Methods
|
|
65
|
+
|
|
66
|
+
- `bind<T>(token: InjectionToken<T, any>): TestBindingBuilder<T>` - Creates a binding builder
|
|
67
|
+
- `bind<T>(token: ClassType): TestBindingBuilder<T>` - Creates a binding builder
|
|
68
|
+
- `bindValue<T>(token: InjectionToken<T, any>, value: T): TestContainer` - Binds a value to a token
|
|
69
|
+
- `bindValue<T>(token: ClassType, value: T): TestContainer` - Binds a value to a token
|
|
70
|
+
- `bindClass<T>(token: InjectionToken<T, any>, target: ClassType): TestContainer` - Binds a class to a token
|
|
71
|
+
- `bindClass<T>(token: ClassType, target: ClassType): TestContainer` - Binds a class to a token
|
|
72
|
+
- `createChild(): TestContainer` - Creates a new isolated test container
|
|
73
|
+
- `clear(): void` - Clears all instances and bindings
|
|
74
|
+
|
|
75
|
+
### TestBindingBuilder
|
|
76
|
+
|
|
77
|
+
#### Methods
|
|
78
|
+
|
|
79
|
+
- `toValue(value: T): TestContainer` - Binds the token to a specific value
|
|
80
|
+
- `toClass(target: ClassType): TestContainer` - Binds the token to a class constructor
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { Injectable } from '../../decorators/injectable.decorator.mjs'
|
|
4
|
+
import { InjectionToken } from '../../injection-token.mjs'
|
|
5
|
+
import { inject } from '../../injector.mjs'
|
|
6
|
+
import { TestContainer } from '../test-container.mjs'
|
|
7
|
+
|
|
8
|
+
describe('TestContainer', () => {
|
|
9
|
+
let container: TestContainer
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
container = new TestContainer()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('clear method', () => {
|
|
16
|
+
it('should clear all instances from the container', () => {
|
|
17
|
+
// This test verifies that the clear method exists and can be called
|
|
18
|
+
expect(() => container.clear()).not.toThrow()
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('bind method', () => {
|
|
23
|
+
it('should create a TestBindingBuilder', () => {
|
|
24
|
+
const token = InjectionToken.create<string>('test-token')
|
|
25
|
+
const builder = container.bind(token)
|
|
26
|
+
expect(builder).toBeDefined()
|
|
27
|
+
expect(builder).toHaveProperty('toValue')
|
|
28
|
+
expect(builder).toHaveProperty('toClass')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should allow chaining bind operations', () => {
|
|
32
|
+
const token = InjectionToken.create<string>('test-token')
|
|
33
|
+
const result = container.bind(token).toValue('test-value')
|
|
34
|
+
expect(result).toBe(container)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should bind value and retrieve it correctly', async () => {
|
|
38
|
+
const token = InjectionToken.create<string>('test-token')
|
|
39
|
+
const testValue = 'test-value'
|
|
40
|
+
|
|
41
|
+
container.bind(token).toValue(testValue)
|
|
42
|
+
|
|
43
|
+
const retrievedValue = await container.get(token)
|
|
44
|
+
expect(retrievedValue).toBe(testValue)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should bind class and retrieve instance correctly', async () => {
|
|
48
|
+
@Injectable()
|
|
49
|
+
class TestService {
|
|
50
|
+
getValue() {
|
|
51
|
+
return 'test-service-value'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const token = InjectionToken.create<TestService>('test-service')
|
|
56
|
+
|
|
57
|
+
container.bind(token).toClass(TestService)
|
|
58
|
+
|
|
59
|
+
const instance = await container.get(token)
|
|
60
|
+
expect(instance).toBeInstanceOf(TestService)
|
|
61
|
+
expect(instance.getValue()).toBe('test-service-value')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('bindValue method', () => {
|
|
66
|
+
it('should bind a value to a token', () => {
|
|
67
|
+
const token = InjectionToken.create<string>('test-token')
|
|
68
|
+
const result = container.bindValue(token, 'test-value')
|
|
69
|
+
expect(result).toBe(container)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should bind value and retrieve it correctly', async () => {
|
|
73
|
+
const token = InjectionToken.create<number>('number-token')
|
|
74
|
+
const testValue = 42
|
|
75
|
+
|
|
76
|
+
container.bindValue(token, testValue)
|
|
77
|
+
|
|
78
|
+
const retrievedValue = await container.get(token)
|
|
79
|
+
expect(retrievedValue).toBe(testValue)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should bind object value and retrieve it correctly', async () => {
|
|
83
|
+
interface TestConfig {
|
|
84
|
+
apiUrl: string
|
|
85
|
+
timeout: number
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const token = InjectionToken.create<TestConfig>('config-token')
|
|
89
|
+
const testConfig: TestConfig = {
|
|
90
|
+
apiUrl: 'https://api.example.com',
|
|
91
|
+
timeout: 5000,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
container.bindValue(token, testConfig)
|
|
95
|
+
|
|
96
|
+
const retrievedConfig = await container.get(token)
|
|
97
|
+
expect(retrievedConfig).toEqual(testConfig)
|
|
98
|
+
expect(retrievedConfig.apiUrl).toBe('https://api.example.com')
|
|
99
|
+
expect(retrievedConfig.timeout).toBe(5000)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('bindClass method', () => {
|
|
104
|
+
it('should bind a class to a token', () => {
|
|
105
|
+
class TestClass {}
|
|
106
|
+
const token = InjectionToken.create<TestClass>('test-token')
|
|
107
|
+
const result = container.bindClass(token, TestClass)
|
|
108
|
+
expect(result).toBe(container)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should bind class and retrieve instance correctly', async () => {
|
|
112
|
+
@Injectable()
|
|
113
|
+
class UserService {
|
|
114
|
+
private users: string[] = ['alice', 'bob']
|
|
115
|
+
|
|
116
|
+
getUsers() {
|
|
117
|
+
return this.users
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
addUser(user: string) {
|
|
121
|
+
this.users.push(user)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const token = InjectionToken.create<UserService>('user-service')
|
|
126
|
+
|
|
127
|
+
container.bindClass(token, UserService)
|
|
128
|
+
|
|
129
|
+
const instance = await container.get(token)
|
|
130
|
+
expect(instance).toBeInstanceOf(UserService)
|
|
131
|
+
expect(instance.getUsers()).toEqual(['alice', 'bob'])
|
|
132
|
+
|
|
133
|
+
instance.addUser('charlie')
|
|
134
|
+
expect(instance.getUsers()).toEqual(['alice', 'bob', 'charlie'])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should bind class with dependencies and retrieve instance correctly', async () => {
|
|
138
|
+
@Injectable()
|
|
139
|
+
class DatabaseService {
|
|
140
|
+
connect() {
|
|
141
|
+
return 'connected'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Injectable()
|
|
146
|
+
class UserRepository {
|
|
147
|
+
db = inject(DatabaseService)
|
|
148
|
+
|
|
149
|
+
findUser(id: string) {
|
|
150
|
+
return `User ${id} from ${this.db.connect()}`
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const dbToken = InjectionToken.create<DatabaseService>('db-service')
|
|
155
|
+
const userRepoToken = InjectionToken.create<UserRepository>('user-repo')
|
|
156
|
+
|
|
157
|
+
container.bindClass(dbToken, DatabaseService)
|
|
158
|
+
container.bindClass(userRepoToken, UserRepository)
|
|
159
|
+
|
|
160
|
+
const userRepo = await container.get(userRepoToken)
|
|
161
|
+
expect(userRepo).toBeInstanceOf(UserRepository)
|
|
162
|
+
expect(userRepo.findUser('123')).toBe('User 123 from connected')
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('createChild method', () => {
|
|
167
|
+
it('should create a new TestContainer instance', () => {
|
|
168
|
+
const child = container.createChild()
|
|
169
|
+
expect(child).toBeInstanceOf(TestContainer)
|
|
170
|
+
expect(child).not.toBe(container)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './test-container.mjs'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { ClassType, InjectionToken } from '../injection-token.mjs'
|
|
2
|
+
import type { Registry } from '../registry.mjs'
|
|
3
|
+
import type { Injectors } from '../utils/index.mjs'
|
|
4
|
+
|
|
5
|
+
import { Container } from '../container.mjs'
|
|
6
|
+
import { Injectable } from '../decorators/injectable.decorator.mjs'
|
|
7
|
+
import { InjectableScope, InjectableType } from '../enums/index.mjs'
|
|
8
|
+
import { globalRegistry } from '../registry.mjs'
|
|
9
|
+
import { getInjectableToken } from '../utils/index.mjs'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A binding builder for the TestContainer that allows chaining binding operations.
|
|
13
|
+
*/
|
|
14
|
+
export class TestBindingBuilder<T> {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly container: TestContainer,
|
|
17
|
+
private readonly token: InjectionToken<T, any>,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Binds the token to a specific value.
|
|
22
|
+
* This is useful for testing with mock values or constants.
|
|
23
|
+
* @param value The value to bind to the token
|
|
24
|
+
*/
|
|
25
|
+
toValue(value: T): TestContainer {
|
|
26
|
+
const instanceName = this.container
|
|
27
|
+
.getServiceLocator()
|
|
28
|
+
.getInstanceIdentifier(this.token)
|
|
29
|
+
this.container
|
|
30
|
+
.getServiceLocator()
|
|
31
|
+
.getManager()
|
|
32
|
+
.storeCreatedHolder(
|
|
33
|
+
instanceName,
|
|
34
|
+
value,
|
|
35
|
+
InjectableType.Class,
|
|
36
|
+
InjectableScope.Singleton,
|
|
37
|
+
)
|
|
38
|
+
return this.container
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Binds the token to a class constructor.
|
|
43
|
+
* @param target The class constructor to bind to
|
|
44
|
+
*/
|
|
45
|
+
toClass(target: ClassType): TestContainer {
|
|
46
|
+
this.container['registry'].set(
|
|
47
|
+
this.token,
|
|
48
|
+
InjectableScope.Singleton,
|
|
49
|
+
target,
|
|
50
|
+
InjectableType.Class,
|
|
51
|
+
)
|
|
52
|
+
return this.container
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* TestContainer extends the base Container with additional methods useful for testing.
|
|
58
|
+
* It provides a simplified API for binding values and classes during test setup.
|
|
59
|
+
*/
|
|
60
|
+
@Injectable()
|
|
61
|
+
export class TestContainer extends Container {
|
|
62
|
+
constructor(
|
|
63
|
+
registry: Registry = globalRegistry,
|
|
64
|
+
logger: Console | null = null,
|
|
65
|
+
injectors: Injectors = undefined as any,
|
|
66
|
+
) {
|
|
67
|
+
super(registry, logger, injectors)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a binding builder for the given token.
|
|
72
|
+
* This allows chaining binding operations like bind(Token).toValue(value).
|
|
73
|
+
* @param token The injection token to bind
|
|
74
|
+
* @returns A TestBindingBuilder for chaining binding operations
|
|
75
|
+
*/
|
|
76
|
+
bind<T>(token: ClassType): TestBindingBuilder<T>
|
|
77
|
+
bind<T>(token: InjectionToken<T, any>): TestBindingBuilder<T>
|
|
78
|
+
bind(token: any): TestBindingBuilder<any> {
|
|
79
|
+
let realToken = token
|
|
80
|
+
if (typeof token === 'function') {
|
|
81
|
+
realToken = getInjectableToken(token)
|
|
82
|
+
}
|
|
83
|
+
return new TestBindingBuilder(this, realToken)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Binds a value directly to a token.
|
|
88
|
+
* This is a convenience method equivalent to bind(token).toValue(value).
|
|
89
|
+
* @param token The injection token to bind
|
|
90
|
+
* @param value The value to bind to the token
|
|
91
|
+
* @returns The TestContainer instance for chaining
|
|
92
|
+
*/
|
|
93
|
+
bindValue<T>(token: ClassType, value: T): TestContainer
|
|
94
|
+
bindValue<T>(token: InjectionToken<T, any>, value: T): TestContainer
|
|
95
|
+
bindValue(token: any, value: any): TestContainer {
|
|
96
|
+
return this.bind(token).toValue(value)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Binds a class to a token.
|
|
101
|
+
* This is a convenience method equivalent to bind(token).toClass(target).
|
|
102
|
+
* @param token The injection token to bind
|
|
103
|
+
* @param target The class constructor to bind to
|
|
104
|
+
* @returns The TestContainer instance for chaining
|
|
105
|
+
*/
|
|
106
|
+
bindClass(token: ClassType, target: ClassType): TestContainer
|
|
107
|
+
bindClass<T>(token: InjectionToken<T, any>, target: ClassType): TestContainer
|
|
108
|
+
bindClass(token: any, target: any): TestContainer {
|
|
109
|
+
return this.bind(token).toClass(target)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates a new TestContainer instance with the same configuration.
|
|
114
|
+
* This is useful for creating isolated test containers.
|
|
115
|
+
* @returns A new TestContainer instance
|
|
116
|
+
*/
|
|
117
|
+
createChild(): TestContainer {
|
|
118
|
+
return new TestContainer(this.registry, this.logger, this.injectors)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
|
3
|
+
|
|
4
|
+
import type { FactoryContext } from './factory-context.mjs'
|
|
5
|
+
import type {
|
|
6
|
+
AnyInjectableType,
|
|
7
|
+
InjectionTokenType,
|
|
8
|
+
} from './injection-token.mjs'
|
|
9
|
+
import type { RequestContextHolder } from './request-context-holder.mjs'
|
|
10
|
+
import type { ServiceLocator } from './service-locator.mjs'
|
|
11
|
+
|
|
12
|
+
import { DIError } from './errors/index.mjs'
|
|
13
|
+
import {
|
|
14
|
+
BoundInjectionToken,
|
|
15
|
+
FactoryInjectionToken,
|
|
16
|
+
InjectionToken,
|
|
17
|
+
} from './injection-token.mjs'
|
|
18
|
+
import { getInjectableToken } from './utils/index.mjs'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* TokenProcessor handles token validation, resolution, and instance name generation.
|
|
22
|
+
* Extracted from ServiceLocator to improve separation of concerns.
|
|
23
|
+
*/
|
|
24
|
+
export class TokenProcessor {
|
|
25
|
+
constructor(private readonly logger: Console | null = null) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validates and resolves token arguments, handling factory token resolution and validation.
|
|
29
|
+
*/
|
|
30
|
+
validateAndResolveTokenArgs(
|
|
31
|
+
token: AnyInjectableType,
|
|
32
|
+
args?: any,
|
|
33
|
+
): [
|
|
34
|
+
DIError | undefined,
|
|
35
|
+
{ actualToken: InjectionTokenType; validatedArgs?: any },
|
|
36
|
+
] {
|
|
37
|
+
let actualToken = token as InjectionToken<any, any>
|
|
38
|
+
if (typeof token === 'function') {
|
|
39
|
+
actualToken = getInjectableToken(token)
|
|
40
|
+
}
|
|
41
|
+
let realArgs = args
|
|
42
|
+
if (actualToken instanceof BoundInjectionToken) {
|
|
43
|
+
realArgs = actualToken.value
|
|
44
|
+
} else if (actualToken instanceof FactoryInjectionToken) {
|
|
45
|
+
if (actualToken.resolved) {
|
|
46
|
+
realArgs = actualToken.value
|
|
47
|
+
} else {
|
|
48
|
+
return [DIError.factoryTokenNotResolved(token.name), { actualToken }]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!actualToken.schema) {
|
|
52
|
+
return [undefined, { actualToken, validatedArgs: realArgs }]
|
|
53
|
+
}
|
|
54
|
+
const validatedArgs = actualToken.schema?.safeParse(realArgs)
|
|
55
|
+
if (validatedArgs && !validatedArgs.success) {
|
|
56
|
+
this.logger?.error(
|
|
57
|
+
`[TokenProcessor]#validateAndResolveTokenArgs(): Error validating args for ${actualToken.name.toString()}`,
|
|
58
|
+
validatedArgs.error,
|
|
59
|
+
)
|
|
60
|
+
return [DIError.unknown(validatedArgs.error), { actualToken }]
|
|
61
|
+
}
|
|
62
|
+
return [undefined, { actualToken, validatedArgs: validatedArgs?.data }]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generates a unique instance name based on token and arguments.
|
|
67
|
+
*/
|
|
68
|
+
generateInstanceName(token: InjectionTokenType, args: any): string {
|
|
69
|
+
if (!args) {
|
|
70
|
+
return token.toString()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const formattedArgs = Object.entries(args)
|
|
74
|
+
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
|
75
|
+
.map(([key, value]) => `${key}=${this.formatArgValue(value)}`)
|
|
76
|
+
.join(',')
|
|
77
|
+
|
|
78
|
+
return `${token.toString()}:${formattedArgs.replaceAll(/"/g, '').replaceAll(/:/g, '=')}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Formats a single argument value for instance name generation.
|
|
83
|
+
*/
|
|
84
|
+
formatArgValue(value: any): string {
|
|
85
|
+
if (typeof value === 'function') {
|
|
86
|
+
return `fn_${value.name}(${value.length})`
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === 'symbol') {
|
|
89
|
+
return value.toString()
|
|
90
|
+
}
|
|
91
|
+
return JSON.stringify(value).slice(0, 40)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a factory context for dependency injection during service instantiation.
|
|
96
|
+
* @param serviceLocator Reference to the service locator for dependency resolution
|
|
97
|
+
*/
|
|
98
|
+
createFactoryContext(
|
|
99
|
+
serviceLocator: ServiceLocator, // ServiceLocator reference for dependency resolution
|
|
100
|
+
): FactoryContext & {
|
|
101
|
+
getDestroyListeners: () => (() => void)[]
|
|
102
|
+
deps: Set<string>
|
|
103
|
+
} {
|
|
104
|
+
const destroyListeners = new Set<() => void>()
|
|
105
|
+
const deps = new Set<string>()
|
|
106
|
+
|
|
107
|
+
function addDestroyListener(listener: () => void) {
|
|
108
|
+
destroyListeners.add(listener)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getDestroyListeners() {
|
|
112
|
+
return Array.from(destroyListeners)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
// @ts-expect-error This is correct type
|
|
117
|
+
async inject(token, args) {
|
|
118
|
+
// Fall back to normal resolution
|
|
119
|
+
const [error, instance] = await serviceLocator.getInstance(
|
|
120
|
+
token,
|
|
121
|
+
args,
|
|
122
|
+
({ instanceName }: { instanceName: string }) => {
|
|
123
|
+
deps.add(instanceName)
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
if (error) {
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
return instance
|
|
130
|
+
},
|
|
131
|
+
addDestroyListener,
|
|
132
|
+
getDestroyListeners,
|
|
133
|
+
locator: serviceLocator,
|
|
134
|
+
deps,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Tries to get a pre-prepared instance from request contexts.
|
|
140
|
+
*/
|
|
141
|
+
tryGetPrePreparedInstance(
|
|
142
|
+
instanceName: string,
|
|
143
|
+
contextHolder: RequestContextHolder | undefined,
|
|
144
|
+
deps: Set<string>,
|
|
145
|
+
currentRequestContext: RequestContextHolder | null,
|
|
146
|
+
): any {
|
|
147
|
+
// Check provided context holder first (if has higher priority)
|
|
148
|
+
if (contextHolder && contextHolder.priority > 0) {
|
|
149
|
+
const prePreparedInstance = contextHolder.get(instanceName)?.instance
|
|
150
|
+
if (prePreparedInstance !== undefined) {
|
|
151
|
+
this.logger?.debug(
|
|
152
|
+
`[TokenProcessor] Using pre-prepared instance ${instanceName} from request context ${contextHolder.requestId}`,
|
|
153
|
+
)
|
|
154
|
+
deps.add(instanceName)
|
|
155
|
+
return prePreparedInstance
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check current request context (if different from provided contextHolder)
|
|
160
|
+
if (currentRequestContext && currentRequestContext !== contextHolder) {
|
|
161
|
+
const prePreparedInstance =
|
|
162
|
+
currentRequestContext.get(instanceName)?.instance
|
|
163
|
+
if (prePreparedInstance !== undefined) {
|
|
164
|
+
this.logger?.debug(
|
|
165
|
+
`[TokenProcessor] Using pre-prepared instance ${instanceName} from current request context ${currentRequestContext.requestId}`,
|
|
166
|
+
)
|
|
167
|
+
deps.add(instanceName)
|
|
168
|
+
return prePreparedInstance
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return undefined
|
|
173
|
+
}
|
|
174
|
+
}
|