-import {
- tokensToParser,
- tokenizePath,
- comparePathParser,
-} from '../../src/matcher/tokenizer'
+import { tokensToParser, tokenizePath } from '../../src/matcher/tokenizer'
+import { comparePathParserScore } from '../../src/matcher/path-ranker'
type PathParserOptions = Parameters<typeof tokensToParser>[1]
describe('Path ranking', () => {
+ describe('comparePathParser', () => {
+ it('same length', () => {
+ expect(comparePathParserScore([2], [3])).toEqual(1)
+ expect(comparePathParserScore([2], [2])).toEqual(0)
+ expect(comparePathParserScore([4], [3])).toEqual(-1)
+ })
+
+ it('longer', () => {
+ expect(comparePathParserScore([2], [3, 1])).toEqual(1)
+ // TODO: we are assuming we never pass end: false
+ expect(comparePathParserScore([3], [3, 1])).toEqual(1)
+ expect(comparePathParserScore([1, 3], [2])).toEqual(1)
+ expect(comparePathParserScore([4], [3])).toEqual(-1)
+ expect(comparePathParserScore([], [3])).toEqual(1)
+ })
+ })
+
function checkPathOrder(paths: Array<string | [string, PathParserOptions]>) {
const pathsAsStrings = paths.map(path =>
- typeof path === 'string' ? path : path[0]
+ typeof path === 'string' ? path : path[0] + JSON.stringify(path[1])
)
// reverse the array to force some reordering
const parsers = paths.reverse().map(path => {
? tokensToParser(tokenizePath(path))
: tokensToParser(tokenizePath(path[0]), path[1])
// @ts-ignore
- parser.path = typeof path === 'string' ? path : path[0]
+ parser.path =
+ typeof path === 'string' ? path : path[0] + JSON.stringify(path[1])
return parser
})
- parsers.sort(comparePathParser)
+ parsers.sort((a, b) => comparePathParserScore(a.score, b.score))
try {
expect(
.map(
parser =>
// @ts-ignore
- `${parser.path}: [${parser.score.join(', ')}]`
+ `${parser.path} -> [${parser.score.join(', ')}]`
)
.join('\n')
)
it('orders static before params', () => {
checkPathOrder(['/a', '/:id'])
})
+
+ it('empty path before slash', () => {
+ checkPathOrder(['', '/'])
+ })
+
+ it('works with long paths', () => {
+ checkPathOrder(['/a/b/c/d/e', '/:k/b/c/d/e', '/:k/b/c/d/:j'])
+ })
+
+ it('puts the wildcard at the end', () => {
+ checkPathOrder(['/', '/:rest(.*)'])
+ checkPathOrder(['/static', '/:rest(.*)'])
+ checkPathOrder(['/:other', '/:rest(.*)'])
+ })
+
+ it('prioritises custom regex', () => {
+ checkPathOrder(['/:a(\\d+)', '/:a', '/:a(.*)'])
+ checkPathOrder(['/b-:a(\\d+)', '/b-:a', '/b-:a(.*)'])
+ })
+
+ it('prioritizes ending slashes', () => {
+ checkPathOrder([
+ // no strict
+ '/a/',
+ '/a',
+ ])
+ checkPathOrder([
+ // no strict
+ '/a/b/',
+ '/a/b',
+ ])
+
+ // does this really make sense?
+ // checkPathOrder([['/a/', { strict: true }], '/a/'])
+ // checkPathOrder([['/a', { strict: true }], '/a'])
+ })
})
optional: boolean
}
-interface PathParser {
+export interface PathParser {
re: RegExp
score: number[]
keys: ParamKey[]
decode: v => v,
}
+// const enum PathScore {
+// _multiplier = 10,
+// Segment = 4 * _multiplier, // /a-segment
+// SubSegment = 2 * _multiplier, // /multiple-:things-in-one-:segment
+// Static = 3 * _multiplier, // /static
+// Dynamic = 2 * _multiplier, // /:someId
+// DynamicCustomRegexp = 2.5 * _multiplier, // /:someId(\\d+)
+// Wildcard = -1 * _multiplier, // /:namedWildcard(.*)
+// SubWildcard = 1 * _multiplier, // Wildcard as a subsegment
+// Repeatable = -0.5 * _multiplier, // /:w+ or /:w*
+// // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
+// Strict = 0.07 * _multiplier, // when options strict: true is passed, as the regex omits \/?
+// CaseSensitive = 0.025 * _multiplier, // when options strict: true is passed, as the regex omits \/?
+// Optional = -4 * _multiplier, // /:w? or /:w*
+// SubOptional = -0.1 * _multiplier, // optional inside a subsegment /a-:w? or /a-:w*
+// Root = 1 * _multiplier, // just /
+// }
+
+const enum PathScore {
+ _multiplier = 10,
+ Root = 8 * _multiplier, // just /
+ Segment = 4 * _multiplier, // /a-segment
+ SubSegment = 2 * _multiplier, // /multiple-:things-in-one-:segment
+ Static = 3 * _multiplier, // /static
+ Dynamic = 2 * _multiplier, // /:someId
+ BonusCustomRegExp = 1 * _multiplier, // /:someId(\\d+)
+ BonusWildcard = -3 * _multiplier, // /:namedWildcard(.*)
+ BonusRepeatable = -0.5 * _multiplier, // /:w+ or /:w*
+ BonusOptional = -4 * _multiplier, // /:w? or /:w*
+ // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
+ BonusStrict = 0.07 * _multiplier, // when options strict: true is passed, as the regex omits \/?
+ BonusCaseSensitive = 0.025 * _multiplier, // when options strict: true is passed, as the regex omits \/?
+}
+
/**
- * TODO: add options strict, sensitive, encode, decode
+ * Creates a path parser from an array of Segments (a segment is an array of Tokens)
+ *
+ * @param segments array of segments returned by tokenizePath
+ * @param extraOptions optional options for the regexp
*/
-
export function tokensToParser(
segments: Array<Token[]>,
extraOptions?: PathParserOptions
...extraOptions,
}
+ // the amount of scores is the same as the length of segments
let score: number[] = []
let pattern = options.start ? '^' : ''
const keys: ParamKey[] = []
// allow an empty path to be different from slash
// if (!segment.length) pattern += '/'
+ // TODO: add strict and sensitive
+ let segmentScore = segment.length
+ ? segment.length > 1
+ ? PathScore.SubSegment
+ : PathScore.Segment
+ : PathScore.Root
+
for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
const token = segment[tokenIndex]
if (token.type === TokenType.Static) {
// prepend the slash if we are starting a new segment
if (!tokenIndex) pattern += '/'
pattern += token.value
+
+ segmentScore += PathScore.Static
} else if (token.type === TokenType.Param) {
keys.push({
name: token.value,
})
const re = token.regexp ? token.regexp : BASE_PARAM_PATTERN
if (re !== BASE_PARAM_PATTERN) {
+ segmentScore += PathScore.BonusCustomRegExp
try {
new RegExp(`(${re})`)
} catch (err) {
else subPattern += token.optional ? '?' : ''
pattern += subPattern
+
+ segmentScore += PathScore.Dynamic
+ if (re === '.*') segmentScore += PathScore.BonusWildcard
}
}
+
+ score.push(segmentScore)
}
// TODO: warn double trailing slash
stringify,
}
}
-
-export function comparePathParser(a: PathParser, b: PathParser): number {
- let i = 0
- while (i < a.score.length && i < b.score.length) {
- if (a.score[i] < b.score[i]) return -1
- else if (a.score[i] > b.score[i]) 1
-
- i++
- }
-
- return 0
-}