From 623cf161f7dd445168d075f65c76fa5b084c6c7d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 29 Mar 2019 10:26:50 -0700 Subject: [PATCH] feat(context): add `@inject.binding` to set the value to underlying binding --- docs/site/Decorators_inject.md | 33 +++++- docs/site/Dependency-injection.md | 1 + .../class-level-bindings.acceptance.ts | 110 ++++++++++++++++-- packages/context/src/inject.ts | 54 +++++++-- 4 files changed, 172 insertions(+), 26 deletions(-) diff --git a/docs/site/Decorators_inject.md b/docs/site/Decorators_inject.md index 5d8332c167e8..463c97909f17 100644 --- a/docs/site/Decorators_inject.md +++ b/docs/site/Decorators_inject.md @@ -157,7 +157,7 @@ export type Setter = * @param value Optional value. If not provided, the underlying binding won't * be changed and returned as-is. */ - (value?: T) => Binding; + (value?: T) => void; ``` If you simply want to set a constant value for the underlying binding: @@ -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, + ) {} + + @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.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: 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/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts b/packages/context/src/__tests__/acceptance/class-level-bindings.acceptance.ts index 17b8d5d713f3..ff005ae79b15 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, @@ -17,7 +19,6 @@ import { ResolutionSession, Setter, } from '../..'; -import {BindingCreationPolicy} from '../../context'; const INFO_CONTROLLER = 'controllers.info'; @@ -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_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 @@ -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) {} + } + + 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/inject.ts b/packages/context/src/inject.ts index b6da28f28724..8075cb63d658 100644 --- a/packages/context/src/inject.ts +++ b/packages/context/src/inject.ts @@ -221,9 +221,9 @@ export type Setter = (value?: T) => Binding; /** - * 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 */ @@ -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 * @@ -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 = 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, +) { + 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,