Skip to content

Commit

Permalink
Merge 6b33806 into 5f1cd3d
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Jan 16, 2019
2 parents 5f1cd3d + 6b33806 commit ad8c361
Show file tree
Hide file tree
Showing 10 changed files with 1,002 additions and 8 deletions.
16 changes: 16 additions & 0 deletions packages/context/src/binding-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,20 @@ export class BindingKey<ValueType> {
keyWithPath.substr(index + 1),
);
}

static CONFIG_NAMESPACE = '$config';
/**
* Build a binding key for the configuration of the given binding and env.
* The format is `$config.<env>:<key>`
*
* @param key The binding key that accepts the configuration
* @param env The environment such as `dev`, `test`, and `prod`
*/
static buildKeyForConfig<T>(key: BindingAddress<T> = '', env: string = '') {
const namespace = env
? `${BindingKey.CONFIG_NAMESPACE}.${env}`
: BindingKey.CONFIG_NAMESPACE;
const bindingKey = key ? `${namespace}.${key}` : BindingKey.CONFIG_NAMESPACE;
return bindingKey;
}
}
13 changes: 13 additions & 0 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ export class Binding<T = BoundValue> {
);
}

/**
* Lock the binding so that it cannot be rebound
*/
lock(): this {
this.isLocked = true;
return this;
Expand Down Expand Up @@ -302,6 +305,10 @@ 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;
return this;
Expand Down Expand Up @@ -429,6 +436,9 @@ export class Binding<T = BoundValue> {
return this;
}

/**
* Unlock the binding
*/
unlock(): this {
this.isLocked = false;
return this;
Expand All @@ -453,6 +463,9 @@ export class Binding<T = BoundValue> {
return this;
}

/**
* Convert to a plain JSON object
*/
toJSON(): Object {
// tslint:disable-next-line:no-any
const json: {[name: string]: any} = {
Expand Down
157 changes: 157 additions & 0 deletions packages/context/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright IBM Corp. 2017,2018. All Rights Reserved.
// Node module: @loopback/context
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import * as debugModule from 'debug';
import {BindingAddress, BindingKey} from './binding-key';
import {Context} from './context';
import {ResolutionOptions} from './resolution-session';
import {
BoundValue,
ValueOrPromise,
resolveUntil,
transformValueOrPromise,
} from './value-promise';

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

/**
* Interface for configuration resolver
*/
export interface ConfigurationResolver {
/**
* Resolve the configuration value for a given binding key and config property
* path
* @param key Binding key
* @param configPath Config property path
* @param resolutionOptions Resolution options
*/
getConfigAsValueOrPromise<ConfigValueType>(
key: BindingAddress<BoundValue>,
configPath?: string,
resolutionOptions?: ResolutionOptions,
): ValueOrPromise<ConfigValueType | undefined>;
}

/**
* Resolver for configurations of bindings
*/
export class DefaultConfigurationResolver implements ConfigurationResolver {
constructor(public readonly context: Context) {}

/**
* Resolve config from the binding key hierarchy using namespaces
* separated by `.`
*
* For example, if the binding key is `servers.rest.server1`, we'll try the
* following entries:
* 1. servers.rest.server1:$config#host (namespace: server1)
* 2. servers.rest:$config#server1.host (namespace: rest)
* 3. servers.$config#rest.server1.host` (namespace: server)
* 4. $config#servers.rest.server1.host (namespace: '' - root)
*
* @param key Binding key with namespaces separated by `.`
* @param configPath Property path for the option. For example, `x.y`
* requests for `config.x.y`. If not set, the `config` object will be
* returned.
* @param resolutionOptions Options for the resolution.
* - localConfigOnly: if set to `true`, no parent namespaces will be checked
* - optional: if not set or set to `true`, `undefined` will be returned if
* no corresponding value is found. Otherwise, an error will be thrown.
*/
getConfigAsValueOrPromise<ConfigValueType>(
key: BindingAddress<BoundValue>,
configPath?: string,
resolutionOptions?: ResolutionOptions,
): ValueOrPromise<ConfigValueType | undefined> {
const env = resolutionOptions && resolutionOptions.environment;
configPath = configPath || '';
const configKey = BindingKey.create<ConfigValueType>(
BindingKey.buildKeyForConfig(key, env),
configPath,
);

const localConfigOnly =
resolutionOptions && resolutionOptions.localConfigOnly;

/**
* Set up possible keys to resolve the config value
*/
key = key.toString();
const keys = [];
while (true) {
const configKeyAndPath = BindingKey.create<ConfigValueType>(
BindingKey.buildKeyForConfig(key, env),
configPath,
);
keys.push(configKeyAndPath);
if (env) {
// The `environment` is set, let's try the non env specific binding too
keys.push(
BindingKey.create<ConfigValueType>(
BindingKey.buildKeyForConfig(key),
configPath,
),
);
}
if (!key || localConfigOnly) {
// No more keys
break;
}
// Shift last part of the key into the path as we'll try the parent
// namespace in the next iteration
const index = key.lastIndexOf('.');
configPath = configPath
? `${key.substring(index + 1)}.${configPath}`
: `${key.substring(index + 1)}`;
key = key.substring(0, index);
}
/* istanbul ignore if */
if (debug.enabled) {
debug('Configuration keyWithPaths: %j', keys);
}

const resolveConfig = (keyWithPath: string) => {
// Set `optional` to `true` to resolve config locally
const options = Object.assign(
{}, // Make sure resolutionOptions is copied
resolutionOptions,
{optional: true}, // Force optional to be true
);
return this.context.getValueOrPromise<ConfigValueType>(
keyWithPath,
options,
);
};

const evaluateConfig = (keyWithPath: string, val: ConfigValueType) => {
/* istanbul ignore if */
if (debug.enabled) {
debug('Configuration keyWithPath: %s => value: %j', keyWithPath, val);
}
// Found the corresponding config
if (val !== undefined) return true;

if (localConfigOnly) {
return true;
}
return false;
};

const required = resolutionOptions && resolutionOptions.optional === false;
const valueOrPromise = resolveUntil<
BindingAddress<ConfigValueType>,
ConfigValueType
>(keys[Symbol.iterator](), resolveConfig, evaluateConfig);
return transformValueOrPromise<
ConfigValueType | undefined,
ConfigValueType | undefined
>(valueOrPromise, val => {
if (val === undefined && required) {
throw Error(`Configuration '${configKey}' cannot be resolved`);
}
return val;
});
}
}
139 changes: 137 additions & 2 deletions packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@

import * as debugModule from 'debug';
import {v1 as uuidv1} from 'uuid';
import {ValueOrPromise} from '.';
import {Binding, BindingTag} from './binding';
import {BindingAddress, BindingKey} from './binding-key';
import {ConfigurationResolver, DefaultConfigurationResolver} from './config';
import {ResolutionOptions, ResolutionSession} from './resolution-session';
import {BoundValue, getDeepProperty, isPromiseLike} from './value-promise';
import {
BoundValue,
getDeepProperty,
isPromiseLike,
ValueOrPromise,
} from './value-promise';

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

Expand All @@ -24,6 +29,8 @@ export class Context {
protected readonly registry: Map<string, Binding> = new Map();
protected _parent?: Context;

private configResolver: ConfigurationResolver;

/**
* Create a new context
* @param _parent The optional parent context
Expand Down Expand Up @@ -74,6 +81,134 @@ export class Context {
return this;
}

/**
* Create a corresponding binding for configuration of the target bound by
* the given key in the context.
*
* For example, `ctx.configure('controllers.MyController').to({x: 1})` will
* create binding `controllers.MyController:$config` with value `{x: 1}`.
*
* @param key The key for the binding that accepts the config
* @param env The env (such as `dev`, `test`, and `prod`) for the config
*/
configure<ConfigValueType = BoundValue>(
key: BindingAddress<BoundValue> = '',
env: string = '',
): Binding<ConfigValueType> {
const keyForConfig = BindingKey.buildKeyForConfig(key, env);
const bindingForConfig = this.bind<ConfigValueType>(keyForConfig).tag({
config: key,
});
return bindingForConfig;
}

/**
* Resolve config from the binding key hierarchy using namespaces
* separated by `.`
*
* For example, if the binding key is `servers.rest.server1`, we'll try the
* following entries:
* 1. servers.rest.server1:$config#host (namespace: server1)
* 2. servers.rest:$config#server1.host (namespace: rest)
* 3. servers.$config#rest.server1.host` (namespace: server)
* 4. $config#servers.rest.server1.host (namespace: '' - root)
*
* @param key Binding key with namespaces separated by `.`
* @param configPath Property path for the option. For example, `x.y`
* requests for `config.x.y`. If not set, the `config` object will be
* returned.
* @param resolutionOptions Options for the resolution.
* - localConfigOnly: if set to `true`, no parent namespaces will be checked
* - optional: if not set or set to `true`, `undefined` will be returned if
* no corresponding value is found. Otherwise, an error will be thrown.
*/
getConfigAsValueOrPromise<ConfigValueType>(
key: BindingAddress<BoundValue>,
configPath?: string,
resolutionOptions?: ResolutionOptions,
): ValueOrPromise<ConfigValueType | undefined> {
return this.getConfigResolver().getConfigAsValueOrPromise(
key,
configPath,
resolutionOptions,
);
}

private getConfigResolver() {
if (!this.configResolver) {
// TODO: Check bound ConfigurationResolver
this.configResolver = new DefaultConfigurationResolver(this);
}
return this.configResolver;
}

/**
* Resolve config from the binding key hierarchy using namespaces
* separated by `.`
*
* For example, if the binding key is `servers.rest.server1`, we'll try the
* following entries:
* 1. servers.rest.server1:$config#host (namespace: server1)
* 2. servers.rest:$config#server1.host (namespace: rest)
* 3. servers.$config#rest.server1.host` (namespace: server)
* 4. $config#servers.rest.server1.host (namespace: '' - root)
*
* @param key Binding key with namespaces separated by `.`
* @param configPath Property path for the option. For example, `x.y`
* requests for `config.x.y`. If not set, the `config` object will be
* returned.
* @param resolutionOptions Options for the resolution. If `localConfigOnly` is
* set to true, no parent namespaces will be looked up.
*/
async getConfig<ConfigValueType>(
key: BindingAddress<BoundValue>,
configPath?: string,
resolutionOptions?: ResolutionOptions,
): Promise<ConfigValueType | undefined> {
return await this.getConfigAsValueOrPromise<ConfigValueType>(
key,
configPath,
resolutionOptions,
);
}

/**
* Resolve config synchronously from the binding key hierarchy using
* namespaces separated by `.`
*
* For example, if the binding key is `servers.rest.server1`, we'll try the
* following entries:
* 1. servers.rest.server1:$config#host (namespace: server1)
* 2. servers.rest:$config#server1.host (namespace: rest)
* 3. servers.$config#rest.server1.host` (namespace: server)
* 4. $config#servers.rest.server1.host (namespace: '' - root)
*
* @param key Binding key with namespaces separated by `.`
* @param configPath Property path for the option. For example, `x.y`
* requests for `config.x.y`. If not set, the `config` object will be
* returned.
* @param resolutionOptions Options for the resolution. If `localConfigOnly`
* is set to `true`, no parent namespaces will be looked up.
*/
getConfigSync<ConfigValueType>(
key: BindingAddress<BoundValue>,
configPath?: string,
resolutionOptions?: ResolutionOptions,
): ConfigValueType | undefined {
const valueOrPromise = this.getConfigAsValueOrPromise<ConfigValueType>(
key,
configPath,
resolutionOptions,
);
if (isPromiseLike(valueOrPromise)) {
throw new Error(
`Cannot get config[${configPath ||
''}] for ${key} synchronously: the value is a promise`,
);
}
return valueOrPromise;
}

/**
* Unbind a binding from the context. No parent contexts will be checked. If
* you need to unbind a binding owned by a parent context, use the code below:
Expand Down

0 comments on commit ad8c361

Please sign in to comment.