mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 07:14:47 -05:00
360 lines
14 KiB
JavaScript
360 lines
14 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.RegExpRouter = void 0;
|
|
const router_1 = require("../../router");
|
|
const trie_1 = require("./trie");
|
|
const emptyParam = {};
|
|
const nullMatcher = [/^$/, []];
|
|
function initHint(path) {
|
|
const components = path.match(/\/(?::\w+{[^}]+}|[^\/]*)/g) || [];
|
|
let componentsLength = components.length;
|
|
const paramIndexList = [];
|
|
const regExpComponents = [];
|
|
const namedParams = [];
|
|
for (let i = 0, len = components.length; i < len; i++) {
|
|
if (i === len - 1 && components[i] === '/*') {
|
|
componentsLength--;
|
|
break;
|
|
}
|
|
const m = components[i].match(/^\/:(\w+)({[^}]+})?/);
|
|
if (m) {
|
|
namedParams.push([i, m[1], m[2] || '[^/]+']);
|
|
regExpComponents[i] = m[2] || true;
|
|
}
|
|
else if (components[i] === '/*') {
|
|
regExpComponents[i] = true;
|
|
}
|
|
else {
|
|
regExpComponents[i] = components[i];
|
|
}
|
|
if (/\/(?::|\*)/.test(components[i])) {
|
|
paramIndexList.push(i);
|
|
}
|
|
}
|
|
return {
|
|
components,
|
|
regExpComponents,
|
|
componentsLength,
|
|
endWithWildcard: path.endsWith('*'),
|
|
paramIndexList,
|
|
namedParams,
|
|
maybeHandler: true,
|
|
};
|
|
}
|
|
function compareRoute(a, b) {
|
|
if (a.path === '*') {
|
|
return 1;
|
|
}
|
|
let i = 0;
|
|
const len = a.hint.regExpComponents.length;
|
|
for (; i < len; i++) {
|
|
if (a.hint.regExpComponents[i] !== b.hint.regExpComponents[i]) {
|
|
if (a.hint.regExpComponents[i] === true) {
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
// may be ambiguous
|
|
for (; i < len; i++) {
|
|
if (a.hint.regExpComponents[i] !== true &&
|
|
a.hint.regExpComponents[i] !== b.hint.regExpComponents[i]) {
|
|
return 2;
|
|
}
|
|
}
|
|
return i === b.hint.regExpComponents.length || a.hint.endWithWildcard ? 1 : 0;
|
|
}
|
|
function compareHandler(a, b) {
|
|
return a.index - b.index;
|
|
}
|
|
function getSortedHandlers(handlers) {
|
|
return [...handlers].sort(compareHandler).map((h) => h.handler);
|
|
}
|
|
function buildMatcherFromPreprocessedRoutes(routes, hasAmbiguous = false) {
|
|
const trie = new trie_1.Trie({ reverse: hasAmbiguous });
|
|
const handlers = [];
|
|
if (routes.length === 0) {
|
|
return nullMatcher;
|
|
}
|
|
for (let i = 0, len = routes.length; i < len; i++) {
|
|
const paramMap = trie.insert(routes[i].path, i);
|
|
handlers[i] = [
|
|
[...routes[i].middleware, ...routes[i].handlers],
|
|
Object.keys(paramMap).length !== 0 ? paramMap : null,
|
|
];
|
|
if (!hasAmbiguous) {
|
|
handlers[i][0] = getSortedHandlers(handlers[i][0]);
|
|
}
|
|
}
|
|
const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
|
|
for (let i = 0, len = handlers.length; i < len; i++) {
|
|
const paramMap = handlers[i][1];
|
|
if (paramMap) {
|
|
for (let j = 0, len = paramMap.length; j < len; j++) {
|
|
paramMap[j][1] = paramReplacementMap[paramMap[j][1]];
|
|
const aliasTo = routes[i].paramAliasMap[paramMap[j][0]];
|
|
if (aliasTo) {
|
|
for (let k = 0, len = aliasTo.length; k < len; k++) {
|
|
paramMap.push([aliasTo[k], paramMap[j][1]]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const handlerMap = [];
|
|
// using `in` because indexReplacementMap is a sparse array
|
|
for (const i in indexReplacementMap) {
|
|
handlerMap[i] = handlers[indexReplacementMap[i]];
|
|
}
|
|
return [regexp, handlerMap];
|
|
}
|
|
function verifyDuplicateParam(routes) {
|
|
const nameMap = {};
|
|
for (let i = 0, len = routes.length; i < len; i++) {
|
|
const route = routes[i];
|
|
for (let k = 0, len = route.hint.namedParams.length; k < len; k++) {
|
|
const [index, name] = route.hint.namedParams[k];
|
|
if (name in nameMap && index !== nameMap[name]) {
|
|
return false;
|
|
}
|
|
else {
|
|
nameMap[name] = index;
|
|
}
|
|
}
|
|
const paramAliasMap = route.paramAliasMap;
|
|
const paramAliasMapKeys = Object.keys(paramAliasMap);
|
|
for (let k = 0, len = paramAliasMapKeys.length; k < len; k++) {
|
|
const aliasFrom = paramAliasMapKeys[k];
|
|
for (let l = 0, len = paramAliasMap[aliasFrom].length; l < len; l++) {
|
|
const aliasTo = paramAliasMap[aliasFrom][l];
|
|
const index = nameMap[aliasFrom];
|
|
if (aliasTo in nameMap && index !== nameMap[aliasTo]) {
|
|
return false;
|
|
}
|
|
else {
|
|
nameMap[aliasTo] = index;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
class RegExpRouter {
|
|
constructor() {
|
|
this.routeData = { index: 0, routes: [], methods: new Set() };
|
|
}
|
|
add(method, path, handler) {
|
|
if (!this.routeData) {
|
|
throw new Error('Can not add a route since the matcher is already built.');
|
|
}
|
|
this.routeData.index++;
|
|
const { index, routes, methods } = this.routeData;
|
|
if (path === '/*') {
|
|
path = '*';
|
|
}
|
|
const hint = initHint(path);
|
|
const handlerWithSortIndex = {
|
|
index,
|
|
handler,
|
|
};
|
|
for (let i = 0, len = routes.length; i < len; i++) {
|
|
if (routes[i].method === method && routes[i].path === path) {
|
|
routes[i].handlers.push(handlerWithSortIndex);
|
|
return;
|
|
}
|
|
}
|
|
methods.add(method);
|
|
routes.push({
|
|
method,
|
|
path,
|
|
hint,
|
|
handlers: [handlerWithSortIndex],
|
|
middleware: [],
|
|
paramAliasMap: {},
|
|
});
|
|
}
|
|
match(method, path) {
|
|
const [primaryMatchers, secondaryMatchers, hasAmbiguous] = this.buildAllMatchers();
|
|
this.match = hasAmbiguous
|
|
? (method, path) => {
|
|
const matcher = (primaryMatchers[method] ||
|
|
primaryMatchers[router_1.METHOD_NAME_ALL]);
|
|
let match = path.match(matcher[0]);
|
|
if (!match) {
|
|
// do not support secondary matchers here.
|
|
return null;
|
|
}
|
|
const params = {};
|
|
const handlers = new Set();
|
|
let regExpSrc = matcher[0].source;
|
|
while (match) {
|
|
let index = match.indexOf('', 1);
|
|
for (;;) {
|
|
const [handler, paramMap] = matcher[1][index];
|
|
if (paramMap) {
|
|
for (let i = 0, len = paramMap.length; i < len; i++) {
|
|
params[paramMap[i][0]] = match[paramMap[i][1]];
|
|
}
|
|
}
|
|
for (let i = 0, len = handler.length; i < len; i++) {
|
|
handlers.add(handler[i]);
|
|
}
|
|
const newIndex = match.indexOf('', index + 1);
|
|
if (newIndex === -1) {
|
|
break;
|
|
}
|
|
index = newIndex;
|
|
}
|
|
regExpSrc = regExpSrc.replace(new RegExp(`((?:(?:\\(\\?:|.)*?\\([^)]*\\)){${index - 1}}.*?)\\(\\)`), '$1(^)');
|
|
match = path.match(new RegExp(regExpSrc));
|
|
}
|
|
return { handlers: getSortedHandlers(handlers.values()), params };
|
|
}
|
|
: (method, path) => {
|
|
let matcher = (primaryMatchers[method] || primaryMatchers[router_1.METHOD_NAME_ALL]);
|
|
let match = path.match(matcher[0]);
|
|
if (!match) {
|
|
const matchers = secondaryMatchers[method] || secondaryMatchers[router_1.METHOD_NAME_ALL];
|
|
for (let i = 0, len = matchers.length; i < len && !match; i++) {
|
|
matcher = matchers[i];
|
|
match = path.match(matcher[0]);
|
|
}
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
}
|
|
const index = match.indexOf('', 1);
|
|
const [handlers, paramMap] = matcher[1][index];
|
|
if (!paramMap) {
|
|
return { handlers, params: emptyParam };
|
|
}
|
|
const params = {};
|
|
for (let i = 0, len = paramMap.length; i < len; i++) {
|
|
params[paramMap[i][0]] = match[paramMap[i][1]];
|
|
}
|
|
return { handlers, params };
|
|
};
|
|
return this.match(method, path);
|
|
}
|
|
buildAllMatchers() {
|
|
// @ts-ignore
|
|
this.routeData.routes.sort(({ hint: a }, { hint: b }) => {
|
|
if (a.componentsLength !== b.componentsLength) {
|
|
return a.componentsLength - b.componentsLength;
|
|
}
|
|
for (let i = 0, len = Math.min(a.paramIndexList.length, b.paramIndexList.length) + 1; i < len; i++) {
|
|
if (a.paramIndexList[i] !== b.paramIndexList[i]) {
|
|
if (a.paramIndexList[i] === undefined) {
|
|
return -1;
|
|
}
|
|
else if (b.paramIndexList[i] === undefined) {
|
|
return 1;
|
|
}
|
|
else {
|
|
return a.paramIndexList[i] - b.paramIndexList[i];
|
|
}
|
|
}
|
|
}
|
|
if (a.endWithWildcard !== b.endWithWildcard) {
|
|
return a.endWithWildcard ? -1 : 1;
|
|
}
|
|
return 0;
|
|
});
|
|
const primaryMatchers = {};
|
|
const secondaryMatchers = {};
|
|
let hasAmbiguous = false;
|
|
// @ts-ignore
|
|
this.routeData.methods.forEach((method) => {
|
|
let _hasAmbiguous;
|
|
[primaryMatchers[method], secondaryMatchers[method], _hasAmbiguous] =
|
|
this.buildMatcher(method);
|
|
hasAmbiguous = hasAmbiguous || _hasAmbiguous;
|
|
});
|
|
primaryMatchers[router_1.METHOD_NAME_ALL] || (primaryMatchers[router_1.METHOD_NAME_ALL] = nullMatcher);
|
|
secondaryMatchers[router_1.METHOD_NAME_ALL] || (secondaryMatchers[router_1.METHOD_NAME_ALL] = []);
|
|
delete this.routeData; // to reduce memory usage
|
|
return [primaryMatchers, secondaryMatchers, hasAmbiguous];
|
|
}
|
|
buildMatcher(method) {
|
|
var _a, _b;
|
|
let hasAmbiguous = false;
|
|
const targetMethods = new Set([method, router_1.METHOD_NAME_ALL]);
|
|
// @ts-ignore
|
|
const routes = this.routeData.routes.filter(({ method }) => targetMethods.has(method));
|
|
// Reset temporary data per method
|
|
for (let i = 0, len = routes.length; i < len; i++) {
|
|
routes[i].middleware = [];
|
|
routes[i].paramAliasMap = {};
|
|
}
|
|
// preprocess routes
|
|
for (let i = 0, len = routes.length; i < len; i++) {
|
|
for (let j = i + 1; j < len; j++) {
|
|
const compareResult = compareRoute(routes[i], routes[j]);
|
|
// i includes j
|
|
if (compareResult === 1) {
|
|
const components = routes[j].hint.components;
|
|
const namedParams = routes[i].hint.namedParams;
|
|
for (let k = 0, len = namedParams.length; k < len; k++) {
|
|
const c = components[namedParams[k][0]];
|
|
const m = c.match(/^\/:(\w+)({[^}]+})?/);
|
|
if (m && namedParams[k][1] === m[1]) {
|
|
continue;
|
|
}
|
|
if (m) {
|
|
(_a = routes[j].paramAliasMap)[_b = m[1]] || (_a[_b] = []);
|
|
routes[j].paramAliasMap[m[1]].push(namedParams[k][1]);
|
|
}
|
|
else {
|
|
components[namedParams[k][0]] = `/:${namedParams[k][1]}{${c.substring(1)}}`;
|
|
routes[j].hint.namedParams.push([
|
|
namedParams[k][0],
|
|
namedParams[k][1],
|
|
c.substring(1),
|
|
]);
|
|
routes[j].path = components.join('');
|
|
}
|
|
}
|
|
if (routes[j].hint.components.length < routes[i].hint.components.length) {
|
|
routes[j].middleware.push(...routes[i].handlers.map((h) => ({
|
|
index: h.index,
|
|
handler: h.handler,
|
|
})));
|
|
}
|
|
else {
|
|
routes[j].middleware.push(...routes[i].handlers);
|
|
}
|
|
routes[i].hint.maybeHandler = false;
|
|
}
|
|
else if (compareResult === 2) {
|
|
// ambiguous
|
|
hasAmbiguous = true;
|
|
if (!verifyDuplicateParam([routes[i], routes[j]])) {
|
|
throw new Error('Duplicate param name');
|
|
}
|
|
}
|
|
}
|
|
if (!verifyDuplicateParam([routes[i]])) {
|
|
throw new Error('Duplicate param name');
|
|
}
|
|
}
|
|
if (hasAmbiguous) {
|
|
return [buildMatcherFromPreprocessedRoutes(routes, hasAmbiguous), [], hasAmbiguous];
|
|
}
|
|
const primaryRoutes = [];
|
|
const secondaryRoutes = [];
|
|
for (let i = 0, len = routes.length; i < len; i++) {
|
|
if (routes[i].hint.maybeHandler || !routes[i].hint.endWithWildcard) {
|
|
primaryRoutes.push(routes[i]);
|
|
}
|
|
else {
|
|
secondaryRoutes.push(routes[i]);
|
|
}
|
|
}
|
|
return [
|
|
buildMatcherFromPreprocessedRoutes(primaryRoutes, hasAmbiguous),
|
|
[buildMatcherFromPreprocessedRoutes(secondaryRoutes, hasAmbiguous)],
|
|
hasAmbiguous,
|
|
];
|
|
}
|
|
}
|
|
exports.RegExpRouter = RegExpRouter;
|