Skip to content

Commit

Permalink
Add possibility to specify types and typedMatch function on regexp ob…
Browse files Browse the repository at this point in the history
…jects.

This is the first step towards achieving expressjs/express#2756.

/:foo{<type>}     - adds a valType=<type> property to the key object
                  - sets the pattern to match floats for 'number' and
                    'float'
                  - sets the pattern to match integers for 'integer'

<type> ::= string | float | number | integer

A new function is added as typedMatch(str) to the regexp objects
returned by pathToRegexp(...). It is a wrapper on top of String.match(regexp),
and it uses the keys property to change the types of the matching groups
accordingly. The changes happen for "float", "number", and "integer".

The pattern is only changed when a different pattern is not defined.
For example, /:foo([123]){integer} will have the pattern [123], but will
also benefit from automatic type changes when using typedMatch.
  • Loading branch information
vladvelici committed Dec 1, 2015
1 parent dc9a133 commit afebe50
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 6 deletions.
61 changes: 55 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,36 @@ var PATH_REGEXP = new RegExp([
// "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
// "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined]
// "/*" => ["/", undefined, undefined, undefined, undefined, "*"]
'([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^()])+)\\))?|\\(((?:\\\\.|[^()])+)\\))([+*?])?|(\\*))'
[
'([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^()])+)\\))?|\\(((?:\\\\.|[^()])+)\\))',
// match valType
'(\{(number|integer|float|string)\})?',
// match suffix and asterisk
'([+*?])?|(\\*))'
].join('')
].join('|'), 'g')

/**
* Given a value type as string, return a regex that would
* match it, if applicable.
*
* "string" returns undefined, as it depends on delimiter and asterisk.
*
* @param {string} valType
* @return Regexp pattern as string.
*/
function typeRegex (valType) {
// Assumes numbers starting with "0" are valid.
// They're parsed fine by parseFloat and parseInt.
if (valType === 'number' || valType === 'float') {
return '[+-]?[0-9]*\.?[0-9]+'
}
if (valType === 'integer') {
return '[+-]?[0-9]+'
}
return undefined
}

/**
* Parse a string for the raw tokens.
*
Expand Down Expand Up @@ -63,22 +90,28 @@ function parse (str) {
var name = res[3]
var capture = res[4]
var group = res[5]
var suffix = res[6]
var asterisk = res[7]
// res[6] is "{valType}"
var valType = res[7]
var suffix = res[8]
var asterisk = res[9]

var repeat = suffix === '+' || suffix === '*'
var optional = suffix === '?' || suffix === '*'
var delimiter = prefix || '/'
var pattern = capture || group || (asterisk ? '.*' : '[^' + delimiter + ']+?')
var pattern = capture || group || typeRegex(valType) || (asterisk ? '.*' : '[^' + delimiter + ']+?')

tokens.push({
var token = {
name: name || key++,
prefix: prefix || '',
delimiter: delimiter,
optional: optional,
repeat: repeat,
pattern: escapeGroup(pattern)
})
}
if (valType !== undefined) {
token.valType = valType
}
tokens.push(token)
}

// Match any characters still remaining.
Expand Down Expand Up @@ -210,6 +243,22 @@ function escapeGroup (group) {
*/
function attachKeys (re, keys) {
re.keys = keys
re.typedMatch = function (str) {
var m = str.match(re)
if (!m) {
return
}
var t
for (var i = 1; i < m.length; i++) {
t = keys[i - 1].valType
if (t === 'number' || t === 'float') {
m[i] = parseFloat(m[i])
} else if (t === 'integer') {
m[i] = parseInt(m[i], 10)
}
}
return m
}
return re
}

Expand Down
123 changes: 123 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1763,6 +1763,120 @@ var TESTS = [
[
[{ foo: 'café' }, '/caf%C3%A9']
]
],

/**
* Typed matches
*/
[
'/:foo{float}',
null,
[
{
name: 'foo',
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
pattern: '[+-]?[0-9]*\.?[0-9]+',
valType: 'float'
}
],
[
['/432.94', ['/432.94', '432.94']],
['/-432.94', ['/-432.94', '-432.94']],
['/-.9', ['/-.9', '-.9']],
['/+0.9', ['/+0.9', '+0.9']],
['/+5', ['/+5', '+5']],
['/notanumber', null]
],
[
[{ foo: 432.94 }, '/432.94'],
[{ foo: 'notanumber' }, null]
]
],
[
'/:foo{number}',
null,
[
{
name: 'foo',
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
pattern: '[+-]?[0-9]*\.?[0-9]+',
valType: 'number'
}
],
[
['/432.94', ['/432.94', '432.94']],
['/-432.94', ['/-432.94', '-432.94']],
['/-.9', ['/-.9', '-.9']],
['/+0.9', ['/+0.9', '+0.9']],
['/+5', ['/+5', '+5']],
['/notanumber', null]
],
[
[{ foo: 432.94 }, '/432.94'],
[{ foo: 'notanumber' }, null]
]
],
[
'/:foo{integer}',
null,
[
{
name: 'foo',
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
pattern: '[+-]?[0-9]+',
valType: 'integer'
}
],
[
['/-3', ['/-3', '-3']],
['/432.94', null],
['/+334', ['/+334', '+334']],
['/334', ['/334', '334']],
['/3notanumber', null],
['/notanumber', null]
],
[
[{ foo: 432.94 }, null],
[{ foo: 432 }, '/432'],
[{ foo: -9 }, '/-9'],
[{ foo: 'notanumber' }, null]
]
],
[
'/:foo([234]){integer}',
null,
[
{
name: 'foo',
prefix: '/',
delimiter: '/',
optional: false,
repeat: false,
pattern: '[234]',
valType: 'integer'
}
],
[
['/3', ['/3', '3']],
['/9', null],
['/3notanumber', null],
['/notanumber', null]
],
[
[{ foo: 432.94 }, null],
[{ foo: 432 }, null],
[{ foo: 4 }, '/4'],
[{ foo: 'notanumber' }, null]
]
]
]

Expand Down Expand Up @@ -1915,6 +2029,15 @@ describe('path-to-regexp', function () {
}).to.throw(TypeError, 'Expected all "foo" to match "\\d+"')
})
})

describe('typedMatch', function () {
var r = pathToRegexp('/:foo{number}')
it('should have type number', function () {
var match = r.typedMatch('/340.5')
expect(match[1]).to.equal(340.5)
expect(typeof match[1]).to.equal(typeof 340.5)
})
})
})

/**
Expand Down

0 comments on commit afebe50

Please sign in to comment.