Skip to content
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

feat(core): add injection token factories #2687

Merged
merged 6 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading