Skip to content

Commit

Permalink
feat(context): add binding.toInjectable shortcut
Browse files Browse the repository at this point in the history
Some developers forget to use `createBindingFromClass` and use `toClass`
or `toProvider` directly. In such cases, the metadata from `@injectable`
are not honored. This shortcut method makes it easy to bind all types of
injectable classes.

Signed-off-by: Raymond Feng <[email protected]>
  • Loading branch information
raymondfeng committed Nov 5, 2020
1 parent 72f7eab commit 230923a
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 4 deletions.
35 changes: 35 additions & 0 deletions docs/site/Binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,41 @@ binding.toProvider(MyValueProvider);
The provider class serves as the wrapper to declare dependency injections. If
dependency is not needed, `toDynamicValue` can be used instead.

#### An injectable class

An injectable class is one of the following types of classes optionally
decorated with `@injectable`.

- A class
- A provider class
- A dynamic value factory class

The `toInjectable()` method is a shortcut to bind such classes using
`toClass/toProvider/toDynamicValue` respectively by introspecting the class,
including the binding metadata added by `@injectable`.

```ts
@injectable({scope: BindingScope.SINGLETON})
class MyController {
constructor(@inject('my-options') private options: MyOptions) {
// ...
}
}

binding.toInjectable(MyController);
```

The code above is similar as follows:

```ts
const binding = createBindingFromClass(MyController);
```

{% include note.html content="
If `binding.toClass(MyController)` is used, the binding scope set by
`@injectable` is NOT honored.
" %}

#### An alias

An alias is the key with optional path to resolve the value from another
Expand Down
59 changes: 57 additions & 2 deletions packages/context/src/__tests__/unit/binding.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import {
BindingKey,
BindingScope,
BindingType,
config,
Context,
filterByTag,
inject,
injectable,
Provider,
ValueFactory,
} from '../..';
import {ValueFactory} from '../../binding';
import {config} from '../../inject-config';

const key = 'foo';

Expand Down Expand Up @@ -368,6 +369,54 @@ describe('Binding', () => {
});
});

describe('toInjectable(class)', () => {
it('binds to a class', async () => {
ctx.bind('msg').toDynamicValue(() => Promise.resolve('world'));
const serviceBinding = ctx.bind('myService').toInjectable(MyService);
expect(serviceBinding.type).to.eql(BindingType.CLASS);
const myService = await ctx.get<MyService>('myService');
expect(myService.getMessage()).to.equal('hello world');
});

it('binds to a class with @injectable', async () => {
@injectable({scope: BindingScope.SINGLETON, tags: {x: 1}})
class MyInjectableService {
constructor(@inject('msg') private _msg: string) {}

getMessage(): string {
return 'hello ' + this._msg;
}
}
ctx.bind('msg').toDynamicValue(() => Promise.resolve('world'));
const serviceBinding = ctx
.bind('myService')
.toInjectable(MyInjectableService);
expect(serviceBinding.type).to.eql(BindingType.CLASS);
expect(serviceBinding.scope).to.eql(BindingScope.SINGLETON);
expect(serviceBinding.tagMap.x).to.eql(1);
const myService = await ctx.get<MyInjectableService>('myService');
expect(myService.getMessage()).to.equal('hello world');
});

it('binds to a provider', async () => {
ctx.bind('msg').to('hello');
const providerBinding = ctx.bind('provider_key').toInjectable(MyProvider);
expect(providerBinding.type).to.eql(BindingType.PROVIDER);
const value = await ctx.get<string>('provider_key');
expect(value).to.equal('hello world');
});

it('binds to a dynamic value provider class', async () => {
ctx.bind('msg').to('hello');
const providerBinding = ctx
.bind('provider_key')
.toInjectable(MyDynamicValueProvider);
expect(providerBinding.type).to.eql(BindingType.DYNAMIC_VALUE);
const value = await ctx.get<string>('provider_key');
expect(value).to.equal('hello world');
});
});

describe('toAlias(bindingKeyWithPath)', () => {
it('binds to another binding with sync value', () => {
ctx.bind('parent.options').to({child: {disabled: true}});
Expand Down Expand Up @@ -748,4 +797,10 @@ describe('Binding', () => {
return 'hello ' + this._msg;
}
}

class MyDynamicValueProvider {
static value(@inject('msg') _msg: string): string {
return _msg + ' world';
}
}
});
7 changes: 5 additions & 2 deletions packages/context/src/binding-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export function removeNameAndKeyTags(binding: Binding<unknown>) {
*/
export function bindingTemplateFor<T>(
cls: Constructor<T | Provider<T>> | DynamicValueProviderClass<T>,
options?: BindingFromClassOptions,
): BindingTemplate<T> {
const spec = getBindingMetadata(cls);
debug('class %s has binding metadata', cls.name, spec);
Expand All @@ -182,6 +183,9 @@ export function bindingTemplateFor<T>(
// Remove name/key tags inherited from base classes
binding.apply(removeNameAndKeyTags);
}
if (options != null) {
applyClassBindingOptions(binding, options);
}
};
}

Expand Down Expand Up @@ -260,10 +264,9 @@ export function createBindingFromClass<T>(
): Binding<T> {
debug('create binding from class %s with options', cls.name, options);
try {
const templateFn = bindingTemplateFor(cls);
const templateFn = bindingTemplateFor(cls, options);
const key = buildBindingKey(cls, options);
const binding = Binding.bind<T>(key).apply(templateFn);
applyClassBindingOptions(binding, options);
return binding;
} catch (err) {
err.message += ` (while building binding for class ${cls.name})`;
Expand Down
31 changes: 31 additions & 0 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import debugFactory from 'debug';
import {EventEmitter} from 'events';
import {bindingTemplateFor} from './binding-inspector';
import {BindingAddress, BindingKey} from './binding-key';
import {Context} from './context';
import {inspectInjections} from './inject';
Expand Down Expand Up @@ -843,6 +844,36 @@ export class Binding<T = BoundValue> extends EventEmitter {
return this;
}

/**
* Bind to a class optionally decorated with `@injectable`. Based on the
* introspection of the class, it calls `toClass/toProvider/toDynamicValue`
* internally. The current binding key will be preserved (not being overridden
* by the key inferred from the class or options).
*
* This is similar to {@link createBindingFromClass} but applies to an
* existing binding.
*
* @example
*
* ```ts
* @injectable({scope: BindingScope.SINGLETON, tags: {service: 'MyService}})
* class MyService {
* // ...
* }
*
* const ctx = new Context();
* ctx.bind('services.MyService').toInjectable(MyService);
* ```
*
* @param ctor - A class decorated with `@injectable`.
*/
toInjectable(
ctor: DynamicValueProviderClass<T> | Constructor<T | Provider<T>>,
) {
this.apply(bindingTemplateFor(ctor));
return this;
}

/**
* Bind the key to an alias of another binding
* @param keyWithPath - Target binding key with optional path,
Expand Down

0 comments on commit 230923a

Please sign in to comment.