Skip to content

Commit

Permalink
feat(context): add @inject.binding to set the value to underlying b…
Browse files Browse the repository at this point in the history
…inding
  • Loading branch information
raymondfeng committed Mar 29, 2019
1 parent d095823 commit 623cf16
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 26 deletions.
33 changes: 27 additions & 6 deletions docs/site/Decorators_inject.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export type Setter<T> =
* @param value Optional value. If not provided, the underlying binding won't
* be changed and returned as-is.
*/
(value?: T) => Binding<T>;
(value?: T) => void;
```

If you simply want to set a constant value for the underlying binding:
Expand All @@ -166,15 +166,36 @@ If you simply want to set a constant value for the underlying binding:
this.greetingSetter('Greetings!');
```

To set other types of value providers such as `toDynamicValue`or `toClass`, call
the setter function without any arguments and use the returned `binding` to
configure with binding APIs.
To set other types of value providers such as `toDynamicValue`or `toClass`, use
`@inject.binding` instead.

### @inject.binding

`@inject.binding` injects a binding for the given key. It can be used to bind
various types of value providers to the underlying binding or configure the
binding. This is an advanced form of `@inject.setter`, which only allows to set
a constant value (using `Binding.to(value)` behind the scene) to the underlying
binding.

Syntax: `@inject.binding(bindingKey: string, {bindingCreation?: ...})`.

```ts
const binding = this.greetingSetter().toDynamicValue(() => 'Greetings!');
export class HelloController {
constructor(
@inject.binding('greeting') private greetingBinding: Binding<string>,
) {}

@get('/hello')
async greet() {
// Bind `greeting` to a factory function that reads default greeting
// from a file or database
this.greetingBinding.toDynamicValue(() => readDefaultGreeting());
return await this.greetingBinding.get<string>(this.greetingBinding.key);
}
}
```

The `@inject.setter` takes an optional `metadata` object which can contain
The `@inject.binding` takes an optional `metadata` object which can contain
`bindingCreation` to control how underlying binding is resolved or created based
on the following values:

Expand Down
1 change: 1 addition & 0 deletions docs/site/Dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ There are a few special decorators from the `inject` namespace.

- [`@inject.getter`](Decorators_inject.md#@inject.getter)
- [`@inject.setter`](Decorators_inject.md#@inject.setter)
- [`@inject.binding`](Decorators_inject.md#@inject.binding)
- [`@inject.context`](Decorators_inject.md#@inject.context)
- [`@inject.tag`](Decorators_inject.md#@inject.tag)
- [`@inject.view`](Decorators_inject.md#@inject.view)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import {expect} from '@loopback/testlab';
import {
Binding,
BindingCreationPolicy,
BindingKey,
BindingScope,
Constructor,
Expand All @@ -17,7 +19,6 @@ import {
ResolutionSession,
Setter,
} from '../..';
import {BindingCreationPolicy} from '../../context';

const INFO_CONTROLLER = 'controllers.info';

Expand Down Expand Up @@ -351,15 +352,6 @@ describe('Context bindings - Injecting dependencies of classes', () => {
expect(ctx.getSync(HASH_KEY)).to.equal('a-value');
});

it('injects a setter function that returns Binding', async () => {
ctx.bind(STORE_KEY).toClass(Store);
const store = ctx.getSync<Store>(STORE_KEY);

expect(store.setter).to.be.Function();
store.setter().toDynamicValue(() => Promise.resolve('a-value'));
expect(await ctx.get(HASH_KEY)).to.equal('a-value');
});

it('injects a setter function that uses an existing binding', () => {
// Create a binding for hash key
ctx
Expand Down Expand Up @@ -473,6 +465,104 @@ describe('Context bindings - Injecting dependencies of classes', () => {
expect(resolved.config).to.equal('test-config');
});

describe('@inject.binding', () => {
class Store {
constructor(@inject.binding(HASH_KEY) public binding: Binding<string>) {}
}

it('injects a binding', () => {
ctx.bind(STORE_KEY).toClass(Store);
const store = ctx.getSync<Store>(STORE_KEY);
expect(store.binding).to.be.instanceOf(Binding);
});

it('injects a binding that exists', () => {
// Create a binding for hash key
const hashBinding = ctx
.bind(HASH_KEY)
.to('123')
.tag('hash');
ctx.bind(STORE_KEY).toClass(Store);
const store = ctx.getSync<Store>(STORE_KEY);
expect(store.binding).to.be.exactly(hashBinding);
});

it('reports an error if @inject.binding has a wrong target type', () => {
class StoreWithWrongBindingType {
constructor(@inject.binding(HASH_KEY) public binding: object) {}
}

ctx.bind(STORE_KEY).toClass(StoreWithWrongBindingType);
expect(() => ctx.getSync<Store>(STORE_KEY)).to.throw(
'The type of StoreWithWrongBindingType.constructor[0] (Object) is not Binding',
);
});

describe('bindingCreation option', () => {
it('supports ALWAYS_CREATE', () => {
ctx
.bind(STORE_KEY)
.toClass(givenStoreClass(BindingCreationPolicy.ALWAYS_CREATE));
const binding1 = ctx.getSync<Store>(STORE_KEY).binding;
const binding2 = ctx.getSync<Store>(STORE_KEY).binding;
expect(binding1).to.not.be.exactly(binding2);
});

it('supports NEVER_CREATE - throws if not bound', () => {
ctx
.bind(STORE_KEY)
.toClass(givenStoreClass(BindingCreationPolicy.NEVER_CREATE));
expect(() => ctx.getSync<Store>(STORE_KEY)).to.throw(
/The key 'hash' is not bound to any value in context/,
);
});

it('supports NEVER_CREATE with an existing binding', () => {
// Create a binding for hash key
const hashBinding = ctx
.bind(HASH_KEY)
.to('123')
.tag('hash');
ctx
.bind(STORE_KEY)
.toClass(givenStoreClass(BindingCreationPolicy.NEVER_CREATE));
const store = ctx.getSync<Store>(STORE_KEY);
expect(store.binding).to.be.exactly(hashBinding);
});

it('supports CREATE_IF_NOT_BOUND without an existing binding', async () => {
ctx
.bind(STORE_KEY)
.toClass(givenStoreClass(BindingCreationPolicy.CREATE_IF_NOT_BOUND));
const store = ctx.getSync<Store>(STORE_KEY);
expect(store.binding).to.be.instanceOf(Binding);
});

it('supports CREATE_IF_NOT_BOUND with an existing binding', () => {
// Create a binding for hash key
const hashBinding = ctx
.bind(HASH_KEY)
.to('123')
.tag('hash');
ctx
.bind(STORE_KEY)
.toClass(givenStoreClass(BindingCreationPolicy.CREATE_IF_NOT_BOUND));
const store = ctx.getSync<Store>(STORE_KEY);
expect(store.binding).to.be.exactly(hashBinding);
});

function givenStoreClass(bindingCreation?: BindingCreationPolicy) {
class StoreWithInjectBindingMetadata {
constructor(
@inject.binding(HASH_KEY, {bindingCreation})
public binding: Binding<string>,
) {}
}
return StoreWithInjectBindingMetadata;
}
});
});

it('injects context with @inject.context', () => {
class Store {
constructor(@inject.context() public context: Context) {}
Expand Down
54 changes: 44 additions & 10 deletions packages/context/src/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,9 @@ export type Setter<T> =
(value?: T) => Binding<T>;

/**
* Metadata for `@inject.setter`
* Metadata for `@inject.binding`
*/
export interface InjectSetterMetadata extends InjectionMetadata {
export interface InjectBindingMetadata extends InjectionMetadata {
/**
* Controls how the underlying binding is resolved/created
*/
Expand Down Expand Up @@ -274,12 +274,20 @@ export namespace inject {
*/
export const setter = function injectSetter(
bindingKey: BindingAddress,
metadata?: InjectionMetadata & InjectSetterMetadata,
metadata?: InjectBindingMetadata,
) {
metadata = Object.assign({decorator: '@inject.setter'}, metadata);
return inject(bindingKey, metadata, resolveAsSetter);
};

export const binding = function injectBinding(
bindingKey: BindingAddress,
metadata?: InjectBindingMetadata,
) {
metadata = Object.assign({decorator: '@inject.binding'}, metadata);
return inject(bindingKey, metadata, resolveAsBinding);
};

/**
* Inject an array of values by a tag pattern string or regexp
*
Expand Down Expand Up @@ -384,17 +392,43 @@ function resolveAsSetter(ctx: Context, injection: Injection) {
}
// No resolution session should be propagated into the setter
return function setter(value: unknown) {
const metadata = (injection.metadata || {}) as InjectSetterMetadata;
const bindingCreation = metadata.bindingCreation;
const binding: Binding<unknown> = ctx.findOrCreateBinding(
bindingSelector,
bindingCreation,
);
if (arguments.length) binding.to(value);
const binding = findOrCreateBindingForInjection(ctx, injection);
binding.to(value);
return binding;
};
}

function resolveAsBinding(ctx: Context, injection: Injection) {
const targetType = inspectTargetType(injection);
const targetName = ResolutionSession.describeInjection(injection)!.targetName;
if (targetType && targetType !== Binding) {
throw new Error(
`The type of ${targetName} (${targetType.name}) is not Binding`,
);
}
const bindingSelector = injection.bindingSelector;
if (!isBindingAddress(bindingSelector)) {
throw new Error(
`@inject.binding for (${targetType.name}) does not allow BindingFilter`,
);
}
return findOrCreateBindingForInjection(ctx, injection);
}

function findOrCreateBindingForInjection(
ctx: Context,
injection: Injection<unknown>,
) {
const bindingCreation =
injection.metadata &&
(injection.metadata as InjectBindingMetadata).bindingCreation;
const binding: Binding<unknown> = ctx.findOrCreateBinding(
injection.bindingSelector as BindingAddress,
bindingCreation,
);
return binding;
}

/**
* Return an array of injection objects for parameters
* @param target The target class for constructor or static methods,
Expand Down

0 comments on commit 623cf16

Please sign in to comment.