Skip to content

Commit fc8f260

Browse files
committed
feat(context): add @inject.tag to allow injection by a tag
1 parent 25a9e91 commit fc8f260

File tree

2 files changed

+175
-1
lines changed

2 files changed

+175
-1
lines changed

packages/context/src/inject.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import {BoundValue, ValueOrPromise} from './binding';
1414
import {Context} from './context';
1515
import {ResolutionSession} from './resolution-session';
16+
import {isPromise} from './is-promise';
1617

1718
const PARAMETERS_KEY = 'inject:parameters';
1819
const PROPERTIES_KEY = 'inject:properties';
@@ -185,6 +186,22 @@ export namespace inject {
185186
) {
186187
return inject(bindingKey, metadata, resolveAsSetter);
187188
};
189+
190+
/**
191+
* Inject an array of values by a tag
192+
* @param bindingTag Tag name or regex
193+
* @example
194+
* ```ts
195+
* class AuthenticationManager {
196+
* constructor(
197+
* @inject.tag('authentication.strategy') public strategies: Strategy[],
198+
* ) { }
199+
* }
200+
* ```
201+
*/
202+
export const tag = function injectTag(bindingTag: string | RegExp) {
203+
return inject('', {tag: bindingTag}, resolveByTag);
204+
};
188205
}
189206

190207
function resolveAsGetter(
@@ -225,6 +242,37 @@ export function describeInjectedArguments(
225242
return meta || [];
226243
}
227244

245+
function resolveByTag(
246+
ctx: Context,
247+
injection: Injection,
248+
session?: ResolutionSession,
249+
) {
250+
const tag: string | RegExp = injection.metadata!.tag;
251+
const bindings = ctx.findByTag(tag);
252+
const values: BoundValue[] = new Array(bindings.length);
253+
254+
// A closure to set a value by index
255+
const valSetter = (i: number) => (val: BoundValue) => (values[i] = val);
256+
257+
let asyncResolvers: PromiseLike<BoundValue>[] = [];
258+
// tslint:disable-next-line:prefer-for-of
259+
for (let i = 0; i < bindings.length; i++) {
260+
// We need to clone the session so that resolution of multiple bindings
261+
// can be tracked in parallel
262+
const val = bindings[i].getValue(ctx, ResolutionSession.fork(session));
263+
if (isPromise(val)) {
264+
asyncResolvers.push(val.then(valSetter(i)));
265+
} else {
266+
values[i] = val;
267+
}
268+
}
269+
if (asyncResolvers.length) {
270+
return Promise.all(asyncResolvers).then(vals => values);
271+
} else {
272+
return values;
273+
}
274+
}
275+
228276
/**
229277
* Return a map of injection objects for properties
230278
* @param target The target class for static properties or

packages/context/test/acceptance/class-level-bindings.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55

66
import {expect} from '@loopback/testlab';
77
import {Context, inject, Setter, Getter} from '../..';
8+
import {Provider} from '../../src/provider';
9+
import {Injection} from '../../src/inject';
10+
import {ResolutionSession} from '../../src/resolution-session';
811

912
const INFO_CONTROLLER = 'controllers.info';
1013

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

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

183+
it('injects values by tag', () => {
184+
class Store {
185+
constructor(@inject.tag('store:location') public locations: string[]) {}
186+
}
187+
188+
ctx.bind('store').toClass(Store);
189+
ctx
190+
.bind('store.locations.sf')
191+
.to('San Francisco')
192+
.tag('store:location');
193+
ctx
194+
.bind('store.locations.sj')
195+
.to('San Jose')
196+
.tag('store:location');
197+
const store: Store = ctx.getSync('store');
198+
expect(store.locations).to.eql(['San Francisco', 'San Jose']);
199+
});
200+
201+
it('injects values by tag regex', () => {
202+
class Store {
203+
constructor(
204+
@inject.tag(/.+:location:sj/)
205+
public locations: string[],
206+
) {}
207+
}
208+
209+
ctx.bind('store').toClass(Store);
210+
ctx
211+
.bind('store.locations.sf')
212+
.to('San Francisco')
213+
.tag('store:location:sf');
214+
ctx
215+
.bind('store.locations.sj')
216+
.to('San Jose')
217+
.tag('store:location:sj');
218+
const store: Store = ctx.getSync('store');
219+
expect(store.locations).to.eql(['San Jose']);
220+
});
221+
222+
it('injects empty values by tag if not found', () => {
223+
class Store {
224+
constructor(@inject.tag('store:location') public locations: string[]) {}
225+
}
226+
227+
ctx.bind('store').toClass(Store);
228+
const store: Store = ctx.getSync('store');
229+
expect(store.locations).to.eql([]);
230+
});
231+
232+
it('injects values by tag asynchronously', async () => {
233+
class Store {
234+
constructor(@inject.tag('store:location') public locations: string[]) {}
235+
}
236+
237+
ctx.bind('store').toClass(Store);
238+
ctx
239+
.bind('store.locations.sf')
240+
.to('San Francisco')
241+
.tag('store:location');
242+
ctx
243+
.bind('store.locations.sj')
244+
.toDynamicValue(async () => 'San Jose')
245+
.tag('store:location');
246+
const store: Store = await ctx.get('store');
247+
expect(store.locations).to.eql(['San Francisco', 'San Jose']);
248+
});
249+
250+
it('injects values by tag asynchronously', async () => {
251+
class Store {
252+
constructor(@inject.tag('store:location') public locations: string[]) {}
253+
}
254+
255+
let resolutionPath;
256+
class LocationProvider implements Provider<string> {
257+
@inject(
258+
'location',
259+
{},
260+
// Set up a custom resolve() to access information from the session
261+
(c: Context, injection: Injection, session: ResolutionSession) => {
262+
resolutionPath = session.getResolutionPath();
263+
return 'San Jose';
264+
},
265+
)
266+
location: string;
267+
value() {
268+
return this.location;
269+
}
270+
}
271+
272+
ctx.bind('store').toClass(Store);
273+
ctx
274+
.bind('store.locations.sf')
275+
.to('San Francisco')
276+
.tag('store:location');
277+
ctx
278+
.bind('store.locations.sj')
279+
.toProvider(LocationProvider)
280+
.tag('store:location');
281+
const store: Store = await ctx.get('store');
282+
expect(store.locations).to.eql(['San Francisco', 'San Jose']);
283+
expect(resolutionPath).to.eql(
284+
'store --> @Store.constructor[0] --> store.locations.sj --> ' +
285+
'@LocationProvider.prototype.location',
286+
);
287+
});
288+
289+
it('reports error when @inject.tag rejects a promise', async () => {
290+
class Store {
291+
constructor(@inject.tag('store:location') public locations: string[]) {}
292+
}
293+
294+
ctx.bind('store').toClass(Store);
295+
ctx
296+
.bind('store.locations.sf')
297+
.to('San Francisco')
298+
.tag('store:location');
299+
ctx
300+
.bind('store.locations.sj')
301+
.toDynamicValue(() => Promise.reject(new Error('Bad')))
302+
.tag('store:location');
303+
await expect(ctx.get('store')).to.be.rejectedWith('Bad');
304+
});
305+
180306
function createContext() {
181307
ctx = new Context();
182308
}

0 commit comments

Comments
 (0)