Skip to content

Commit

Permalink
feat(context): add @inject.tag to allow injection by a tag
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Jan 16, 2018
1 parent 25a9e91 commit fc8f260
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 1 deletion.
48 changes: 48 additions & 0 deletions packages/context/src/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import {BoundValue, ValueOrPromise} from './binding';
import {Context} from './context';
import {ResolutionSession} from './resolution-session';
import {isPromise} from './is-promise';

const PARAMETERS_KEY = 'inject:parameters';
const PROPERTIES_KEY = 'inject:properties';
Expand Down Expand Up @@ -185,6 +186,22 @@ export namespace inject {
) {
return inject(bindingKey, metadata, resolveAsSetter);
};

/**
* Inject an array of values by a tag
* @param bindingTag Tag name or regex
* @example
* ```ts
* class AuthenticationManager {
* constructor(
* @inject.tag('authentication.strategy') public strategies: Strategy[],
* ) { }
* }
* ```
*/
export const tag = function injectTag(bindingTag: string | RegExp) {
return inject('', {tag: bindingTag}, resolveByTag);
};
}

function resolveAsGetter(
Expand Down Expand Up @@ -225,6 +242,37 @@ export function describeInjectedArguments(
return meta || [];
}

function resolveByTag(
ctx: Context,
injection: Injection,
session?: ResolutionSession,
) {
const tag: string | RegExp = injection.metadata!.tag;
const bindings = ctx.findByTag(tag);
const values: BoundValue[] = new Array(bindings.length);

// A closure to set a value by index
const valSetter = (i: number) => (val: BoundValue) => (values[i] = val);

let asyncResolvers: PromiseLike<BoundValue>[] = [];
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < bindings.length; i++) {
// We need to clone the session so that resolution of multiple bindings
// can be tracked in parallel
const val = bindings[i].getValue(ctx, ResolutionSession.fork(session));
if (isPromise(val)) {
asyncResolvers.push(val.then(valSetter(i)));
} else {
values[i] = val;
}
}
if (asyncResolvers.length) {
return Promise.all(asyncResolvers).then(vals => values);
} else {
return values;
}
}

/**
* Return a map of injection objects for properties
* @param target The target class for static properties or
Expand Down
128 changes: 127 additions & 1 deletion packages/context/test/acceptance/class-level-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@

import {expect} from '@loopback/testlab';
import {Context, inject, Setter, Getter} from '../..';
import {Provider} from '../../src/provider';
import {Injection} from '../../src/inject';
import {ResolutionSession} from '../../src/resolution-session';

const INFO_CONTROLLER = 'controllers.info';

describe('Context bindings - Injecting dependencies of classes', () => {
let ctx: Context;
before('given a context', createContext);
beforeEach('given a context', createContext);

it('injects constructor args', async () => {
ctx.bind('application.name').to('CodeHub');
Expand Down Expand Up @@ -177,6 +180,129 @@ describe('Context bindings - Injecting dependencies of classes', () => {
expect(resolved.config).to.equal('test-config');
});

it('injects values by tag', () => {
class Store {
constructor(@inject.tag('store:location') public locations: string[]) {}
}

ctx.bind('store').toClass(Store);
ctx
.bind('store.locations.sf')
.to('San Francisco')
.tag('store:location');
ctx
.bind('store.locations.sj')
.to('San Jose')
.tag('store:location');
const store: Store = ctx.getSync('store');
expect(store.locations).to.eql(['San Francisco', 'San Jose']);
});

it('injects values by tag regex', () => {
class Store {
constructor(
@inject.tag(/.+:location:sj/)
public locations: string[],
) {}
}

ctx.bind('store').toClass(Store);
ctx
.bind('store.locations.sf')
.to('San Francisco')
.tag('store:location:sf');
ctx
.bind('store.locations.sj')
.to('San Jose')
.tag('store:location:sj');
const store: Store = ctx.getSync('store');
expect(store.locations).to.eql(['San Jose']);
});

it('injects empty values by tag if not found', () => {
class Store {
constructor(@inject.tag('store:location') public locations: string[]) {}
}

ctx.bind('store').toClass(Store);
const store: Store = ctx.getSync('store');
expect(store.locations).to.eql([]);
});

it('injects values by tag asynchronously', async () => {
class Store {
constructor(@inject.tag('store:location') public locations: string[]) {}
}

ctx.bind('store').toClass(Store);
ctx
.bind('store.locations.sf')
.to('San Francisco')
.tag('store:location');
ctx
.bind('store.locations.sj')
.toDynamicValue(async () => 'San Jose')
.tag('store:location');
const store: Store = await ctx.get('store');
expect(store.locations).to.eql(['San Francisco', 'San Jose']);
});

it('injects values by tag asynchronously', async () => {
class Store {
constructor(@inject.tag('store:location') public locations: string[]) {}
}

let resolutionPath;
class LocationProvider implements Provider<string> {
@inject(
'location',
{},
// Set up a custom resolve() to access information from the session
(c: Context, injection: Injection, session: ResolutionSession) => {
resolutionPath = session.getResolutionPath();
return 'San Jose';
},
)
location: string;
value() {
return this.location;
}
}

ctx.bind('store').toClass(Store);
ctx
.bind('store.locations.sf')
.to('San Francisco')
.tag('store:location');
ctx
.bind('store.locations.sj')
.toProvider(LocationProvider)
.tag('store:location');
const store: Store = await ctx.get('store');
expect(store.locations).to.eql(['San Francisco', 'San Jose']);
expect(resolutionPath).to.eql(
'store --> @Store.constructor[0] --> store.locations.sj --> ' +
'@LocationProvider.prototype.location',
);
});

it('reports error when @inject.tag rejects a promise', async () => {
class Store {
constructor(@inject.tag('store:location') public locations: string[]) {}
}

ctx.bind('store').toClass(Store);
ctx
.bind('store.locations.sf')
.to('San Francisco')
.tag('store:location');
ctx
.bind('store.locations.sj')
.toDynamicValue(() => Promise.reject(new Error('Bad')))
.tag('store:location');
await expect(ctx.get('store')).to.be.rejectedWith('Bad');
});

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

0 comments on commit fc8f260

Please sign in to comment.