From 8c2930136519468788cb5860af33c89990baa10c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 25 Mar 2019 10:30:44 -0700 Subject: [PATCH] feat(context): add `@inject.binding` and improve `@inject.setter` 1. The decorators now allow binding creation policy control 2. `@inject.binding` can be used to resolve/configure a binding --- docs/site/Decorators_inject.md | 78 +++++- docs/site/Dependency-injection.md | 1 + .../providers/authentication.provider.unit.ts | 6 +- .../class-level-bindings.acceptance.ts | 256 +++++++++++++++--- .../src/__tests__/unit/binding.unit.ts | 13 + .../src/__tests__/unit/context.unit.ts | 84 ++++++ packages/context/src/binding.ts | 12 +- packages/context/src/context.ts | 50 +++- packages/context/src/inject.ts | 99 ++++++- 9 files changed, 549 insertions(+), 50 deletions(-) diff --git a/docs/site/Decorators_inject.md b/docs/site/Decorators_inject.md index e87131f13306..5f8e5115ace2 100644 --- a/docs/site/Decorators_inject.md +++ b/docs/site/Decorators_inject.md @@ -126,7 +126,19 @@ class MyControllerWithGetter { `@inject.setter` injects a setter function to set the bound value of the key. -Syntax: `@inject.setter(bindingKey: string)`. +Syntax: `@inject.setter(bindingKey: BindingAddress, {bindingCreation?: ...})`. + +The `setter` function injected has the following signature: + +```ts +export type Setter = (value?: T) => void; +``` + +The binding resolution/creation is controlled by `bindingCreation` option. See +[@inject.binding](#injectbinding) for possible settings. + +The following example shows the usage of `@inject.setter` and the injected +setter function. ```ts export class HelloController { @@ -143,6 +155,70 @@ export class HelloController { } ``` +Please note the setter function simply binds a `value` to the underlying binding +using `binding.to(value)`. + +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: BindingAddress, {bindingCreation?: ...})`. + +```ts +export class HelloController { + constructor( + @inject.binding('greeting') private greetingBinding: Binding, + ) {} + + @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(this.greetingBinding.key); + } +} +``` + +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: + +```ts +/** + * Policy to control if a binding should be created for the context + */ +export enum BindingCreationPolicy { + /** + * Always create a binding with the key for the context + */ + ALWAYS_CREATE = 'Always', + /** + * Never create a binding for the context. If the key is not bound in the + * context, throw an error. + */ + NEVER_CREATE = 'Never', + /** + * Create a binding if the key is not bound in the context. Otherwise, return + * the existing binding. + */ + CREATE_IF_NOT_BOUND = 'IfNotBound', +} +``` + +For example: + +```ts +@inject.setter('binding-key', {bindingCreation: BindingCreationPolicy.NEVER_CREATES}) +``` + ### @inject.tag `@inject.tag` injects an array of values by a pattern or regexp to match binding diff --git a/docs/site/Dependency-injection.md b/docs/site/Dependency-injection.md index e164ca3b02dd..d08600f208a4 100644 --- a/docs/site/Dependency-injection.md +++ b/docs/site/Dependency-injection.md @@ -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) diff --git a/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts b/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts index 26b116420691..58bf4c1a155f 100644 --- a/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts +++ b/packages/authentication/src/__tests__/unit/providers/authentication.provider.unit.ts @@ -3,13 +3,13 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {expect} from '@loopback/testlab'; import {Context, instantiateClass} from '@loopback/context'; import {Request} from '@loopback/rest'; -import {AuthenticateFn, UserProfile, AuthenticationBindings} from '../../..'; -import {MockStrategy} from '../fixtures/mock-strategy'; +import {expect} from '@loopback/testlab'; import {Strategy} from 'passport'; +import {AuthenticateFn, AuthenticationBindings, UserProfile} from '../../..'; import {AuthenticateActionProvider} from '../../../providers'; +import {MockStrategy} from '../fixtures/mock-strategy'; describe('AuthenticateActionProvider', () => { describe('constructor()', () => { diff --git a/packages/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts b/packages/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts index 046c83cd69dd..2e2b8d003196 100644 --- a/packages/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts +++ b/packages/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts @@ -5,6 +5,8 @@ import {expect} from '@loopback/testlab'; import { + Binding, + BindingCreationPolicy, BindingKey, BindingScope, Constructor, @@ -169,6 +171,25 @@ describe('Context bindings - Injecting dependencies of classes', () => { expect(await store.getter()).to.equal('456'); }); + it('creates getter from a value', () => { + const getter = Getter.fromValue('data'); + expect(getter).to.be.a.Function(); + return expect(getter()).to.be.fulfilledWith('data'); + }); + + it('reports an error if @inject.getter has a non-function target', async () => { + ctx.bind('key').to('value'); + + class Store { + constructor(@inject.getter('key') public getter: string) {} + } + + ctx.bind(STORE_KEY).toClass(Store); + expect(() => ctx.getSync(STORE_KEY)).to.throw( + 'The type of Store.constructor[0] (String) is not a Getter function', + ); + }); + describe('in SINGLETON scope', () => { it('throws if a getter cannot be resolved by the owning context', async () => { class Store { @@ -317,49 +338,120 @@ describe('Context bindings - Injecting dependencies of classes', () => { } }); - it('injects a setter function', async () => { + describe('@inject.setter', () => { class Store { constructor(@inject.setter(HASH_KEY) public setter: Setter) {} } - ctx.bind(STORE_KEY).toClass(Store); - const store = ctx.getSync(STORE_KEY); - - expect(store.setter).to.be.Function(); - store.setter('a-value'); - expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); - }); - - it('creates getter from a value', () => { - const getter = Getter.fromValue('data'); - expect(getter).to.be.a.Function(); - return expect(getter()).to.be.fulfilledWith('data'); - }); + it('injects a setter function', () => { + ctx.bind(STORE_KEY).toClass(Store); + const store = ctx.getSync(STORE_KEY); - it('reports an error if @inject.getter has a non-function target', async () => { - ctx.bind('key').to('value'); + expect(store.setter).to.be.Function(); + store.setter('a-value'); + expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); + }); - class Store { - constructor(@inject.getter('key') public getter: string) {} - } + it('injects a setter function that uses an existing binding', () => { + // Create a binding for hash key + ctx + .bind(HASH_KEY) + .to('123') + .tag('hash'); + ctx.bind(STORE_KEY).toClass(Store); + const store = ctx.getSync(STORE_KEY); + // Change the hash value + store.setter('a-value'); + expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); + // The tag is kept + expect(ctx.getBinding(HASH_KEY).tagNames).to.containEql('hash'); + }); - ctx.bind(STORE_KEY).toClass(Store); - expect(() => ctx.getSync(STORE_KEY)).to.throw( - 'The type of Store.constructor[0] (String) is not a Getter function', - ); - }); + it('reports an error if @inject.setter has a non-function target', () => { + class StoreWithWrongSetterType { + constructor(@inject.setter(HASH_KEY) public setter: object) {} + } - it('reports an error if @inject.setter has a non-function target', async () => { - ctx.bind('key').to('value'); + ctx.bind('key').to('value'); - class Store { - constructor(@inject.setter('key') public setter: object) {} - } + ctx.bind(STORE_KEY).toClass(StoreWithWrongSetterType); + expect(() => ctx.getSync(STORE_KEY)).to.throw( + 'The type of StoreWithWrongSetterType.constructor[0] (Object) is not a Setter function', + ); + }); - ctx.bind(STORE_KEY).toClass(Store); - expect(() => ctx.getSync(STORE_KEY)).to.throw( - 'The type of Store.constructor[0] (Object) is not a Setter function', - ); + describe('bindingCreation option', () => { + it('supports ALWAYS_CREATE', () => { + ctx + .bind(STORE_KEY) + .toClass(givenStoreClass(BindingCreationPolicy.ALWAYS_CREATE)); + const store = ctx.getSync(STORE_KEY); + store.setter('a-value'); + const binding1 = ctx.getBinding(HASH_KEY); + store.setter('b-value'); + const binding2 = ctx.getBinding(HASH_KEY); + expect(binding1).to.not.exactly(binding2); + }); + + it('supports NEVER_CREATE - throws if not bound', () => { + ctx + .bind(STORE_KEY) + .toClass(givenStoreClass(BindingCreationPolicy.NEVER_CREATE)); + const store = ctx.getSync(STORE_KEY); + expect(() => store.setter('a-value')).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_KEY); + store.setter('a-value'); + expect(ctx.getBinding(HASH_KEY)).to.exactly(hashBinding); + expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); + }); + + 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_KEY); + store.setter('a-value'); + expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); + }); + + 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_KEY); + store.setter('a-value'); + expect(ctx.getBinding(HASH_KEY)).to.exactly(hashBinding); + expect(ctx.getSync(HASH_KEY)).to.equal('a-value'); + }); + + function givenStoreClass(bindingCreation?: BindingCreationPolicy) { + class StoreWithInjectSetterMetadata { + constructor( + @inject.setter(HASH_KEY, {bindingCreation}) + public setter: Setter, + ) {} + } + return StoreWithInjectSetterMetadata; + } + }); }); it('injects a nested property', async () => { @@ -374,6 +466,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) {} + } + + it('injects a binding', () => { + ctx.bind(STORE_KEY).toClass(Store); + const store = ctx.getSync(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_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_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_KEY).binding; + const binding2 = ctx.getSync(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_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_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_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_KEY); + expect(store.binding).to.be.exactly(hashBinding); + }); + + function givenStoreClass(bindingCreation?: BindingCreationPolicy) { + class StoreWithInjectBindingMetadata { + constructor( + @inject.binding(HASH_KEY, {bindingCreation}) + public binding: Binding, + ) {} + } + return StoreWithInjectBindingMetadata; + } + }); + }); + it('injects context with @inject.context', () => { class Store { constructor(@inject.context() public context: Context) {} diff --git a/packages/context/src/__tests__/unit/binding.unit.ts b/packages/context/src/__tests__/unit/binding.unit.ts index ff32055720b0..4239b189ce9c 100644 --- a/packages/context/src/__tests__/unit/binding.unit.ts +++ b/packages/context/src/__tests__/unit/binding.unit.ts @@ -258,6 +258,19 @@ describe('Binding', () => { expect(binding.tagNames).to.eql(['myTag']); }); + it('applies multiple template functions', async () => { + binding.apply( + b => { + b.inScope(BindingScope.SINGLETON); + }, + b => { + b.tag('myTag'); + }, + ); + expect(binding.scope).to.eql(BindingScope.SINGLETON); + expect(binding.tagNames).to.eql(['myTag']); + }); + it('sets up a placeholder value', async () => { const toBeBound = (b: Binding) => { b.toDynamicValue(() => { diff --git a/packages/context/src/__tests__/unit/context.unit.ts b/packages/context/src/__tests__/unit/context.unit.ts index f035fdaa6ed0..fbe2b47432e8 100644 --- a/packages/context/src/__tests__/unit/context.unit.ts +++ b/packages/context/src/__tests__/unit/context.unit.ts @@ -6,6 +6,7 @@ import {expect} from '@loopback/testlab'; import { Binding, + BindingCreationPolicy, BindingKey, BindingScope, BindingType, @@ -342,6 +343,89 @@ describe('Context', () => { }); }); + describe('findOrCreateBinding', () => { + context('with BindingCreationPolicy.ALWAYS_CREATE', () => { + it('creates a new binding even the key is bound', () => { + const current = ctx.bind('foo'); + const actual: Binding = ctx.findOrCreateBinding( + 'foo', + BindingCreationPolicy.ALWAYS_CREATE, + ); + expect(actual).to.be.not.exactly(current); + }); + + it('creates a new binding if not bound', () => { + const binding = ctx.findOrCreateBinding( + 'a-new-key', + BindingCreationPolicy.ALWAYS_CREATE, + ); + expect(binding.key).to.eql('a-new-key'); + }); + }); + + context('with BindingCreationPolicy.NEVER_CREATE', () => { + it('returns the exiting binding if the key is bound', () => { + const current = ctx.bind('foo'); + const actual: Binding = ctx.findOrCreateBinding( + 'foo', + BindingCreationPolicy.NEVER_CREATE, + ); + expect(actual).to.be.exactly(current); + }); + + it('throws an error if the key is not bound', () => { + expect(() => + ctx.findOrCreateBinding( + 'a-new-key', + BindingCreationPolicy.NEVER_CREATE, + ), + ).to.throw(/The key 'a-new-key' is not bound to any value in context/); + }); + }); + + context('with BindingCreationPolicy.CREATE_IF_NOT_BOUND', () => { + it('returns the binding object registered under the given key', () => { + const expected = ctx.bind('foo'); + const actual: Binding = ctx.findOrCreateBinding( + 'foo', + BindingCreationPolicy.CREATE_IF_NOT_BOUND, + ); + expect(actual).to.be.exactly(expected); + }); + + it('creates a new binding if the key is not bound', () => { + const binding = ctx.findOrCreateBinding( + 'a-new-key', + BindingCreationPolicy.CREATE_IF_NOT_BOUND, + ); + expect(binding.key).to.eql('a-new-key'); + }); + }); + + context( + 'without bindingCreationPolicy (default: CREATE_IF_NOT_BOUND)', + () => { + it('returns the binding object registered under the given key', () => { + const expected = ctx.bind('foo'); + const actual: Binding = ctx.findOrCreateBinding('foo'); + expect(actual).to.be.exactly(expected); + }); + + it('creates a new binding if the key is not bound', () => { + const binding = ctx.findOrCreateBinding('a-new-key'); + expect(binding.key).to.eql('a-new-key'); + }); + }, + ); + + it('rejects a key containing property separator', () => { + const key = 'a' + BindingKey.PROPERTY_SEPARATOR + 'b'; + expect(() => ctx.findOrCreateBinding(key)).to.throw( + /Binding key .* cannot contain/, + ); + }); + }); + describe('getSync', () => { it('returns the value immediately when the binding is sync', () => { ctx.bind('foo').to('bar'); diff --git a/packages/context/src/binding.ts b/packages/context/src/binding.ts index cdef21f06de2..7e1d1748ef50 100644 --- a/packages/context/src/binding.ts +++ b/packages/context/src/binding.ts @@ -537,8 +537,8 @@ export class Binding { } /** - * Apply a template function to set up the binding with scope, tags, and - * other attributes as a group. + * Apply one or more template functions to set up the binding with scope, + * tags, and other attributes as a group. * * For example, * ```ts @@ -548,10 +548,12 @@ export class Binding { * const serverBinding = new Binding('servers.RestServer1'); * serverBinding.apply(serverTemplate); * ``` - * @param templateFn A function to configure the binding + * @param templateFns One or more functions to configure the binding */ - apply(templateFn: BindingTemplate): this { - templateFn(this); + apply(...templateFns: BindingTemplate[]): this { + for (const fn of templateFns) { + fn(this); + } return this; } diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index 9b74d65dfd2a..568aa17d51e7 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -38,7 +38,11 @@ if (!Symbol.asyncIterator) { // tslint:disable-next-line:no-any (Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator'); } -// This import must happen after the polyfill +/** + * This import must happen after the polyfill. + * + * WARNING: VSCode organize import may change the order of this import + */ import {iterator, multiple} from 'p-event'; const debug = debugFactory('loopback:context'); @@ -751,6 +755,30 @@ export class Context extends EventEmitter { ); } + /** + * Find or create a binding for the given key + * @param key Binding address + * @param policy Binding creation policy + */ + findOrCreateBinding( + key: BindingAddress, + policy?: BindingCreationPolicy, + ) { + let binding: Binding; + if (policy === BindingCreationPolicy.ALWAYS_CREATE) { + binding = this.bind(key); + } else if (policy === BindingCreationPolicy.NEVER_CREATE) { + binding = this.getBinding(key); + } else if (this.isBound(key)) { + // CREATE_IF_NOT_BOUND - the key is bound + binding = this.getBinding(key); + } else { + // CREATE_IF_NOT_BOUND - the key is not bound + binding = this.bind(key); + } + return binding; + } + /** * Get the value bound to the given key. * @@ -833,3 +861,23 @@ class ContextSubscription implements Subscription { return this._closed; } } + +/** + * Policy to control if a binding should be created for the context + */ +export enum BindingCreationPolicy { + /** + * Always create a binding with the key for the context + */ + ALWAYS_CREATE = 'Always', + /** + * Never create a binding for the context. If the key is not bound in the + * context, throw an error. + */ + NEVER_CREATE = 'Never', + /** + * Create a binding if the key is not bound in the context. Otherwise, return + * the existing binding. + */ + CREATE_IF_NOT_BOUND = 'IfNotBound', +} diff --git a/packages/context/src/inject.ts b/packages/context/src/inject.ts index 0b19d7759356..2223879117d9 100644 --- a/packages/context/src/inject.ts +++ b/packages/context/src/inject.ts @@ -12,7 +12,7 @@ import { ParameterDecoratorFactory, PropertyDecoratorFactory, } from '@loopback/metadata'; -import {BindingTag} from './binding'; +import {Binding, BindingTag} from './binding'; import { BindingFilter, BindingSelector, @@ -20,7 +20,7 @@ import { isBindingAddress, } from './binding-filter'; import {BindingAddress} from './binding-key'; -import {Context} from './context'; +import {BindingCreationPolicy, Context} from './context'; import {ContextView, createViewGetter} from './context-view'; import {ResolutionSession} from './resolution-session'; import {BoundValue, ValueOrPromise} from './value-promise'; @@ -186,7 +186,9 @@ export function inject( } /** - * The function injected by `@inject.getter(bindingSelector)`. + * The function injected by `@inject.getter(bindingSelector)`. It can be used + * to fetch bound value(s) from the underlying binding(s). The return value will + * be an array if the `bindingSelector` is a `BindingFilter` function. */ export type Getter = () => Promise; @@ -201,10 +203,28 @@ export namespace Getter { } /** - * The function injected by `@inject.setter(key)`. + * The function injected by `@inject.setter(bindingKey)`. It sets the underlying + * binding to a constant value using `binding.to(value)`. + * + * For example: + * + * ```ts + * setterFn('my-value'); + * ``` + * @param value The value for the underlying binding */ export type Setter = (value: T) => void; +/** + * Metadata for `@inject.binding` + */ +export interface InjectBindingMetadata extends InjectionMetadata { + /** + * Controls how the underlying binding is resolved/created + */ + bindingCreation?: BindingCreationPolicy; +} + export namespace inject { /** * Inject a function for getting the actual bound value. @@ -249,16 +269,49 @@ export namespace inject { */ export const setter = function injectSetter( bindingKey: BindingAddress, - metadata?: InjectionMetadata, + metadata?: InjectBindingMetadata, ) { metadata = Object.assign({decorator: '@inject.setter'}, metadata); return inject(bindingKey, metadata, resolveAsSetter); }; + /** + * Inject the binding object for the given key. This is useful if a binding + * needs to be set up beyond just a constant value allowed by + * `@inject.setter`. The injected binding is found or created based on the + * `metadata.bindingCreation` option. See `BindingCreationPolicy` for more + * details. + * + * For example: + * + * ```ts + * class MyAuthAction { + * @inject.binding('current-user', { + * bindingCreation: BindingCreationPolicy.ALWAYS_CREATE, + * }) + * private userBinding: Binding; + * + * async authenticate() { + * this.userBinding.toDynamicValue(() => {...}); + * } + * } + * ``` + * + * @param bindingKey Binding key + * @param metadata Metadata for the injection + */ + 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 * - * @example + * For example, * ```ts * class AuthenticationManager { * constructor( @@ -359,10 +412,42 @@ function resolveAsSetter(ctx: Context, injection: Injection) { } // No resolution session should be propagated into the setter return function setter(value: unknown) { - ctx.bind(bindingSelector).to(value); + const binding = findOrCreateBindingForInjection(ctx, injection); + binding.to(value); }; } +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, +) { + const bindingCreation = + injection.metadata && + (injection.metadata as InjectBindingMetadata).bindingCreation; + const binding: Binding = 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,