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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 3b30f01

Please sign in to comment.