From db9bd6d4e04d1de9bf6db94d6b95d8eabc02049e Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sat, 27 Jan 2018 12:08:12 -0800 Subject: [PATCH 1/4] feat(context): add support for context.add(binding) --- packages/context/src/context.ts | 18 +++++++++++++----- packages/context/test/unit/context.unit.ts | 7 +++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index 7878c5a41efd..70fdc346c623 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -42,16 +42,26 @@ export class Context { * Create a binding with the given key in the context. If a locked binding * already exists with the same key, an error will be thrown. * - * @param key Binding key + * @param keyOrBinding Binding key or a binding */ bind( - key: BindingAddress, + keyOrBinding: BindingAddress | Binding, ): Binding { + let key: string; + let binding: Binding; + if (keyOrBinding instanceof Binding) { + key = keyOrBinding.key; + binding = keyOrBinding; + } else { + key = keyOrBinding.toString(); + binding = new Binding(key); + } + /* istanbul ignore if */ if (debug.enabled) { debug('Adding binding: %s', key); } - key = BindingKey.validate(key); + const keyExists = this.registry.has(key); if (keyExists) { const existingBinding = this.registry.get(key); @@ -59,8 +69,6 @@ export class Context { if (bindingIsLocked) throw new Error(`Cannot rebind key "${key}" to a locked binding`); } - - const binding = new Binding(key); this.registry.set(key, binding); return binding; } diff --git a/packages/context/test/unit/context.unit.ts b/packages/context/test/unit/context.unit.ts index fe762e5ba563..347c3080f399 100644 --- a/packages/context/test/unit/context.unit.ts +++ b/packages/context/test/unit/context.unit.ts @@ -71,6 +71,13 @@ describe('Context', () => { expect(result).to.be.true(); }); + it('accepts a binding', () => { + const binding = new Binding('foo').to('bar'); + expect(ctx.bind(binding)).to.be.exactly(binding); + const result = ctx.contains('foo'); + expect(result).to.be.true(); + }); + it('returns a binding', () => { const binding = ctx.bind('foo'); expect(binding).to.be.instanceOf(Binding); From 199a0943b9f2eb2c60b1db6dc42eadf7358231cb Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sat, 27 Jan 2018 12:08:41 -0800 Subject: [PATCH 2/4] feat(core): allow components to expose an array of bindings --- docs/site/Creating-components.md | 23 +++++- packages/context/src/binding.ts | 12 ++- packages/context/src/context.ts | 26 ++++--- packages/context/test/unit/context.unit.ts | 31 ++++++-- packages/core/src/component.ts | 60 +++++++++++++-- packages/core/test/unit/application.unit.ts | 83 +++++++++++++++++++-- 6 files changed, 201 insertions(+), 34 deletions(-) diff --git a/docs/site/Creating-components.md b/docs/site/Creating-components.md index 4dbaa449dbc7..ca5239069db6 100644 --- a/docs/site/Creating-components.md +++ b/docs/site/Creating-components.md @@ -20,6 +20,13 @@ export class MyComponent implements Component { this.providers = { 'my-value': MyValueProvider, }; + this.classes = { + 'my-validator': MyValidator, + }; + + const bindingX = new Binding('x').to('Value X'); + const bindingY = new Binding('y').toClass(ClassY); + this.bindings = [bindingX, bindingY]; } } ``` @@ -28,10 +35,22 @@ When a component is mounted to an application, a new instance of the component class is created and then: - Each Controller class is registered via `app.controller()`, -- Each Provider is bound to its key in `providers` object. +- Each Provider is bound to its key in `providers` object via + `app.bind(key).toProvider(providerClass)` +- Each Class is bound to its key in `classes` object via + `app.bind(key).toClass(cls)` +- Each Binding is added via `app.add(binding)` + +Please note that `providers` and `classes` are shortcuts for provider and class +`bindings`. The example `MyComponent` above will add `MyController` to application's API and -create a new binding `my-value` that will be resolved using `MyValueProvider`. +create the following bindings to the application context: + +- `my-value` -> `MyValueProvider` (provider) +- `my-validator` -> `MyValidator` (class) +- `x` -> `'Value X'` (value) +- `y` -> `ClassY` (class) ## Providers diff --git a/packages/context/src/binding.ts b/packages/context/src/binding.ts index 0ec79929353d..1ca87fac2d84 100644 --- a/packages/context/src/binding.ts +++ b/packages/context/src/binding.ts @@ -383,7 +383,7 @@ export class Binding { * * @param provider The value provider to use. */ - public toProvider(providerClass: Constructor>): this { + toProvider(providerClass: Constructor>): this { /* istanbul ignore if */ if (debug.enabled) { debug('Bind %s to provider %s', this.key, providerClass.name); @@ -436,4 +436,14 @@ export class Binding { } return json; } + + /** + * A static method to create a binding so that we can do + * `Binding.bind('foo').to('bar');` as `new Binding('foo').to('bar')` is not + * easy to read. + * @param key Binding key + */ + static bind(key: string): Binding { + return new Binding(key); + } } diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index 70fdc346c623..5575e3edea78 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -42,21 +42,23 @@ export class Context { * Create a binding with the given key in the context. If a locked binding * already exists with the same key, an error will be thrown. * - * @param keyOrBinding Binding key or a binding + * @param key Binding key */ bind( - keyOrBinding: BindingAddress | Binding, + key: BindingAddress, ): Binding { - let key: string; - let binding: Binding; - if (keyOrBinding instanceof Binding) { - key = keyOrBinding.key; - binding = keyOrBinding; - } else { - key = keyOrBinding.toString(); - binding = new Binding(key); - } + const binding = new Binding(key.toString()); + this.add(binding); + return binding; + } + /** + * Add a binding to the context. If a locked binding already exists with the + * same key, an error will be thrown. + * @param binding The configured binding to be added + */ + add(binding: Binding): this { + const key = binding.key; /* istanbul ignore if */ if (debug.enabled) { debug('Adding binding: %s', key); @@ -70,7 +72,7 @@ export class Context { throw new Error(`Cannot rebind key "${key}" to a locked binding`); } this.registry.set(key, binding); - return binding; + return this; } /** diff --git a/packages/context/test/unit/context.unit.ts b/packages/context/test/unit/context.unit.ts index 347c3080f399..e0896a8c9265 100644 --- a/packages/context/test/unit/context.unit.ts +++ b/packages/context/test/unit/context.unit.ts @@ -71,13 +71,6 @@ describe('Context', () => { expect(result).to.be.true(); }); - it('accepts a binding', () => { - const binding = new Binding('foo').to('bar'); - expect(ctx.bind(binding)).to.be.exactly(binding); - const result = ctx.contains('foo'); - expect(result).to.be.true(); - }); - it('returns a binding', () => { const binding = ctx.bind('foo'); expect(binding).to.be.instanceOf(Binding); @@ -87,6 +80,30 @@ describe('Context', () => { const key = 'a' + BindingKey.PROPERTY_SEPARATOR + 'b'; expect(() => ctx.bind(key)).to.throw(/Binding key .* cannot contain/); }); + + it('rejects rebinding of a locked key', () => { + ctx.bind('foo').lock(); + expect(() => ctx.bind('foo')).to.throw( + 'Cannot rebind key "foo" to a locked binding', + ); + }); + }); + + describe('add', () => { + it('accepts a binding', () => { + const binding = new Binding('foo').to('bar'); + ctx.add(binding); + expect(ctx.getBinding(binding.key)).to.be.exactly(binding); + const result = ctx.contains('foo'); + expect(result).to.be.true(); + }); + + it('rejects rebinding of a locked key', () => { + ctx.bind('foo').lock(); + expect(() => ctx.add(new Binding('foo'))).to.throw( + 'Cannot rebind key "foo" to a locked binding', + ); + }); }); describe('contains', () => { diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index f1553c2fbcf3..1c051f381d3c 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -3,17 +3,24 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Constructor, Provider, BoundValue} from '@loopback/context'; +import {Constructor, Provider, BoundValue, Binding} from '@loopback/context'; import {Server} from './server'; import {Application, ControllerClass} from './application'; /** - * A map of name/class pairs for binding providers + * A map of provider classes to be bound to a context */ export interface ProviderMap { [key: string]: Constructor>; } +/** + * A map of classes to be bound to a context + */ +export interface ClassMap { + [key: string]: Constructor; +} + /** * A component declares a set of artifacts so that they cane be contributed to * an application as a group @@ -23,10 +30,30 @@ export interface Component { * An array of controller classes */ controllers?: ControllerClass[]; + /** - * A map of name/class pairs for binding providers + * A map of providers to be bound to the application context + * * For example: + * ```ts + * { + * 'authentication.strategies.ldap': LdapStrategyProvider + * } + * ``` */ providers?: ProviderMap; + + /** + * A map of classes to be bound to the application context. + * + * For example: + * ```ts + * { + * 'rest.body-parsers.xml': XmlBodyParser + * } + * ``` + */ + classes?: ClassMap; + /** * A map of name/class pairs for servers */ @@ -34,6 +61,15 @@ export interface Component { [name: string]: Constructor; }; + /** + * An array of bindings to be aded to the application context. For example, + * ```ts + * const bindingX = new Binding('x').to('Value X'); + * this.bindings = [bindingX] + * ``` + */ + bindings?: Binding[]; + /** * Other properties */ @@ -49,9 +85,9 @@ export interface Component { * @param {Component} component */ export function mountComponent(app: Application, component: Component) { - if (component.controllers) { - for (const controllerCtor of component.controllers) { - app.controller(controllerCtor); + if (component.classes) { + for (const classKey in component.classes) { + app.bind(classKey).toClass(component.classes[classKey]); } } @@ -61,6 +97,18 @@ export function mountComponent(app: Application, component: Component) { } } + if (component.bindings) { + for (const binding of component.bindings) { + app.add(binding); + } + } + + if (component.controllers) { + for (const controllerCtor of component.controllers) { + app.controller(controllerCtor); + } + } + if (component.servers) { for (const serverKey in component.servers) { app.server(component.servers[serverKey], serverKey); diff --git a/packages/core/test/unit/application.unit.ts b/packages/core/test/unit/application.unit.ts index 7bf921875ecc..c2c09b649b81 100644 --- a/packages/core/test/unit/application.unit.ts +++ b/packages/core/test/unit/application.unit.ts @@ -3,9 +3,15 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Constructor, Context} from '@loopback/context'; import {expect} from '@loopback/testlab'; -import {Application, Component, Server} from '../..'; +import {Application, Server, Component, CoreBindings} from '../..'; +import { + Context, + Constructor, + Binding, + Provider, + inject, +} from '@loopback/context'; describe('Application', () => { describe('controller binding', () => { @@ -35,10 +41,8 @@ describe('Application', () => { describe('component binding', () => { let app: Application; - class MyController {} - class MyComponent implements Component { - controllers = [MyController]; - } + + class MyComponent implements Component {} beforeEach(givenApp); @@ -56,6 +60,73 @@ describe('Application', () => { ); }); + it('binds controllers from a component', () => { + class MyController {} + + class MyComponentWithControllers implements Component { + controllers = [MyController]; + } + + app.component(MyComponentWithControllers); + expect( + app.getBinding('controllers.MyController').valueConstructor, + ).to.be.exactly(MyController); + }); + + it('binds bindings from a component', () => { + const binding = new Binding('foo'); + class MyComponentWithBindings implements Component { + bindings = [binding]; + } + + app.component(MyComponentWithBindings); + expect(app.getBinding('foo')).to.be.exactly(binding); + }); + + it('binds classes from a component', () => { + class MyClass {} + + class MyComponentWithClasses implements Component { + classes = {'my-class': MyClass}; + } + + app.component(MyComponentWithClasses); + expect(app.contains('my-class')).to.be.true(); + expect(app.getBinding('my-class').valueConstructor).to.be.exactly( + MyClass, + ); + expect(app.getSync('my-class')).to.be.instanceof(MyClass); + }); + + it('binds providers from a component', () => { + class MyProvider implements Provider { + value() { + return 'my-str'; + } + } + + class MyComponentWithProviders implements Component { + providers = {'my-provider': MyProvider}; + } + + app.component(MyComponentWithProviders); + expect(app.contains('my-provider')).to.be.true(); + expect(app.getSync('my-provider')).to.be.eql('my-str'); + }); + + it('binds from a component constructor', () => { + class MyComponentWithDI implements Component { + constructor(@inject(CoreBindings.APPLICATION_INSTANCE) ctx: Context) { + // Programmatically bind to the context + ctx.bind('foo').to('bar'); + } + } + + app.component(MyComponentWithDI); + expect(app.contains('foo')).to.be.true(); + expect(app.getSync('foo')).to.be.eql('bar'); + }); + function givenApp() { app = new Application(); } From 8075c525e311c4f1096c3c34c875207b04df32c4 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 8 Nov 2018 08:19:49 -0800 Subject: [PATCH 3/4] chore(context): simplify generic typing for context methods --- packages/context/src/context.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index 5575e3edea78..b512d58630ac 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -57,7 +57,7 @@ export class Context { * same key, an error will be thrown. * @param binding The configured binding to be added */ - add(binding: Binding): this { + add(binding: Binding): this { const key = binding.key; /* istanbul ignore if */ if (debug.enabled) { @@ -85,7 +85,7 @@ export class Context { * @param key Binding key * @returns true if the binding key is found and removed from this context */ - unbind(key: BindingAddress): boolean { + unbind(key: BindingAddress): boolean { key = BindingKey.validate(key); const binding = this.registry.get(key); if (binding == null) return false; @@ -99,7 +99,7 @@ export class Context { * delegating to the parent context * @param key Binding key */ - contains(key: BindingAddress): boolean { + contains(key: BindingAddress): boolean { key = BindingKey.validate(key); return this.registry.has(key); } @@ -108,7 +108,7 @@ export class Context { * Check if a key is bound in the context or its ancestors * @param key Binding key */ - isBound(key: BindingAddress): boolean { + isBound(key: BindingAddress): boolean { if (this.contains(key)) return true; if (this._parent) { return this._parent.isBound(key); @@ -120,9 +120,7 @@ export class Context { * Get the owning context for a binding key * @param key Binding key */ - getOwnerContext( - key: BindingAddress, - ): Context | undefined { + getOwnerContext(key: BindingAddress): Context | undefined { if (this.contains(key)) return this; if (this._parent) { return this._parent.getOwnerContext(key); From cc96229ff27b590943612e0cc3804b360e55e012 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 9 Nov 2018 08:21:57 -0800 Subject: [PATCH 4/4] chore(core): use Binding.bind() to create new bindings --- docs/site/Creating-components.md | 6 +++--- packages/core/src/component.ts | 2 +- packages/core/test/unit/application.unit.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/site/Creating-components.md b/docs/site/Creating-components.md index ca5239069db6..ea9b8ed2e398 100644 --- a/docs/site/Creating-components.md +++ b/docs/site/Creating-components.md @@ -24,8 +24,8 @@ export class MyComponent implements Component { 'my-validator': MyValidator, }; - const bindingX = new Binding('x').to('Value X'); - const bindingY = new Binding('y').toClass(ClassY); + const bindingX = Binding.bind('x').to('Value X'); + const bindingY = Binding.bind('y').toClass(ClassY); this.bindings = [bindingX, bindingY]; } } @@ -45,7 +45,7 @@ Please note that `providers` and `classes` are shortcuts for provider and class `bindings`. The example `MyComponent` above will add `MyController` to application's API and -create the following bindings to the application context: +create the following bindings in the application context: - `my-value` -> `MyValueProvider` (provider) - `my-validator` -> `MyValidator` (class) diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 1c051f381d3c..5ff08d93b5da 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -64,7 +64,7 @@ export interface Component { /** * An array of bindings to be aded to the application context. For example, * ```ts - * const bindingX = new Binding('x').to('Value X'); + * const bindingX = Binding.bind('x').to('Value X'); * this.bindings = [bindingX] * ``` */ diff --git a/packages/core/test/unit/application.unit.ts b/packages/core/test/unit/application.unit.ts index c2c09b649b81..38cff50cb2e3 100644 --- a/packages/core/test/unit/application.unit.ts +++ b/packages/core/test/unit/application.unit.ts @@ -74,7 +74,7 @@ describe('Application', () => { }); it('binds bindings from a component', () => { - const binding = new Binding('foo'); + const binding = Binding.bind('foo'); class MyComponentWithBindings implements Component { bindings = [binding]; }