Skip to content

Commit

Permalink
feat(core): add injection token factories (#2687)
Browse files Browse the repository at this point in the history
  • Loading branch information
griest024 committed Feb 23, 2024
1 parent fe28294 commit 8531484
Show file tree
Hide file tree
Showing 15 changed files with 472 additions and 0 deletions.
41 changes: 41 additions & 0 deletions libs/core/src/injection-tokens/config.factory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { faker } from '@faker-js/faker/locale/en_US';

import { DaffConfigInjectionToken } from '@daffodil/core';

import { createConfigInjectionToken } from './config.factory';

interface Config {
field: string;
other: string;
}

describe('@daffodil/core | createConfigInjectionToken', () => {
let name: string;
let value: number;
let defaultConfig: Config;

let result: DaffConfigInjectionToken<Config>;

beforeEach(() => {
name = faker.random.word();
defaultConfig = {
field: faker.random.word(),
other: faker.random.word(),
};
result = createConfigInjectionToken(defaultConfig, name);
});

it('should return a token', () => {
expect(result.token.toString()).toContain(name);
});

it('should return a provider that spreads in passed values with the default', () => {
const val = faker.random.word();
const res = result.provider({
field: val,
});
expect(res.provide).toEqual(result.token);
expect(res.useValue.field).toEqual(val);
expect(res.useValue.other).toEqual(defaultConfig.other);
});
});
38 changes: 38 additions & 0 deletions libs/core/src/injection-tokens/config.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { InjectionToken } from '@angular/core';

import { DaffConfigInjectionToken } from './config.type';
import {
TokenDesc,
TokenOptions,
} from './token-constuctor-params.type';

/**
* Creates an injection token/provider pair for a DI token that holds a configuration.
*
* See {@link DaffConfigInjectionToken}.
*/
export const createConfigInjectionToken = <T = unknown>(
defaultConfig: T,
desc: TokenDesc<T>,
options?: Partial<TokenOptions<T>>,
): DaffConfigInjectionToken<T> => {
const token = new InjectionToken<T>(
desc,
{
factory: () => defaultConfig,
...options,
},
);
const provider = <R extends T = T>(config: Partial<R>) => ({
provide: token,
useValue: {
...defaultConfig,
...config,
},
});

return {
token,
provider,
};
};
22 changes: 22 additions & 0 deletions libs/core/src/injection-tokens/config.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
InjectionToken,
ValueProvider,
} from '@angular/core';

/**
* A injection token to hold and provide a config value.
*/
export interface DaffConfigInjectionToken<T = unknown> {
/**
* The injection token.
* Its default value is the default config passed during token creation.
*/
token: InjectionToken<T>;

/**
* A helper function to provide a value to the token.
* It will shallow merge the passed config with the default config
* with the passed config keys taking precedence.
*/
provider: <R extends T = T>(config: Partial<R>) => ValueProvider;
}
71 changes: 71 additions & 0 deletions libs/core/src/injection-tokens/multi.factory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { TestBed } from '@angular/core/testing';
import { faker } from '@faker-js/faker/locale/en_US';

import { DaffMultiInjectionToken } from '@daffodil/core';

import { createMultiInjectionToken } from './multi.factory';

describe('@daffodil/core | createMultiInjectionToken', () => {
let name: string;
let values: Array<number>;

let result: DaffMultiInjectionToken<number>;

beforeEach(() => {
name = faker.random.word();
values = [
faker.datatype.number(),
faker.datatype.number(),
];
result = createMultiInjectionToken(name);
});

it('should return a token', () => {
expect(result.token.toString()).toContain(name);
});

it('should return a provider', () => {
const res = result.provider(...values);
values.forEach((value, i) => {
expect(res[i].provide).toEqual(result.token);
expect(res[i].useValue).toEqual(value);
});
});
});

describe('@daffodil/core | createMultiInjectionToken | Integration', () => {
let name: string;
let values: Array<number>;

let result: DaffMultiInjectionToken<number>;

beforeEach(() => {
name = faker.random.word();
values = [
faker.datatype.number(),
faker.datatype.number(),
];

result = createMultiInjectionToken(name);
});

describe('when values are provided', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
result.provider(...values),
],
});
});

it('should inject the values', () => {
expect(TestBed.inject(result.token)).toEqual(values);
});
});

describe('when values are not provided', () => {
it('should inject an empty array', () => {
expect(TestBed.inject(result.token)).toEqual([]);
});
});
});
39 changes: 39 additions & 0 deletions libs/core/src/injection-tokens/multi.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { InjectionToken } from '@angular/core';

import { DaffMultiInjectionToken } from './multi.type';
import {
TokenDesc,
TokenOptions,
} from './token-constuctor-params.type';

// having a single instance of the default factory
// will hopefully reduce memory footprint
const defaultFactory = () => [];

/**
* Creates an injection token/provider pair for a multi valued DI token.
*
* See {@link DaffMultiInjectionToken}.
*/
export const createMultiInjectionToken = <T = unknown>(
desc: TokenDesc<Array<T>>,
options?: Partial<TokenOptions<Array<T>>>,
): DaffMultiInjectionToken<T> => {
const token = new InjectionToken<Array<T>>(
desc,
{
factory: defaultFactory,
...options,
},
);
const provider = <R extends T = T>(...values: Array<R>) => values.map((value) => ({
provide: token,
useValue: value,
multi: true,
}));

return {
token,
provider,
};
};
20 changes: 20 additions & 0 deletions libs/core/src/injection-tokens/multi.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
InjectionToken,
ValueProvider,
} from '@angular/core';

/**
* A injection token to hold and provide multiple values.
*/
export interface DaffMultiInjectionToken<T = unknown> {
/**
* The injection token.
* Its default value is an empty array.
*/
token: InjectionToken<Array<T>>;

/**
* A helper function to provide values to the token.
*/
provider: <R extends T = T>(...values: Array<R>) => Array<ValueProvider>;
}
8 changes: 8 additions & 0 deletions libs/core/src/injection-tokens/public_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export * from './single.type';
export * from './single.factory';
export * from './multi.type';
export * from './multi.factory';
export * from './config.type';
export * from './config.factory';
export * from './services.type';
export * from './services.factory';
97 changes: 97 additions & 0 deletions libs/core/src/injection-tokens/services.factory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
Injectable,
Type,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { faker } from '@faker-js/faker/locale/en_US';

import { DaffServicesInjectionToken } from '@daffodil/core';

import { createServicesInjectionToken } from './services.factory';

interface TestType {
get(): string;
}

@Injectable({
providedIn: 'root',
})
class Test1 implements TestType {
get() {
return 'test1';
}
}

@Injectable({
providedIn: 'root',
})
class Test2 implements TestType {
get() {
return 'test2';
}
}

describe('@daffodil/core | createServicesInjectionToken', () => {
let name: string;
let values: Array<Type<TestType>>;

let result: DaffServicesInjectionToken<TestType>;

beforeEach(() => {
name = faker.random.word();
values = [
Test1,
Test2,
];
result = createServicesInjectionToken(name);
});

it('should return a token', () => {
expect(result.token.toString()).toContain(name);
});

it('should return a provider', () => {
const res = result.provider(...values);
values.forEach((value, i) => {
expect(res[i].provide).toEqual(result.token);
expect(res[i].useExisting).toEqual(value);
});
});
});

describe('@daffodil/core | createServicesInjectionToken | Integration', () => {
let name: string;
let values: Array<Type<TestType>>;

let result: DaffServicesInjectionToken<TestType>;

beforeEach(() => {
name = faker.random.word();
values = [
Test1,
Test2,
];
result = createServicesInjectionToken(name);
});

describe('when values are provided', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
result.provider(...values),
],
});
});

it('should inject the values', () => {
expect(TestBed.inject(result.token)[0].get()).toEqual('test1');
expect(TestBed.inject(result.token)[1].get()).toEqual('test2');
});
});

describe('when values are not provided', () => {
it('should inject an empty array', () => {
expect(TestBed.inject(result.token)).toEqual([]);
});
});
});
42 changes: 42 additions & 0 deletions libs/core/src/injection-tokens/services.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
InjectionToken,
Type,
} from '@angular/core';

import { DaffServicesInjectionToken } from './services.type';
import {
TokenDesc,
TokenOptions,
} from './token-constuctor-params.type';

// having a single instance of the default factory
// will hopefully reduce memory footprint
const defaultFactory = () => [];

/**
* Creates an injection token/provider pair for a DI token that holds services.
*
* See {@link DaffServicesInjectionToken}.
*/
export const createServicesInjectionToken = <T = unknown>(
desc: TokenDesc<Array<T>>,
options?: Partial<TokenOptions<Array<T>>>,
): DaffServicesInjectionToken<T> => {
const token = new InjectionToken<Array<T>>(
desc,
{
factory: defaultFactory,
...options,
},
);
const provider = <R extends T = T>(...classes: Array<Type<R>>) => classes.map((klass) => ({
provide: token,
useExisting: klass,
multi: true,
}));

return {
token,
provider,
};
};
Loading

0 comments on commit 8531484

Please sign in to comment.