Skip to content

Commit

Permalink
feat(context): Add toJSON() for Context & Binding
Browse files Browse the repository at this point in the history
Provide customization to serialize Context and Binding objects to JSON
  • Loading branch information
Raymond Feng authored and raymondfeng committed Oct 4, 2017
1 parent 1499eb5 commit b6ce426
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 13 deletions.
36 changes: 31 additions & 5 deletions packages/context/src/binding.ts
Expand Up @@ -34,7 +34,7 @@ export enum BindingScope {
* req2.get('b1') ==> 3
* app.get('b1') ==> 4
*/
TRANSIENT,
TRANSIENT = 'Transient',

/**
* The binding provides a value as a singleton within each local context. The
Expand All @@ -59,7 +59,7 @@ export enum BindingScope {
* // 2 is the singleton for req2 afterward
* req2.get('b1') ==> 2
*/
CONTEXT,
CONTEXT = 'Context',

/**
* The binding provides a value as a singleton within the context hierarchy
Expand All @@ -82,7 +82,14 @@ export enum BindingScope {
* // 'b1' is found in app, reuse it
* req2.get('b1') ==> 0
*/
SINGLETON,
SINGLETON = 'Singleton',
}

export enum BindingType {
CONSTANT = 'Constant',
DYNAMIC_VALUE = 'DynamicValue',
CLASS = 'Class',
PROVIDER = 'Provider',
}

// FIXME(bajtos) The binding class should be parameterized by the value
Expand Down Expand Up @@ -127,6 +134,7 @@ export class Binding {
public readonly key: string;
public readonly tags: Set<string> = new Set();
public scope: BindingScope = BindingScope.TRANSIENT;
public type: BindingType;

private _cache: BoundValue;
private _getValue: (ctx?: Context) => BoundValue | Promise<BoundValue>;
Expand Down Expand Up @@ -243,8 +251,9 @@ export class Binding {
return this;
}

inScope(scope: BindingScope) {
inScope(scope: BindingScope): this {
this.scope = scope;
return this;
}

/**
Expand All @@ -259,6 +268,7 @@ export class Binding {
* ```
*/
to(value: BoundValue): this {
this.type = BindingType.CONSTANT;
this._getValue = () => value;
return this;
}
Expand All @@ -282,7 +292,7 @@ export class Binding {
* ```
*/
toDynamicValue(factoryFn: () => BoundValue | Promise<BoundValue>): this {
// TODO(bajtos) allow factoryFn with @inject arguments
this.type = BindingType.DYNAMIC_VALUE;
this._getValue = ctx => factoryFn();
return this;
}
Expand All @@ -304,6 +314,7 @@ export class Binding {
* @param provider The value provider to use.
*/
public toProvider<T>(providerClass: Constructor<Provider<T>>): this {
this.type = BindingType.PROVIDER;
this._getValue = ctx => {
const providerOrPromise = instantiateClass<Provider<T>>(
providerClass,
Expand All @@ -326,6 +337,7 @@ export class Binding {
* we can resolve them from the context.
*/
toClass<T>(ctor: Constructor<T>): this {
this.type = BindingType.CLASS;
this._getValue = ctx => instantiateClass(ctor, ctx!);
this.valueConstructor = ctor;
return this;
Expand All @@ -335,4 +347,18 @@ export class Binding {
this.isLocked = false;
return this;
}

toJSON(): Object {
// tslint:disable-next-line:no-any
const json: {[name: string]: any} = {
key: this.key,
scope: this.scope,
tags: Array.from(this.tags),
isLocked: this.isLocked,
};
if (this.type != null) {
json.type = this.type;
}
return json;
}
}
11 changes: 11 additions & 0 deletions packages/context/src/context.ts
Expand Up @@ -192,6 +192,17 @@ export class Context {

return getDeepProperty(boundValue, path);
}

/**
* Create a plain JSON object for the context
*/
toJSON(): Object {
const json: {[key: string]: Object} = {};
for (const [k, v] of this.registry) {
json[k] = v.toJSON();
}
return json;
}
}

function getDeepProperty(value: BoundValue, path: string) {
Expand Down
9 changes: 8 additions & 1 deletion packages/context/src/index.ts
Expand Up @@ -3,7 +3,14 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export {Binding, BindingScope, BoundValue, ValueOrPromise} from './binding';
export {
Binding,
BindingScope,
BindingType,
BoundValue,
ValueOrPromise,
} from './binding';

export {Context} from './context';
export {Constructor} from './resolver';
export {inject, Setter, Getter} from './inject';
Expand Down
84 changes: 78 additions & 6 deletions packages/context/test/unit/binding.ts
Expand Up @@ -4,7 +4,14 @@
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {Binding, BindingScope, Context, inject, Provider} from '../..';
import {
Binding,
BindingScope,
BindingType,
Context,
inject,
Provider,
} from '../..';

const key = 'foo';

Expand Down Expand Up @@ -73,40 +80,105 @@ describe('Binding', () => {
binding.to('value');
expect(binding.getValue(ctx)).to.equal('value');
});

it('sets the type to CONSTANT', () => {
binding.to('value');
expect(binding.type).to.equal(BindingType.CONSTANT);
});
});

describe('toDynamicValue(dynamicValueFn)', () => {
it('support a factory', async () => {
const b = ctx.bind('msg').toDynamicValue(() => Promise.resolve('hello'));
const value: string = await ctx.get('msg');
expect(value).to.equal('hello');
expect(b.type).to.equal(BindingType.DYNAMIC_VALUE);
});
});

describe('toClass(cls)', () => {
it('support a class', async () => {
ctx.bind('msg').toDynamicValue(() => Promise.resolve('world'));
const b = ctx.bind('myService').toClass(MyService);
expect(b.type).to.equal(BindingType.CLASS);
const myService: MyService = await ctx.get('myService');
expect(myService.getMessage()).to.equal('hello world');
});
});

describe('toProvider(provider)', () => {
it('binding returns the expected value', async () => {
ctx.bind('msg').to('hello');
ctx.bind('provider_key').toProvider(MyProvider);
const value: String = await ctx.get('provider_key');
const value: string = await ctx.get('provider_key');
expect(value).to.equal('hello world');
});

it('can resolve provided value synchronously', () => {
ctx.bind('msg').to('hello');
ctx.bind('provider_key').toProvider(MyProvider);
const value: String = ctx.getSync('provider_key');
const value: string = ctx.getSync('provider_key');
expect(value).to.equal('hello world');
});

it('support asynchronous dependencies of provider class', async () => {
ctx.bind('msg').toDynamicValue(() => Promise.resolve('hello'));
ctx.bind('provider_key').toProvider(MyProvider);
const value: String = await ctx.get('provider_key');
const value: string = await ctx.get('provider_key');
expect(value).to.equal('hello world');
});

it('sets the type to PROVIDER', () => {
ctx.bind('msg').to('hello');
const b = ctx.bind('provider_key').toProvider(MyProvider);
expect(b.type).to.equal(BindingType.PROVIDER);
});
});

describe('toJSON()', () => {
it('converts a keyed binding to plain JSON object', () => {
const json = binding.toJSON();
expect(json).to.eql({
key: key,
scope: BindingScope.TRANSIENT,
tags: [],
isLocked: false,
});
});

it('converts a binding with more attributes to plain JSON object', () => {
const myBinding = new Binding(key, true)
.inScope(BindingScope.CONTEXT)
.tag('model')
.to('a');
const json = myBinding.toJSON();
expect(json).to.eql({
key: key,
scope: BindingScope.CONTEXT,
tags: ['model'],
isLocked: true,
type: BindingType.CONSTANT,
});
});
});

function givenBinding() {
ctx = new Context();
binding = new Binding(key);
}

class MyProvider implements Provider<String> {
class MyProvider implements Provider<string> {
constructor(@inject('msg') private _msg: string) {}
value(): String {
value(): string {
return this._msg + ' world';
}
}

class MyService {
constructor(@inject('msg') private _msg: string) {}

getMessage(): string {
return 'hello ' + this._msg;
}
}
});
43 changes: 42 additions & 1 deletion packages/context/test/unit/context.ts
Expand Up @@ -4,7 +4,7 @@
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {Context, Binding, BindingScope, isPromise} from '../..';
import {Context, Binding, BindingScope, BindingType, isPromise} from '../..';

describe('Context', () => {
let ctx: Context;
Expand Down Expand Up @@ -375,6 +375,47 @@ describe('Context', () => {
});
});

describe('toJSON()', () => {
it('converts to plain JSON object', () => {
ctx
.bind('a')
.to('1')
.lock();
ctx
.bind('b')
.toDynamicValue(() => 2)
.inScope(BindingScope.SINGLETON)
.tag(['X', 'Y']);
ctx
.bind('c')
.to(3)
.tag('Z');
expect(ctx.toJSON()).to.eql({
a: {
key: 'a',
scope: BindingScope.TRANSIENT,
tags: [],
isLocked: true,
type: BindingType.CONSTANT,
},
b: {
key: 'b',
scope: BindingScope.SINGLETON,
tags: ['X', 'Y'],
isLocked: false,
type: BindingType.DYNAMIC_VALUE,
},
c: {
key: 'c',
scope: BindingScope.TRANSIENT,
tags: ['Z'],
isLocked: false,
type: BindingType.CONSTANT,
},
});
});
});

function createContext() {
ctx = new Context();
}
Expand Down

0 comments on commit b6ce426

Please sign in to comment.