Skip to content

Commit

Permalink
feat(context): allow @inject.setter to accept binding templates
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Mar 27, 2019
1 parent be5b8f6 commit e88e35f
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 48 deletions.
23 changes: 23 additions & 0 deletions docs/site/Decorators_inject.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,29 @@ export class HelloController {
}
```

The `setter` function injected has the following signature:

```ts
/**
* Set the binding with one or more `BindingTemplate` functions or values
* @param templateFnsOrValues
*/
(...templateFnsOrValues: (T | BindingTemplate<T>)[]): Binding<T>;
```
It takes either a const value or `BindingTemplate` functions to create (if not
existent) or update the binding. For example:
```ts
const binding = this.greetingSetter('Greetings!');
```

or

```ts
const binding = this.greetingSetter(b => b.toDynamicValue(() => 'Greetings!'));
```

### @inject.tag

`@inject.tag` injects an array of values by a pattern or regexp to match binding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {Context, instantiateClass} from '@loopback/context';
import {Binding, Context, instantiateClass} from '@loopback/context';
import {Request} from '@loopback/rest';
import {AuthenticateFn, UserProfile, AuthenticationBindings} from '../../..';
import {MockStrategy} from '../fixtures/mock-strategy';
import {expect} from '@loopback/testlab';
import {Strategy} from 'passport';
import {AuthenticateFn, AuthenticationBindings, UserProfile} from '../../..';
import {AuthenticateActionProvider} from '../../../providers';
import {MockStrategy} from '../fixtures/mock-strategy';

describe('AuthenticateActionProvider', () => {
describe('constructor()', () => {
Expand Down Expand Up @@ -110,7 +110,10 @@ describe('AuthenticateActionProvider', () => {
strategy.setMockUser(mockUser);
provider = new AuthenticateActionProvider(
() => Promise.resolve(strategy),
u => (currentUser = u),
(...u) => {
currentUser = u[0] as UserProfile;
return new Binding('authentication.currentUser').to(currentUser);
},
);
currentUser = undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,25 @@ describe('Context bindings - Injecting dependencies of classes', () => {
expect(await store.getter()).to.equal('456');
});

it('creates getter from a value', () => {
const getter = Getter.fromValue('data');
expect(getter).to.be.a.Function();
return expect(getter()).to.be.fulfilledWith('data');
});

it('reports an error if @inject.getter has a non-function target', async () => {
ctx.bind('key').to('value');

class Store {
constructor(@inject.getter('key') public getter: string) {}
}

ctx.bind(STORE_KEY).toClass(Store);
expect(() => ctx.getSync<Store>(STORE_KEY)).to.throw(
'The type of Store.constructor[0] (String) is not a Getter function',
);
});

describe('in SINGLETON scope', () => {
it('throws if a getter cannot be resolved by the owning context', async () => {
class Store {
Expand Down Expand Up @@ -317,49 +336,66 @@ describe('Context bindings - Injecting dependencies of classes', () => {
}
});

it('injects a setter function', async () => {
describe('@inject.setter', () => {
class Store {
constructor(@inject.setter(HASH_KEY) public setter: Setter<string>) {}
}

ctx.bind(STORE_KEY).toClass(Store);
const store = ctx.getSync<Store>(STORE_KEY);
it('injects a setter function', async () => {
ctx.bind(STORE_KEY).toClass(Store);
const store = ctx.getSync<Store>(STORE_KEY);

expect(store.setter).to.be.Function();
store.setter('a-value');
expect(ctx.getSync(HASH_KEY)).to.equal('a-value');
});
expect(store.setter).to.be.Function();
store.setter('a-value');
expect(ctx.getSync(HASH_KEY)).to.equal('a-value');
});

it('creates getter from a value', () => {
const getter = Getter.fromValue('data');
expect(getter).to.be.a.Function();
return expect(getter()).to.be.fulfilledWith('data');
});
it('injects a setter function that returns Binding', async () => {
ctx.bind(STORE_KEY).toClass(Store);
const store = ctx.getSync<Store>(STORE_KEY);

it('reports an error if @inject.getter has a non-function target', async () => {
ctx.bind('key').to('value');
expect(store.setter).to.be.Function();
store.setter().to('a-value');
expect(ctx.getSync(HASH_KEY)).to.equal('a-value');
});

class Store {
constructor(@inject.getter('key') public getter: string) {}
}
it('injects a setter function to apply templates', async () => {
ctx.bind(STORE_KEY).toClass(Store);
const store = ctx.getSync<Store>(STORE_KEY);

ctx.bind(STORE_KEY).toClass(Store);
expect(() => ctx.getSync<Store>(STORE_KEY)).to.throw(
'The type of Store.constructor[0] (String) is not a Getter function',
);
});
expect(store.setter).to.be.Function();
store.setter(b => b.to('a-value').tag('a-tag'));
expect(ctx.getSync(HASH_KEY)).to.equal('a-value');
expect(ctx.getBinding(HASH_KEY).tagNames).to.containEql('a-tag');
});

it('reports an error if @inject.setter has a non-function target', async () => {
ctx.bind('key').to('value');
it('injects a setter function that uses an existing binding', async () => {
// Create a binding for hash key
ctx
.bind(HASH_KEY)
.to('123')
.tag('hash');
ctx.bind(STORE_KEY).toClass(Store);
const store = ctx.getSync<Store>(STORE_KEY);
// Change the hash value
store.setter('a-value');
expect(ctx.getSync(HASH_KEY)).to.equal('a-value');
// The tag is kept
expect(ctx.getBinding(HASH_KEY).tagNames).to.containEql('hash');
});

class Store {
constructor(@inject.setter('key') public setter: object) {}
}
it('reports an error if @inject.setter has a non-function target', async () => {
class StoreWithWrongSetterType {
constructor(@inject.setter(HASH_KEY) public setter: object) {}
}

ctx.bind(STORE_KEY).toClass(Store);
expect(() => ctx.getSync<Store>(STORE_KEY)).to.throw(
'The type of Store.constructor[0] (Object) is not a Setter function',
);
ctx.bind('key').to('value');

ctx.bind(STORE_KEY).toClass(StoreWithWrongSetterType);
expect(() => ctx.getSync<Store>(STORE_KEY)).to.throw(
'The type of StoreWithWrongSetterType.constructor[0] (Object) is not a Setter function',
);
});
});

it('injects a nested property', async () => {
Expand Down
13 changes: 13 additions & 0 deletions packages/context/src/__tests__/unit/binding.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@ describe('Binding', () => {
expect(binding.tagNames).to.eql(['myTag']);
});

it('applies multiple template functions', async () => {
binding.apply(
b => {
b.inScope(BindingScope.SINGLETON);
},
b => {
b.tag('myTag');
},
);
expect(binding.scope).to.eql(BindingScope.SINGLETON);
expect(binding.tagNames).to.eql(['myTag']);
});

it('sets up a placeholder value', async () => {
const toBeBound = (b: Binding<unknown>) => {
b.toDynamicValue(() => {
Expand Down
20 changes: 20 additions & 0 deletions packages/context/src/__tests__/unit/context.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,26 @@ describe('Context', () => {
});
});

describe('findOrCreateBinding', () => {
it('returns the binding object registered under the given key', () => {
const expected = ctx.bind('foo');
const actual: Binding = ctx.findOrCreateBinding('foo');
expect(actual).to.be.exactly(expected);
});

it('creates a new binding if not found', () => {
const binding = ctx.findOrCreateBinding('a-new-key');
expect(binding.key).to.eql('a-new-key');
});

it('rejects a key containing property separator', () => {
const key = 'a' + BindingKey.PROPERTY_SEPARATOR + 'b';
expect(() => ctx.findOrCreateBinding(key)).to.throw(
/Binding key .* cannot contain/,
);
});
});

describe('getSync', () => {
it('returns the value immediately when the binding is sync', () => {
ctx.bind('foo').to('bar');
Expand Down
12 changes: 7 additions & 5 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,8 @@ export class Binding<T = BoundValue> {
}

/**
* Apply a template function to set up the binding with scope, tags, and
* other attributes as a group.
* Apply one or more template functions to set up the binding with scope,
* tags, and other attributes as a group.
*
* For example,
* ```ts
Expand All @@ -517,10 +517,12 @@ export class Binding<T = BoundValue> {
* const serverBinding = new Binding<RestServer>('servers.RestServer1');
* serverBinding.apply(serverTemplate);
* ```
* @param templateFn A function to configure the binding
* @param templateFns One or more functions to configure the binding
*/
apply(templateFn: BindingTemplate<T>): this {
templateFn(this);
apply(...templateFns: BindingTemplate<T>[]): this {
for (const fn of templateFns) {
fn(this);
}
return this;
}

Expand Down
20 changes: 19 additions & 1 deletion packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ if (!Symbol.asyncIterator) {
// tslint:disable-next-line:no-any
(Symbol as any).asyncIterator = Symbol.for('Symbol.asyncIterator');
}
// This import must happen after the polyfill
/**
* This import must happen after the polyfill.
*
* WARNING: VSCode organize import may change the order of this import
*/
import {iterator, multiple} from 'p-event';

const debug = debugModule('loopback:context');
Expand Down Expand Up @@ -746,6 +750,20 @@ export class Context extends EventEmitter {
);
}

/**
* Find or create a binding for the given key
* @param key Binding address
*/
findOrCreateBinding(key: BindingAddress) {
let binding: Binding<unknown>;
if (this.isBound(key)) {
binding = this.getBinding(key);
} else {
binding = this.bind(key);
}
return binding;
}

/**
* Get the value bound to the given key.
*
Expand Down
43 changes: 37 additions & 6 deletions packages/context/src/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
ParameterDecoratorFactory,
PropertyDecoratorFactory,
} from '@loopback/metadata';
import {BindingTag} from './binding';
import {Binding, BindingTag, BindingTemplate} from './binding';
import {
BindingFilter,
BindingSelector,
Expand Down Expand Up @@ -188,7 +188,9 @@ export function inject(
/**
* The function injected by `@inject.getter(bindingSelector)`.
*/
export type Getter<T> = () => Promise<T>;
export interface Getter<T> {
(): Promise<T>;
}

export namespace Getter {
/**
Expand All @@ -201,9 +203,28 @@ export namespace Getter {
}

/**
* The function injected by `@inject.setter(key)`.
* The function injected by `@inject.setter(bindingKey)`.
*/
export type Setter<T> = (value: T) => void;
export interface Setter<T> {
/**
* Set the binding with one or more `BindingTemplate` functions or values.
* The usages are:
*
* ```ts
* setterFn('my-value');
* setterFn(binding => binding.toClass(MyClass).tag('my-tag'));
* setterFn().toClass(MyClass);
* ```
* @param templateFnsOrValues Binding template functions or values. Please
* note a parameter with function as the value will be treated as a template
* function. To set a function as constant value, you need to wrap it inside
* a template function, such as:
* ```ts
* setterFn(binding => binding.to(aFunction))
* ```
*/
(...templateFnsOrValues: (T | BindingTemplate<T>)[]): Binding<T>;
}

export namespace inject {
/**
Expand Down Expand Up @@ -358,8 +379,18 @@ function resolveAsSetter(ctx: Context, injection: Injection) {
);
}
// No resolution session should be propagated into the setter
return function setter(value: unknown) {
ctx.bind(bindingSelector).to(value);
return function setter(
...templateFnsOrValues: (BindingTemplate | unknown)[]
) {
const binding: Binding<unknown> = ctx.findOrCreateBinding(bindingSelector);
for (const templateFnOrValue of templateFnsOrValues) {
if (typeof templateFnOrValue === 'function') {
binding.apply(templateFnOrValue as BindingTemplate);
} else {
binding.to(templateFnOrValue);
}
}
return binding;
};
}

Expand Down

0 comments on commit e88e35f

Please sign in to comment.