Skip to content
Merged
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
23 changes: 21 additions & 2 deletions docs/site/Creating-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export class MyComponent implements Component {
this.providers = {
'my-value': MyValueProvider,
};
this.classes = {
'my-validator': MyValidator,
};

const bindingX = Binding.bind('x').to('Value X');
const bindingY = Binding.bind('y').toClass(ClassY);
this.bindings = [bindingX, bindingY];
}
}
```
Expand All @@ -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 in the application context:

- `my-value` -> `MyValueProvider` (provider)
- `my-validator` -> `MyValidator` (class)
- `x` -> `'Value X'` (value)
- `y` -> `ClassY` (class)

## Providers

Expand Down
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);
}
}
28 changes: 18 additions & 10 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(binding: Binding<unknown>): 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 All @@ -75,7 +85,7 @@ export class Context {
* @param key Binding key
* @returns true if the binding key is found and removed from this context
*/
unbind<ValueType = BoundValue>(key: BindingAddress<ValueType>): boolean {
unbind(key: BindingAddress<unknown>): boolean {
key = BindingKey.validate(key);
const binding = this.registry.get(key);
if (binding == null) return false;
Expand All @@ -89,7 +99,7 @@ export class Context {
* delegating to the parent context
* @param key Binding key
*/
contains<ValueType = BoundValue>(key: BindingAddress<ValueType>): boolean {
contains(key: BindingAddress<unknown>): boolean {
key = BindingKey.validate(key);
return this.registry.has(key);
}
Expand All @@ -98,7 +108,7 @@ export class Context {
* Check if a key is bound in the context or its ancestors
* @param key Binding key
*/
isBound<ValueType = BoundValue>(key: BindingAddress<ValueType>): boolean {
isBound(key: BindingAddress<unknown>): boolean {
if (this.contains(key)) return true;
if (this._parent) {
return this._parent.isBound(key);
Expand All @@ -110,9 +120,7 @@ export class Context {
* Get the owning context for a binding key
* @param key Binding key
*/
getOwnerContext<ValueType = BoundValue>(
key: BindingAddress<ValueType>,
): Context | undefined {
getOwnerContext(key: BindingAddress<unknown>): Context | undefined {
if (this.contains(key)) return this;
if (this._parent) {
return this._parent.getOwnerContext(key);
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
60 changes: 54 additions & 6 deletions packages/core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Provider<BoundValue>>;
}

/**
* A map of classes to be bound to a context
*/
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 @@ -23,17 +30,46 @@ 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;
Copy link
Member

@bajtos bajtos Nov 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this name confusing. What does it mean for a component to export a class? Some classes are exported via JavaScript/TypeScript exports, e.g. export class FooBar. Why should be some classes exported via JS/TS and some others exported via Component properties?

Personally, I prefer to remove this API entirely, keep providers for backwards compatibility (possibly even deprecate it) and ask users to leverage bindings API for all bindings.

If you feel it's important to keep this shortcut, then let's find a better name please. For example:classBindings or classesToBind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added jsdocs to make it clear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am afraid jsdocs are not good enough. Consider the following code snippet from your pull request:

      class MyComponentWithClasses implements Component {
         classes = {'my-class': MyClass};
       }

The person reading the code (e.g. while reviewing a pull request on GitHub) does not see tscode comments.


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

/**
* An array of bindings to be aded to the application context. For example,
* ```ts
* const bindingX = Binding.bind('x').to('Value X');
* this.bindings = [bindingX]
* ```
*/
bindings?: Binding[];

/**
* Other properties
*/
Expand All @@ -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]);
}
}

Expand All @@ -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);
Expand Down
83 changes: 77 additions & 6 deletions packages/core/test/unit/application.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);

Expand All @@ -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 = Binding.bind('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<string> {
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();
}
Expand Down