From 279ffc465b9a590940161b1369c33bb4f1a804d3 Mon Sep 17 00:00:00 2001 From: Mikhail Nasyrov Date: Thu, 7 Sep 2023 23:19:24 +0300 Subject: [PATCH] feat: Ability to create a shareable token by providing a key for its symbol --- .../src/{ditox.test.ts => container.test.ts} | 18 +++- packages/ditox/src/{ditox.ts => container.ts} | 56 +----------- packages/ditox/src/index.ts | 14 ++- packages/ditox/src/modules.test.ts | 3 +- packages/ditox/src/modules.ts | 3 +- packages/ditox/src/tokens.test.ts | 54 ++++++++++++ packages/ditox/src/tokens.ts | 88 +++++++++++++++++++ packages/ditox/src/utils.test.ts | 3 +- packages/ditox/src/utils.ts | 3 +- 9 files changed, 171 insertions(+), 71 deletions(-) rename packages/ditox/src/{ditox.test.ts => container.test.ts} (97%) rename packages/ditox/src/{ditox.ts => container.ts} (86%) create mode 100644 packages/ditox/src/tokens.test.ts create mode 100644 packages/ditox/src/tokens.ts diff --git a/packages/ditox/src/ditox.test.ts b/packages/ditox/src/container.test.ts similarity index 97% rename from packages/ditox/src/ditox.test.ts rename to packages/ditox/src/container.test.ts index a6db503..80a9cca 100644 --- a/packages/ditox/src/ditox.test.ts +++ b/packages/ditox/src/container.test.ts @@ -3,12 +3,11 @@ import { createContainer, FACTORIES_MAP, FAKE_FACTORY, - optional, PARENT_CONTAINER, ResolverError, - token, -} from './ditox'; +} from './container'; import {injectable} from './utils'; +import {optional, token} from './tokens'; const NUMBER = token('number'); const STRING = token('string'); @@ -595,5 +594,18 @@ describe('Container', () => { expect(parent.resolve(optionalNumber)).toBe(1); expect(container.resolve(optionalNumber)).toBe(1); }); + + it('should resolve a value by shared tokens', () => { + const key = 'token-' + Date.now(); + const t1 = token({key}); + const t2 = token({key}); + expect(t1).not.toBe(t2); + + const container = createContainer(); + container.bindValue(t1, 1); + + expect(container.resolve(t1)).toBe(1); + expect(container.resolve(t2)).toBe(1); + }); }); }); diff --git a/packages/ditox/src/ditox.ts b/packages/ditox/src/container.ts similarity index 86% rename from packages/ditox/src/ditox.ts rename to packages/ditox/src/container.ts index 221d73d..a888878 100644 --- a/packages/ditox/src/ditox.ts +++ b/packages/ditox/src/container.ts @@ -1,58 +1,4 @@ -/** - * @ignore - * Binding token for mandatory value - */ -export type RequiredToken = { - symbol: symbol; - type?: T; // Anchor for Typescript type inference. - isOptional?: false; -}; - -/** - * @ignore - * Binding token for optional value - */ -export type OptionalToken = { - symbol: symbol; - type?: T; // Anchor for Typescript type inference. - isOptional: true; - optionalValue: T; -}; - -/** - * Binding token. - */ -export type Token = RequiredToken | OptionalToken; - -/** - * Creates a new binding token. - * @param description - Token description for better error messages. - */ -export function token(description?: string): Token { - return {symbol: Symbol(description)}; -} - -/** - * Decorate a token with an optional value. - * This value is be used as default value in case a container does not have registered token. - * @param token - Existed token. - * @param optionalValue - Default value for the resolver. - */ -export function optional( - token: Token, - optionalValue: T, -): OptionalToken; -export function optional(token: Token): OptionalToken; -export function optional( - token: Token, - optionalValue?: T, -): OptionalToken { - return { - symbol: token.symbol, - isOptional: true, - optionalValue, - }; -} +import {Token, token} from './tokens'; /** * ResolverError is thrown by the resolver when a token is not found in a container. diff --git a/packages/ditox/src/index.ts b/packages/ditox/src/index.ts index 00ef18e..e1a99e8 100644 --- a/packages/ditox/src/index.ts +++ b/packages/ditox/src/index.ts @@ -1,12 +1,8 @@ -export {token, optional, ResolverError, createContainer} from './ditox'; -export type { - RequiredToken, - OptionalToken, - Token, - FactoryScope, - FactoryOptions, - Container, -} from './ditox'; +export {token, optional} from './tokens'; +export type {RequiredToken, OptionalToken, Token} from './tokens'; + +export {ResolverError, createContainer} from './container'; +export type {FactoryScope, FactoryOptions, Container} from './container'; export { isToken, diff --git a/packages/ditox/src/modules.test.ts b/packages/ditox/src/modules.test.ts index 8df2b4d..eacfd17 100644 --- a/packages/ditox/src/modules.test.ts +++ b/packages/ditox/src/modules.test.ts @@ -1,4 +1,4 @@ -import {createContainer, token} from './ditox'; +import {createContainer} from './container'; import { bindModule, bindModules, @@ -8,6 +8,7 @@ import { ModuleDeclaration, } from './modules'; import {injectable} from './utils'; +import {token} from './tokens'; describe('bindModule()', () => { type TestQueries = {getValue: () => number}; diff --git a/packages/ditox/src/modules.ts b/packages/ditox/src/modules.ts index 1267e47..517179b 100644 --- a/packages/ditox/src/modules.ts +++ b/packages/ditox/src/modules.ts @@ -1,5 +1,6 @@ -import {Container, token, Token} from './ditox'; +import {Container} from './container'; import {injectable} from './utils'; +import {Token, token} from './tokens'; type AnyObject = Record; type EmptyObject = Record; diff --git a/packages/ditox/src/tokens.test.ts b/packages/ditox/src/tokens.test.ts new file mode 100644 index 0000000..f0e5606 --- /dev/null +++ b/packages/ditox/src/tokens.test.ts @@ -0,0 +1,54 @@ +import {optional, token} from './tokens'; + +describe('token()', () => { + it('should return a token with description it is specified', () => { + expect(token().symbol.description).toBeUndefined(); + expect(token('text1').symbol.description).toBe('text1'); + expect(token({description: 'text2'}).symbol.description).toBe('text2'); + }); + + it('should return independent tokens if key is not specified', () => { + const t1 = token(); + const t2 = token(); + + expect(t1).not.toBe(t2); + expect(t1.symbol).not.toBe(t2.symbol); + expect(t1.isOptional).not.toBeTruthy(); + }); + + it('should return tokens with the same symbol if key is specified', () => { + const source = token({key: 'test-token'}); + const clone = token({key: 'test-token'}); + const something = token({key: 'something-else'}); + + expect(source).not.toBe(clone); + expect(source.symbol).toBe(clone.symbol); + + expect(something).not.toBe(source); + expect(something.symbol).not.toBe(source); + }); +}); + +describe('optional()', () => { + it('should decorate a source token to attach an optional value', () => { + const t1 = token(); + expect(t1.isOptional).not.toBeTruthy(); + + const o1 = optional(t1); + expect(o1.symbol).toBe(t1.symbol); + expect(o1.isOptional).toBe(true); + + const o2 = optional(t1, -1); + expect(o2.symbol).toBe(t1.symbol); + expect(o2.isOptional).toBe(true); + expect(o2.optionalValue).toBe(-1); + }); + + it('should reuse symbol from a source token ifits key is specified', () => { + const source = token({key: 'token-key'}); + const clone = token({key: 'token-key'}); + const optClone = optional(clone); + + expect(optClone.symbol).toBe(source.symbol); + }); +}); diff --git a/packages/ditox/src/tokens.ts b/packages/ditox/src/tokens.ts new file mode 100644 index 0000000..41215ca --- /dev/null +++ b/packages/ditox/src/tokens.ts @@ -0,0 +1,88 @@ +/** + * @ignore + * Binding token for mandatory value + */ +export type RequiredToken = { + symbol: symbol; + type?: T; // Anchor for Typescript type inference. + isOptional?: false; +}; + +/** + * @ignore + * Binding token for optional value + */ +export type OptionalToken = { + symbol: symbol; + type?: T; // Anchor for Typescript type inference. + isOptional: true; + optionalValue: T; +}; + +/** + * Binding token + */ +export type Token = RequiredToken | OptionalToken; + +/** + * Token options + */ +export type TokenOptions = + | { + /** + * Key for token's symbol. It allows to create shareable tokens. + */ + key: string; + + /** @ignore */ + description?: undefined; + } + | { + /** Description for better error messages */ + description?: string; + + /** @ignore */ + key?: undefined; + }; + +/** + * Creates a new binding token. + * @param description - Token description for better error messages. + */ +export function token(description?: string): Token; +/** + * Creates a new binding token. + * @param options - Token description for better error messages. + */ export function token(options?: TokenOptions): Token; +export function token(options?: TokenOptions | string): Token { + const normalized: TokenOptions | undefined = + typeof options === 'string' ? {description: options} : options; + + const symbol: symbol = normalized?.key + ? Symbol.for(normalized.key) + : Symbol(normalized?.description); + + return {symbol}; +} + +/** + * Decorate a token with an optional value. + * This value is be used as default value in case a container does not have registered token. + * @param token - Existed token. + * @param optionalValue - Default value for the resolver. + */ +export function optional( + token: Token, + optionalValue: T, +): OptionalToken; +export function optional(token: Token): OptionalToken; +export function optional( + token: Token, + optionalValue?: T, +): OptionalToken { + return { + symbol: token.symbol, + isOptional: true, + optionalValue, + }; +} diff --git a/packages/ditox/src/utils.test.ts b/packages/ditox/src/utils.test.ts index 6238758..85e64e6 100644 --- a/packages/ditox/src/utils.test.ts +++ b/packages/ditox/src/utils.test.ts @@ -1,4 +1,4 @@ -import {createContainer, optional, ResolverError, token} from './ditox'; +import {createContainer, ResolverError} from './container'; import { bindMultiValue, injectable, @@ -8,6 +8,7 @@ import { tryResolveValue, tryResolveValues, } from './utils'; +import {optional, token} from './tokens'; const NUMBER = token('number'); const STRING = token('string'); diff --git a/packages/ditox/src/utils.ts b/packages/ditox/src/utils.ts index 88fae97..d57cb6e 100644 --- a/packages/ditox/src/utils.ts +++ b/packages/ditox/src/utils.ts @@ -1,4 +1,5 @@ -import type {Container, Token} from './ditox'; +import type {Container} from './container'; +import {Token} from './tokens'; type ValuesProps = {[key: string]: unknown}; type TokenProps = {