Skip to content

Commit

Permalink
feat(context): improve context/binding with inspect/toJSON for metada…
Browse files Browse the repository at this point in the history
…ta dumping

ctx.inspect() can be now used to print out the context hierarchy in JSON.
This is useful for troubeshooting and rendering in UI.
  • Loading branch information
raymondfeng committed Nov 22, 2019
1 parent c6df67b commit ac399f7
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 34 deletions.
6 changes: 6 additions & 0 deletions packages/context/src/__tests__/unit/binding.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ describe('Binding', () => {
const b = ctx.bind('provider_key').toProvider(MyProvider);
expect(b.type).to.equal(BindingType.PROVIDER);
});

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

describe('toAlias(bindingKeyWithPath)', () => {
Expand Down
107 changes: 81 additions & 26 deletions packages/context/src/__tests__/unit/context.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Context,
ContextEventObserver,
isPromiseLike,
Provider,
} from '../..';

/**
Expand Down Expand Up @@ -790,8 +791,82 @@ describe('Context', () => {
});
});

describe('toJSON()', () => {
describe('toJSON() and inspect()', () => {
beforeEach(setupBindings);

const expectedBindings = {
a: {
key: 'a',
scope: BindingScope.TRANSIENT,
tags: {},
isLocked: true,
type: BindingType.CONSTANT,
},
b: {
key: 'b',
scope: BindingScope.SINGLETON,
tags: {X: 'X', Y: 'Y'},
isLocked: false,
type: BindingType.DYNAMIC_VALUE,
},
c: {
key: 'c',
scope: BindingScope.TRANSIENT,
tags: {Z: 'Z', a: 1},
isLocked: false,
type: BindingType.CONSTANT,
},
d: {
key: 'd',
scope: BindingScope.TRANSIENT,
tags: {},
isLocked: false,
type: BindingType.CLASS,
valueConstructor: 'MyService',
},
e: {
key: 'e',
scope: BindingScope.TRANSIENT,
tags: {},
isLocked: false,
type: BindingType.PROVIDER,
providerConstructor: 'MyServiceProvider',
},
};

it('converts to plain JSON object', () => {
expect(ctx.toJSON()).to.eql(expectedBindings);
});

it('inspects as plain JSON object', () => {
expect(ctx.inspect()).to.eql({
name: 'app',
bindings: expectedBindings,
});
});

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

expect(childCtx.inspect()).to.eql({
name: 'server',
bindings: childCtx.toJSON(),
parent: {
name: 'app',
bindings: expectedBindings,
},
});
});

class MyService {}
class MyServiceProvider implements Provider<MyService> {
value() {
return new MyService();
}
}

function setupBindings() {
ctx
.bind('a')
.to('1')
Expand All @@ -805,33 +880,13 @@ describe('Context', () => {
.bind('c')
.to(3)
.tag('Z', {a: 1});
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: 'X', Y: 'Y'},
isLocked: false,
type: BindingType.DYNAMIC_VALUE,
},
c: {
key: 'c',
scope: BindingScope.TRANSIENT,
tags: {Z: 'Z', a: 1},
isLocked: false,
type: BindingType.CONSTANT,
},
});
});

ctx.bind('d').toClass(MyService);
ctx.bind('e').toProvider(MyServiceProvider);
}
});

function createContext() {
ctx = new TestContext();
ctx = new TestContext('app');
}
});
26 changes: 21 additions & 5 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,24 @@ export class Binding<T = BoundValue> {
private _getValue: ValueGetter<T>;

private _valueConstructor?: Constructor<T>;
private _providerConstructor?: Constructor<Provider<T>>;

/**
* For bindings bound via toClass, this property contains the constructor
* function
* For bindings bound via `toClass()`, this property contains the constructor
* function of the class
*/
public get valueConstructor(): Constructor<T> | undefined {
return this._valueConstructor;
}

/**
* For bindings bound via `toProvider()`, this property contains the
* constructor function of the provider class
*/
public get providerConstructor(): Constructor<Provider<T>> | undefined {
return this._providerConstructor;
}

constructor(key: BindingAddress<T>, public isLocked: boolean = false) {
BindingKey.validate(key);
this.key = key.toString();
Expand Down Expand Up @@ -494,6 +504,7 @@ export class Binding<T = BoundValue> {
debug('Bind %s to provider %s', this.key, providerClass.name);
}
this._type = BindingType.PROVIDER;
this._providerConstructor = providerClass;
this._setValueGetter((ctx, options) => {
const providerOrPromise = instantiateClass<Provider<T>>(
providerClass,
Expand Down Expand Up @@ -576,9 +587,8 @@ export class Binding<T = BoundValue> {
/**
* Convert to a plain JSON object
*/
toJSON(): Object {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const json: {[name: string]: any} = {
toJSON(): object {
const json: Record<string, unknown> = {
key: this.key,
scope: this.scope,
tags: this.tagMap,
Expand All @@ -587,6 +597,12 @@ export class Binding<T = BoundValue> {
if (this.type != null) {
json.type = this.type;
}
if (this._valueConstructor != null) {
json.valueConstructor = this._valueConstructor.name;
}
if (this._providerConstructor != null) {
json.providerConstructor = this._providerConstructor.name;
}
return json;
}

Expand Down
22 changes: 19 additions & 3 deletions packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -968,10 +968,26 @@ export class Context extends EventEmitter {
/**
* Create a plain JSON object for the context
*/
toJSON(): Object {
const json: {[key: string]: Object} = {};
toJSON(): object {
const bindings: Record<string, object> = {};
for (const [k, v] of this.registry) {
json[k] = v.toJSON();
bindings[k] = v.toJSON();
}
return bindings;
}

/**
* Inspect the context and dump out a JSON object representing the context
* hierarchy
*/
// TODO(rfeng): Evaluate https://nodejs.org/api/util.html#util_custom_inspection_functions_on_objects
inspect(): object {
const json: Record<string, unknown> = {
name: this.name,
bindings: this.toJSON(),
};
if (this._parent) {
json.parent = this._parent.inspect();
}
return json;
}
Expand Down

0 comments on commit ac399f7

Please sign in to comment.