Skip to content

Commit

Permalink
feat: resolving props - an object with tokens.
Browse files Browse the repository at this point in the history
  • Loading branch information
mnasyrov committed Mar 28, 2021
1 parent 375959b commit 27e58cd
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 60 deletions.
24 changes: 12 additions & 12 deletions packages/ditox/src/index.js.flow
Expand Up @@ -136,6 +136,18 @@ type ValuesTuple =
| [*, *, *, *, *, *]
| [*, *, *, *, *, *, *];

/**
* Rebinds the array by the token with added new value.
* @param container - Dependency container.
* @param token - Token for an array of values.
* @param value - New value which is added to the end of the array.
*/
declare export function bindMultiValue<T>(
container: Container,
token: Token<Array<T>>,
value: T,
): void;

/**
* Returns an array of resolved values by the specified token.
* If a token is not found, then `undefined` value is used.
Expand Down Expand Up @@ -174,15 +186,3 @@ declare export function injectable<
factory: (...params: Values) => Result,
...tokens: Tokens
): (container: Container) => Result;

/**
* Rebinds the array by the token with added new value.
* @param container - Dependency container.
* @param token - Token for an array of values.
* @param value - New value which is added to the end of the array.
*/
declare export function bindMultiValue<T>(
container: Container,
token: Token<Array<T>>,
value: T,
): void;
171 changes: 145 additions & 26 deletions packages/ditox/src/utils.test.ts
@@ -1,9 +1,58 @@
import {createContainer, optional, ResolverError, token} from './ditox';
import {bindMultiValue, getValues, injectable, resolveValues} from './utils';
import {
bindMultiValue,
getProps,
getValues,
injectable,
injectableProps,
resolveProps,
resolveValues,
} from './utils';

const NUMBER = token<number>('number');
const STRING = token<string>('string');

describe('bindMultiValue', () => {
const NUMBERS = token<Array<number>>('numbers');

it('should append a value to an array declared by a token', () => {
const container = createContainer();

bindMultiValue(container, NUMBERS, 1);
bindMultiValue(container, NUMBERS, 2);

const values: Array<number> = container.resolve(NUMBERS);
expect(values).toEqual([1, 2]);
});

it('should append new values to an array declared by a token', () => {
const container = createContainer();

bindMultiValue(container, NUMBERS, 1);
bindMultiValue(container, NUMBERS, 2);
expect(container.resolve(NUMBERS)).toEqual([1, 2]);

bindMultiValue(container, NUMBERS, 3);
bindMultiValue(container, NUMBERS, 4);
expect(container.resolve(NUMBERS)).toEqual([1, 2, 3, 4]);
});

it('should add new values to a copy of array from the parent container', () => {
const parent = createContainer();
parent.bindValue(NUMBERS, [1, 2]);
const container = createContainer(parent);

bindMultiValue(container, NUMBERS, 3);
bindMultiValue(container, NUMBERS, 4);
expect(parent.resolve(NUMBERS)).toEqual([1, 2]);
expect(container.resolve(NUMBERS)).toEqual([1, 2, 3, 4]);

container.remove(NUMBERS);
expect(parent.resolve(NUMBERS)).toEqual([1, 2]);
expect(container.resolve(NUMBERS)).toEqual([1, 2]);
});
});

describe('getValues', () => {
it('should return values for the tokens', () => {
const container = createContainer();
Expand Down Expand Up @@ -107,43 +156,113 @@ describe('injectable()', () => {
});
});

describe('bindMultiValue', () => {
const NUMBERS = token<Array<number>>('numbers');
describe('getProps', () => {
it('should return an object with values by the tokens', () => {
const container = createContainer();
container.bindValue(NUMBER, 1);
container.bindValue(STRING, 'abc');

it('should append a value to an array declared by a token', () => {
const props: {a: number; b: string} = getProps(container, {
a: NUMBER,
b: STRING,
});
expect(props).toEqual({a: 1, b: 'abc'});
});

it('should return "undefined" item in case a value is not provided', () => {
const container = createContainer();
container.bindValue(NUMBER, 1);

bindMultiValue(container, NUMBERS, 1);
bindMultiValue(container, NUMBERS, 2);
const props = getProps(container, {a: NUMBER, b: STRING});
expect(props).toEqual({a: 1, b: undefined});
});

const values: Array<number> = container.resolve(NUMBERS);
expect(values).toEqual([1, 2]);
it('should return values of optional tokens in case they are not provided', () => {
const container = createContainer();
container.bindValue(NUMBER, 1);

const OPTIONAL_STRING = optional(STRING, 'value');

const props = getProps(container, {
a: NUMBER,
b: STRING,
c: OPTIONAL_STRING,
});
expect(props).toEqual({a: 1, b: undefined, c: 'value'});
});
});

it('should append new values to an array declared by a token', () => {
describe('resolveProps', () => {
it('should an object with resolved values from the container for the specified token props', () => {
const container = createContainer();
container.bindValue(NUMBER, 1);
container.bindValue(STRING, 'abc');

bindMultiValue(container, NUMBERS, 1);
bindMultiValue(container, NUMBERS, 2);
expect(container.resolve(NUMBERS)).toEqual([1, 2]);
const props: {a: number; b: string} = resolveProps(container, {
a: NUMBER,
b: STRING,
});
expect(props).toEqual({a: 1, b: 'abc'});
});

bindMultiValue(container, NUMBERS, 3);
bindMultiValue(container, NUMBERS, 4);
expect(container.resolve(NUMBERS)).toEqual([1, 2, 3, 4]);
it('should throw an error in case a value is not provided', () => {
const container = createContainer();
container.bindValue(NUMBER, 1);

expect(() => {
resolveProps(container, {a: NUMBER, b: STRING});
}).toThrowError(
new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
);
});

it('should add new values to a copy of array from the parent container', () => {
const parent = createContainer();
parent.bindValue(NUMBERS, [1, 2]);
const container = createContainer(parent);
it('should resolve values of optional tokens in case they are not provided', () => {
const container = createContainer();
container.bindValue(NUMBER, 1);

bindMultiValue(container, NUMBERS, 3);
bindMultiValue(container, NUMBERS, 4);
expect(parent.resolve(NUMBERS)).toEqual([1, 2]);
expect(container.resolve(NUMBERS)).toEqual([1, 2, 3, 4]);
const props = resolveProps(container, {
a: NUMBER,
b: optional(STRING, 'value'),
});
expect(props).toEqual({a: 1, b: 'value'});
});
});

container.remove(NUMBERS);
expect(parent.resolve(NUMBERS)).toEqual([1, 2]);
expect(container.resolve(NUMBERS)).toEqual([1, 2]);
describe('injectableProps()', () => {
function join({a, b}: {a: number; b: string}): string {
return `${a}/${b}`;
}

it('should inject values from Container as an object for the first argument of the factory', () => {
const container = createContainer();
container.bindValue(NUMBER, 1);
container.bindValue(STRING, '2');

const decoratedFactory = injectableProps(join, {a: NUMBER, b: STRING});

expect(decoratedFactory(container)).toBe('1/2');
});

it('should throw ResolverError error in case a value is not provided', () => {
const container = createContainer();
container.bindValue(NUMBER, 1);

const decoratedFactory = injectableProps(join, {a: NUMBER, b: STRING});

expect(() => decoratedFactory(container)).toThrowError(
new ResolverError(`Token "${STRING.symbol.description}" is not provided`),
);
});

it('should inject values of optional tokens in case values are not provided', () => {
const container = createContainer();
container.bindValue(NUMBER, 1);

const decoratedFactory = injectableProps(join, {
a: NUMBER,
b: optional(STRING, 'value'),
});

expect(decoratedFactory(container)).toBe('1/value');
});
});
116 changes: 94 additions & 22 deletions packages/ditox/src/utils.ts
@@ -1,27 +1,5 @@
import {Container, Token} from './ditox';

/**
* Decorates a factory by passing resolved tokens as factory arguments.
* @param factory - A factory.
* @param tokens - Tokens which correspond to factory arguments.
* @return Decorated factory which takes a dependency container as a single argument.
*/
export function injectable<
Tokens extends Token<unknown>[],
Values extends {
[K in keyof Tokens]: Tokens[K] extends Token<infer V> ? V : never;
},
Result
>(
factory: (...params: Values) => Result,
...tokens: Tokens
): (container: Container) => Result {
return (container) => {
const values: Values = tokens.map(container.resolve) as Values;
return factory(...values);
};
}

/**
* Rebinds the array by the token with added new value.
* @param container - Dependency container.
Expand Down Expand Up @@ -63,3 +41,97 @@ export function resolveValues<
>(container: Container, ...tokens: Tokens): Values {
return tokens.map(container.resolve) as Values;
}

/**
* Decorates a factory by passing resolved tokens as factory arguments.
* @param factory - A factory.
* @param tokens - Tokens which correspond to factory arguments.
* @return Decorated factory which takes a dependency container as a single argument.
*/
export function injectable<
Tokens extends Token<unknown>[],
Values extends {
[K in keyof Tokens]: Tokens[K] extends Token<infer V> ? V : never;
},
Result
>(
this: unknown,
factory: (...params: Values) => Result,
...tokens: Tokens
): (container: Container) => Result {
return (container) => {
const values: Values = resolveValues(container, ...tokens);
return factory.apply(this, values);
};
}

/**
* Returns an object with resolved properties which are specified by token properties.
* If a token is not found, then `undefined` value is used.
*
* @example
* ```ts
* const props = getProps(container, {a: tokenA, b: tokenB});
* console.log(props); // {a: 1, b: 2}
* ```
*/
export function getProps<
TokenObject extends {[key: string]: Token<unknown>},
ValueObject extends {
[K in keyof TokenObject]: TokenObject[K] extends Token<infer V> ? V : never;
}
>(container: Container, tokens: TokenObject): ValueObject {
const obj: any = {...tokens};
Object.keys(obj).forEach((key) => (obj[key] = container.get(obj[key])));
return obj;
}

/**
* Returns an object with resolved properties which are specified by token properties.
* If a token is not found, then `ResolverError` is thrown.
*
* @example
* ```ts
* const props = resolveProps(container, {a: tokenA, b: tokenB});
* console.log(props); // {a: 1, b: 2}
* ```
*/
export function resolveProps<
TokenObject extends {[key: string]: Token<unknown>},
ValueObject extends {
[K in keyof TokenObject]: TokenObject[K] extends Token<infer V> ? V : never;
}
>(container: Container, tokens: TokenObject): ValueObject {
const obj: any = {...tokens};
Object.keys(obj).forEach((key) => (obj[key] = container.resolve(obj[key])));
return obj;
}

/**
* Decorates a factory by passing a resolved object with tokens as the first argument.
* @param factory - A factory.
* @param tokens - Object with tokens.
* @return Decorated factory which takes a dependency container as a single argument.
*
* @example
* ```ts
* const factory = ({a, b}: {a: number, b: number}) => a + b;
* const decoratedFactory = injectableProps(factory, {a: tokenA, b: tokenB});
* const result = decoratedFactory(container);
* ```
*/
export function injectableProps<
TokenObject extends {[key: string]: Token<unknown>},
ValueObject extends {
[K in keyof TokenObject]: TokenObject[K] extends Token<infer V> ? V : never;
},
Result
>(
factory: (props: ValueObject) => Result,
tokens: TokenObject,
): (container: Container) => Result {
return (container) => {
const values: ValueObject = resolveProps(container, tokens);
return factory(values);
};
}

0 comments on commit 27e58cd

Please sign in to comment.