Skip to content

Commit ff85e74

Browse files
bajtosraymondfeng
andcommitted
feat(context): binding filters
Co-Authored-By: Raymond Feng <raymondfeng@users.noreply.github.com>
1 parent 7113105 commit ff85e74

File tree

4 files changed

+143
-64
lines changed

4 files changed

+143
-64
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright IBM Corp. 2018. All Rights Reserved.
2+
// Node module: @loopback/context
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {Binding, BindingTag} from './binding';
7+
8+
/**
9+
* A function that filters bindings. It returns `true` to select a given
10+
* binding.
11+
*/
12+
export type BindingFilter<ValueType = unknown> = (
13+
binding: Readonly<Binding<ValueType>>,
14+
) => boolean;
15+
16+
/**
17+
* Create a binding filter for the tag pattern
18+
* @param tagPattern Binding tag name, regexp, or object
19+
*/
20+
export function filterByTag(tagPattern: BindingTag | RegExp): BindingFilter {
21+
if (typeof tagPattern === 'string' || tagPattern instanceof RegExp) {
22+
const regexp =
23+
typeof tagPattern === 'string'
24+
? wildcardToRegExp(tagPattern)
25+
: tagPattern;
26+
return b => Array.from(b.tagNames).some(t => regexp!.test(t));
27+
} else {
28+
return b => {
29+
for (const t in tagPattern) {
30+
// One tag name/value does not match
31+
if (b.tagMap[t] !== tagPattern[t]) return false;
32+
}
33+
// All tag name/value pairs match
34+
return true;
35+
};
36+
}
37+
}
38+
39+
/**
40+
* Create a binding filter from key pattern
41+
* @param keyPattern Binding key/wildcard, regexp, or a filter function
42+
*/
43+
export function filterByKey(
44+
keyPattern?: string | RegExp | BindingFilter,
45+
): BindingFilter {
46+
if (typeof keyPattern === 'string') {
47+
const regex = wildcardToRegExp(keyPattern);
48+
return binding => regex.test(binding.key);
49+
} else if (keyPattern instanceof RegExp) {
50+
return binding => keyPattern.test(binding.key);
51+
} else if (typeof keyPattern === 'function') {
52+
return keyPattern;
53+
}
54+
return () => true;
55+
}
56+
57+
/**
58+
* Convert a wildcard pattern to RegExp
59+
* @param pattern A wildcard string with `*` and `?` as special characters.
60+
* - `*` matches zero or more characters except `.` and `:`
61+
* - `?` matches exactly one character except `.` and `:`
62+
*/
63+
function wildcardToRegExp(pattern: string): RegExp {
64+
// Escape reserved chars for RegExp:
65+
// `- \ ^ $ + . ( ) | { } [ ] :`
66+
let regexp = pattern.replace(/[\-\[\]\/\{\}\(\)\+\.\\\^\$\|\:]/g, '\\$&');
67+
// Replace wildcard chars `*` and `?`
68+
// `*` matches zero or more characters except `.` and `:`
69+
// `?` matches one character except `.` and `:`
70+
regexp = regexp.replace(/\*/g, '[^.:]*').replace(/\?/g, '[^.:]');
71+
return new RegExp(`^${regexp}$`);
72+
}

packages/context/src/context.ts

Lines changed: 22 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {Binding, BindingTag} from './binding';
1010
import {BindingAddress, BindingKey} from './binding-key';
1111
import {ResolutionOptions, ResolutionSession} from './resolution-session';
1212
import {BoundValue, getDeepProperty, isPromiseLike} from './value-promise';
13+
import {BindingFilter, filterByKey, filterByTag} from './binding-filter';
1314

1415
const debug = debugModule('loopback:context');
1516

@@ -21,7 +22,15 @@ export class Context {
2122
* Name of the context
2223
*/
2324
readonly name: string;
25+
26+
/**
27+
* Key to binding map as the internal registry
28+
*/
2429
protected readonly registry: Map<string, Binding> = new Map();
30+
31+
/**
32+
* Parent context
33+
*/
2534
protected _parent?: Context;
2635

2736
/**
@@ -127,61 +136,25 @@ export class Context {
127136
return undefined;
128137
}
129138

130-
/**
131-
* Convert a wildcard pattern to RegExp
132-
* @param pattern A wildcard string with `*` and `?` as special characters.
133-
* - `*` matches zero or more characters except `.` and `:`
134-
* - `?` matches exactly one character except `.` and `:`
135-
*/
136-
private wildcardToRegExp(pattern: string): RegExp {
137-
// Escape reserved chars for RegExp:
138-
// `- \ ^ $ + . ( ) | { } [ ] :`
139-
let regexp = pattern.replace(/[\-\[\]\/\{\}\(\)\+\.\\\^\$\|\:]/g, '\\$&');
140-
// Replace wildcard chars `*` and `?`
141-
// `*` matches zero or more characters except `.` and `:`
142-
// `?` matches one character except `.` and `:`
143-
regexp = regexp.replace(/\*/g, '[^.:]*').replace(/\?/g, '[^.:]');
144-
return new RegExp(`^${regexp}$`);
145-
}
146-
147139
/**
148140
* Find bindings using the key pattern
149-
* @param pattern A regexp or wildcard pattern with optional `*` and `?`. If
150-
* it matches the binding key, the binding is included. For a wildcard:
141+
* @param pattern A filter function, a regexp or a wildcard pattern with
142+
* optional `*` and `?`. Find returns such bindings where the key matches
143+
* the provided pattern.
144+
*
145+
* For a wildcard:
151146
* - `*` matches zero or more characters except `.` and `:`
152147
* - `?` matches exactly one character except `.` and `:`
148+
*
149+
* For a filter function:
150+
* - return `true` to include the binding in the results
151+
* - return `false` to exclude it.
153152
*/
154153
find<ValueType = BoundValue>(
155-
pattern?: string | RegExp,
156-
): Readonly<Binding<ValueType>>[];
157-
158-
/**
159-
* Find bindings using a filter function
160-
* @param filter A function to test on the binding. It returns `true` to
161-
* include the binding or `false` to exclude the binding.
162-
*/
163-
find<ValueType = BoundValue>(
164-
filter: (binding: Readonly<Binding<ValueType>>) => boolean,
165-
): Readonly<Binding<ValueType>>[];
166-
167-
find<ValueType = BoundValue>(
168-
pattern?:
169-
| string
170-
| RegExp
171-
| ((binding: Readonly<Binding<ValueType>>) => boolean),
154+
pattern?: string | RegExp | BindingFilter,
172155
): Readonly<Binding<ValueType>>[] {
173-
let bindings: Readonly<Binding>[] = [];
174-
let filter: (binding: Readonly<Binding>) => boolean;
175-
if (!pattern) {
176-
filter = binding => true;
177-
} else if (typeof pattern === 'string') {
178-
const regex = this.wildcardToRegExp(pattern);
179-
filter = binding => regex.test(binding.key);
180-
} else if (pattern instanceof RegExp) {
181-
filter = binding => pattern.test(binding.key);
182-
} else {
183-
filter = pattern;
184-
}
156+
const bindings: Readonly<Binding>[] = [];
157+
const filter = filterByKey(pattern);
185158

186159
for (const b of this.registry.values()) {
187160
if (filter(b)) bindings.push(b);
@@ -208,22 +181,7 @@ export class Context {
208181
findByTag<ValueType = BoundValue>(
209182
tagFilter: BindingTag | RegExp,
210183
): Readonly<Binding<ValueType>>[] {
211-
if (typeof tagFilter === 'string' || tagFilter instanceof RegExp) {
212-
const regexp =
213-
typeof tagFilter === 'string'
214-
? this.wildcardToRegExp(tagFilter)
215-
: tagFilter;
216-
return this.find(b => Array.from(b.tagNames).some(t => regexp!.test(t)));
217-
}
218-
219-
return this.find(b => {
220-
for (const t in tagFilter) {
221-
// One tag name/value does not match
222-
if (b.tagMap[t] !== tagFilter[t]) return false;
223-
}
224-
// All tag name/value pairs match
225-
return true;
226-
});
184+
return this.find(filterByTag(tagFilter));
227185
}
228186

229187
protected _mergeWithParent<ValueType>(

packages/context/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './binding';
88
export * from './binding-decorator';
99
export * from './binding-inspector';
1010
export * from './binding-key';
11+
export * from './binding-filter';
1112
export * from './context';
1213
export * from './inject';
1314
export * from './keys';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright IBM Corp. 2017,2018. All Rights Reserved.
2+
// Node module: @loopback/context
3+
// This file is licensed under the MIT License.
4+
// License text available at https://opensource.org/licenses/MIT
5+
6+
import {expect} from '@loopback/testlab';
7+
import {Binding, filterByKey, filterByTag} from '../..';
8+
9+
const key = 'foo';
10+
11+
describe('BindingFilter', () => {
12+
let binding: Binding;
13+
beforeEach(givenBinding);
14+
15+
describe('filterByTag', () => {
16+
it('accepts bindings MATCHING the provided tag name', () => {
17+
const filter = filterByTag('controller');
18+
binding.tag('controller');
19+
expect(filter(binding)).to.be.true();
20+
});
21+
22+
it('rejects bindings NOT MATCHING the provided tag name', () => {
23+
const filter = filterByTag('controller');
24+
binding.tag('dataSource');
25+
expect(filter(binding)).to.be.false();
26+
});
27+
28+
// TODO: filter by tag map, filter by regexp
29+
});
30+
31+
describe('filterByKey', () => {
32+
it('accepts bindings MATCHING the provided key', () => {
33+
const filter = filterByKey(key);
34+
expect(filter(binding)).to.be.true();
35+
});
36+
37+
it('rejects bindings NOT MATCHING the provided key', () => {
38+
const filter = filterByKey(`another-${key}`);
39+
expect(filter(binding)).to.be.false();
40+
});
41+
42+
// TODO: filter by regexp, filter by BindingFunction
43+
});
44+
45+
function givenBinding() {
46+
binding = new Binding(key);
47+
}
48+
});

0 commit comments

Comments
 (0)