Skip to content
This repository has been archived by the owner on Nov 27, 2023. It is now read-only.

Commit

Permalink
feat: allow scoped selected container configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
KnisterPeter committed Oct 7, 2020
1 parent ad3a7b5 commit 520d6be
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 23 deletions.
71 changes: 59 additions & 12 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,23 +394,70 @@ class RestApi {
> This does not work with dynamic injections and will throw an error. Please note that async injections can not be lazy and will not be lazy by default.
## Declarative configuration of dependencies
## Configured sets
A container instance could be configured from a configuration class. This configuration could be used to limit the container visible components.
When using [`enableComponentScanner()`](api-tsdi.md#enablecomponentscanner) all components of the whole project are added to the container instance. This is often not whats required or useful.
There are multiple ways around this and configured sets are one of them.
Another reason to give your container a bit more structure is, if you need to have multiple container with different sets of components to create a hierarchy. This isn't possible with [`externals`](externals.md#external-dependencies) otherwise. These are only working with `enableComponentScanner()` otherwise.
> Just like with `enableComponentScanner()`, externals with configured sets could only occur in one container at once. It does not even give a good error message when misconfigured.
```js
class SomeClass {
private property!: SomeDependency;
@component
class Dice {
public roll(): number {
return 1;
}
}

constructor(private parameter: SomeDependency) {}
class Player {
constructor(public dice: Dice) {
}
}

class SomeDependency {
class Game {
constructor(private player: Player) {}

public start(): void {
this.player.dice.roll();
}
}

const tsdi = new TSDI();
tsdi.configure(
SomeClass,
{
constructorDependencies: [SomeDependency],
propertyDependencies: [{ property: 'property', type: SomeDependency }]
});
@external
class UI {
@inject
private readonly game!: Game;

public render(): void {
// ...
}
}

class Config {
@configure
public dice!: Dice;

@configure
public ui!: UI;

@configure
public player(): Player {
return new Player(this.dice());
}

@configure
public game(): Game {
return new Game(this.player());
}
}

const tsdi = new TSDI(new Config());
const game = tsdi.get(Game);
game.start();

const ui = tsdi.get(UI);
ui.render();
```
1 change: 1 addition & 0 deletions lib/__integration__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// these imports run the test directly
// in the default window of cypress
import '../__tests__/configure-test';
import '../__tests__/index-test';
import '../__tests__/mock-test';

Expand Down
92 changes: 92 additions & 0 deletions lib/__tests__/configure-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// tslint:disable: no-implicit-dependencies
import { assert } from 'chai';
import { TSDI, configure, component, external, inject } from '..';

class Component1 {
//
}

class Component2 {
// tslint:disable-next-line: no-parameter-properties
constructor(public comp1: Component1, public tsdi: TSDI) {
//
}
}

@component
class Component3 {
@inject
public comp2!: Component2;
}

@external
class Component4 {
@inject
public comp3!: Component3;
}

class Container {
@configure
public comp3!: Component3;

@configure
public comp4!: Component4;

@configure
public comp1(): Component1 {
return new Component1();
}

@configure()
public comp2(tsdi: TSDI): Component2 {
return new Component2(this.comp1(), tsdi);
}
}

describe('TSDI', () => {
describe('with a configuration setup', () => {
let tsdi: TSDI;

beforeEach(() => {
tsdi = new TSDI(new Container());
});

it('should create requested components', () => {
const comp = tsdi.get(Component1);

assert.instanceOf(comp, Component1);
});

it('should inject dependencies', () => {
const comp1 = tsdi.get(Component1);
const comp2 = tsdi.get(Component2);

assert.strictEqual(comp1, comp2.comp1);
});

it('should support singletons', () => {
const a = tsdi.get(Component1);
const b = tsdi.get(Component1);

assert.strictEqual(a, b);
});

it('should inject itself', () => {
const comp = tsdi.get(Component2);

assert.strictEqual(comp.tsdi, tsdi);
});

it('should allow decorated components', () => {
const comp = tsdi.get(Component3);

assert.strictEqual(comp.comp2.comp1, tsdi.get(Component1));
});

it('should allow external components', () => {
const comp = new Component4();

assert.strictEqual(comp.comp3.comp2.comp1, tsdi.get(Component1));
});
});
});
65 changes: 65 additions & 0 deletions lib/configure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Constructable, TSDI } from '.';

export function Configure(target: Object, property: string | symbol): any;
export function Configure(): any;
export function Configure(...args: any[]): any {
const decorate = (
target: Record<string, Function>,
property: string | symbol
) => {
Reflect.defineMetadata('component:configured', true, target, property);

const orig = target[property.toString()];
const cacheKey = `__tsdi__${property.toString()}__`;

const methodReturnType = Reflect.getMetadata(
'design:returntype',
target,
property
);
const propertyType = Reflect.getMetadata('design:type', target, property);

function propertyCreator(this: {
__tsdi__: TSDI;
[cacheKey: string]: any;
}): any {
if (!methodReturnType) {
return this.__tsdi__.get(propertyType);
}

if (cacheKey in this) {
return this[cacheKey];
}

const values = Reflect.getMetadata(
'design:paramtypes',
target,
property
).map((param: Constructable<unknown>) => this.__tsdi__.get(param));

const value = orig.call(this, ...values);
Object.defineProperty(this, cacheKey, {
configurable: false,
enumerable: false,
writable: false,
value,
});
return value;
}

return {
configurable: true,
enumerable: true,
writable: true,
value: propertyCreator,
};
};
if (args.length > 0) {
return decorate(args[0], args[1]);
}
return (target: Object, property: string | symbol) => {
return decorate(target as any, property) as any;
};
}

export { Configure as configure };
3 changes: 3 additions & 0 deletions lib/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export function External<TFunction extends Function>(
): any {
return (target as any).__tsdi__.configureExternal(args, target);
};
Object.defineProperty(constructor, '__tsdi__external__', {
value: target,
});
(constructor as any).displayName = target.name;
Object.getOwnPropertyNames(target)
.filter(
Expand Down
18 changes: 11 additions & 7 deletions lib/global-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,34 @@ export function addKnownComponent(metadata: ComponentOrFactoryMetadata): void {
metadata.options.name &&
findIndexOf(
knownComponents,
meta => meta.options.name === metadata.options.name
(meta) => meta.options.name === metadata.options.name
) > -1
) {
throw new Error(
`Duplicate name '${metadata.options.name}' for known Components.`
);
}
knownComponents.push(metadata);
listeners.forEach(listener => listener(metadata));
listeners.forEach((listener) => listener(metadata));
}

export function addKnownExternal(external: Function): void {
if (findIndexOf(knownExternals, fn => fn === external) === -1) {
if (!isKnownExternal(external)) {
knownExternals.push(external);
listeners.forEach(listener => listener(external));
listeners.forEach((listener) => listener(external));
}
}

export function isKnownExternal(external: Function): boolean {
return findIndexOf(knownExternals, (fn) => fn === external) !== -1;
}

export function addListener(listener: ComponentListener): void {
listeners.push(listener);
knownComponents.forEach(metadata => listener(metadata));
knownExternals.forEach(external => listener(external));
knownComponents.forEach((metadata) => listener(metadata));
knownExternals.forEach((external) => listener(external));
}

export function removeListener(listener: ComponentListener): void {
listeners = removeElement(listeners, l => l === listener);
listeners = removeElement(listeners, (l) => l === listener);
}
61 changes: 58 additions & 3 deletions lib/tsdi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,67 @@ export class TSDI {

private readonly scopes: { [name: string]: boolean } = {};

constructor() {
constructor(configuration?: Object) {
this.registerComponent({
fn: TSDI,
options: {},
});
this.instances[0] = this;

if (configuration) {
this.registerComponent({
fn: configuration.constructor as Constructable<unknown>,
options: {},
});
this.instances[1] = configuration;
Object.defineProperty(configuration, '__tsdi__', {
configurable: false,
enumerable: false,
writable: false,
value: this,
});

Object.getOwnPropertyNames(configuration.constructor.prototype)
.filter((name) =>
Reflect.getMetadata(
'component:configured',
configuration.constructor.prototype,
name
)
)
.forEach((property) => {
const rtti = Reflect.getMetadata(
'design:returntype',
configuration,
property
);
if (rtti) {
// method
this.registerComponent({
target: configuration,
property,
options: {},
rtti,
});
} else {
const fn = Reflect.getMetadata(
'design:type',
configuration,
property
);
if (fn.__tsdi__external__) {
// external component
fn.__tsdi__external__.__tsdi__ = this;
} else {
// property component
this.registerComponent({
fn,
options: {},
});
}
}
});
}
}

public addLifecycleListener(lifecycleListener: LifecycleListener): void {
Expand Down Expand Up @@ -296,8 +351,7 @@ export class TSDI {
): boolean {
return (
typeof component !== 'undefined' &&
((metadata as ComponentMetadata).fn === component ||
(isFactoryMetadata(metadata) && metadata.rtti === component))
(isFactoryMetadata(metadata) ? metadata.rtti : metadata.fn) === component
);
}

Expand Down Expand Up @@ -820,3 +874,4 @@ export { external, External } from './external';
export { factory, Factory } from './factory';
export { initialize, Initialize } from './initialize';
export { inject, Inject } from './inject';
export { configure, Configure } from './configure';
Loading

0 comments on commit 520d6be

Please sign in to comment.