Skip to content

Commit

Permalink
feat(context): pass resolution options into binding.getValue()
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Apr 1, 2019
1 parent 5f1e526 commit 705dcd5
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 53 deletions.
76 changes: 59 additions & 17 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import * as debugModule from 'debug';
import * as debugFactory from 'debug';
import {BindingAddress, BindingKey} from './binding-key';
import {Context} from './context';
import {Provider} from './provider';
import {ResolutionSession} from './resolution-session';
import {
asResolutionOptions,
ResolutionOptions,
ResolutionOptionsOrSession,
ResolutionSession,
} from './resolution-session';
import {instantiateClass} from './resolver';
import {
BoundValue,
Expand All @@ -18,7 +23,7 @@ import {
ValueOrPromise,
} from './value-promise';

const debug = debugModule('loopback:context:binding');
const debug = debugFactory('loopback:context:binding');

/**
* Scope for binding values
Expand Down Expand Up @@ -128,6 +133,11 @@ export type BindingTag = TagMap | string;
*/
export type BindingTemplate<T = unknown> = (binding: Binding<T>) => void;

type ValueGetter<T> = (
ctx: Context,
options: ResolutionOptions,
) => ValueOrPromise<T | undefined>;

/**
* Binding represents an entry in the `Context`. Each binding has a key and a
* corresponding value getter.
Expand Down Expand Up @@ -161,10 +171,7 @@ export class Binding<T = BoundValue> {
}

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

private _valueConstructor?: Constructor<T>;
/**
Expand Down Expand Up @@ -228,7 +235,25 @@ 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, session?: ResolutionSession): ValueOrPromise<T>;

/**
* Returns a value or promise for this binding in the given context. The
* resolved value can be `undefined` if `optional` is set to `true` in
* `options`.
* @param ctx Context for the resolution
* @param options Optional options for binding and dependency resolution
*/
getValue(
ctx: Context,
options?: ResolutionOptions,
): ValueOrPromise<T | undefined>;

// Implementation
getValue(
ctx: Context,
optionsOrSession?: ResolutionOptionsOrSession,
): ValueOrPromise<T | undefined> {
/* istanbul ignore if */
if (debug.enabled) {
debug('Get value for binding %s', this.key);
Expand All @@ -246,11 +271,15 @@ export class Binding<T = BoundValue> {
}
}
}
const options = asResolutionOptions(optionsOrSession);
if (this._getValue) {
let result = ResolutionSession.runWithBinding(
s => this._getValue(ctx, s),
s => {
const optionsWithSession = Object.assign({}, options, {session: s});
return this._getValue(ctx, optionsWithSession);
},
this,
session,
options.session,
);
return this._cacheValue(ctx, result);
}
Expand Down Expand Up @@ -333,6 +362,14 @@ export class Binding<T = BoundValue> {
return this;
}

/**
* Set the `_getValue` function
* @param getValue getValue function
*/
private _setValueGetter(getValue: ValueGetter<T>) {
this._getValue = getValue;
}

/**
* Bind the key to a constant value. The value must be already available
* at binding time, it is not allowed to pass a Promise instance.
Expand Down Expand Up @@ -372,7 +409,7 @@ export class Binding<T = BoundValue> {
debug('Bind %s to constant:', this.key, value);
}
this._type = BindingType.CONSTANT;
this._getValue = () => value;
this._setValueGetter(() => value);
return this;
}

Expand Down Expand Up @@ -400,7 +437,7 @@ export class Binding<T = BoundValue> {
debug('Bind %s to dynamic value:', this.key, factoryFn);
}
this._type = BindingType.DYNAMIC_VALUE;
this._getValue = ctx => factoryFn();
this._setValueGetter(ctx => factoryFn());
return this;
}

Expand All @@ -426,14 +463,14 @@ export class Binding<T = BoundValue> {
debug('Bind %s to provider %s', this.key, providerClass.name);
}
this._type = BindingType.PROVIDER;
this._getValue = (ctx, session) => {
this._setValueGetter((ctx, options) => {
const providerOrPromise = instantiateClass<Provider<T>>(
providerClass,
ctx!,
session,
ctx,
options.session,
);
return transformValueOrPromise(providerOrPromise, p => p.value());
};
});
return this;
}

Expand All @@ -450,11 +487,16 @@ 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._setValueGetter((ctx, options) =>
instantiateClass(ctor, ctx, options.session),
);
this._valueConstructor = ctor;
return this;
}

/**
* Unlock the binding
*/
unlock(): this {
this.isLocked = false;
return this;
Expand Down
62 changes: 35 additions & 27 deletions packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,32 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import * as debugModule from 'debug';
import * as debugFactory 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 All @@ -32,7 +41,7 @@ if (!Symbol.asyncIterator) {
// This import must happen after the polyfill
import {iterator, multiple} from 'p-event';

const debug = debugModule('loopback:context');
const debug = debugFactory('loopback:context');

/**
* Context provides an implementation of Inversion of Control (IoC) container
Expand Down Expand Up @@ -574,9 +583,14 @@ export class Context extends EventEmitter {
*
* @param keyWithPath The binding key, optionally suffixed with a path to the
* (deeply) nested property to retrieve.
* @param session Optional session for resolution (accepted for backward
* compatibility)
* @returns A promise of the bound value.
*/
get<ValueType>(keyWithPath: BindingAddress<ValueType>): Promise<ValueType>;
get<ValueType>(
keyWithPath: BindingAddress<ValueType>,
session?: ResolutionSession,
): Promise<ValueType>;

/**
* Get the value bound to the given key, optionally return a (deep) property
Expand All @@ -594,20 +608,19 @@ export class Context extends EventEmitter {
*
* @param keyWithPath The binding key, optionally suffixed with a path to the
* (deeply) nested property to retrieve.
* @param optionsOrSession Options or session for resolution. An instance of
* `ResolutionSession` is accepted for backward compatibility.
* @param options Options for resolution.
* @returns A promise of the bound value, or a promise of undefined when
* the optional binding is not found.
*/
get<ValueType>(
keyWithPath: BindingAddress<ValueType>,
optionsOrSession?: ResolutionOptions | ResolutionSession,
options: ResolutionOptions,
): Promise<ValueType | undefined>;

// Implementation
async get<ValueType>(
keyWithPath: BindingAddress<ValueType>,
optionsOrSession?: ResolutionOptions | ResolutionSession,
optionsOrSession?: ResolutionOptionsOrSession,
): Promise<ValueType | undefined> {
this._debug('Resolving binding: %s', keyWithPath);
return await this.getValueOrPromise<ValueType | undefined>(
Expand Down Expand Up @@ -636,11 +649,13 @@ export class Context extends EventEmitter {
*
* @param keyWithPath The binding key, optionally suffixed with a path to the
* (deeply) nested property to retrieve.
* * @param optionsOrSession Options or session for resolution. An instance of
* `ResolutionSession` is accepted for backward compatibility.
* @param session Session for resolution (accepted for backward compatibility)
* @returns A promise of the bound value.
*/
getSync<ValueType>(keyWithPath: BindingAddress<ValueType>): ValueType;
getSync<ValueType>(
keyWithPath: BindingAddress<ValueType>,
session?: ResolutionSession,
): ValueType;

/**
* Get the synchronous value bound to the given key, optionally
Expand All @@ -662,19 +677,18 @@ export class Context extends EventEmitter {
*
* @param keyWithPath The binding key, optionally suffixed with a path to the
* (deeply) nested property to retrieve.
* * @param optionsOrSession Options or session for resolution. An instance of
* `ResolutionSession` is accepted for backward compatibility.
* @param options Options for resolution.
* @returns The bound value, or undefined when an optional binding is not found.
*/
getSync<ValueType>(
keyWithPath: BindingAddress<ValueType>,
optionsOrSession?: ResolutionOptions | ResolutionSession,
options?: ResolutionOptions,
): ValueType | undefined;

// Implementation
getSync<ValueType>(
keyWithPath: BindingAddress<ValueType>,
optionsOrSession?: ResolutionOptions | ResolutionSession,
optionsOrSession?: ResolutionOptionsOrSession,
): ValueType | undefined {
this._debug('Resolving binding synchronously: %s', keyWithPath);

Expand Down Expand Up @@ -766,22 +780,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
31 changes: 23 additions & 8 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 @@ -169,7 +165,7 @@ export class ResolutionSession {
);
return {
targetName: name,
bindingKey: injection.bindingSelector,
bindingSelector: injection.bindingSelector,
// Cast to Object so that we don't have to expose InjectionMetadata
metadata: injection.metadata as Object,
};
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) {
return {session: optionsOrSession};
}
return optionsOrSession || {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ describe('Routing', () => {
server.bind('flag').to('original');

// create a special binding returning the current context instance
server.bind('context').getValue = ctx => ctx;
server.bind('context').getValue = (ctx: Context) => ctx;

const spec = anOpenApiSpec()
.withOperationReturningString('put', '/flag', 'setFlag')
Expand Down

0 comments on commit 705dcd5

Please sign in to comment.