@websdk/rhumb
Advanced tools
Comparing version 0.3.9 to 0.4.0
473
lib/rhumb.js
@@ -16,49 +16,53 @@ (function (global, factory) { | ||
function findIn(parts, tree) { | ||
var params = {}; | ||
function findIn(path, tree) { | ||
var parsedPath = parse(path); | ||
var find = function (remaining, node) { | ||
var part = remaining.shift(); | ||
if (!part) return node.leaf || false; | ||
var find = function (part, node, params) { | ||
var segment = part.segments.shift(); | ||
if (node.fixed && part in node.fixed) { | ||
return find(remaining, node.fixed[part]); | ||
if (!segment) { | ||
return node.leaf ? { fn: node.leaf, params: params } : false; | ||
} | ||
switch (segment.type) { | ||
case 'fixed': | ||
break; | ||
case 'var': | ||
throw new InvalidPathException('Must not contain a variable segment', path); | ||
case 'partial': | ||
throw new InvalidPathException('Must not contain a partial variable segment', path); | ||
case 'empty': | ||
throw new InvalidPathException('Must not contain an empty segment', path); | ||
default: | ||
throw new InvalidPathException('Must not contain an optional path', path); | ||
} | ||
if (node.fixed && segment.identifier in node.fixed) { | ||
return find(part, node.fixed[segment.identifier], params); | ||
} | ||
if (node.partial) { | ||
var tests = node.partial.tests, | ||
found = tests.some(function (partial) { | ||
if (partial.ptn.test(part)) { | ||
var match = part.match(partial.ptn); | ||
partial.vars.forEach(function (d, i) { | ||
params[d] = match[i + 1]; | ||
}); | ||
node = partial; | ||
return true; | ||
found = tests.map(function (partial) { | ||
var params = partial.matchFunction(segment.identifier); | ||
return params ? { partial: partial, params: params } : null; | ||
}).filter(falsy), | ||
matchingPartial = found.length > 0 ? found[0] : null; | ||
if (matchingPartial) { | ||
for (var key in matchingPartial.params) { | ||
params[key] = matchingPartial.params[key]; | ||
} | ||
}); | ||
if (found) { | ||
return find(remaining, node); | ||
return find(part, matchingPartial.partial, params); | ||
} | ||
} | ||
if (node['var']) { | ||
params[node['var'].name] = part; | ||
return find(remaining, node['var']); | ||
if (node.variable) { | ||
params[node.variable.identifier] = segment.identifier; | ||
return find(part, node.variable, params); | ||
} | ||
return false; | ||
}; | ||
var found = find(parts, tree, params); | ||
if (found) { | ||
return { | ||
fn: found, | ||
params: params | ||
}; | ||
} | ||
return false; | ||
return find(parsedPath, tree, parsedPath.queryParams); | ||
} | ||
@@ -70,81 +74,113 @@ | ||
function updateTree(parts, node, fn) { | ||
var part = parts.shift(), | ||
more = !!parts.length, | ||
function updateTree(part, node, route, fn) { | ||
var segment = part.segments.shift(), | ||
more = !!part.segments.length, | ||
peek; | ||
if (Array.isArray(part)) { | ||
if (!segment) { | ||
if (node.leaf) { | ||
throw new Error('Ambiguity'); | ||
} | ||
node.leaf = fn; | ||
updateTree(part, node, fn); | ||
return; | ||
} | ||
if (!part) return; | ||
if (Array.isArray(segment.segments)) { | ||
if (node.leaf) { | ||
throw new Error('Ambiguity'); | ||
} | ||
node.leaf = fn; | ||
updateTree(segment, node, route, fn); | ||
return; | ||
} | ||
if (part.type === "fixed") { | ||
node.fixed || (node.fixed = {}); | ||
peek = node.fixed[part.input] || (node.fixed[part.input] = {}); | ||
switch (segment.type) { | ||
case 'fixed': | ||
if (!node.fixed) { | ||
node.fixed = {}; | ||
} | ||
if (!node.fixed[segment.identifier]) { | ||
node.fixed[segment.identifier] = {}; | ||
} | ||
peek = node.fixed[segment.identifier]; | ||
if (peek.leaf && !more) { | ||
throw new Error("Ambiguity"); | ||
} | ||
} else if (part.type === "var") { | ||
if (node['var']) { | ||
if (node['var'].name === part.input) { | ||
peek = node['var']; | ||
if (peek.leaf && !more) { | ||
throw new Error('Ambiguity'); | ||
} | ||
break; | ||
case 'var': | ||
if (node.variable) { | ||
if (node.variable.identifier === segment.identifier) { | ||
peek = node.variable; | ||
} else { | ||
throw new Error('Ambiguity'); | ||
} | ||
} else { | ||
throw new Error("Ambiguity"); | ||
node.variable = { identifier: segment.identifier }; | ||
peek = node.variable; | ||
} | ||
} else { | ||
peek = node['var'] = { | ||
name: part.input | ||
}; | ||
} | ||
} else if (part.type = "partial") { | ||
if (node.partial) { | ||
if (node.partial.names[part.name]) { | ||
throw new Error("Ambiguity"); | ||
break; | ||
case 'partial': | ||
if (node.partial) { | ||
if (node.partial.identifiers[segment.identifier]) { | ||
throw new Error('Ambiguity'); | ||
} | ||
} else { | ||
node.partial = { identifiers: {}, tests: [] }; | ||
} | ||
} | ||
node.partial || (node.partial = { | ||
names: {}, | ||
tests: [] | ||
}); | ||
peek = {}; | ||
peek.ptn = part.input; | ||
peek.vars = part.vars; | ||
node.partial.names[part.name] = peek; | ||
node.partial.tests.push(peek); | ||
peek = { matchFunction: segment.matchFunction }; | ||
node.partial.identifiers[segment.identifier] = peek; | ||
node.partial.tests.push(peek); | ||
break; | ||
case 'empty': | ||
throw new InvalidRouteException('Must not contain an empty segment', route); | ||
} | ||
if (!more) { | ||
peek.leaf = fn; | ||
} else { | ||
updateTree(parts, peek, fn); | ||
updateTree(part, peek, route, fn); | ||
} | ||
} | ||
router.add = function (ptn, callback) { | ||
updateTree(parse(ptn), tree, callback); | ||
router.add = function (route, callback) { | ||
var parsedRoute = parse(route); | ||
if (Object.keys(parsedRoute.queryParams).length > 0) { | ||
throw new InvalidRouteException('Must not contain a query string', route); | ||
} | ||
updateTree(parsedRoute, tree, route, callback); | ||
}; | ||
router.match = function (path) { | ||
var split = path.split("?").filter(falsy), | ||
parts = ['/'].concat(split[0].split("/").filter(falsy)), | ||
params = parseQueryString(split[1]), | ||
match = findIn(parts, tree); | ||
var match = findIn(path, tree); | ||
if (match) { | ||
for (var prop in match.params) { | ||
params[prop] = match.params[prop]; | ||
} | ||
return match.fn.apply(match.fn, [params]); | ||
return match.fn.apply(match.fn, [match.params]); | ||
} | ||
}; | ||
return router; | ||
} | ||
function InvalidRouteException(message, route) { | ||
this.message = message; | ||
this.name = 'InvalidRouteException'; | ||
this.route = route; | ||
this.toString = function () { | ||
return 'Invalid route: ' + message; | ||
}; | ||
} | ||
function InvalidPathException(message, path) { | ||
this.message = message; | ||
this.name = 'InvalidPathException'; | ||
this.path = path; | ||
this.toString = function () { | ||
return 'Invalid path: ' + message; | ||
}; | ||
} | ||
function falsy(d) { | ||
@@ -163,106 +199,223 @@ return !!d; | ||
function parse(ptn) { | ||
var variable = /^{(\w+)}$/, | ||
partial = /([\w'-]+)?{([\w-]+)}([\w'-]+)?/, | ||
bracks = /^[)]+/; | ||
return ~ptn.indexOf('(') ? parseOptional(ptn) : parsePtn(ptn); | ||
function asEmptySegment(pathSegment) { | ||
return pathSegment === '' ? { type: 'empty' } : null; | ||
} | ||
function parseVar(part) { | ||
var match = part.match(variable); | ||
return { | ||
type: "var", | ||
input: match[1] | ||
}; | ||
function asFixedSegment(pathSegment) { | ||
return { type: 'fixed', identifier: pathSegment }; | ||
} | ||
function asPartialSegment(pathSegment) { | ||
var partialRegex = /([\w'-]+)?{([\w-]+)}([\w'-]+)?/, | ||
match = pathSegment.match(partialRegex), | ||
vars = [], | ||
ptn = '', | ||
len = pathSegment.length, | ||
index = 0, | ||
identifier = pathSegment.replace(/{([\w-]+)}/g, extractAndTransformVariableName); | ||
if (!match) { | ||
return null; | ||
} | ||
function parseFixed(part) { | ||
return { | ||
type: "fixed", | ||
input: part | ||
}; | ||
while (index < len && match) { | ||
index += match[0].length; | ||
if (match[1]) { | ||
ptn += match[1]; | ||
} | ||
ptn += '([\\w-]+)'; | ||
if (match[3]) { | ||
ptn += match[3]; | ||
} | ||
match = pathSegment.substr(index).match(partialRegex); | ||
} | ||
function parsePartial(part) { | ||
var match = part.match(partial), | ||
ptn = "", | ||
len = part.length, | ||
i = 0; | ||
var matchRegex = new RegExp(ptn); | ||
while (i < len && match) { | ||
i += match[0].length; | ||
return { | ||
type: 'partial', | ||
identifier: identifier, | ||
matchFunction: matchFunction, | ||
vars: vars | ||
}; | ||
if (match[1]) { | ||
ptn += match[1]; | ||
} | ||
function extractAndTransformVariableName(matchedPattern, variableName) { | ||
vars.push(variableName); | ||
return '{var}'; | ||
} | ||
ptn += "([\\w-]+)"; | ||
function matchFunction(segment) { | ||
var segmentMatches = segment.match(matchRegex); | ||
if (match[3]) { | ||
ptn += match[3]; | ||
} | ||
return segmentMatches ? vars.reduce(function (params, variable, index) { | ||
params[variable] = segmentMatches[index + 1]; | ||
return params; | ||
}, {}) : null; | ||
} | ||
} | ||
match = part.substr(i).match(partial); | ||
function asVarSegment(pathSegment) { | ||
var match = pathSegment.match(/^{(\w+)}$/); | ||
return match ? { type: 'var', identifier: match[1] } : null; | ||
} | ||
function parsePtn(ptn) { | ||
return ptn.split('/').reduce(function (parsedRecord, pathSegment, index, allSegments) { | ||
if (pathSegment === '' && index === 0) { | ||
parsedRecord.leadingSlash = allSegments.length > 1; | ||
} else if (pathSegment === '' && index === allSegments.length - 1) { | ||
parsedRecord.trailingSlash = true; | ||
} else { | ||
var newSegment = asEmptySegment(pathSegment) || asVarSegment(pathSegment) || asPartialSegment(pathSegment) || asFixedSegment(pathSegment); | ||
parsedRecord.segments.push(newSegment); | ||
} | ||
return parsedRecord; | ||
}, { leadingSlash: false, segments: [], trailingSlash: false }); | ||
} | ||
var vars = [], | ||
name = part.replace(/{([\w-]+)}/g, function (p, d) { | ||
vars.push(d); | ||
return "{var}"; | ||
}); | ||
return { | ||
type: "partial", | ||
input: new RegExp(ptn), | ||
name: name, | ||
vars: vars | ||
}; | ||
function parseOptional(ptn) { | ||
var out = ''; | ||
var i = 0, | ||
len = ptn.length, | ||
isOptionalSegment = false; | ||
while (!isOptionalSegment && i < len) { | ||
var curr = ptn.charAt(i); | ||
switch (curr) { | ||
case ')': | ||
case '(': | ||
isOptionalSegment = true; | ||
break; | ||
default: | ||
out += curr; | ||
break; | ||
} | ||
i++; | ||
} | ||
function parsePtn(ptn) { | ||
return ['/'].concat(ptn.split("/")).filter(falsy).map(function (d) { | ||
if (variable.test(d)) { | ||
return parseVar(d); | ||
} | ||
var parsedOutput = parsePtn(out); | ||
if (isOptionalSegment) { | ||
var optionalSegment = parseOptional(ptn.substr(i)); | ||
if (optionalSegment.segments.length > 0) { | ||
parsedOutput.segments.push(optionalSegment); | ||
parsedOutput.trailingSlash = optionalSegment.trailingSlash; | ||
} | ||
} | ||
if (partial.test(d)) { | ||
return parsePartial(d); | ||
} | ||
return parsedOutput; | ||
} | ||
return parseFixed(d); | ||
}); | ||
function parse(route) { | ||
var split = route.split('?'), | ||
parsedPath = split[0].indexOf('(/') > -1 ? parseOptional(split[0]) : parsePtn(split[0]); | ||
parsedPath.queryParams = parseQueryString(split[1]); | ||
parsedPath.queryParamsString = split[1] ? '?' + split[1] : null; | ||
return parsedPath; | ||
} | ||
function tryReadingParamValue(params, key) { | ||
if (!(key in params)) { | ||
throw new Error('Invalid parameter: "' + key + '" is not supplied'); | ||
} | ||
function parseOptional(ptn) { | ||
var out = "", | ||
list = []; | ||
var i = 0, | ||
len = ptn.length, | ||
onePart = true; | ||
var value = params[key]; | ||
switch (value) { | ||
case '': | ||
throw new Error('Invalid parameter: "' + key + '" is an empty value'); | ||
case undefined: | ||
throw new Error('Invalid parameter: "' + key + '" is undefined'); | ||
case null: | ||
throw new Error('Invalid parameter: "' + key + '" is null'); | ||
default: | ||
return value; | ||
} | ||
} | ||
while (onePart && i < len) { | ||
var curr = ptn.charAt(i); | ||
function joinPaths(path, item) { | ||
var pathStartingWithSlashes = /^\/?(%2F)+$/; | ||
if (path === '' || path.match(pathStartingWithSlashes) || path[path.length - 1] === '/') { | ||
return path + item; | ||
} | ||
return path + '/' + item; | ||
} | ||
switch (curr) { | ||
case ")": | ||
case "(": | ||
onePart = false; | ||
break; | ||
function interpolateEmpty(path) { | ||
return path + '%2F'; | ||
} | ||
function interpolateVar(path, segment, params) { | ||
var value = tryReadingParamValue(params, segment.identifier); | ||
return joinPaths(path, value); | ||
} | ||
function interpolateFixed(path, segment) { | ||
return joinPaths(path, segment.identifier); | ||
} | ||
function interpolatePartial(path, segment, params) { | ||
var i = 0, | ||
match = segment.identifier.replace(/\{var\}/g, function () { | ||
var varName = segment.vars[i++], | ||
value = tryReadingParamValue(params, varName); | ||
return value; | ||
}); | ||
return joinPaths(path, match); | ||
} | ||
function interpolateOptional(path, optionalSegment, params) { | ||
try { | ||
return joinPaths(path, optionalSegment.segments.reduce(function (optionalPath, segment) { | ||
switch (segment.type) { | ||
case 'empty': | ||
return interpolateEmpty(optionalPath); | ||
case 'var': | ||
return interpolateVar(optionalPath, segment, params); | ||
case 'partial': | ||
return interpolatePartial(optionalPath, segment, params); | ||
case 'fixed': | ||
return interpolateFixed(optionalPath, segment); | ||
default: | ||
out += curr; | ||
break; | ||
return optionalPath ? interpolateOptional(optionalPath, segment, params) : ''; | ||
} | ||
}, '')); | ||
} catch (ex) { | ||
return path; | ||
} | ||
} | ||
i++; | ||
function interpolate(route, params) { | ||
var parsedRoute = parse(route), | ||
queryParamsString = parsedRoute.queryParamsString, | ||
interpolatedPath = parsedRoute.segments.reduce(function (path, segment) { | ||
switch (segment.type) { | ||
case 'empty': | ||
return interpolateEmpty(path); | ||
case 'var': | ||
return interpolateVar(path, segment, params); | ||
case 'partial': | ||
return interpolatePartial(path, segment, params); | ||
case 'fixed': | ||
return interpolateFixed(path, segment); | ||
default: | ||
return interpolateOptional(path, segment, params); | ||
} | ||
}, parsedRoute.leadingSlash ? '/' : ''); | ||
if (!onePart) { | ||
var next = parseOptional(ptn.substr(i + 1)).slice(1); | ||
if (parsedRoute.trailingSlash) { | ||
interpolatedPath = joinPaths(interpolatedPath, ''); | ||
} | ||
if (next.length) { | ||
list.push(next); | ||
} | ||
} | ||
if (parsedRoute.queryParamsString) { | ||
interpolatedPath += queryParamsString; | ||
} | ||
return parsePtn(out).concat(list); | ||
} | ||
return interpolatedPath; | ||
} | ||
@@ -272,4 +425,6 @@ | ||
rhumb.create = create; | ||
rhumb.interpolate = interpolate; | ||
rhumb._parse = parse; | ||
rhumb._findInTree = findIn; | ||
module.exports = rhumb; | ||
@@ -276,0 +431,0 @@ }); |
{ | ||
"name": "@websdk/rhumb", | ||
"version": "0.3.9", | ||
"version": "0.4.0", | ||
"description": "URL routing in js", | ||
@@ -28,6 +28,3 @@ "main": "lib/rhumb.js", | ||
}, | ||
"homepage": "https://github.com/sammyt/rhumb#readme", | ||
"scripts": { | ||
"prepublish": "make clean build" | ||
} | ||
"homepage": "https://github.com/sammyt/rhumb#readme" | ||
} |
124
README.md
Rhumb | ||
===== | ||
routing, this and that. | ||
Rhumb is a highly efficient and flexible router. | ||
Given a URI – where the constituent parts may be fixed or variable – Rhumb will unambiguously and with negligable overhead find a matching function, then apply it with whatever parameters might have been extracted from the URI. | ||
[Read more](RATIONALE.md). | ||
Bells and Whistles | ||
@@ -15,2 +19,3 @@ ------------------ | ||
* parameter parsing | ||
* interpolate params to produce paths | ||
@@ -43,2 +48,8 @@ | ||
When you need to create a URI from a set of params, the `interpolate` function can be used | ||
```javascript | ||
redShoesUri = rhumb.interpolate("/happy/shoes/{color}", { color: "red" }) | ||
``` | ||
Route Syntax | ||
@@ -121,8 +132,113 @@ ------------ | ||
Have fun! | ||
Parameter Interpolation | ||
----------------------- | ||
Have fun! | ||
Rhumb allows you to take a route and a set of params and produce a path that can be matched against a route. | ||
--- | ||
#### fixed paths | ||
[LICENSE](LICENSE) | ||
When you give `.interpolate(...)` a route without any declared variables or partial variable parts, then a valid path will be returned and you will typically not see any changes: | ||
```javascript | ||
rhumb.interpolate("/stories", {}) | ||
// returns "/stories" | ||
rhumb.interpolate("/stories?sortBy=publishedDate", {}) | ||
// returns "/stories?sortBy=publishedDate" | ||
``` | ||
When the route you supplied is not a valid path, Rhumb will step in and escape some of the characters, so that a valid path can be produced. | ||
If your route has empty parts, then some of the slash characters will be encoded to `%2F`: | ||
```javascript | ||
rhumb.interpolate('//sarah/scary', {}) | ||
// returns "/%2Fsarah/scary" | ||
rhumb.interpolate('stories//scary', {}) | ||
// returns "stories%2F/scary" | ||
``` | ||
#### variable parts | ||
When variables are present, Rhumb will interpolate the variables with the params you supply: | ||
```javascript | ||
rhumb.interpolate("/potatoes/{variety}", { variety: "marabel" }) | ||
// returns "/potatoes/marabel" | ||
rhumb.interpolate("/shoes/{color}/{size}", { color: "red", size: "6" }) | ||
// returns "/shoes/red/6" | ||
``` | ||
For interpolation to produce a valid path, it will throw an error when a required variable is absent, `""`, `null` or `undefined`: | ||
```javascript | ||
rhumb.interpolate("/potatoes/{variety}", {}) | ||
// throws 'Invalid parameter: "variety" is missing' | ||
rhumb.interpolate("/shoes/{color}/{size}", { color: "red", size: null }) | ||
// throws 'Invalid parameter: "size" is null' | ||
``` | ||
To mark a variable part as not-required, it has to be wrapped in an optional path, as shown later. | ||
#### partially variable parts | ||
Like variables, Rhumb will interpolate partially variable parts when they are defined and not empty in the supplied params: | ||
```javascript | ||
rhumb.interpolate("/orders/{days}-days-ago", { days: "40" }) | ||
// returns "/orders/40-days-ago" | ||
rhumb.interpolate("/author/{forename}-{surname}", { forename: "susan", surname: "smith" }) | ||
// returns "/author/susan-smith" | ||
``` | ||
It will also throw an error when a required partial variable is absent, `""`, `null` or `undefined`: | ||
```javascript | ||
rhumb.interpolate("/orders/{days}-days-ago", { days: "" }) | ||
// throws 'Invalid parameter: "days" is empty' | ||
rhumb.interpolate("/author/{forename}-{surname}", { forename: "susan", surname: undefined }) | ||
// throws 'Invalid parameter: "surname" is undefined' | ||
``` | ||
To mark a partially variable part as not-required, it has to be wrapped in an optional path, as shown later. | ||
#### optional parts | ||
Rhumb is greedy with how it handles optional paths when interpolating, so expect optional parts to be included whenever possible. | ||
```javascript | ||
rhumb.interpolate("/stories(/bob)", {}) | ||
// returns "/stories/bob" | ||
rhumb.interpolate("/stories(/sarah(/scary))", {}) | ||
// returns "/stories/sarah/scary" | ||
``` | ||
When variables or partially variables in optional parts are absent, `""`, `null` or `undefined` then no error is thrown and the optional part is dropped. | ||
```javascript | ||
rhumb.interpolate("/stories(/by-{name})", {}) | ||
// returns "/stories" | ||
rhumb.interpolate("/stories(/{author}(/{genre}))", { author: "sarah", genre: "" }) | ||
// returns "/stories/sarah" | ||
``` | ||
Found an issue, or want to contribute? | ||
-------------------------------------- | ||
If you find an issue, want to start a discussion on something related to this project, or have suggestions on how to improve it? Please [create an issue](../../issues/new)! | ||
See an error and want to fix it? Want to add a file or otherwise make some changes? All contributions are welcome! Please refer to the [contribution guidelines](CONTRIBUTING.md) for more information. | ||
License | ||
------- | ||
Please refer to the [license](LICENSE.md) for more information on licensing and copyright information. |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
41686
361
242
1