Skip to content

Commit

Permalink
feat(context): allow context.find by a filter function
Browse files Browse the repository at this point in the history
Sometimes we need to find bindings using multiple conditions, such as
multiple tags or the bound class. The filter function gives us the
maximum flexibility.
  • Loading branch information
raymondfeng committed Feb 26, 2018
1 parent 4ee3429 commit 9b1e26c
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 31 deletions.
82 changes: 52 additions & 30 deletions packages/context/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,51 +119,73 @@ export class Context {
return undefined;
}

/**
* Convert a wildcard pattern to RegExp
* @param pattern A wildcard string with `*` and `?` as special characters.
* - `*` matches zero or more characters except `.` and `:`
* - `?` matches exactly one character except `.` and `:`
*/
private wildcardToRegExp(pattern: string): RegExp {
// Escape reserved chars for RegExp:
// `- \ ^ $ + . ( ) | { } [ ] :`
let regexp = pattern.replace(/[\-\[\]\/\{\}\(\)\+\.\\\^\$\|\:]/g, '\\$&');
// Replace wildcard chars `*` and `?`
// `*` matches zero or more characters except `.` and `:`
// `?` matches one character except `.` and `:`
regexp = regexp.replace(/\*/g, '[^.:]*').replace(/\?/g, '[^.:]');
return new RegExp(`^${regexp}$`);
}

/**
* Find bindings using the key pattern
* @param pattern Key regexp or pattern with optional `*` wildcards
* @param pattern A regexp or wildcard pattern with optional `*` and `?`. If
* it matches the binding key, the binding is included. For a wildcard:
* - `*` matches zero or more characters except `.` and `:`
* - `?` matches exactly one character except `.` and `:`
*/
find(pattern?: string | RegExp): Binding[];

/**
* Find bindings using a filter function
* @param filter A function to test on the binding. It returns `true` to
* include the binding or `false` to exclude the binding.
*/
find(pattern?: string | RegExp): Binding[] {
find(filter: (binding: Binding) => boolean): Binding[];

find(pattern?: string | RegExp | ((binding: Binding) => boolean)): Binding[] {
let bindings: Binding[] = [];
let glob: RegExp | undefined = undefined;
if (typeof pattern === 'string') {
// TODO(@superkhau): swap with production grade glob to regex lib
Binding.validateKey(pattern);
glob = new RegExp('^' + pattern.split('*').join('.*') + '$');
let filter: (binding: Binding) => boolean;
if (!pattern) {
filter = binding => true;
} else if (typeof pattern === 'string') {
const regex = this.wildcardToRegExp(pattern);
filter = binding => regex.test(binding.key);
} else if (pattern instanceof RegExp) {
glob = pattern;
}
if (glob) {
this.registry.forEach(binding => {
const isMatch = glob!.test(binding.key);
if (isMatch) bindings.push(binding);
});
filter = binding => pattern.test(binding.key);
} else {
bindings = Array.from(this.registry.values());
filter = pattern;
}

for (const b of this.registry.values()) {
if (filter(b)) bindings.push(b);
}

const parentBindings = this._parent && this._parent.find(pattern);
const parentBindings = this._parent && this._parent.find(filter);
return this._mergeWithParent(bindings, parentBindings);
}

/**
* Find bindings using the tag pattern
* @param pattern Tag name regexp or pattern with optional `*` wildcards
* @param pattern A regexp or wildcard pattern with optional `*` and `?`. If
* it matches one of the binding tags, the binding is included. For a
* wildcard:
* - `*` matches zero or more characters except `.` and `:`
* - `?` matches exactly one character except `.` and `:`
*/
findByTag(pattern: string | RegExp): Binding[] {
const bindings: Binding[] = [];
// TODO(@superkhau): swap with production grade glob to regex lib
const glob =
typeof pattern === 'string'
? new RegExp('^' + pattern.split('*').join('.*') + '$')
: pattern;
this.registry.forEach(binding => {
const isMatch = Array.from(binding.tags).some(tag => glob.test(tag));
if (isMatch) bindings.push(binding);
});

const parentBindings = this._parent && this._parent.findByTag(pattern);
return this._mergeWithParent(bindings, parentBindings);
const regexp =
typeof pattern === 'string' ? this.wildcardToRegExp(pattern) : pattern;
return this.find(b => Array.from(b.tags).some(t => regexp.test(t)));
}

protected _mergeWithParent(childList: Binding[], parentList?: Binding[]) {
Expand Down
56 changes: 56 additions & 0 deletions packages/context/test/unit/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,46 @@ describe('Context', () => {
expect(result).to.be.eql([b2, b3]);
});

it('returns matching binding with * respecting key separators', () => {
const b1 = ctx.bind('foo');
const b2 = ctx.bind('foo.bar');
const b3 = ctx.bind('foo:bar');
let result = ctx.find('*');
expect(result).to.be.eql([b1]);
result = ctx.find('*.*');
expect(result).to.be.eql([b2]);
result = ctx.find('*:ba*');
expect(result).to.be.eql([b3]);
});

it('returns matching binding with ? respecting separators', () => {
const b1 = ctx.bind('foo');
const b2 = ctx.bind('foo.bar');
const b3 = ctx.bind('foo:bar');
let result = ctx.find('???');
expect(result).to.be.eql([b1]);
result = ctx.find('???.???');
expect(result).to.be.eql([b2]);
result = ctx.find('???:???');
expect(result).to.be.eql([b3]);
result = ctx.find('?');
expect(result).to.be.eql([]);
result = ctx.find('???????');
expect(result).to.be.eql([]);
});

it('escapes reserved chars for regexp', () => {
const b1 = ctx.bind('foo');
const b2 = ctx.bind('foo+bar');
const b3 = ctx.bind('foo|baz');
let result = ctx.find('fo+');
expect(result).to.be.eql([]);
result = ctx.find('foo+bar');
expect(result).to.be.eql([b2]);
result = ctx.find('foo|baz');
expect(result).to.be.eql([b3]);
});

it('returns matching binding with regexp', () => {
const b1 = ctx.bind('foo');
const b2 = ctx.bind('bar');
Expand All @@ -182,6 +222,22 @@ describe('Context', () => {
result = ctx.find(/ba/);
expect(result).to.be.eql([b2, b3]);
});

it('returns matching binding with filter', () => {
const b1 = ctx.bind('foo').inScope(BindingScope.SINGLETON);
const b2 = ctx.bind('bar').tag('b');
const b3 = ctx.bind('baz').tag('b');
let result = ctx.find(() => true);
expect(result).to.be.eql([b1, b2, b3]);
result = ctx.find(() => false);
expect(result).to.be.eql([]);
result = ctx.find(binding => binding.key.startsWith('ba'));
expect(result).to.be.eql([b2, b3]);
result = ctx.find(binding => binding.scope === BindingScope.SINGLETON);
expect(result).to.be.eql([b1]);
result = ctx.find(binding => binding.tags.has('b'));
expect(result).to.be.eql([b2, b3]);
});
});

describe('findByTag', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/acceptance/application.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('Bootstrapping the application', () => {
class AuditComponent {}
const app = new Application();
app.component(AuditComponent);
const componentKeys = app.find('component.*').map(b => b.key);
const componentKeys = app.find('components.*').map(b => b.key);
expect(componentKeys).to.containEql('components.AuditComponent');

const componentInstance = app.getSync('components.AuditComponent');
Expand Down

0 comments on commit 9b1e26c

Please sign in to comment.