-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Partial container API improvements #6
Changes from 4 commits
18fab13
5b63805
f67cf80
0470b79
6b8b6d4
f16ba65
60fa73c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
import { isMemoized, memoize } from "./memoize"; | ||
import type { Memoized } from "./memoize"; | ||
import { isMemoized, memoize } from "./memoize"; | ||
import { PartialContainer } from "./PartialContainer"; | ||
import type { AddService, AddServices, InjectableClass, InjectableFunction, TokenType, ValidTokens } from "./types"; | ||
import { ConcatInjectable } from "./Injectable"; | ||
import { ClassInjectable, ConcatInjectable, Injectable } from "./Injectable"; | ||
import { entries } from "./entries"; | ||
|
||
type MaybeMemoizedFactories<Services> = { | ||
|
@@ -407,13 +407,6 @@ export class Container<Services = {}> { | |
return this.providesService(fnOrContainer); | ||
} | ||
|
||
/** | ||
* Create a new Container which provides a Service created by the given [InjectableClass]. | ||
* | ||
* @param token - A unique Token which will correspond to the created Service. | ||
* @param cls - A class with a constructor that takes dependencies as arguments, which returns the Service. | ||
*/ | ||
|
||
/** | ||
* Registers a service in the container using a class constructor, simplifying the service creation process. | ||
* | ||
|
@@ -425,22 +418,10 @@ export class Container<Services = {}> { | |
* specifying these dependencies. | ||
* @returns A new Container instance containing the newly created service, allowing for method chaining. | ||
*/ | ||
providesClass<Token extends TokenType, Service, Tokens extends readonly ValidTokens<Services>[]>( | ||
providesClass = <Token extends TokenType, Service, Tokens extends readonly ValidTokens<Services>[]>( | ||
token: Token, | ||
cls: InjectableClass<Services, Service, Tokens> | ||
): Container<AddService<Services, Token, Service>> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was almost complete duplicate of providesService method, which we can now reuse. |
||
const dependencies: readonly any[] = cls.dependencies; | ||
// If the service depends on itself, e.g. in the multi-binding case, where we call append multiple times with | ||
// the same token, we always must resolve the dependency using the parent container to avoid infinite loop. | ||
const getFromParent = dependencies.indexOf(token) !== -1 ? () => this.get(token as any) : undefined; | ||
const factory = memoize(this, function (this: Container<Services>) { | ||
// Safety: getFromParent is defined if the token is in the dependencies list, so it is safe to call it. | ||
return new cls(...(dependencies.map((t) => (t === token ? getFromParent!() : this.get(t))) as any)); | ||
}); | ||
|
||
const factories = { ...this.factories, [token]: factory }; | ||
return new Container(factories as unknown as MaybeMemoizedFactories<AddService<Services, Token, Service>>); | ||
} | ||
) => this.providesService(ClassInjectable(token, cls)); | ||
|
||
/** | ||
* Registers a static value as a service in the container. This method is ideal for services that do not | ||
|
@@ -452,14 +433,8 @@ export class Container<Services = {}> { | |
* @returns A new Container instance that includes the provided service, allowing for chaining additional | ||
* `provides` calls. | ||
*/ | ||
providesValue<Token extends TokenType, Service>( | ||
token: Token, | ||
value: Service | ||
): Container<AddService<Services, Token, Service>> { | ||
const factory = memoize(this, () => value); | ||
const factories = { ...this.factories, [token]: factory }; | ||
return new Container(factories as unknown as MaybeMemoizedFactories<AddService<Services, Token, Service>>); | ||
} | ||
providesValue = <Token extends TokenType, Service>(token: Token, value: Service) => | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. re-using providesService |
||
this.providesService(Injectable(token, [], () => value)); | ||
|
||
/** | ||
* Appends a value to the array associated with a specified token in the current Container, then returns | ||
|
@@ -477,14 +452,10 @@ export class Container<Services = {}> { | |
* @param value - A value to append to the array. | ||
* @returns The updated Container with the appended value in the specified array. | ||
*/ | ||
appendValue<Token extends keyof Services, Service extends ArrayElement<Services[Token]>>( | ||
appendValue = <Token extends keyof Services, Service extends ArrayElement<Services[Token]>>( | ||
token: Token, | ||
value: Service | ||
): Service extends any ? Container<Services> : never; | ||
|
||
appendValue<Token extends TokenType, Service>(token: Token, value: Service): Container<any> { | ||
return this.providesService(ConcatInjectable(token, () => value)); | ||
} | ||
) => this.providesService(ConcatInjectable(token, () => value)) as Container<Services>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. re-using providesService |
||
|
||
/** | ||
* Appends an injectable class factory to the array associated with a specified token in the current Container, | ||
|
@@ -501,18 +472,17 @@ export class Container<Services = {}> { | |
* @param cls - A class with a constructor that takes dependencies as arguments, which returns the Service. | ||
* @returns The updated Container with the new service instance appended to the specified array. | ||
*/ | ||
appendClass< | ||
appendClass = < | ||
Token extends keyof Services, | ||
Tokens extends readonly ValidTokens<Services>[], | ||
Service extends ArrayElement<Services[Token]>, | ||
>(token: Token, cls: InjectableClass<Services, Service, Tokens>): Service extends any ? Container<Services> : never; | ||
|
||
appendClass<Token extends TokenType, Tokens extends readonly ValidTokens<Services>[], Service>( | ||
>( | ||
token: Token, | ||
cls: InjectableClass<Services, Service, Tokens> | ||
): Container<any> { | ||
return this.providesService(ConcatInjectable(token, () => this.providesClass(token, cls).get(token))); | ||
} | ||
) => | ||
this.providesService( | ||
ConcatInjectable(token, () => this.providesClass(token, cls).get(token)) | ||
) as Container<Services>; | ||
|
||
/** | ||
* Appends a new service instance to an existing array within the container using an `InjectableFunction`. | ||
|
@@ -531,17 +501,16 @@ export class Container<Services = {}> { | |
* @returns The updated Container, now including the new service instance appended to the array | ||
* specified by the token. | ||
*/ | ||
append< | ||
append = < | ||
Token extends keyof Services, | ||
Tokens extends readonly ValidTokens<Services>[], | ||
Service extends ArrayElement<Services[Token]>, | ||
>(fn: InjectableFunction<Services, Tokens, Token, Service>): Service extends any ? Container<Services> : never; | ||
|
||
append<Token extends TokenType, Tokens extends readonly ValidTokens<Services>[], Service>( | ||
>( | ||
fn: InjectableFunction<Services, Tokens, Token, Service> | ||
): Container<any> { | ||
return this.providesService(ConcatInjectable(fn.token, () => this.providesService(fn).get(fn.token))); | ||
} | ||
) => | ||
this.providesService( | ||
ConcatInjectable(fn.token, () => this.providesService(fn).get(fn.token)) | ||
) as Container<Services>; | ||
|
||
private providesService<Token extends TokenType, Tokens extends readonly ValidTokens<Services>[], Service>( | ||
fn: InjectableFunction<Services, Tokens, Token, Service> | ||
|
@@ -559,6 +528,6 @@ export class Container<Services = {}> { | |
// MaybeMemoizedFactories object with the expected set of services – but when using the spread operation to | ||
// merge two objects, the compiler widens the Token type to string. So we must re-narrow via casting. | ||
const factories = { ...this.factories, [token]: factory }; | ||
return new Container(factories as unknown as MaybeMemoizedFactories<AddService<Services, Token, Service>>); | ||
return new Container(factories) as Container<AddService<Services, Token, Service>>; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import type { InjectableFunction, ServicesFromTokenizedParams, TokenType } from "./types"; | ||
import type { InjectableClass, InjectableFunction, ServicesFromTokenizedParams, TokenType } from "./types"; | ||
|
||
/** | ||
* Creates an Injectable factory function designed for services without dependencies. | ||
|
@@ -100,6 +100,27 @@ export function Injectable( | |
return factory; | ||
} | ||
|
||
/** | ||
* Creates an Injectable factory function for an InjectableClass. | ||
* | ||
* @param token Token identifying the Service. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's provide an example how this can be used similarly how we do that for other exported stuff here |
||
* @param cls InjectableClass to instantiate. | ||
*/ | ||
export function ClassInjectable<Services, Token extends TokenType, const Tokens extends readonly TokenType[], Service>( | ||
token: Token, | ||
cls: InjectableClass<Services, Service, Tokens> | ||
): InjectableFunction<Services, Tokens, Token, Service>; | ||
|
||
export function ClassInjectable( | ||
token: TokenType, | ||
cls: InjectableClass<any, any, readonly TokenType[]> | ||
): InjectableFunction<any, readonly TokenType[], TokenType, any> { | ||
const factory = (...args: any[]) => new cls(...args); | ||
factory.token = token; | ||
factory.dependencies = cls.dependencies; | ||
return factory; | ||
} | ||
|
||
/** | ||
* Creates an Injectable factory function without dependencies that appends a Service | ||
* to an existing array of Services of the same type. Useful for dynamically expanding | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -152,6 +152,43 @@ describe("PartialContainer", () => { | |
}); | ||
}); | ||
|
||
describe("provide service using provideValue", () => { | ||
const dependenciesContainer = Container.provides(Injectable("TestService", () => "old service")); | ||
|
||
describe("and the new Service does not override", () => { | ||
const partialContainer = new PartialContainer({}).providesValue("NewTestService", "new service"); | ||
expect(dependenciesContainer.provides(partialContainer).get("NewTestService")).toEqual("new service"); | ||
}); | ||
|
||
describe("and the new Service does override", () => { | ||
const partialContainer = new PartialContainer({}).providesValue("TestService", "new service"); | ||
expect(dependenciesContainer.provides(partialContainer).get("TestService")).toEqual("new service"); | ||
}); | ||
}); | ||
|
||
describe("provide service using provideClass", () => { | ||
const dependenciesContainer = Container.provides(Injectable("TestService", () => "old service")); | ||
|
||
class NewTestService { | ||
static dependencies = ["TestService"] as const; | ||
constructor(public testService: string) {} | ||
} | ||
|
||
describe("and the new Service does not override", () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please replace |
||
const partialContainer = new PartialContainer({}).providesClass("NewTestService", NewTestService); | ||
expect(dependenciesContainer.provides(partialContainer).get("NewTestService")).toBeInstanceOf(NewTestService); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need an expectation that a dependency is missing, something line: const partialContainer = new PartialContainer({}).providesClass("NewTestService", NewTestService);
// @ts-expect-error
expect(() => Container.provides(partialContainer).get("NewTestService")).toThrow(
'[Container::get] Could not find Service for Token "TestService". ' +
"This should've caused a compile-time error. If the Token is 'undefined', " +
"check all your calls to the Injectable function. Make sure you define dependencies " +
"using string literals or string constants that are definitely initialized before the call to Injectable."
); Would be nice to have that for other |
||
}); | ||
|
||
describe("and the new Service does override", () => { | ||
const partialContainer = new PartialContainer({}) | ||
.providesValue("TestService", "old service") | ||
.providesClass("TestService", NewTestService); | ||
let testService = dependenciesContainer.provides(partialContainer).get("TestService"); | ||
expect(testService).toBeInstanceOf(NewTestService); | ||
expect(testService.testService).toEqual("old service"); | ||
}); | ||
}); | ||
|
||
describe("provided by an existing Container", () => { | ||
const dependenciesContainer = Container.provides(Injectable("TestService", () => "old service")); | ||
let combinedContainer: Container<{ TestService: string }>; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate comment