Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ export class Binding<T = BoundValue> {
*
* @param provider The value provider to use.
*/
public toProvider(providerClass: Constructor<Provider<T>>): this {
toProvider(providerClass: Constructor<Provider<T>>): this {
/* istanbul ignore if */
if (debug.enabled) {
debug('Bind %s to provider %s', this.key, providerClass.name);
Expand Down Expand Up @@ -436,4 +436,14 @@ export class Binding<T = BoundValue> {
}
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);
}
}
18 changes: 14 additions & 4 deletions packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,32 @@ export class Context {
bind<ValueType = BoundValue>(
key: BindingAddress<ValueType>,
): Binding<ValueType> {
const binding = new Binding<ValueType>(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<ValueType = BoundValue>(binding: Binding<ValueType>): this {
const key = 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);
const bindingIsLocked = existingBinding && existingBinding.isLocked;
if (bindingIsLocked)
throw new Error(`Cannot rebind key "${key}" to a locked binding`);
}

const binding = new Binding<ValueType>(key);
this.registry.set(key, binding);
return binding;
return this;
}

/**
Expand Down
24 changes: 24 additions & 0 deletions packages/context/test/unit/context.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,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', () => {
Expand Down
36 changes: 26 additions & 10 deletions packages/core/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class Application extends Context {
* Add a component to this application and register extensions such as
* controllers, providers, and servers from the component.
*
* @param componentCtor The component class to add.
* @param componentClassOrInstance The component class or instance to add.
* @param {string=} name Optional component name, default to the class name
*
* ```ts
Expand All @@ -183,16 +183,32 @@ export class Application extends Context {
* app.component(ProductComponent);
* ```
*/
public component(componentCtor: Constructor<Component>, name?: string) {
name = name || componentCtor.name;
const componentKey = `components.${name}`;
this.bind(componentKey)
.toClass(componentCtor)
.inScope(BindingScope.SINGLETON)
.tag('component');
// Assuming components can be synchronously instantiated
const instance = this.getSync<Component>(componentKey);
public component(
componentClassOrInstance: Constructor<Component> | Component,
name?: string,
) {
let binding: Binding;
let instance: Component;
if (typeof componentClassOrInstance === 'function') {
name = name || componentClassOrInstance.name;
const componentKey = `components.${name}`;
binding = this.bind(componentKey)
.toClass(componentClassOrInstance)
.inScope(BindingScope.SINGLETON)
.tag('component');
// Assuming components can be synchronously instantiated
instance = this.getSync<Component>(componentKey);
} else {
name = name || componentClassOrInstance.name;
instance = componentClassOrInstance;
const componentKey = `components.${name}`;
binding = this.bind(componentKey)
.to(componentClassOrInstance)
.inScope(BindingScope.SINGLETON)
.tag('component');
}
mountComponent(this, instance);
return binding;
}
}

Expand Down
40 changes: 35 additions & 5 deletions packages/core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// 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';

Expand All @@ -14,6 +14,10 @@ export interface ProviderMap {
[key: string]: Constructor<Provider<BoundValue>>;
}

export interface ClassMap {
[key: string]: Constructor<BoundValue>;
}

/**
* A component declares a set of artifacts so that they cane be contributed to
* an application as a group
Expand All @@ -27,13 +31,21 @@ export interface Component {
* A map of name/class pairs for binding providers
*/
providers?: ProviderMap;

classes?: ClassMap;

/**
* A map of name/class pairs for servers
*/
servers?: {
[name: string]: Constructor<Server>;
};

/**
* An array of bindings
*/
bindings?: Binding[];

/**
* Other properties
*/
Expand All @@ -49,15 +61,33 @@ 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) {
const binding = Binding.bind(classKey).toClass(
component.classes[classKey],
);
app.add(binding);
}
}

if (component.providers) {
for (const providerKey in component.providers) {
app.bind(providerKey).toProvider(component.providers[providerKey]);
const binding = Binding.bind(providerKey).toProvider(
component.providers[providerKey],
);
app.add(binding);
}
}

if (component.bindings) {
for (const binding of component.bindings) {
app.add(binding);
}
}

if (component.controllers) {
for (const controllerCtor of component.controllers) {
app.controller(controllerCtor);
}
}

Expand Down
74 changes: 71 additions & 3 deletions packages/core/test/unit/application.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {Application, Server, Component} from '../../index';
import {Context, Constructor} from '@loopback/context';
import {Application, Server, Component, CoreBindings} from '../..';
import {
Context,
Constructor,
Binding,
Provider,
inject,
} from '@loopback/context';

describe('Application', () => {
describe('controller binding', () => {
Expand Down Expand Up @@ -35,27 +41,89 @@ describe('Application', () => {

describe('component binding', () => {
let app: Application;
const binding = new Binding('foo');
class MyController {}
class MyClass {}
class MyProvider implements Provider<string> {
value() {
return 'my-str';
}
}
class MyComponent implements Component {
controllers = [MyController];
bindings = [binding];
classes = {'my-class': MyClass};
providers = {'my-provider': MyProvider};
}

const aComponent: Component = {
controllers: [MyController],
name: 'AnotherComponent',
};

class MyComponentWithDI implements Component {
constructor(@inject(CoreBindings.APPLICATION_INSTANCE) ctx: Context) {
// Porgramatically bind to the context
ctx.bind('foo').to('bar');
}
}

beforeEach(givenApp);

it('binds a component', () => {
it('binds a component by class', () => {
app.component(MyComponent);
expect(findKeysByTag(app, 'component')).to.containEql(
'components.MyComponent',
);
});

it('binds a component by instance with a custom name', () => {
app.component(aComponent, 'YourComponent');
expect(findKeysByTag(app, 'component')).to.containEql(
'components.YourComponent',
);
});

it('binds a component by instance', () => {
app.component(aComponent);
expect(findKeysByTag(app, 'component')).to.containEql(
'components.AnotherComponent',
);
});

it('binds a component with custom name', () => {
app.component(MyComponent, 'my-component');
expect(findKeysByTag(app, 'component')).to.containEql(
'components.my-component',
);
});

it('binds bindings from a component', () => {
app.component(MyComponent);
expect(app.contains('controllers.MyController')).to.be.true();
expect(app.getBinding('foo')).to.be.exactly(binding);
});

it('binds classes from a component', () => {
app.component(MyComponent);
expect(app.contains('my-class')).to.be.true();
expect(app.getBinding('my-class').valueConstructor).to.be.exactly(
MyClass,
);
});

it('binds providers from a component', () => {
app.component(MyComponent);
expect(app.contains('my-provider')).to.be.true();
expect(app.getSync('my-provider')).to.be.eql('my-str');
});

it('binds from a component constructor', () => {
app.component(MyComponentWithDI);
expect(app.contains('foo')).to.be.true();
expect(app.getSync('foo')).to.be.eql('bar');
});

function givenApp() {
app = new Application();
}
Expand Down