Skip to content

Commit

Permalink
feat: add matrix and url parameter constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
troch committed Jul 6, 2015
1 parent 95c37c0 commit a567ba1
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 22 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ p.build({id: '00123'}) // => "users/profile/00123"
- `*splat`: for parameters spanning over multiple segments. Handle with care
- `?param1&param2` or `?:param1&:param2`: for query parameters. Colons `:` are optional

## Parameter constraints

For URL parameters and matrix parameters, you can add a constraint in the form of a regular expression.
Note that back slashes have to be escaped.

- `:param<\\d+>` will match numbers only for parameter `param`
- `;id<[a-fA-F0-9]{8}` will match 8 characters hexadecimal strings for parameter `id`

Constraints are also applied when building paths, unless specified otherwise

```javascript
// Path.build(params, ignore)
var Path = new Path('/users/profile/:id<\d+>');

path.build({id: 'not-a-number'}); // => Will throw an error
path.build({id: 'not-a-number'}, true); // => '/users/profile/not-a-number'
```

## Related modules

- [route-parser](https://github.com/rcs/route-parser)
Expand Down
31 changes: 25 additions & 6 deletions dist/commonjs/path-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,28 @@ var _createClass = (function () { function defineProperties(target, props) { for

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

var defaultOrConstrained = function defaultOrConstrained(match) {
return '(' + (match ? match.replace(/(^<|>$)/g, '') : '[a-zA-Z0-9-_.~]+') + ')';
};

var rules = [{
// An URL can contain a parameter :paramName
// - and _ are allowed but not in last position
name: 'url-parameter',
pattern: /^:([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/,
regex: /([a-zA-Z0-9-_.~]+)/
pattern: /^:([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})(<(.+?)>)?/,
regex: function regex(match) {
return new RegExp(defaultOrConstrained(match[2]));
}
}, {
// Url parameter (splat)
name: 'url-parameter-splat',
pattern: /^\*([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/,
regex: /([^\?]*)/
}, {
name: 'url-parameter-matrix',
pattern: /^\;([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/,
pattern: /^\;([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})(<(.+?)>)?/,
regex: function regex(match) {
return new RegExp(';' + match[1] + '=([a-zA-Z0-9-_.~]+)');
return new RegExp(';' + match[1] + '=' + defaultOrConstrained(match[2]));
}
}, {
// Query parameter: ?param1&param2
Expand Down Expand Up @@ -64,7 +70,8 @@ var tokenise = function tokenise(str) {
tokens.push({
type: rule.name,
match: match[0],
val: match.length > 1 ? match.slice(1) : null,
val: match.slice(1, 2),
otherVal: match.slice(2),
regex: rule.regex instanceof Function ? rule.regex(match) : rule.regex
});

Expand Down Expand Up @@ -103,7 +110,7 @@ var Path = (function () {
this.urlParams = !this.hasUrlParams ? [] : this.tokens.filter(function (t) {
return /^url-parameter/.test(t.type);
}).map(function (t) {
return t.val;
return t.val.slice(0, 1);
})
// Flatten
.reduce(function (r, v) {
Expand Down Expand Up @@ -182,12 +189,24 @@ var Path = (function () {
key: 'build',
value: function build() {
var params = arguments[0] === undefined ? {} : arguments[0];
var ignoreConstraints = arguments[1] === undefined ? false : arguments[1];

// Check all params are provided (not search parameters which are optional)
if (!this.params.every(function (p) {
return params[p] !== undefined;
})) throw new Error('Missing parameters');

// Check constraints
if (!ignoreConstraints) {
var constraintsPassed = this.tokens.filter(function (t) {
return /^url-parameter/.test(t.type) && !/-splat$/.test(t.type);
}).every(function (t) {
return new RegExp('^' + defaultOrConstrained(t.otherVal[0]) + '$').test(params[t.val]);
});

if (!constraintsPassed) throw new Error('Some parameters are of invalid format');
}

var base = this.tokens.filter(function (t) {
return t.type !== 'query-parameter';
}).map(function (t) {
Expand Down
31 changes: 25 additions & 6 deletions dist/umd/path-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,28 @@

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

var defaultOrConstrained = function defaultOrConstrained(match) {
return '(' + (match ? match.replace(/(^<|>$)/g, '') : '[a-zA-Z0-9-_.~]+') + ')';
};

var rules = [{
// An URL can contain a parameter :paramName
// - and _ are allowed but not in last position
name: 'url-parameter',
pattern: /^:([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/,
regex: /([a-zA-Z0-9-_.~]+)/
pattern: /^:([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})(<(.+?)>)?/,
regex: function regex(match) {
return new RegExp(defaultOrConstrained(match[2]));
}
}, {
// Url parameter (splat)
name: 'url-parameter-splat',
pattern: /^\*([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/,
regex: /([^\?]*)/
}, {
name: 'url-parameter-matrix',
pattern: /^\;([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/,
pattern: /^\;([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})(<(.+?)>)?/,
regex: function regex(match) {
return new RegExp(';' + match[1] + '=([a-zA-Z0-9-_.~]+)');
return new RegExp(';' + match[1] + '=' + defaultOrConstrained(match[2]));
}
}, {
// Query parameter: ?param1&param2
Expand Down Expand Up @@ -73,7 +79,8 @@
tokens.push({
type: rule.name,
match: match[0],
val: match.length > 1 ? match.slice(1) : null,
val: match.slice(1, 2),
otherVal: match.slice(2),
regex: rule.regex instanceof Function ? rule.regex(match) : rule.regex
});

Expand Down Expand Up @@ -112,7 +119,7 @@
this.urlParams = !this.hasUrlParams ? [] : this.tokens.filter(function (t) {
return /^url-parameter/.test(t.type);
}).map(function (t) {
return t.val;
return t.val.slice(0, 1);
})
// Flatten
.reduce(function (r, v) {
Expand Down Expand Up @@ -191,12 +198,24 @@
key: 'build',
value: function build() {
var params = arguments[0] === undefined ? {} : arguments[0];
var ignoreConstraints = arguments[1] === undefined ? false : arguments[1];

// Check all params are provided (not search parameters which are optional)
if (!this.params.every(function (p) {
return params[p] !== undefined;
})) throw new Error('Missing parameters');

// Check constraints
if (!ignoreConstraints) {
var constraintsPassed = this.tokens.filter(function (t) {
return /^url-parameter/.test(t.type) && !/-splat$/.test(t.type);
}).every(function (t) {
return new RegExp('^' + defaultOrConstrained(t.otherVal[0]) + '$').test(params[t.val]);
});

if (!constraintsPassed) throw new Error('Some parameters are of invalid format');
}

var base = this.tokens.filter(function (t) {
return t.type !== 'query-parameter';
}).map(function (t) {
Expand Down
34 changes: 24 additions & 10 deletions modules/Path.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
let defaultOrConstrained = (match) => {
return '(' + (match ? match.replace(/(^<|>$)/g, '') : '[a-zA-Z0-9-_.~]+') + ')'
}

const rules = [
{
// An URL can contain a parameter :paramName
// - and _ are allowed but not in last position
name: 'url-parameter',
pattern: /^:([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/,
regex: /([a-zA-Z0-9-_.~]+)/
pattern: /^:([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})(<(.+?)>)?/,
regex: match => new RegExp(defaultOrConstrained(match[2]))
},
{
// Url parameter (splat)
Expand All @@ -14,8 +18,8 @@ const rules = [
},
{
name: 'url-parameter-matrix',
pattern: /^\;([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})/,
regex: match => new RegExp(';' + match[1] + '=([a-zA-Z0-9-_.~]+)')
pattern: /^\;([a-zA-Z0-9-_]*[a-zA-Z0-9]{1})(<(.+?)>)?/,
regex: match => new RegExp(';' + match[1] + '=' + defaultOrConstrained(match[2]))
},
{
// Query parameter: ?param1&param2
Expand Down Expand Up @@ -52,10 +56,11 @@ let tokenise = (str, tokens = []) => {
if (!match) return false

tokens.push({
type: rule.name,
match: match[0],
val: match.length > 1 ? match.slice(1) : null,
regex: rule.regex instanceof Function ? rule.regex(match) : rule.regex
type: rule.name,
match: match[0],
val: match.slice(1, 2),
otherVal: match.slice(2),
regex: rule.regex instanceof Function ? rule.regex(match) : rule.regex
})

if (match[0].length < str.length) tokens = tokenise(str.substr(match[0].length), tokens)
Expand All @@ -82,7 +87,7 @@ export default class Path {
// Extract named parameters from tokens
this.urlParams = !this.hasUrlParams ? [] : this.tokens
.filter(t => /^url-parameter/.test(t.type))
.map(t => t.val)
.map(t => t.val.slice(0, 1))
// Flatten
.reduce((r, v) => r.concat(v))
// Query params
Expand Down Expand Up @@ -142,10 +147,19 @@ export default class Path {
return this._urlMatch(path, new RegExp('^' + this.source))
}

build(params = {}) {
build(params = {}, ignoreConstraints = false) {
// Check all params are provided (not search parameters which are optional)
if (!this.params.every(p => params[p] !== undefined)) throw new Error('Missing parameters')

// Check constraints
if (!ignoreConstraints) {
let constraintsPassed = this.tokens
.filter(t => /^url-parameter/.test(t.type) && !/-splat$/.test(t.type))
.every(t => new RegExp('^' + defaultOrConstrained(t.otherVal[0]) + '$').test(params[t.val]))

if (!constraintsPassed) throw new Error('Some parameters are of invalid format');
}

let base = this.tokens
.filter(t => t.type !== 'query-parameter')
.map(t => {
Expand Down
1 change: 1 addition & 0 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ async.parallel([
buildFactory('common', 'dist/commonjs/path-parser.js'),
buildFactory('umd', 'dist/umd/path-parser.js')
], function (err) {
if (err) console.log(err);
process.exit(err ? 1 : 0);
})
24 changes: 24 additions & 0 deletions test/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,28 @@ describe('Path', function () {
// Successful match
path.match('/users/;section=profile;id=123').should.eql({ section: 'profile', id: '123' });
});

it('should match and build paths with constrained parameters', function () {
var path = new Path('/users/:id<\\d+>');
// Build path
path.build({id: 99}).should.equal('/users/99');
// Match path
path.match('/users/11').should.eql({id: '11'});
should.not.exist(path.match('/users/thomas'));

path = new Path('/users/;id<[A-F0-9]{6}>');
// Build path
path.build({id: 'A76FE4'}).should.equal('/users/;id=A76FE4');
// Error because of incorrect parameter format
(function () {
path.build({id: 'incorrect-param'});
}).should.throw();
// Force
path.build({id: 'fake'}, true).should.equal('/users/;id=fake');


// Match path
path.match('/users/;id=A76FE4').should.eql({id: 'A76FE4'});
should.not.exist(path.match('/users;id=Z12345'));
});
});

0 comments on commit a567ba1

Please sign in to comment.