Skip to content

Commit

Permalink
feat(context): allow binding sorter on ContextView and @Inject.*
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed May 7, 2019
1 parent f9f7515 commit 2aaa0b7
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 13 deletions.
3 changes: 2 additions & 1 deletion docs/site/Context.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,8 @@ should be able to pick up these new routes without restarting.
To support the dynamic tracking of such artifacts registered within a context
chain, we introduce `ContextObserver` interface and `ContextView` class that can
be used to watch a list of bindings matching certain criteria depicted by a
`BindingFilter` function.
`BindingFilter` function and an optional `BindingSorter` function to sort
matched bindings.

```ts
import {Context, ContextView} from '@loopback/context';
Expand Down
17 changes: 17 additions & 0 deletions docs/site/Decorators_inject.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ class MyControllerWithValues {
}
```

To sort matched bindings found by the binding filter function, `@inject` honors
`bindingSorter` in `metadata`:

```ts
class MyControllerWithValues {
constructor(
@inject(binding => binding.tagNames.includes('foo'), {
bindingSorter: (a, b) => {
// Sort by value of `foo` tag
return a.tagMap.foo.localCompare(b.tagMap.foo);
},
})
public values: string[],
) {}
}
```

A few variants of `@inject` are provided to declare special forms of
dependencies.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {Context, ContextView, filterByTag, Getter, inject} from '../..';
import {
Context,
ContextView,
createSorterByGroup,
filterByTag,
Getter,
inject,
} from '../..';

let app: Context;
let server: Context;
Expand All @@ -29,6 +36,26 @@ describe('@inject.* to receive multiple values matching a filter', async () => {
expect(await getter()).to.eql([3, 7, 5]);
});

it('injects as getter with bindingSorter', async () => {
class MyControllerWithGetter {
@inject.getter(workloadMonitorFilter, {
bindingSorter: createSorterByGroup('name'),
})
getter: Getter<number[]>;
}

server.bind('my-controller').toClass(MyControllerWithGetter);
const inst = await server.get<MyControllerWithGetter>('my-controller');
const getter = inst.getter;
// app-reporter, server-reporter
expect(await getter()).to.eql([5, 3]);
// Add a new binding that matches the filter
givenWorkloadMonitor(server, 'server-reporter-2', 7);
// The getter picks up the new binding by order
// // app-reporter, server-reporter, server-reporter-2
expect(await getter()).to.eql([5, 3, 7]);
});

describe('@inject', () => {
class MyControllerWithValues {
constructor(
Expand All @@ -48,6 +75,23 @@ describe('@inject.* to receive multiple values matching a filter', async () => {
const inst = server.getSync<MyControllerWithValues>('my-controller');
expect(inst.values).to.eql([3, 5]);
});

it('injects as values with bindingSorter', async () => {
class MyControllerWithBindingSorter {
constructor(
@inject(workloadMonitorFilter, {
bindingSorter: createSorterByGroup('name'),
})
public values: number[],
) {}
}
server.bind('my-controller').toClass(MyControllerWithBindingSorter);
const inst = await server.get<MyControllerWithBindingSorter>(
'my-controller',
);
// app-reporter, server-reporter
expect(inst.values).to.eql([5, 3]);
});
});

it('injects as a view', async () => {
Expand All @@ -68,6 +112,24 @@ describe('@inject.* to receive multiple values matching a filter', async () => {
expect(await view.values()).to.eql([3, 5]);
});

it('injects as a view with bindingSorter', async () => {
class MyControllerWithView {
@inject.view(workloadMonitorFilter, {
bindingSorter: createSorterByGroup('name'),
})
view: ContextView<number[]>;
}

server.bind('my-controller').toClass(MyControllerWithView);
const inst = await server.get<MyControllerWithView>('my-controller');
const view = inst.view;
expect(view.bindings.map(b => b.tagMap.name)).to.eql([
'app-reporter',
'server-reporter',
]);
expect(await view.values()).to.eql([5, 3]);
});

function givenWorkloadMonitors() {
givenServerWithinAnApp();
givenWorkloadMonitor(server, 'server-reporter', 3);
Expand All @@ -84,7 +146,8 @@ describe('@inject.* to receive multiple values matching a filter', async () => {
return ctx
.bind(`workloadMonitors.${name}`)
.to(workload)
.tag('workloadMonitor');
.tag('workloadMonitor')
.tag({name});
}
});

Expand Down
30 changes: 28 additions & 2 deletions packages/context/src/__tests__/unit/context-view.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
BindingScope,
Context,
ContextView,
createSorterByGroup,
createViewGetter,
filterByTag,
} from '../..';
Expand All @@ -26,6 +27,15 @@ describe('ContextView', () => {
expect(taggedAsFoo.bindings).to.eql(bindings);
});

it('sorts matched bindings', () => {
const view = new ContextView(
server,
filterByTag('foo'),
createSorterByGroup('foo', ['b', 'a']),
);
expect(view.bindings).to.eql([bindings[1], bindings[0]]);
});

it('resolves bindings', async () => {
expect(await taggedAsFoo.resolve()).to.eql(['BAR', 'FOO']);
expect(await taggedAsFoo.values()).to.eql(['BAR', 'FOO']);
Expand Down Expand Up @@ -154,6 +164,22 @@ describe('ContextView', () => {
.tag('foo');
expect(await getter()).to.eql(['BAR', 'XYZ', 'FOO']);
});

it('creates a getter function for the binding filter and sorter', async () => {
const getter = createViewGetter(server, filterByTag('foo'), (a, b) => {
return a.key.localeCompare(b.key);
});
expect(await getter()).to.eql(['BAR', 'FOO']);
server
.bind('abc')
.to('ABC')
.tag('abc');
server
.bind('xyz')
.to('XYZ')
.tag('foo');
expect(await getter()).to.eql(['BAR', 'FOO', 'XYZ']);
});
});

function givenContextView() {
Expand All @@ -169,14 +195,14 @@ describe('ContextView', () => {
server
.bind('bar')
.toDynamicValue(() => Promise.resolve('BAR'))
.tag('foo', 'bar')
.tag({foo: 'a'})
.inScope(BindingScope.SINGLETON),
);
bindings.push(
app
.bind('foo')
.to('FOO')
.tag('foo', 'bar'),
.tag({foo: 'b'}),
);
}
});
16 changes: 15 additions & 1 deletion packages/context/src/context-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {EventEmitter} from 'events';
import {promisify} from 'util';
import {Binding} from './binding';
import {BindingFilter} from './binding-filter';
import {BindingSorter} from './binding-sorter';
import {Context} from './context';
import {
ContextEventType,
Expand Down Expand Up @@ -43,6 +44,7 @@ export class ContextView<T = unknown> extends EventEmitter
constructor(
protected readonly context: Context,
public readonly filter: BindingFilter,
public readonly sorter?: BindingSorter,
) {
super();
}
Expand Down Expand Up @@ -88,6 +90,9 @@ export class ContextView<T = unknown> extends EventEmitter
protected findBindings(): Readonly<Binding<T>>[] {
debug('Finding matching bindings');
const found = this.context.find(this.filter);
if (typeof this.sorter === 'function') {
found.sort(this.sorter);
}
this._cachedBindings = found;
return found;
}
Expand Down Expand Up @@ -158,14 +163,23 @@ export class ContextView<T = unknown> extends EventEmitter
* Create a context view as a getter
* @param ctx Context object
* @param bindingFilter A function to match bindings
* @param bindingSorter A function to sort matched bindings
* @param session Resolution session
*/
export function createViewGetter<T = unknown>(
ctx: Context,
bindingFilter: BindingFilter,
bindingSorterOrSession?: BindingSorter | ResolutionSession,
session?: ResolutionSession,
): Getter<T[]> {
const view = new ContextView<T>(ctx, bindingFilter);
let bindingSorter: BindingSorter | undefined = undefined;
if (typeof bindingSorterOrSession === 'function') {
bindingSorter = bindingSorterOrSession;
} else if (bindingSorterOrSession instanceof ResolutionSession) {
session = bindingSorterOrSession;
}

const view = new ContextView<T>(ctx, bindingFilter, bindingSorter);
view.open();
return view.asGetter(session);
}
6 changes: 4 additions & 2 deletions packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {v1 as uuidv1} from 'uuid';
import {Binding, BindingTag} from './binding';
import {BindingFilter, filterByKey, filterByTag} from './binding-filter';
import {BindingAddress, BindingKey} from './binding-key';
import {BindingSorter} from './binding-sorter';
import {
ContextEventObserver,
ContextEventType,
Expand Down Expand Up @@ -436,9 +437,10 @@ export class Context extends EventEmitter {
/**
* Create a view of the context chain with the given binding filter
* @param filter A function to match bindings
* @param sorter A function to sort matched bindings
*/
createView<T = unknown>(filter: BindingFilter) {
const view = new ContextView<T>(this, filter);
createView<T = unknown>(filter: BindingFilter, sorter?: BindingSorter) {
const view = new ContextView<T>(this, filter, sorter);
view.open();
return view;
}
Expand Down
26 changes: 22 additions & 4 deletions packages/context/src/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isBindingAddress,
} from './binding-filter';
import {BindingAddress} from './binding-key';
import {BindingSorter} from './binding-sorter';
import {BindingCreationPolicy, Context} from './context';
import {ContextView, createViewGetter} from './context-view';
import {ResolutionSession} from './resolution-session';
Expand Down Expand Up @@ -56,6 +57,10 @@ export interface InjectionMetadata {
* Control if the dependency is optional, default to false
*/
optional?: boolean;
/**
* Optional sorter for matched bindings
*/
bindingSorter?: BindingSorter;
/**
* Other attributes
*/
Expand Down Expand Up @@ -345,7 +350,7 @@ export namespace inject {
* @param bindingFilter A binding filter function
* @param metadata
*/
export const view = function injectByFilter(
export const view = function injectContextView(
bindingFilter: BindingFilter,
metadata?: InjectionMetadata,
) {
Expand Down Expand Up @@ -531,7 +536,11 @@ function resolveValuesByFilter(
) {
assertTargetType(injection, Array);
const bindingFilter = injection.bindingSelector as BindingFilter;
const view = new ContextView(ctx, bindingFilter);
const view = new ContextView(
ctx,
bindingFilter,
injection.metadata.bindingSorter,
);
return view.resolve(session);
}

Expand All @@ -550,7 +559,12 @@ function resolveAsGetterByFilter(
) {
assertTargetType(injection, Function, 'Getter function');
const bindingFilter = injection.bindingSelector as BindingFilter;
return createViewGetter(ctx, bindingFilter, session);
return createViewGetter(
ctx,
bindingFilter,
injection.metadata.bindingSorter,
session,
);
}

/**
Expand All @@ -563,7 +577,11 @@ function resolveAsContextView(ctx: Context, injection: Readonly<Injection>) {
assertTargetType(injection, ContextView);

const bindingFilter = injection.bindingSelector as BindingFilter;
const view = new ContextView(ctx, bindingFilter);
const view = new ContextView(
ctx,
bindingFilter,
injection.metadata.bindingSorter,
);
view.open();
return view;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/extension-point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ export function extensions(extensionPointName?: string) {
inferExtensionPointName(injection.target, session.currentBinding);

const bindingFilter = extensionFilter(extensionPointName);
return createViewGetter(ctx, bindingFilter, session);
return createViewGetter(
ctx,
bindingFilter,
injection.metadata.bindingSorter,
session,
);
});
}

Expand Down

0 comments on commit 2aaa0b7

Please sign in to comment.