Skip to content

Commit

Permalink
feat(context): honor binding scope from @Bind
Browse files Browse the repository at this point in the history
- Refine access visibility for binding attributes
- Introduce `defaultScope` to only override binding scope if not set
  • Loading branch information
raymondfeng committed Mar 15, 2019
1 parent f6cf0c6 commit 3b30f01
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 29 deletions.
22 changes: 22 additions & 0 deletions docs/site/Binding.md
Expand Up @@ -229,6 +229,28 @@ const binding = createBindingFromClass(MyService);
ctx.add(binding);
```
Please note `createBindingFromClass` also accepts an optional `options`
parameter of `BindingFromClassOptions` type with the following settings:
- key: Binding key, such as `controllers.MyController`
- type: Artifact type, such as `server`, `controller`, `repository` or `service`
- name: Artifact name, such as `my-rest-server` and `my-controller`, default to
the name of the bound class
- namespace: Namespace for the binding key, such as `servers` and `controllers`.
If `key` does not exist, its value is calculated as `<namespace>.<name>`.
- typeNamespaceMapping: Mapping artifact type to binding key namespaces, such
as:
```ts
{
controller: 'controllers',
repository: 'repositories'
}
```
- defaultScope: Default scope if the binding does not have an explicit scope
set. The `scope` from `@bind` of the bound class takes precedence.
### Encoding value types in binding keys
String keys for bindings do not help enforce the value type. Consider the
Expand Down
Expand Up @@ -72,4 +72,42 @@ describe('@bind - customize classes with binding attributes', () => {
'controllers.my-controller',
]);
});

it('supports default binding scope in options', () => {
const binding = createBindingFromClass(MyController, {
defaultScope: BindingScope.SINGLETON,
});
expect(binding.scope).to.equal(BindingScope.SINGLETON);
});

describe('binding scope', () => {
@bind({
// Explicitly set the binding scope to be `SINGLETON` as the developer
// choose to implement the controller as a singleton without depending
// on request specific information
scope: BindingScope.SINGLETON,
})
class MySingletonController {}

it('allows singleton controller with @bind', () => {
const binding = createBindingFromClass(MySingletonController, {
type: 'controller',
});
expect(binding.key).to.equal('controllers.MySingletonController');
expect(binding.tagMap).to.containEql({controller: 'controller'});
expect(binding.scope).to.equal(BindingScope.SINGLETON);
});

it('honors binding scope from @bind over defaultScope', () => {
let binding = createBindingFromClass(MySingletonController, {
defaultScope: BindingScope.TRANSIENT,
});
expect(binding.scope).to.equal(BindingScope.SINGLETON);
});

it('honors binding scope from @bind', () => {
const binding = createBindingFromClass(MySingletonController);
expect(binding.scope).to.equal(BindingScope.SINGLETON);
});
});
});
20 changes: 19 additions & 1 deletion packages/context/src/__tests__/unit/binding.unit.ts
Expand Up @@ -29,6 +29,11 @@ describe('Binding', () => {
it('sets the binding lock state to unlocked by default', () => {
expect(binding.isLocked).to.be.false();
});

it('leaves other states to `undefined` by default', () => {
expect(binding.type).to.be.undefined();
expect(binding.valueConstructor).to.be.undefined();
});
});

describe('lock', () => {
Expand Down Expand Up @@ -76,7 +81,7 @@ describe('Binding', () => {
});

describe('inScope', () => {
it('defaults the transient binding scope', () => {
it('defaults to `TRANSIENT` binding scope', () => {
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
});

Expand All @@ -96,6 +101,19 @@ describe('Binding', () => {
});
});

describe('applyDefaultScope', () => {
it('sets the scope if not set', () => {
binding.applyDefaultScope(BindingScope.SINGLETON);
expect(binding.scope).to.equal(BindingScope.SINGLETON);
});

it('does not override the existing scope', () => {
binding.inScope(BindingScope.TRANSIENT);
binding.applyDefaultScope(BindingScope.SINGLETON);
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
});
});

describe('to(value)', () => {
it('returns the value synchronously', () => {
binding.to('value');
Expand Down
16 changes: 14 additions & 2 deletions packages/context/src/binding-inspector.ts
Expand Up @@ -193,11 +193,20 @@ export type BindingFromClassOptions = {
* Mapping artifact type to binding key namespaces
*/
typeNamespaceMapping?: TypeNamespaceMapping;
/**
* Default scope if the binding does not have an explicit scope
*/
defaultScope?: BindingScope;
};

/**
* Create a binding from a class with decorated metadata
* @param cls A class
* Create a binding from a class with decorated metadata. The class is attached
* to the binding as follows:
* - `binding.toClass(cls)`: if `cls` is a plain class such as `MyController`
* - `binding.toProvider(cls)`: it `cls` is a value provider class with a
* prototype method `value()`
*
* @param cls A class. It can be either a plain class or a value provider class
* @param options Options to customize the binding key
*/
export function createBindingFromClass<T = unknown>(
Expand All @@ -216,6 +225,9 @@ export function createBindingFromClass<T = unknown>(
if (options.type) {
binding.tag({type: options.type}, options.type);
}
if (options.defaultScope) {
binding.applyDefaultScope(options.defaultScope);
}
return binding;
}

Expand Down
46 changes: 36 additions & 10 deletions packages/context/src/binding.ts
Expand Up @@ -141,30 +141,39 @@ export class Binding<T = BoundValue> {
/**
* Map for tag name/value pairs
*/

public readonly tagMap: TagMap = {};

private _scope?: BindingScope;
/**
* Scope of the binding to control how the value is cached/shared
*/
public scope: BindingScope = BindingScope.TRANSIENT;
public get scope(): BindingScope {
// Default to TRANSIENT if not set
return this._scope || BindingScope.TRANSIENT;
}

private _type?: BindingType;
/**
* Type of the binding value getter
*/
public type: BindingType;
public get type(): BindingType | undefined {
return this._type;
}

private _cache: WeakMap<Context, T>;
private _getValue: (
ctx?: Context,
session?: ResolutionSession,
) => ValueOrPromise<T>;

private _valueConstructor?: Constructor<T>;
/**
* For bindings bound via toClass, this property contains the constructor
* function
*/
public valueConstructor: Constructor<T>;
public get valueConstructor(): Constructor<T> | undefined {
return this._valueConstructor;
}

constructor(key: string, public isLocked: boolean = false) {
BindingKey.validate(key);
Expand All @@ -190,6 +199,7 @@ export class Binding<T = BoundValue> {
// Cache the value at the current context
this._cache.set(ctx, val);
}
// Do not cache for `TRANSIENT`
return val;
});
}
Expand Down Expand Up @@ -302,8 +312,24 @@ export class Binding<T = BoundValue> {
return Object.keys(this.tagMap);
}

/**
* Set the binding scope
* @param scope Binding scope
*/
inScope(scope: BindingScope): this {
this.scope = scope;
this._scope = scope;
return this;
}

/**
* Apply default scope to the binding. It only changes the scope if it's not
* set yet
* @param scope Default binding scope
*/
applyDefaultScope(scope: BindingScope): this {
if (!this._scope) {
this._scope = scope;
}
return this;
}

Expand Down Expand Up @@ -345,7 +371,7 @@ export class Binding<T = BoundValue> {
if (debug.enabled) {
debug('Bind %s to constant:', this.key, value);
}
this.type = BindingType.CONSTANT;
this._type = BindingType.CONSTANT;
this._getValue = () => value;
return this;
}
Expand Down Expand Up @@ -373,7 +399,7 @@ export class Binding<T = BoundValue> {
if (debug.enabled) {
debug('Bind %s to dynamic value:', this.key, factoryFn);
}
this.type = BindingType.DYNAMIC_VALUE;
this._type = BindingType.DYNAMIC_VALUE;
this._getValue = ctx => factoryFn();
return this;
}
Expand All @@ -399,7 +425,7 @@ export class Binding<T = BoundValue> {
if (debug.enabled) {
debug('Bind %s to provider %s', this.key, providerClass.name);
}
this.type = BindingType.PROVIDER;
this._type = BindingType.PROVIDER;
this._getValue = (ctx, session) => {
const providerOrPromise = instantiateClass<Provider<T>>(
providerClass,
Expand All @@ -423,9 +449,9 @@ export class Binding<T = BoundValue> {
if (debug.enabled) {
debug('Bind %s to class %s', this.key, ctor.name);
}
this.type = BindingType.CLASS;
this._type = BindingType.CLASS;
this._getValue = (ctx, session) => instantiateClass(ctor, ctx!, session);
this.valueConstructor = ctor;
this._valueConstructor = ctor;
return this;
}

Expand Down
52 changes: 43 additions & 9 deletions packages/core/src/__tests__/unit/application.unit.ts
Expand Up @@ -3,15 +3,17 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

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

describe('Application', () => {
describe('controller binding', () => {
Expand All @@ -24,6 +26,7 @@ describe('Application', () => {
const binding = app.controller(MyController);
expect(Array.from(binding.tagNames)).to.containEql('controller');
expect(binding.key).to.equal('controllers.MyController');
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
expect(findKeysByTag(app, 'controller')).to.containEql(binding.key);
});

Expand All @@ -34,6 +37,15 @@ describe('Application', () => {
expect(findKeysByTag(app, 'controller')).to.containEql(binding.key);
});

it('binds a singleton controller', () => {
@bind({scope: BindingScope.SINGLETON})
class MySingletonController {}

const binding = app.controller(MySingletonController);
expect(binding.scope).to.equal(BindingScope.SINGLETON);
expect(findKeysByTag(app, 'controller')).to.containEql(binding.key);
});

function givenApp() {
app = new Application();
}
Expand All @@ -47,7 +59,8 @@ describe('Application', () => {
beforeEach(givenApp);

it('binds a component', () => {
app.component(MyComponent);
const binding = app.component(MyComponent);
expect(binding.scope).to.equal(BindingScope.SINGLETON);
expect(findKeysByTag(app, 'component')).to.containEql(
'components.MyComponent',
);
Expand All @@ -60,6 +73,14 @@ describe('Application', () => {
);
});

it('binds a transient component', () => {
@bind({scope: BindingScope.TRANSIENT})
class MyTransientComponent {}

const binding = app.component(MyTransientComponent);
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
});

it('binds controllers from a component', () => {
class MyController {}

Expand Down Expand Up @@ -133,24 +154,33 @@ describe('Application', () => {
});

describe('server binding', () => {
let app: Application;
beforeEach(givenApplication);

it('defaults to constructor name', async () => {
const app = new Application();
const binding = app.server(FakeServer);
expect(binding.scope).to.equal(BindingScope.SINGLETON);
expect(Array.from(binding.tagNames)).to.containEql('server');
const result = await app.getServer(FakeServer.name);
expect(result.constructor.name).to.equal(FakeServer.name);
});

it('binds a server with a different scope than SINGLETON', async () => {
@bind({scope: BindingScope.TRANSIENT})
class TransientServer extends FakeServer {}

const binding = app.server(TransientServer);
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
});

it('allows custom name', async () => {
const app = new Application();
const name = 'customName';
app.server(FakeServer, name);
const result = await app.getServer(name);
expect(result.constructor.name).to.equal(FakeServer.name);
});

it('allows binding of multiple servers as an array', async () => {
const app = new Application();
const bindings = app.servers([FakeServer, AnotherServer]);
expect(Array.from(bindings[0].tagNames)).to.containEql('server');
expect(Array.from(bindings[1].tagNames)).to.containEql('server');
Expand All @@ -159,6 +189,10 @@ describe('Application', () => {
const AnotherResult = await app.getServer(AnotherServer);
expect(AnotherResult.constructor.name).to.equal(AnotherServer.name);
});

function givenApplication() {
app = new Application();
}
});

describe('start', () => {
Expand Down

0 comments on commit 3b30f01

Please sign in to comment.