Skip to content

Commit

Permalink
feat(context): allow more options to inspect context/binding objects
Browse files Browse the repository at this point in the history
At binding level, there is one flag:

- includeInjections: control if injections should be inspected

At context level, there are two flags:

- includeInjections: control if binding injections should be inspected
- includeParent: control if parent context should be inspected
  • Loading branch information
raymondfeng committed Feb 4, 2020
1 parent 15d698b commit 3be32a3
Show file tree
Hide file tree
Showing 5 changed files with 368 additions and 11 deletions.
151 changes: 151 additions & 0 deletions packages/context/src/__tests__/unit/binding.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import {expect, sinon, SinonSpy} from '@loopback/testlab';
import {
Binding,
BindingEvent,
BindingKey,
BindingScope,
BindingType,
Context,
filterByTag,
inject,
Provider,
} from '../..';
Expand Down Expand Up @@ -428,6 +430,155 @@ describe('Binding', () => {
type: BindingType.CONSTANT,
});
});

it('converts a keyed binding with alias to plain JSON object', () => {
const myBinding = new Binding(key)
.inScope(BindingScope.TRANSIENT)
.toAlias(BindingKey.create('b', 'x'));
const json = myBinding.toJSON();
expect(json).to.eql({
key: key,
scope: BindingScope.TRANSIENT,
tags: {},
isLocked: false,
type: BindingType.ALIAS,
alias: 'b#x',
});
});
});

describe('inspect()', () => {
it('converts a keyed binding to plain JSON object', () => {
const json = binding.inspect();
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', {name: 'my-model'})
.to('a');
const json = myBinding.inspect();
expect(json).to.eql({
key: key,
scope: BindingScope.CONTEXT,
tags: {model: 'model', name: 'my-model'},
isLocked: true,
type: BindingType.CONSTANT,
});
});

it('converts a binding with toDynamicValue to plain JSON object', () => {
const myBinding = new Binding(key)
.inScope(BindingScope.SINGLETON)
.tag('model', {name: 'my-model'})
.toDynamicValue(() => 'a');
const json = myBinding.inspect({includeInjections: true});
expect(json).to.eql({
key: key,
scope: BindingScope.SINGLETON,
tags: {model: 'model', name: 'my-model'},
isLocked: false,
type: BindingType.DYNAMIC_VALUE,
});
});

it('converts a binding with valueConstructor to plain JSON object', () => {
function myFilter(b: Readonly<Binding<unknown>>) {
return b.key.startsWith('timers.');
}

class MyController {
@inject('y', {optional: true})
private y: number | undefined;

@inject(filterByTag('task'))
private tasks: unknown[];

@inject(myFilter)
private timers: unknown[];

constructor(@inject('x') private x: string) {}
}
const myBinding = new Binding(key, true)
.tag('model', {name: 'my-model'})
.toClass(MyController);
const json = myBinding.inspect({includeInjections: true});
expect(json).to.eql({
key: key,
scope: BindingScope.TRANSIENT,
tags: {model: 'model', name: 'my-model'},
isLocked: true,
type: BindingType.CLASS,
valueConstructor: 'MyController',
injections: {
constructorArguments: [
{targetName: 'MyController.constructor[0]', bindingKey: 'x'},
],
properties: {
y: {
targetName: 'MyController.prototype.y',
bindingKey: 'y',
optional: true,
},
tasks: {
targetName: 'MyController.prototype.tasks',
bindingTagPattern: 'task',
},
timers: {
targetName: 'MyController.prototype.timers',
bindingFilter: 'myFilter',
},
},
},
});
});

it('converts a binding with providerConstructor to plain JSON object', () => {
class MyProvider implements Provider<string> {
@inject('y')
private y: number;

@inject(filterByTag('task'))
private tasks: unknown[];

constructor(@inject('x') private x: string) {}

value() {
return `${this.x}: ${this.y}`;
}
}
const myBinding = new Binding(key, true)
.inScope(BindingScope.CONTEXT)
.tag('model', {name: 'my-model'})
.toProvider(MyProvider);
const json = myBinding.inspect({includeInjections: true});
expect(json).to.eql({
key: key,
scope: BindingScope.CONTEXT,
tags: {model: 'model', name: 'my-model'},
isLocked: true,
type: BindingType.PROVIDER,
providerConstructor: 'MyProvider',
injections: {
constructorArguments: [
{targetName: 'MyProvider.constructor[0]', bindingKey: 'x'},
],
properties: {
y: {targetName: 'MyProvider.prototype.y', bindingKey: 'y'},
tasks: {
targetName: 'MyProvider.prototype.tasks',
bindingTagPattern: 'task',
},
},
},
});
});
});

function givenBinding() {
Expand Down
94 changes: 92 additions & 2 deletions packages/context/src/__tests__/unit/context.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
BindingScope,
BindingType,
Context,
inject,
isPromiseLike,
Provider,
} from '../..';
Expand Down Expand Up @@ -898,10 +899,99 @@ describe('Context', () => {
});
});

class MyService {}
it('inspects as plain JSON object to not include parent', () => {
const childCtx = new TestContext(ctx, 'server');
childCtx.bind('foo').to('foo-value');

expect(childCtx.inspect({includeParent: false})).to.eql({
name: 'server',
bindings: childCtx.toJSON(),
});
});

it('inspects as plain JSON object to include injections', () => {
const childCtx = new TestContext(ctx, 'server');
childCtx.bind('foo').to('foo-value');

const expectedJSON = {
name: 'server',
bindings: {
foo: {
key: 'foo',
scope: 'Transient',
tags: {},
isLocked: false,
type: 'Constant',
},
},
parent: {
name: 'app',
bindings: {
a: {
key: 'a',
scope: 'Transient',
tags: {},
isLocked: true,
type: 'Constant',
},
b: {
key: 'b',
scope: 'Singleton',
tags: {X: 'X', Y: 'Y'},
isLocked: false,
type: 'DynamicValue',
},
c: {
key: 'c',
scope: 'Transient',
tags: {Z: 'Z', a: 1},
isLocked: false,
type: 'Constant',
},
d: {
key: 'd',
scope: 'Transient',
tags: {},
isLocked: false,
type: 'Class',
valueConstructor: 'MyService',
injections: {
constructorArguments: [
{targetName: 'MyService.constructor[0]', bindingKey: 'x'},
],
},
},
e: {
key: 'e',
scope: 'Transient',
tags: {},
isLocked: false,
type: 'Provider',
providerConstructor: 'MyServiceProvider',
injections: {
properties: {
x: {
targetName: 'MyServiceProvider.prototype.x',
bindingKey: 'x',
},
},
},
},
},
},
};
const json = childCtx.inspect({includeInjections: true});
expect(json).to.eql(expectedJSON);
});

class MyService {
constructor(@inject('x') private x: string) {}
}
class MyServiceProvider implements Provider<MyService> {
@inject('x')
private x: string;
value() {
return new MyService();
return new MyService(this.x);
}
}

Expand Down
33 changes: 31 additions & 2 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import debugFactory from 'debug';
import {EventEmitter} from 'events';
import {BindingAddress, BindingKey} from './binding-key';
import {Context} from './context';
import {inspectInjections} from './inject';
import {createProxyWithInterceptors} from './interception-proxy';
import {JSONObject} from './json-types';
import {ContextTags} from './keys';
import {Provider} from './provider';
import {
Expand Down Expand Up @@ -635,8 +637,8 @@ export class Binding<T = BoundValue> extends EventEmitter {
/**
* Convert to a plain JSON object
*/
toJSON(): object {
const json: Record<string, unknown> = {
toJSON(): JSONObject {
const json: JSONObject = {
key: this.key,
scope: this.scope,
tags: this.tagMap,
Expand All @@ -657,6 +659,23 @@ export class Binding<T = BoundValue> extends EventEmitter {
return json;
}

/**
* Inspect the binding to return a json representation of the binding information
* @param options - Options to control what information should be included
*/
inspect(options: BindingInspectOptions = {}): JSONObject {
options = {
includeInjections: false,
...options,
};
const json = this.toJSON();
if (options.includeInjections) {
const injections = inspectInjections(this);
if (Object.keys(injections).length) json.injections = injections;
}
return json;
}

/**
* A static method to create a binding so that we can do
* `Binding.bind('foo').to('bar');` as `new Binding('foo').to('bar')` is not
Expand Down Expand Up @@ -688,6 +707,16 @@ export class Binding<T = BoundValue> extends EventEmitter {
}
}

/**
* Options for binding.inspect()
*/
export interface BindingInspectOptions {
/**
* The flag to control if injections should be inspected
*/
includeInjections?: boolean;
}

function createInterceptionProxyFromInstance<T>(
instOrPromise: ValueOrPromise<T>,
context: Context,
Expand Down
Loading

0 comments on commit 3be32a3

Please sign in to comment.