Skip to content

Commit

Permalink
refactor(Glob): BC-BREAK Use RegExp to implement Glob SWITCH glob to …
Browse files Browse the repository at this point in the history
…use RegExp

Previously, trailing single wildcards '*' could be "zero-length" matches.
The glob `foo.*` would match `foo` and `foo.bar` but not `foo.bar.baz`.
Likewise, the glob `foo.*.*` would also match `foo`.

Now, all single wildcards match one segment.
The glob `foo.*` matches `foo.bar` but does not match `foo`.
Use `foo.**` instead to match zero or more segments.

test(Glob): Add more glob tests
docs(Glob): Document glob
Closes #2965
  • Loading branch information
christopherthielen committed Sep 9, 2016
1 parent 4ede2fb commit d1dff31
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 66 deletions.
68 changes: 33 additions & 35 deletions src/common/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,75 +12,73 @@
* - [[HookMatchCriteria.retained]]
* - [[HookMatchCriteria.entering]]
*
* A `Glob` string is a pattern which matches state names according to the following rules:
* A `Glob` string is a pattern which matches state names.
* Nested state names are split into segments (separated by a dot) when processing.
* The state named `foo.bar.baz` is split into three segments ['foo', 'bar', 'baz']
*
* Globs work according to the following rules:
*
* ### Exact match:
*
* The glob `'A.B'` matches the state named exactly `'A.B'`.
*
* | Glob |Matches states named|Does not match state named|
* |:------------|:--------------------|:-----------------|
* | `'A'` | `'A'` | `'B'` , `'A.C'` |
* | `'A.B'` | `'A.B'` | `'A'` , `'A.B.C'`|
* |:------------|:--------------------|:---------------------|
* | `'A'` | `'A'` | `'B'` , `'A.C'` |
* | `'A.B'` | `'A.B'` | `'A'` , `'A.B.C'` |
* | `'foo'` | `'foo'` | `'FOO'` , `'foo.bar'`|
*
* ### Single wildcard (`*`)
* ### Single star (`*`)
*
* A single wildcard (`*`) matches any value for *a single segment* of a state name.
* A single star (`*`) is a wildcard that matches exactly one segment.
*
* | Glob |Matches states named |Does not match state named |
* |:------------|:---------------------|:--------------------------|
* | `'A.*'` | `'A.B'` , `'A.C'` | `'A'` , `'A.B.C'` |
* | `'*'` | `'A'` , `'Z'` | `'A.B'` , `'Z.Y.X'` |
* | `'A.*'` | `'A.B'` , `'A.C'` | `'A'` , `'A.B.C'` |
* | `'A.*.*'` | `'A.B.C'` , `'A.X.Y'`| `'A'`, `'A.B'` , `'Z.Y.X'`|
*
* ### Double star (`**`)
*
* ### Double wildcards (`**`)
* A double star (`'**'`) is a wildcard that matches *zero or more segments*
*
* Double wildcards (`'**'`) act as a wildcard for *one or more segments*
*
* | Glob |Matches states named |Does not match state named|
* |:------------|:----------------------------------------------|:-------------------------|
* | `'**'` | `'A'` , `'A.B'`, `'Z.Y.X'` | (matches all states) |
* | `'A.**'` | `'A.B'` , `'A.C'` , `'A.B.X'` | `'A'`, `'Z.Y.X'` |
* | `'**.login'`| `'A.login'` , `'A.B.login'` , `'Z.Y.X.login'` | `'A'` , `'login'` , `'A.login.Z'` |
* | Glob |Matches states named |Does not match state named |
* |:------------|:----------------------------------------------|:----------------------------------|
* | `'**'` | `'A'` , `'A.B'`, `'Z.Y.X'` | (matches all states) |
* | `'A.**'` | `'A'` , `'A.B'` , `'A.C.X'` | `'Z.Y.X'` |
* | `'**.X'` | `'X'` , `'A.X'` , `'Z.Y.X'` | `'A'` , `'A.login.Z'` |
* | `'A.**.X'` | `'A.X'` , `'A.B.X'` , `'A.B.C.X'` | `'A'` , `'A.B.C'` |
*
*/
export class Glob {
text: string;
glob: Array<string>;
regexp: RegExp;

constructor(text: string) {
this.text = text;
this.glob = text.split('.');
}

matches(name: string) {
let segments = name.split('.');

// match single stars
for (let i = 0, l = this.glob.length; i < l; i++) {
if (this.glob[i] === '*') segments[i] = '*';
}
let regexpString = this.text.split('.')
.map(seg => {
if (seg === '**') return '(?:|(?:\\.[^.]*)*)';
if (seg === '*') return '\\.[^.]*';
return '\\.' + seg;
}).join('');

// match greedy starts
if (this.glob[0] === '**') {
segments = segments.slice(segments.indexOf(this.glob[1]));
segments.unshift('**');
}
// match greedy ends
if (this.glob[this.glob.length - 1] === '**') {
segments.splice(segments.indexOf(this.glob[this.glob.length - 2]) + 1, Number.MAX_VALUE);
segments.push('**');
}
if (this.glob.length != segments.length) return false;
this.regexp = new RegExp("^" + regexpString + "$");
}

return segments.join('') === this.glob.join('');
matches(name: string) {
return this.regexp.test('.' + name);
}

/** @deprecated whats the point? */
static is(text: string) {
return text.indexOf('*') > -1;
}

/** @deprecated whats the point? */
static fromString(text: string) {
if (!this.is(text)) return null;
return new Glob(text);
Expand Down
30 changes: 0 additions & 30 deletions test/core/commonSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,34 +112,4 @@ describe('common', function() {
expect(isInjectable(fn)).toBeTruthy();
});
});

describe('Glob', function() {
it('should match glob strings', function() {
expect(Glob.is('*')).toBe(true);
expect(Glob.is('**')).toBe(true);
expect(Glob.is('*.*')).toBe(true);

expect(Glob.is('')).toBe(false);
expect(Glob.is('.')).toBe(false);
});

it('should construct glob matchers', function() {
expect(Glob.fromString('')).toBeNull();

var state = 'about.person.item';

expect(Glob.fromString('*.person.*').matches(state)).toBe(true);
expect(Glob.fromString('*.person.**').matches(state)).toBe(true);

expect(Glob.fromString('**.item.*').matches(state)).toBe(false);
expect(Glob.fromString('**.item').matches(state)).toBe(true);
expect(Glob.fromString('**.stuff.*').matches(state)).toBe(false);
expect(Glob.fromString('*.*.*').matches(state)).toBe(true);

expect(Glob.fromString('about.*.*').matches(state)).toBe(true);
expect(Glob.fromString('about.**').matches(state)).toBe(true);
expect(Glob.fromString('*.about.*').matches(state)).toBe(false);
expect(Glob.fromString('about.*.*').matches(state)).toBe(true);
});
});
});
65 changes: 65 additions & 0 deletions test/core/globSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {Glob} from "../../src/common/glob";

describe('Glob', function() {
it('should match exact strings', function() {
var state = 'about.person.item';

expect(new Glob('about.person.item').matches(state)).toBe(true);
expect(new Glob('about.person.item.foo').matches(state)).toBe(false);
expect(new Glob('foo.about.person.item').matches(state)).toBe(false);
});

it('with a single wildcard (*) should match a top level state', function() {
var glob = new Glob('*');

expect(glob.matches('foo')).toBe(true);
expect(glob.matches('bar')).toBe(true);
expect(glob.matches('baz')).toBe(true);
expect(glob.matches('foo.bar')).toBe(false);
expect(glob.matches('.baz')).toBe(false);
});

it('with a single wildcard (*) should match any single non-empty segment', function() {
var state = 'about.person.item';

expect(new Glob('*.person.item').matches(state)).toBe(true);
expect(new Glob('*.*.item').matches(state)).toBe(true);
expect(new Glob('*.person.*').matches(state)).toBe(true);
expect(new Glob('*.*.*').matches(state)).toBe(true);

expect(new Glob('*.*.*.*').matches(state)).toBe(false);
expect(new Glob('*.*.person.item').matches(state)).toBe(false);
expect(new Glob('*.person.item.foo').matches(state)).toBe(false);
expect(new Glob('foo.about.person.*').matches(state)).toBe(false);
});

it('with a double wildcard (**) should match any valid state name', function() {
var glob = new Glob('**');

expect(glob.matches('foo')).toBe(true);
expect(glob.matches('bar')).toBe(true);
expect(glob.matches('foo.bar')).toBe(true);
});

it('with a double wildcard (**) should match zero or more segments', function() {
var state = 'about.person.item';

expect(new Glob('**').matches(state)).toBe(true);
expect(new Glob('**.**').matches(state)).toBe(true);
expect(new Glob('**.*').matches(state)).toBe(true);
expect(new Glob('**.person.item').matches(state)).toBe(true);
expect(new Glob('**.person.**').matches(state)).toBe(true);
expect(new Glob('**.person.**.item').matches(state)).toBe(true);
expect(new Glob('**.person.**.*').matches(state)).toBe(true);
expect(new Glob('**.item').matches(state)).toBe(true);
expect(new Glob('about.**').matches(state)).toBe(true);
expect(new Glob('about.**.person.item').matches(state)).toBe(true);
expect(new Glob('about.person.item.**').matches(state)).toBe(true);
expect(new Glob('**.about.person.item').matches(state)).toBe(true);
expect(new Glob('**.about.**.person.item.**').matches(state)).toBe(true);
expect(new Glob('**.**.about.person.item').matches(state)).toBe(true);

expect(new Glob('**.person.**.*.*').matches(state)).toBe(false);
expect(new Glob('**.person.**.*.item').matches(state)).toBe(false);
});
});
10 changes: 10 additions & 0 deletions test/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ beforeEach(function() {
}
},

toEqualValues: function() {
return {
compare: function(actual, expected) {
let pass = Object.keys(expected)
.reduce((acc, key) => acc && equals(actual[key], expected[key]), true);
return { pass };
}
}
},

toBeResolved: () => ({
compare: actual => ({
pass: !!testablePromise(actual).$$resolved
Expand Down
2 changes: 1 addition & 1 deletion test/ng1/stateDirectivesSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ describe('uiSrefActive', function() {
}));

it('should support multiple <className, stateOrName> pairs', inject(function($compile, $rootScope, $state, $q) {
el = $compile('<div ui-sref-active="{contacts: \'contacts.*\', admin: \'admin.roles({page: 1})\'}"></div>')($rootScope);
el = $compile('<div ui-sref-active="{contacts: \'contacts.**\', admin: \'admin.roles({page: 1})\'}"></div>')($rootScope);
$state.transitionTo('contacts');
$q.flush();
timeoutFlush();
Expand Down
1 change: 1 addition & 0 deletions typings/jasmine/jasmine.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ declare module jasmine {
export interface Matchers {
toBeResolved(): boolean
toEqualData(expected: any): boolean
toEqualValues(expected: any): boolean
toHaveClass(expected: any): boolean
}
}

0 comments on commit d1dff31

Please sign in to comment.