Skip to content

Commit

Permalink
feat(context): add binding.toAlias() to resolve values from another b…
Browse files Browse the repository at this point in the history
…inding
  • Loading branch information
raymondfeng committed Mar 22, 2019
1 parent e615657 commit 214bec4
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 31 deletions.
15 changes: 15 additions & 0 deletions docs/site/Binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,21 @@ class MyValueProvider implements Provider<string> {
binding.toProvider(MyValueProvider);
```

#### An alias

An alias is the key with optional path to resolve the value from another
binding. For example, if we want to get options from RestServer for the API
explorer, we can configure the `apiExplorer.options` to be resolved from
`servers.RestServer.options#apiExplorer`.

```ts
ctx.bind('servers.RestServer.options').to({apiExplorer: {path: '/explorer'}});
ctx
.bind('apiExplorer.options')
.toAlias('servers.RestServer.options#apiExplorer');
const apiExplorerOptions = await ctx.get('apiExplorer.options'); // => {path: '/explorer'}
```

### Configure the scope

We allow a binding to be resolved within a context using one of the following
Expand Down
51 changes: 51 additions & 0 deletions packages/context/src/__tests__/unit/binding.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,57 @@ describe('Binding', () => {
});
});

describe('toAlias(bindingKeyWithPath)', async () => {
it('binds to another binding with sync value', () => {
ctx.bind('parent.options').to({child: {disabled: true}});
ctx.bind('child.options').toAlias('parent.options#child');
const childOptions = ctx.getSync('child.options');
expect(childOptions).to.eql({disabled: true});
});

it('binds to another binding with async value', async () => {
ctx
.bind('parent.options')
.toDynamicValue(() => Promise.resolve({child: {disabled: true}}));
ctx.bind('child.options').toAlias('parent.options#child');
const childOptions = await ctx.get('child.options');
expect(childOptions).to.eql({disabled: true});
});

it('reports error if alias binding cannot be resolved', () => {
ctx.bind('child.options').toAlias('parent.options#child');
expect(() => ctx.getSync('child.options')).to.throw(
/The key 'parent.options' is not bound to any value in context/,
);
});

it('reports error if alias binding cannot be resolved - async', async () => {
ctx.bind('child.options').toAlias('parent.options#child');
return expect(ctx.get('child.options')).to.be.rejectedWith(
/The key 'parent.options' is not bound to any value in context/,
);
});

it('allows optional if alias binding cannot be resolved', () => {
ctx.bind('child.options').toAlias('parent.options#child');
const childOptions = ctx.getSync('child.options', {optional: true});
expect(childOptions).to.be.undefined();
});

it('allows optional if alias binding cannot be resolved - async', async () => {
ctx.bind('child.options').toAlias('parent.options#child');
const childOptions = await ctx.get('child.options', {optional: true});
expect(childOptions).to.be.undefined();
});

it('sets type to ALIAS', () => {
const childBinding = ctx
.bind('child.options')
.toAlias('parent.options#child');
expect(childBinding.type).to.equal(BindingType.ALIAS);
});
});

describe('apply(templateFn)', () => {
it('applies a template function', async () => {
binding.apply(b => {
Expand Down
70 changes: 59 additions & 11 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import * as debugModule from 'debug';
import {BindingAddress, BindingKey} from './binding-key';
import {Context} from './context';
import {Provider} from './provider';
import {ResolutionSession} from './resolution-session';
import {
asResolutionOptions,
ResolutionOptionsOrSession,
ResolutionSession,
} from './resolution-session';
import {instantiateClass} from './resolver';
import {
BoundValue,
Expand Down Expand Up @@ -113,6 +117,10 @@ export enum BindingType {
* A provider class with `value()` function to get the value
*/
PROVIDER = 'Provider',
/**
* A alias to another binding key with optional path
*/
ALIAS = 'Alias',
}

// tslint:disable-next-line:no-any
Expand Down Expand Up @@ -162,9 +170,9 @@ export class Binding<T = BoundValue> {

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

private _valueConstructor?: Constructor<T>;
/**
Expand Down Expand Up @@ -228,7 +236,10 @@ export class Binding<T = BoundValue> {
* @param ctx Context for the resolution
* @param session Optional session for binding and dependency resolution
*/
getValue(ctx: Context, session?: ResolutionSession): ValueOrPromise<T> {
getValue(
ctx: Context,
optionsOrSession?: ResolutionOptionsOrSession,
): ValueOrPromise<T> {
/* istanbul ignore if */
if (debug.enabled) {
debug('Get value for binding %s', this.key);
Expand All @@ -246,11 +257,15 @@ export class Binding<T = BoundValue> {
}
}
}
optionsOrSession = asResolutionOptions(optionsOrSession);
if (this._getValue) {
let result = ResolutionSession.runWithBinding(
s => this._getValue(ctx, s),
s => {
const options = Object.assign({}, optionsOrSession, {session: s});
return this._getValue(ctx, options);
},
this,
session,
optionsOrSession.session,
);
return this._cacheValue(ctx, result);
}
Expand Down Expand Up @@ -426,11 +441,11 @@ export class Binding<T = BoundValue> {
debug('Bind %s to provider %s', this.key, providerClass.name);
}
this._type = BindingType.PROVIDER;
this._getValue = (ctx, session) => {
this._getValue = (ctx, optionsOrSession) => {
const providerOrPromise = instantiateClass<Provider<T>>(
providerClass,
ctx!,
session,
ctx,
asResolutionOptions(optionsOrSession).session,
);
return transformValueOrPromise(providerOrPromise, p => p.value());
};
Expand All @@ -450,11 +465,44 @@ export class Binding<T = BoundValue> {
debug('Bind %s to class %s', this.key, ctor.name);
}
this._type = BindingType.CLASS;
this._getValue = (ctx, session) => instantiateClass(ctor, ctx!, session);
this._getValue = (ctx, optionsOrSession) =>
instantiateClass(
ctor,
ctx,
asResolutionOptions(optionsOrSession).session,
);
this._valueConstructor = ctor;
return this;
}

/**
* Bind the key to an alias of another binding
* @param keyWithPath Target binding key with optional path,
* such as `servers.RestServer.options#apiExplorer`
*/
toAlias(keyWithPath: BindingAddress<T>) {
/* istanbul ignore if */
if (debug.enabled) {
debug('Bind %s to alias %s', this.key, keyWithPath);
}
this._type = BindingType.ALIAS;
this._getValue = (ctx, optionsOrSession) => {
const options = asResolutionOptions(optionsOrSession);
const valueOrPromise = ctx.getValueOrPromise(keyWithPath, options);
return transformValueOrPromise(valueOrPromise, val => {
if (val === undefined && !options.optional) {
throw new Error(
`No value was configured for binding ${keyWithPath}.`,
);
} else return val;
});
};
return this;
}

/**
* Unlock the binding
*/
unlock(): this {
this.isLocked = false;
return this;
Expand Down
29 changes: 16 additions & 13 deletions packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,29 @@
import * as debugModule from 'debug';
import {EventEmitter} from 'events';
import {v1 as uuidv1} from 'uuid';
import {ValueOrPromise} from '.';
import {Binding, BindingTag} from './binding';
import {BindingFilter, filterByKey, filterByTag} from './binding-filter';
import {BindingAddress, BindingKey} from './binding-key';
import {ContextView} from './context-view';
import {
ContextEventObserver,
ContextEventType,
ContextObserver,
Notification,
Subscription,
} from './context-observer';
import {ResolutionOptions, ResolutionSession} from './resolution-session';
import {BoundValue, getDeepProperty, isPromiseLike} from './value-promise';
import {ContextView} from './context-view';
import {
asResolutionOptions,
ResolutionOptions,
ResolutionOptionsOrSession,
ResolutionSession,
} from './resolution-session';
import {
BoundValue,
getDeepProperty,
isPromiseLike,
ValueOrPromise,
} from './value-promise';

/**
* Polyfill Symbol.asyncIterator as required by TypeScript for Node 8.x.
Expand Down Expand Up @@ -766,22 +775,16 @@ export class Context extends EventEmitter {
*/
getValueOrPromise<ValueType>(
keyWithPath: BindingAddress<ValueType>,
optionsOrSession?: ResolutionOptions | ResolutionSession,
optionsOrSession?: ResolutionOptionsOrSession,
): ValueOrPromise<ValueType | undefined> {
const {key, propertyPath} = BindingKey.parseKeyWithPath(keyWithPath);

// backwards compatibility
if (optionsOrSession instanceof ResolutionSession) {
optionsOrSession = {session: optionsOrSession};
}
optionsOrSession = asResolutionOptions(optionsOrSession);

const binding = this.getBinding<ValueType>(key, optionsOrSession);
if (binding == null) return undefined;

const boundValue = binding.getValue(
this,
optionsOrSession && optionsOrSession.session,
);
const boundValue = binding.getValue(this, optionsOrSession);
if (propertyPath === undefined || propertyPath === '') {
return boundValue;
}
Expand Down
29 changes: 22 additions & 7 deletions packages/context/src/resolution-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {DecoratorFactory} from '@loopback/metadata';
import * as debugModule from 'debug';
import {Binding} from './binding';
import {Injection} from './inject';
import {ValueOrPromise, BoundValue, tryWithFinally} from './value-promise';
import * as debugModule from 'debug';
import {DecoratorFactory} from '@loopback/metadata';
import {BoundValue, tryWithFinally, ValueOrPromise} from './value-promise';

const debugSession = debugModule('loopback:context:resolver:session');
const getTargetName = DecoratorFactory.getTargetName;

// NOTE(bajtos) The following import is required to satisfy TypeScript compiler
// tslint:disable-next-line:no-unused
import {BindingKey} from './binding-key';

/**
* A function to be executed with the resolution session
*/
Expand Down Expand Up @@ -343,3 +339,22 @@ export interface ResolutionOptions {
*/
optional?: boolean;
}

/**
* Resolution options or session
*/
export type ResolutionOptionsOrSession = ResolutionOptions | ResolutionSession;

/**
* Normalize ResolutionOptionsOrSession to ResolutionOptions
* @param optionsOrSession resolution options or session
*/
export function asResolutionOptions(
optionsOrSession?: ResolutionOptionsOrSession,
): ResolutionOptions {
// backwards compatibility
if (optionsOrSession instanceof ResolutionSession) {
optionsOrSession = {session: optionsOrSession};
}
return optionsOrSession || {};
}

0 comments on commit 214bec4

Please sign in to comment.