diff --git a/package.json b/package.json index 88c59a7..a280a2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@routejs/router", - "version": "3.0.2", + "version": "3.0.3", "description": "Fast and lightweight http routing engine for nodejs", "main": "index.mjs", "type": "module", diff --git a/src/host-regex.cjs b/src/host-regex.cjs index 6ce8ae1..523cba3 100644 --- a/src/host-regex.cjs +++ b/src/host-regex.cjs @@ -1,249 +1,303 @@ -module.exports = function hostRegex(host, options = { caseSensitive: false }) { - let pathRegex = ""; - let params = []; - let param = {}; - let isEscape = false; - let caseSensitive = options?.caseSensitive ?? false; - for (let i = 0; i < host.length; i++) { - let char = host[i]; - // Match escape character - if (host[i] === "\\" && isEscape === false) { - isEscape = true; - if (param.hasParamName === true && param.hasParamRegex !== true) { - param.nameEnd = i - 1; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } - if (i == host.length - 1) { - char = `\\${char}`; +module.exports = function hostRegex(str, options = { caseSensitive: false }) { + const caseSensitive = options?.caseSensitive ?? false; + const tokens = []; + let i = 0; + let key = 0; + while (i < str.length) { + let char = str[i]; + + if (char === "\\") { + tokens.push({ type: "ESCAPED_CHAR", value: str[i + 1] ?? "\\" }); + i = i + 2; + continue; + } + + if (char === "*" || char === "+" || char === "?") { + if (char === "*") { + tokens.push({ + type: "MODIFIER", + value: str[i++], + name: key++, + regex: "(.*)", + }); + } else { + tokens.push({ type: "MODIFIER", value: str[i++] }); } - } else if (isEscape === true) { - // Ignore escaped character - isEscape = false; - if (param.hasParamRegex !== true && host[i] !== "\\") { - // Escape special characters - if (host[i].match(/[^\.\^\$\[\]\{\}\|\*\+\?\(\)]/g)) { - char = `\\${char}`; + continue; + } + + if (char === ".") { + tokens.push({ type: "DELIMITER", value: str[i++] }); + continue; + } + + if (char === ":") { + let name = ""; + let j = i + 1; + + while (j < str.length) { + const code = str.charCodeAt(j); + if ( + // `0-9` + (code >= 48 && code <= 57) || + // `A-Z` + (code >= 65 && code <= 90) || + // `a-z` + (code >= 97 && code <= 122) || + // `_` + code === 95 + ) { + name += str[j++]; + continue; } + break; } - } else { - // Match params - if (host[i] === ":") { - if (param.hasParamName === true) { - param.nameEnd = i - 1; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } - param.nameStart = i; - param.hasParamName = true; - } else if (param.hasParamRegex !== true && host[i].match(/^[\.]$/i)) { - if (param.hasParamName === true) { - param.nameEnd = i - 1; - param.hasParamRegex = param.hasParamRegex === true; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - } - if (param.hasParamRegex !== true) { - char = `\\${char}`; + + if (!name) throw new TypeError(`Missing parameter name at ${i}`); + + tokens.push({ + type: "PARAM", + value: name, + name, + regex: "([^\\.]+?)", + }); + i = j; + continue; + } + + if (char === "(") { + let count = 1; + let pattern = ""; + let j = i + 1; + + if (str[j] === "?") { + throw new TypeError(`Pattern cannot start with "?" at ${j}`); + } + + while (j < str.length) { + if (str[j] === "\\") { + pattern += str[j++] + str[j++]; + continue; } - param = {}; - } else if (host[i].match(/^[^A-Za-z0-9_]+$/i)) { - if (host[i] === "(") { - if (host[i + 1] === "?") { - if (host[i + 2] == ":") { - throw new TypeError( - `non-capturing groups are not allowed at ${i}` - ); - } - throw new TypeError(`pattern cannot start with "?" at ${i}`); - } - if (param.hasParamRegex === true) { - throw new TypeError(`capturing groups are not allowed at ${i}`); - } - if (param.hasParamName === true && param.nameStart !== i - 1) { - param.hasParamRegex = true; - param.regexStart = i; - param.nameEnd = i - 1; - } else { - param = {}; - param.hasParamName = false; - param.hasParamRegex = true; - param.regexStart = i; + + if (str[j] === ")") { + count--; + if (count === 0) { + j++; + break; } - } else if (param.hasParamRegex === true && host[i] === ")") { - param.regexEnd = i; - if (param.regexStart !== param.regexEnd) { - params.push(param); + } else if (str[j] === "(") { + count++; + if (str[j + 1] !== "?") { + throw new TypeError(`Capturing groups are not allowed at ${j}`); } - param = {}; - } else if (param.hasParamRegex !== true && host[i] === "*") { - param = {}; - param.hasParamName = false; - param.hasParamRegex = true; - param.regexStart = i; - param.regexEnd = i; - params.push(param); - param = {}; - } else if (param.hasParamRegex !== true) { - // Escape special characters - char = char.replace(/[\.\^\$\[\]\{\}\|\)]+/g, "\\$&"); } - if (param.hasParamRegex !== true && param.hasParamName === true) { - param.nameEnd = i - 1; - param.hasParamRegex = false; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } + pattern += str[j++]; + } + + if (count) throw new TypeError(`Unbalanced pattern at ${i}`); + if (!pattern) throw new TypeError(`Missing pattern at ${i}`); + + if (tokens.length > 0 && tokens[tokens.length - 1].type === "PARAM") { + tokens[tokens.length - 1] = { + type: "PARAM_REGEX", + value: pattern, + name: tokens[tokens.length - 1].name, + regex: "(" + pattern + ")", + }; + } else { + tokens.push({ + type: "REGEX", + value: pattern, + name: key++, + regex: "(" + pattern + ")", + }); } + i = j; + continue; } - if (i == host.length - 1) { + + let path = ""; + let j = i; + while (j < str.length) { if ( - param.hasParamRegex === true && - typeof param.regexEnd === "undefined" + str[j] === "\\" || + str[j] === "*" || + str[j] === "+" || + str[j] === "?" || + str[j] === ":" || + str[j] === "(" || + str[j] === "." ) { - throw new TypeError(`unterminated group at ${i}`); - } - if (param.hasParamName === true) { - param.nameEnd = i; - param.hasParamRegex = param.hasParamRegex === true; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } + break; } - param = {}; + // Escape special characters + path += str[j++]; + continue; } - pathRegex += char; - } - if (pathRegex === "") { - pathRegex = ""; - } else { - pathRegex = `^${pathRegex}(?:\\:\\d+)?$`; + tokens.push({ type: "PATH", value: path }); + i = j; } - let allParams = []; - let paramRegexCounter = 0; - for (let e of params) { - let regex = host.slice(e.regexStart, e.regexEnd + 1); - if (regex === "*") { - regex = "(.*)"; - } - let replaceStart = e.nameStart ?? e.regexStart; - let replaceEnd = e.regexEnd ?? e.nameEnd; - let replaceParam = null; - let paramDetails = { - name: host.slice(e.nameStart + 1, e.nameEnd + 1), - paramRegex: regex === "" ? "([^\\.]+?)" : regex, - param: host.slice(replaceStart, replaceEnd + 1), - optional: host[replaceEnd + 1] === "?", - }; - if (paramDetails.optional === true) { + + const params = []; + let pathRegex = ""; + let tokenIndex = 0; + while (tokenIndex < tokens.length) { + if (tokens[tokenIndex].type === "DELIMITER") { if ( - host[replaceStart - 1] === "." && - (typeof host[replaceEnd + 2] === "undefined" || - host[replaceEnd + 2] === ".") + tokens[tokenIndex + 1] && + (tokens[tokenIndex + 1].type === "PARAM" || + tokens[tokenIndex + 1].type === "PARAM_REGEX" || + tokens[tokenIndex + 1].type === "REGEX") ) { - replaceParam = `\\.${paramDetails.param}`; - paramDetails.paramRegex = `(?:\\.${paramDetails.paramRegex})`; - } else if ( - typeof host[replaceStart - 1] === "undefined" && - host[replaceEnd + 2] === "." - ) { - replaceParam = `${paramDetails.param}?\\.`; - paramDetails.paramRegex = `(?:${paramDetails.paramRegex}\\.)?`; - } else { - replaceParam = paramDetails.param; + if ( + tokens[tokenIndex + 2] && + tokens[tokenIndex + 2].type === "MODIFIER" && + tokens[tokenIndex + 2].value === "?" && + (typeof tokens[tokenIndex + 3] === "undefined" || + tokens[tokenIndex + 3].type === "DELIMITER") + ) { + tokenIndex++; + continue; + } } - } else { - replaceParam = paramDetails.param; + pathRegex += tokens[tokenIndex++].value; + continue; } - if (paramDetails.name === "") { - paramDetails.name = paramRegexCounter++; - } - if (replaceParam !== null) { - pathRegex = pathRegex.replace(replaceParam, paramDetails.paramRegex); - } - allParams.push(paramDetails); - } - let regex = - caseSensitive === true ? new RegExp(pathRegex, "i") : new RegExp(pathRegex); - - // Compiler regex to path - function compile(params = {}, options = {}) { - let tmpPath = regex.source; - let validate = options?.validate ?? false; - for (let e of allParams) { - if (params.hasOwnProperty(e.name)) { - let replaceStr = e.paramRegex; - if (e.optional === true) { - if (replaceStr.slice(0, 6) === "(?:\\.(") { - tmpPath = tmpPath.replace(replaceStr, `.${params[e.name]}`); - } else if (replaceStr.slice(-5) === ")\\.)?") { - tmpPath = tmpPath.replace(replaceStr, `${params[e.name]}.`); - } else { - tmpPath = tmpPath.replace(replaceStr, params[e.name]); - } + if ( + tokens[tokenIndex].type === "PARAM" || + tokens[tokenIndex].type === "PARAM_REGEX" || + tokens[tokenIndex].type === "REGEX" + ) { + let param = { + name: tokens[tokenIndex].name, + regex: tokens[tokenIndex].regex, + optional: tokens[tokenIndex + 1] + ? tokens[tokenIndex + 1].type === "MODIFIER" && + tokens[tokenIndex + 1].value === "?" + : false, + }; + + if (param.optional === true) { + if ( + tokens[tokenIndex - 1] && + tokens[tokenIndex - 1].type === "DELIMITER" && + (typeof tokens[tokenIndex + 2] === "undefined" || + tokens[tokenIndex + 2].type === "DELIMITER") + ) { + param.regex = "(?:\\." + param.regex + ")?"; + tokenIndex = tokenIndex + 2; + } else if ( + typeof tokens[tokenIndex - 1] === "undefined" && + typeof tokens[tokenIndex + 2] && + tokens[tokenIndex + 2].type === "DELIMITER" + ) { + param.regex = "(?:" + param.regex + "\\.)?"; + tokenIndex = tokenIndex + 3; } else { - tmpPath = tmpPath.replace(replaceStr, params[e.name]); + param.regex = param.regex; + tokenIndex++; } } else { - if (e.optional === true) { - let replaceStr = e.paramRegex; - tmpPath = tmpPath.replace(replaceStr, ""); - } else { - throw new TypeError( - "invalid route parameters, please provide all route parameters" - ); - } + param.regex = param.regex; + tokenIndex++; } + + params.push(param); + pathRegex += param.regex; + continue; } - let isEscape = false; - let compiledPath = ""; - for (let i = 0; i < tmpPath.length; i++) { - let char = tmpPath[i]; - // Match escape character - if (tmpPath[i] === "\\" && isEscape === false) { - isEscape = true; - continue; - } else if (isEscape === true) { - // Ignore escaped character - isEscape = false; - } else { - // Match params - if (tmpPath[i] === "+") { - char = ""; - } else if (tmpPath[i] === "?") { - char = ""; - } - } - compiledPath += char; + if ( + tokens[tokenIndex].type === "MODIFIER" && + tokens[tokenIndex].value === "*" + ) { + let param = { + name: tokens[tokenIndex].name, + regex: tokens[tokenIndex].regex, + optional: false, + }; + + params.push(param); + pathRegex += param.regex; + tokenIndex++; + continue; } - compiledPath = compiledPath.replace(/(^\^|\(\:\:d\)\$$|\$$)/gm, ""); - if (validate === true) { - if (regex.test(compiledPath)) { - return compiledPath; - } else { - throw new TypeError( - "invalid route parameters, please provide all route parameters" - ); - } - } else { - return compiledPath; + + if (tokens[tokenIndex].type === "ESCAPED_CHAR") { + pathRegex += "\\" + tokens[tokenIndex++].value; + continue; + } + + if (tokens[tokenIndex].type === "PATH") { + pathRegex += tokens[tokenIndex++].value.replace( + /([.+*?=^!:${}()[\]|/\\])/g, + "\\$1" + ); + continue; } + pathRegex += tokens[tokenIndex++].value; } + pathRegex = pathRegex.replace(/\/$/gm, ""); + if (pathRegex === "") { + pathRegex = ""; + } else { + pathRegex = `^${pathRegex}(?:\\:\\d+)?$`; + } + let regex = + caseSensitive === false + ? new RegExp(pathRegex, "i") + : new RegExp(pathRegex); + return { - host: host, - params: allParams, - regex, - compile, + host: str, + params, + regex: regex, + compile(params = {}, options = { validate: false }) { + const validate = options?.validate ?? false; + let compiledPath = ""; + let i = 0; + while (i < tokens.length) { + if (tokens[i].type === "DELIMITER") { + compiledPath += tokens[i++].value; + continue; + } + if (tokens[i].type === "PATH") { + compiledPath += tokens[i++].value; + continue; + } + if ( + tokens[i].type === "PARAM" || + tokens[i].type === "PARAM_REGEX" || + tokens[i].type === "REGEX" || + (tokens[i].type === "MODIFIER" && tokens[i].value === "*") + ) { + if (params.hasOwnProperty(tokens[i].name)) { + compiledPath += params[tokens[i++].name]; + continue; + } else { + if ( + tokens[i + 1] && + tokens[i + 1].type === "MODIFIER" && + tokens[i + 1].value === "?" + ) { + if (tokens[i + 2] && tokens[i + 2].type === "DELIMITER") { + i = i + 3; + continue; + } + } else { + throw new TypeError( + "invalid route parameters, please provide all route parameters" + ); + } + } + } + i++; + } + return compiledPath; + }, }; }; diff --git a/src/host-regex.mjs b/src/host-regex.mjs index 492a1e8..514f782 100644 --- a/src/host-regex.mjs +++ b/src/host-regex.mjs @@ -1,249 +1,303 @@ -export default function hostRegex(host, options = { caseSensitive: false }) { - let pathRegex = ""; - let params = []; - let param = {}; - let isEscape = false; - let caseSensitive = options?.caseSensitive ?? false; - for (let i = 0; i < host.length; i++) { - let char = host[i]; - // Match escape character - if (host[i] === "\\" && isEscape === false) { - isEscape = true; - if (param.hasParamName === true && param.hasParamRegex !== true) { - param.nameEnd = i - 1; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } - if (i == host.length - 1) { - char = `\\${char}`; +export default function hostRegex(str, options = { caseSensitive: false }) { + const caseSensitive = options?.caseSensitive ?? false; + const tokens = []; + let i = 0; + let key = 0; + while (i < str.length) { + let char = str[i]; + + if (char === "\\") { + tokens.push({ type: "ESCAPED_CHAR", value: str[i + 1] ?? "\\" }); + i = i + 2; + continue; + } + + if (char === "*" || char === "+" || char === "?") { + if (char === "*") { + tokens.push({ + type: "MODIFIER", + value: str[i++], + name: key++, + regex: "(.*)", + }); + } else { + tokens.push({ type: "MODIFIER", value: str[i++] }); } - } else if (isEscape === true) { - // Ignore escaped character - isEscape = false; - if (param.hasParamRegex !== true && host[i] !== "\\") { - // Escape special characters - if (host[i].match(/[^\.\^\$\[\]\{\}\|\*\+\?\(\)]/g)) { - char = `\\${char}`; + continue; + } + + if (char === ".") { + tokens.push({ type: "DELIMITER", value: str[i++] }); + continue; + } + + if (char === ":") { + let name = ""; + let j = i + 1; + + while (j < str.length) { + const code = str.charCodeAt(j); + if ( + // `0-9` + (code >= 48 && code <= 57) || + // `A-Z` + (code >= 65 && code <= 90) || + // `a-z` + (code >= 97 && code <= 122) || + // `_` + code === 95 + ) { + name += str[j++]; + continue; } + break; } - } else { - // Match params - if (host[i] === ":") { - if (param.hasParamName === true) { - param.nameEnd = i - 1; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } - param.nameStart = i; - param.hasParamName = true; - } else if (param.hasParamRegex !== true && host[i].match(/^[\.]$/i)) { - if (param.hasParamName === true) { - param.nameEnd = i - 1; - param.hasParamRegex = param.hasParamRegex === true; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - } - if (param.hasParamRegex !== true) { - char = `\\${char}`; + + if (!name) throw new TypeError(`Missing parameter name at ${i}`); + + tokens.push({ + type: "PARAM", + value: name, + name, + regex: "([^\\.]+?)", + }); + i = j; + continue; + } + + if (char === "(") { + let count = 1; + let pattern = ""; + let j = i + 1; + + if (str[j] === "?") { + throw new TypeError(`Pattern cannot start with "?" at ${j}`); + } + + while (j < str.length) { + if (str[j] === "\\") { + pattern += str[j++] + str[j++]; + continue; } - param = {}; - } else if (host[i].match(/^[^A-Za-z0-9_]+$/i)) { - if (host[i] === "(") { - if (host[i + 1] === "?") { - if (host[i + 2] == ":") { - throw new TypeError( - `non-capturing groups are not allowed at ${i}` - ); - } - throw new TypeError(`pattern cannot start with "?" at ${i}`); - } - if (param.hasParamRegex === true) { - throw new TypeError(`capturing groups are not allowed at ${i}`); - } - if (param.hasParamName === true && param.nameStart !== i - 1) { - param.hasParamRegex = true; - param.regexStart = i; - param.nameEnd = i - 1; - } else { - param = {}; - param.hasParamName = false; - param.hasParamRegex = true; - param.regexStart = i; + + if (str[j] === ")") { + count--; + if (count === 0) { + j++; + break; } - } else if (param.hasParamRegex === true && host[i] === ")") { - param.regexEnd = i; - if (param.regexStart !== param.regexEnd) { - params.push(param); + } else if (str[j] === "(") { + count++; + if (str[j + 1] !== "?") { + throw new TypeError(`Capturing groups are not allowed at ${j}`); } - param = {}; - } else if (param.hasParamRegex !== true && host[i] === "*") { - param = {}; - param.hasParamName = false; - param.hasParamRegex = true; - param.regexStart = i; - param.regexEnd = i; - params.push(param); - param = {}; - } else if (param.hasParamRegex !== true) { - // Escape special characters - char = char.replace(/[\.\^\$\[\]\{\}\|\)]+/g, "\\$&"); } - if (param.hasParamRegex !== true && param.hasParamName === true) { - param.nameEnd = i - 1; - param.hasParamRegex = false; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } + pattern += str[j++]; + } + + if (count) throw new TypeError(`Unbalanced pattern at ${i}`); + if (!pattern) throw new TypeError(`Missing pattern at ${i}`); + + if (tokens.length > 0 && tokens[tokens.length - 1].type === "PARAM") { + tokens[tokens.length - 1] = { + type: "PARAM_REGEX", + value: pattern, + name: tokens[tokens.length - 1].name, + regex: "(" + pattern + ")", + }; + } else { + tokens.push({ + type: "REGEX", + value: pattern, + name: key++, + regex: "(" + pattern + ")", + }); } + i = j; + continue; } - if (i == host.length - 1) { + + let path = ""; + let j = i; + while (j < str.length) { if ( - param.hasParamRegex === true && - typeof param.regexEnd === "undefined" + str[j] === "\\" || + str[j] === "*" || + str[j] === "+" || + str[j] === "?" || + str[j] === ":" || + str[j] === "(" || + str[j] === "." ) { - throw new TypeError(`unterminated group at ${i}`); - } - if (param.hasParamName === true) { - param.nameEnd = i; - param.hasParamRegex = param.hasParamRegex === true; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } + break; } - param = {}; + // Escape special characters + path += str[j++]; + continue; } - pathRegex += char; - } - if (pathRegex === "") { - pathRegex = ""; - } else { - pathRegex = `^${pathRegex}(?:\\:\\d+)?$`; + tokens.push({ type: "PATH", value: path }); + i = j; } - let allParams = []; - let paramRegexCounter = 0; - for (let e of params) { - let regex = host.slice(e.regexStart, e.regexEnd + 1); - if (regex === "*") { - regex = "(.*)"; - } - let replaceStart = e.nameStart ?? e.regexStart; - let replaceEnd = e.regexEnd ?? e.nameEnd; - let replaceParam = null; - let paramDetails = { - name: host.slice(e.nameStart + 1, e.nameEnd + 1), - paramRegex: regex === "" ? "([^\\.]+?)" : regex, - param: host.slice(replaceStart, replaceEnd + 1), - optional: host[replaceEnd + 1] === "?", - }; - if (paramDetails.optional === true) { + + const params = []; + let pathRegex = ""; + let tokenIndex = 0; + while (tokenIndex < tokens.length) { + if (tokens[tokenIndex].type === "DELIMITER") { if ( - host[replaceStart - 1] === "." && - (typeof host[replaceEnd + 2] === "undefined" || - host[replaceEnd + 2] === ".") + tokens[tokenIndex + 1] && + (tokens[tokenIndex + 1].type === "PARAM" || + tokens[tokenIndex + 1].type === "PARAM_REGEX" || + tokens[tokenIndex + 1].type === "REGEX") ) { - replaceParam = `\\.${paramDetails.param}`; - paramDetails.paramRegex = `(?:\\.${paramDetails.paramRegex})`; - } else if ( - typeof host[replaceStart - 1] === "undefined" && - host[replaceEnd + 2] === "." - ) { - replaceParam = `${paramDetails.param}?\\.`; - paramDetails.paramRegex = `(?:${paramDetails.paramRegex}\\.)?`; - } else { - replaceParam = paramDetails.param; + if ( + tokens[tokenIndex + 2] && + tokens[tokenIndex + 2].type === "MODIFIER" && + tokens[tokenIndex + 2].value === "?" && + (typeof tokens[tokenIndex + 3] === "undefined" || + tokens[tokenIndex + 3].type === "DELIMITER") + ) { + tokenIndex++; + continue; + } } - } else { - replaceParam = paramDetails.param; + pathRegex += tokens[tokenIndex++].value; + continue; } - if (paramDetails.name === "") { - paramDetails.name = paramRegexCounter++; - } - if (replaceParam !== null) { - pathRegex = pathRegex.replace(replaceParam, paramDetails.paramRegex); - } - allParams.push(paramDetails); - } - let regex = - caseSensitive === true ? new RegExp(pathRegex, "i") : new RegExp(pathRegex); - - // Compiler regex to path - function compile(params = {}, options = {}) { - let tmpPath = regex.source; - let validate = options?.validate ?? false; - for (let e of allParams) { - if (params.hasOwnProperty(e.name)) { - let replaceStr = e.paramRegex; - if (e.optional === true) { - if (replaceStr.slice(0, 6) === "(?:\\.(") { - tmpPath = tmpPath.replace(replaceStr, `.${params[e.name]}`); - } else if (replaceStr.slice(-5) === ")\\.)?") { - tmpPath = tmpPath.replace(replaceStr, `${params[e.name]}.`); - } else { - tmpPath = tmpPath.replace(replaceStr, params[e.name]); - } + if ( + tokens[tokenIndex].type === "PARAM" || + tokens[tokenIndex].type === "PARAM_REGEX" || + tokens[tokenIndex].type === "REGEX" + ) { + let param = { + name: tokens[tokenIndex].name, + regex: tokens[tokenIndex].regex, + optional: tokens[tokenIndex + 1] + ? tokens[tokenIndex + 1].type === "MODIFIER" && + tokens[tokenIndex + 1].value === "?" + : false, + }; + + if (param.optional === true) { + if ( + tokens[tokenIndex - 1] && + tokens[tokenIndex - 1].type === "DELIMITER" && + (typeof tokens[tokenIndex + 2] === "undefined" || + tokens[tokenIndex + 2].type === "DELIMITER") + ) { + param.regex = "(?:\\." + param.regex + ")?"; + tokenIndex = tokenIndex + 2; + } else if ( + typeof tokens[tokenIndex - 1] === "undefined" && + typeof tokens[tokenIndex + 2] && + tokens[tokenIndex + 2].type === "DELIMITER" + ) { + param.regex = "(?:" + param.regex + "\\.)?"; + tokenIndex = tokenIndex + 3; } else { - tmpPath = tmpPath.replace(replaceStr, params[e.name]); + param.regex = param.regex; + tokenIndex++; } } else { - if (e.optional === true) { - let replaceStr = e.paramRegex; - tmpPath = tmpPath.replace(replaceStr, ""); - } else { - throw new TypeError( - "invalid route parameters, please provide all route parameters" - ); - } + param.regex = param.regex; + tokenIndex++; } + + params.push(param); + pathRegex += param.regex; + continue; } - let isEscape = false; - let compiledPath = ""; - for (let i = 0; i < tmpPath.length; i++) { - let char = tmpPath[i]; - // Match escape character - if (tmpPath[i] === "\\" && isEscape === false) { - isEscape = true; - continue; - } else if (isEscape === true) { - // Ignore escaped character - isEscape = false; - } else { - // Match params - if (tmpPath[i] === "+") { - char = ""; - } else if (tmpPath[i] === "?") { - char = ""; - } - } - compiledPath += char; + if ( + tokens[tokenIndex].type === "MODIFIER" && + tokens[tokenIndex].value === "*" + ) { + let param = { + name: tokens[tokenIndex].name, + regex: tokens[tokenIndex].regex, + optional: false, + }; + + params.push(param); + pathRegex += param.regex; + tokenIndex++; + continue; } - compiledPath = compiledPath.replace(/(^\^|\(\:\:d\)\$$|\$$)/gm, ""); - if (validate === true) { - if (regex.test(compiledPath)) { - return compiledPath; - } else { - throw new TypeError( - "invalid route parameters, please provide all route parameters" - ); - } - } else { - return compiledPath; + + if (tokens[tokenIndex].type === "ESCAPED_CHAR") { + pathRegex += "\\" + tokens[tokenIndex++].value; + continue; + } + + if (tokens[tokenIndex].type === "PATH") { + pathRegex += tokens[tokenIndex++].value.replace( + /([.+*?=^!:${}()[\]|/\\])/g, + "\\$1" + ); + continue; } + pathRegex += tokens[tokenIndex++].value; } + pathRegex = pathRegex.replace(/\/$/gm, ""); + if (pathRegex === "") { + pathRegex = ""; + } else { + pathRegex = `^${pathRegex}(?:\\:\\d+)?$`; + } + let regex = + caseSensitive === false + ? new RegExp(pathRegex, "i") + : new RegExp(pathRegex); + return { - host: host, - params: allParams, - regex, - compile, + host: str, + params, + regex: regex, + compile(params = {}, options = { validate: false }) { + const validate = options?.validate ?? false; + let compiledPath = ""; + let i = 0; + while (i < tokens.length) { + if (tokens[i].type === "DELIMITER") { + compiledPath += tokens[i++].value; + continue; + } + if (tokens[i].type === "PATH") { + compiledPath += tokens[i++].value; + continue; + } + if ( + tokens[i].type === "PARAM" || + tokens[i].type === "PARAM_REGEX" || + tokens[i].type === "REGEX" || + (tokens[i].type === "MODIFIER" && tokens[i].value === "*") + ) { + if (params.hasOwnProperty(tokens[i].name)) { + compiledPath += params[tokens[i++].name]; + continue; + } else { + if ( + tokens[i + 1] && + tokens[i + 1].type === "MODIFIER" && + tokens[i + 1].value === "?" + ) { + if (tokens[i + 2] && tokens[i + 2].type === "DELIMITER") { + i = i + 3; + continue; + } + } else { + throw new TypeError( + "invalid route parameters, please provide all route parameters" + ); + } + } + } + i++; + } + return compiledPath; + }, }; } diff --git a/src/path-regex.cjs b/src/path-regex.cjs index 2fc360b..a1cdb9b 100644 --- a/src/path-regex.cjs +++ b/src/path-regex.cjs @@ -1,231 +1,292 @@ -module.exports = function pathRegex(path, options = { caseSensitive: false }) { - let pathRegex = ""; - let params = []; - let param = {}; - let isEscape = false; - let caseSensitive = options?.caseSensitive ?? false; - for (let i = 0; i < path.length; i++) { - let char = path[i]; - // Match escape character - if (path[i] === "\\" && isEscape === false) { - isEscape = true; - if (param.hasParamName === true && param.hasParamRegex !== true) { - param.nameEnd = i - 1; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } - if (i == path.length - 1) { - char += `\\${char}`; +module.exports = function pathRegex(str, options = { caseSensitive: false }) { + const caseSensitive = options?.caseSensitive ?? false; + const tokens = []; + let i = 0; + let key = 0; + while (i < str.length) { + let char = str[i]; + + if (char === "\\") { + tokens.push({ type: "ESCAPED_CHAR", value: str[i + 1] ?? "\\" }); + i = i + 2; + continue; + } + + if (char === "*" || char === "+" || char === "?") { + if (char === "*") { + tokens.push({ + type: "MODIFIER", + value: str[i++], + name: key++, + regex: "(.*)", + }); + } else { + tokens.push({ type: "MODIFIER", value: str[i++] }); } - } else if (isEscape === true) { - // Ignore escaped character - isEscape = false; - if (param.hasParamRegex !== true && path[i] !== "\\") { - // Escape special characters - if (path[i].match(/[^\.\^\$\[\]\{\}\|\*\+\?\(\)]/g)) { - char = `\\${char}`; + continue; + } + + if (char === "/") { + tokens.push({ type: "DELIMITER", value: str[i++] }); + continue; + } + + if (char === ":") { + let name = ""; + let j = i + 1; + + while (j < str.length) { + const code = str.charCodeAt(j); + if ( + // `0-9` + (code >= 48 && code <= 57) || + // `A-Z` + (code >= 65 && code <= 90) || + // `a-z` + (code >= 97 && code <= 122) || + // `_` + code === 95 + ) { + name += str[j++]; + continue; } + break; } - } else { - // Match params - if (path[i] === ":") { - if (param.hasParamName === true) { - param.nameEnd = i - 1; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } - param.nameStart = i; - param.hasParamName = true; - } else if (param.hasParamRegex !== true && path[i].match(/^[\/]$/i)) { - if (param.hasParamName === true) { - param.nameEnd = i - 1; - param.hasParamRegex = param.hasParamRegex === true; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } + + if (!name) throw new TypeError(`Missing parameter name at ${i}`); + + tokens.push({ + type: "PARAM", + value: name, + name, + regex: "([^\\/]+?)", + }); + i = j; + continue; + } + + if (char === "(") { + let count = 1; + let pattern = ""; + let j = i + 1; + + if (str[j] === "?") { + throw new TypeError(`Pattern cannot start with "?" at ${j}`); + } + + while (j < str.length) { + if (str[j] === "\\") { + pattern += str[j++] + str[j++]; + continue; } - param = {}; - } else if (path[i].match(/^[^A-Za-z0-9_]+$/i)) { - if (path[i] === "(") { - if (path[i + 1] === "?") { - if (path[i + 2] == ":") { - throw new TypeError( - `non-capturing groups are not allowed at ${i}` - ); - } - throw new TypeError(`pattern cannot start with "?" at ${i}`); - } - if (param.hasParamRegex === true) { - throw new TypeError(`capturing groups are not allowed at ${i}`); - } - if (param.hasParamName === true && param.nameStart !== i - 1) { - param.hasParamRegex = true; - param.regexStart = i; - param.nameEnd = i - 1; - } else { - param = {}; - param.hasParamName = false; - param.hasParamRegex = true; - param.regexStart = i; + + if (str[j] === ")") { + count--; + if (count === 0) { + j++; + break; } - } else if (param.hasParamRegex === true && path[i] === ")") { - param.regexEnd = i; - if (param.regexStart !== param.regexEnd) { - params.push(param); + } else if (str[j] === "(") { + count++; + if (str[j + 1] !== "?") { + throw new TypeError(`Capturing groups are not allowed at ${j}`); } - param = {}; - } else if (param.hasParamRegex !== true && path[i] === "*") { - param = {}; - param.hasParamName = false; - param.hasParamRegex = true; - param.regexStart = i; - param.regexEnd = i; - params.push(param); - param = {}; - } else if (param.hasParamRegex !== true) { - // Escape special characters - char = char.replace(/[\.\^\$\[\]\{\}\|\)]+/g, "\\$&"); } - if (param.hasParamRegex !== true && param.hasParamName === true) { - param.nameEnd = i - 1; - param.hasParamRegex = false; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } + pattern += str[j++]; } + + if (count) throw new TypeError(`Unbalanced pattern at ${i}`); + if (!pattern) throw new TypeError(`Missing pattern at ${i}`); + + if (tokens.length > 0 && tokens[tokens.length - 1].type === "PARAM") { + tokens[tokens.length - 1] = { + type: "PARAM_REGEX", + value: pattern, + name: tokens[tokens.length - 1].name, + regex: "(" + pattern + ")", + }; + } else { + tokens.push({ + type: "REGEX", + value: pattern, + name: key++, + regex: "(" + pattern + ")", + }); + } + i = j; + continue; } - if (i == path.length - 1) { + + let path = ""; + let j = i; + while (j < str.length) { if ( - param.hasParamRegex === true && - typeof param.regexEnd === "undefined" + str[j] === "\\" || + str[j] === "*" || + str[j] === "+" || + str[j] === "?" || + str[j] === ":" || + str[j] === "(" || + str[j] === "/" ) { - throw new TypeError(`unterminated group at ${i}`); + break; } - if (param.hasParamName === true) { - param.nameEnd = i; - param.hasParamRegex = param.hasParamRegex === true; - if (param.nameStart !== param.nameEnd) { - params.push(param); + // Escape special characters + path += str[j++]; + continue; + } + tokens.push({ type: "PATH", value: path }); + i = j; + } + + const params = []; + let pathRegex = ""; + let tokenIndex = 0; + while (tokenIndex < tokens.length) { + if (tokens[tokenIndex].type === "DELIMITER") { + if ( + tokens[tokenIndex + 1] && + (tokens[tokenIndex + 1].type === "PARAM" || + tokens[tokenIndex + 1].type === "PARAM_REGEX" || + tokens[tokenIndex + 1].type === "REGEX") + ) { + if ( + tokens[tokenIndex + 2] && + tokens[tokenIndex + 2].type === "MODIFIER" && + tokens[tokenIndex + 2].value === "?" && + (typeof tokens[tokenIndex + 3] === "undefined" || + tokens[tokenIndex + 3].type === "DELIMITER") + ) { + tokenIndex++; + continue; } } - param = {}; + pathRegex += tokens[tokenIndex++].value; + continue; } - pathRegex += char; - } - pathRegex = pathRegex.replace(/(^\/|\/$)/gm, ""); - if (pathRegex === "") { - pathRegex = "^/?$"; - } else { - pathRegex = `^/${pathRegex}/?$`; - } - let allParams = []; - let paramRegexCounter = 0; - for (let e of params) { - let regex = path.slice(e.regexStart, e.regexEnd + 1); - if (regex === "*") { - regex = "(.*)"; + + if ( + tokens[tokenIndex].type === "PARAM" || + tokens[tokenIndex].type === "PARAM_REGEX" || + tokens[tokenIndex].type === "REGEX" + ) { + let param = { + name: tokens[tokenIndex].name, + regex: tokens[tokenIndex].regex, + optional: tokens[tokenIndex + 1] + ? tokens[tokenIndex + 1].type === "MODIFIER" && + tokens[tokenIndex + 1].value === "?" + : false, + }; + + if ( + param.optional === true && + tokens[tokenIndex - 1] && + tokens[tokenIndex - 1].type === "DELIMITER" && + (typeof tokens[tokenIndex + 2] === "undefined" || + tokens[tokenIndex + 2].type === "DELIMITER") + ) { + param.regex = "(?:\\/" + param.regex + ")?"; + tokenIndex = tokenIndex + 2; + } else { + param.regex = param.regex; + tokenIndex++; + } + + params.push(param); + pathRegex += param.regex; + continue; } - let replaceStart = e.nameStart ?? e.regexStart; - let replaceEnd = e.regexEnd ?? e.nameEnd; - let replaceParam = null; - let paramDetails = { - name: path.slice(e.nameStart + 1, e.nameEnd + 1), - paramRegex: regex === "" ? "([^\\/]+?)" : regex, - param: path.slice(replaceStart, replaceEnd + 1), - optional: path[replaceEnd + 1] === "?", - }; + if ( - paramDetails.optional === true && - path[replaceStart - 1] === "/" && - (typeof path[replaceEnd + 2] === "undefined" || - path[replaceEnd + 2] === "/") + tokens[tokenIndex].type === "MODIFIER" && + tokens[tokenIndex].value === "*" ) { - replaceParam = `/${paramDetails.param}`; - paramDetails.paramRegex = `(?:\\/${paramDetails.paramRegex})`; - } else { - replaceParam = paramDetails.param; + let param = { + name: tokens[tokenIndex].name, + regex: tokens[tokenIndex].regex, + optional: false, + }; + + params.push(param); + pathRegex += param.regex; + tokenIndex++; + continue; } - if (paramDetails.name === "") { - paramDetails.name = paramRegexCounter++; + + if (tokens[tokenIndex].type === "ESCAPED_CHAR") { + pathRegex += "\\" + tokens[tokenIndex++].value; + continue; } - if (replaceParam !== null) { - pathRegex = pathRegex.replace(replaceParam, paramDetails.paramRegex); + + if (tokens[tokenIndex].type === "PATH") { + pathRegex += tokens[tokenIndex++].value.replace( + /([.+*?=^!:${}()[\]|/\\])/g, + "\\$1" + ); + continue; } - allParams.push(paramDetails); + pathRegex += tokens[tokenIndex++].value; + } + + pathRegex = pathRegex.replace(/\/$/gm, ""); + if (pathRegex === "") { + pathRegex = "^/?$"; + } else { + pathRegex = `^${pathRegex}/?$`; } let regex = - caseSensitive === true ? new RegExp(pathRegex, "i") : new RegExp(pathRegex); - - // Compile regex to path - function compile(params = {}, options = {}) { - let tmpPath = regex.source; - let validate = options?.validate ?? false; - for (let e of allParams) { - if (params.hasOwnProperty(e.name)) { - let replaceStr = e.paramRegex; - if (e.optional === true && replaceStr.slice(0, 6) === "(?:\\/(") { - tmpPath = tmpPath.replace(replaceStr, `/${params[e.name]}`); - } else { - tmpPath = tmpPath.replace(replaceStr, params[e.name]); + caseSensitive === false + ? new RegExp(pathRegex, "i") + : new RegExp(pathRegex); + + return { + path: str, + params, + regex: regex, + compile(params = {}, options = { validate: false }) { + const validate = options?.validate ?? false; + let compiledPath = ""; + let i = 0; + while (i < tokens.length) { + if (tokens[i].type === "DELIMITER") { + compiledPath += tokens[i++].value; + continue; } - } else { - if (e.optional === true) { - let replaceStr = e.paramRegex; - tmpPath = tmpPath.replace(replaceStr, ""); - } else { - throw new TypeError( - "invalid route parameters, please provide all route parameters" - ); + if (tokens[i].type === "PATH") { + compiledPath += tokens[i++].value; + continue; } - } - } - - let isEscape = false; - let compiledPath = ""; - for (let i = 0; i < tmpPath.length; i++) { - let char = tmpPath[i]; - // Match escape character - if (tmpPath[i] === "\\" && isEscape === false) { - isEscape = true; - continue; - } else if (isEscape === true) { - // Ignore escaped character - isEscape = false; - } else { - // Match params - if (tmpPath[i] === "+") { - char = ""; - } else if (tmpPath[i] === "?") { - char = ""; + if ( + tokens[i].type === "PARAM" || + tokens[i].type === "PARAM_REGEX" || + tokens[i].type === "REGEX" || + (tokens[i].type === "MODIFIER" && tokens[i].value === "*") + ) { + if (params.hasOwnProperty(tokens[i].name)) { + compiledPath += params[tokens[i++].name]; + continue; + } else { + if ( + tokens[i + 1] && + tokens[i + 1].type === "MODIFIER" && + tokens[i + 1].value === "?" + ) { + if (tokens[i + 2] && tokens[i + 2].type === "DELIMITER") { + i = i + 3; + continue; + } + } else { + throw new TypeError( + "invalid route parameters, please provide all route parameters" + ); + } + } } + i++; } - compiledPath += char; - } - compiledPath = compiledPath.replace(/(^\^|\/\$$)/gm, ""); - if (validate === true) { - if (regex.test(compiledPath)) { - return compiledPath; - } else { - throw new TypeError( - "invalid route parameters, please provide all route parameters" - ); - } - } else { return compiledPath; - } - } - - return { - path: path, - params: allParams, - regex, - compile, + }, }; }; diff --git a/src/path-regex.mjs b/src/path-regex.mjs index a7af013..093048a 100644 --- a/src/path-regex.mjs +++ b/src/path-regex.mjs @@ -1,231 +1,292 @@ -export default function pathRegex(path, options = { caseSensitive: false }) { - let pathRegex = ""; - let params = []; - let param = {}; - let isEscape = false; - let caseSensitive = options?.caseSensitive ?? false; - for (let i = 0; i < path.length; i++) { - let char = path[i]; - // Match escape character - if (path[i] === "\\" && isEscape === false) { - isEscape = true; - if (param.hasParamName === true && param.hasParamRegex !== true) { - param.nameEnd = i - 1; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } - if (i == path.length - 1) { - char += `\\${char}`; +export default function pathRegex(str, options = { caseSensitive: false }) { + const caseSensitive = options?.caseSensitive ?? false; + const tokens = []; + let i = 0; + let key = 0; + while (i < str.length) { + let char = str[i]; + + if (char === "\\") { + tokens.push({ type: "ESCAPED_CHAR", value: str[i + 1] ?? "\\" }); + i = i + 2; + continue; + } + + if (char === "*" || char === "+" || char === "?") { + if (char === "*") { + tokens.push({ + type: "MODIFIER", + value: str[i++], + name: key++, + regex: "(.*)", + }); + } else { + tokens.push({ type: "MODIFIER", value: str[i++] }); } - } else if (isEscape === true) { - // Ignore escaped character - isEscape = false; - if (param.hasParamRegex !== true && path[i] !== "\\") { - // Escape special characters - if (path[i].match(/[^\.\^\$\[\]\{\}\|\*\+\?\(\)]/g)) { - char = `\\${char}`; + continue; + } + + if (char === "/") { + tokens.push({ type: "DELIMITER", value: str[i++] }); + continue; + } + + if (char === ":") { + let name = ""; + let j = i + 1; + + while (j < str.length) { + const code = str.charCodeAt(j); + if ( + // `0-9` + (code >= 48 && code <= 57) || + // `A-Z` + (code >= 65 && code <= 90) || + // `a-z` + (code >= 97 && code <= 122) || + // `_` + code === 95 + ) { + name += str[j++]; + continue; } + break; } - } else { - // Match params - if (path[i] === ":") { - if (param.hasParamName === true) { - param.nameEnd = i - 1; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } - param.nameStart = i; - param.hasParamName = true; - } else if (param.hasParamRegex !== true && path[i].match(/^[\/]$/i)) { - if (param.hasParamName === true) { - param.nameEnd = i - 1; - param.hasParamRegex = param.hasParamRegex === true; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } + + if (!name) throw new TypeError(`Missing parameter name at ${i}`); + + tokens.push({ + type: "PARAM", + value: name, + name, + regex: "([^\\/]+?)", + }); + i = j; + continue; + } + + if (char === "(") { + let count = 1; + let pattern = ""; + let j = i + 1; + + if (str[j] === "?") { + throw new TypeError(`Pattern cannot start with "?" at ${j}`); + } + + while (j < str.length) { + if (str[j] === "\\") { + pattern += str[j++] + str[j++]; + continue; } - param = {}; - } else if (path[i].match(/^[^A-Za-z0-9_]+$/i)) { - if (path[i] === "(") { - if (path[i + 1] === "?") { - if (path[i + 2] == ":") { - throw new TypeError( - `non-capturing groups are not allowed at ${i}` - ); - } - throw new TypeError(`pattern cannot start with "?" at ${i}`); - } - if (param.hasParamRegex === true) { - throw new TypeError(`capturing groups are not allowed at ${i}`); - } - if (param.hasParamName === true && param.nameStart !== i - 1) { - param.hasParamRegex = true; - param.regexStart = i; - param.nameEnd = i - 1; - } else { - param = {}; - param.hasParamName = false; - param.hasParamRegex = true; - param.regexStart = i; + + if (str[j] === ")") { + count--; + if (count === 0) { + j++; + break; } - } else if (param.hasParamRegex === true && path[i] === ")") { - param.regexEnd = i; - if (param.regexStart !== param.regexEnd) { - params.push(param); + } else if (str[j] === "(") { + count++; + if (str[j + 1] !== "?") { + throw new TypeError(`Capturing groups are not allowed at ${j}`); } - param = {}; - } else if (param.hasParamRegex !== true && path[i] === "*") { - param = {}; - param.hasParamName = false; - param.hasParamRegex = true; - param.regexStart = i; - param.regexEnd = i; - params.push(param); - param = {}; - } else if (param.hasParamRegex !== true) { - // Escape special characters - char = char.replace(/[\.\^\$\[\]\{\}\|\)]+/g, "\\$&"); } - if (param.hasParamRegex !== true && param.hasParamName === true) { - param.nameEnd = i - 1; - param.hasParamRegex = false; - if (param.nameStart !== param.nameEnd) { - params.push(param); - } - param = {}; - } + pattern += str[j++]; } + + if (count) throw new TypeError(`Unbalanced pattern at ${i}`); + if (!pattern) throw new TypeError(`Missing pattern at ${i}`); + + if (tokens.length > 0 && tokens[tokens.length - 1].type === "PARAM") { + tokens[tokens.length - 1] = { + type: "PARAM_REGEX", + value: pattern, + name: tokens[tokens.length - 1].name, + regex: "(" + pattern + ")", + }; + } else { + tokens.push({ + type: "REGEX", + value: pattern, + name: key++, + regex: "(" + pattern + ")", + }); + } + i = j; + continue; } - if (i == path.length - 1) { + + let path = ""; + let j = i; + while (j < str.length) { if ( - param.hasParamRegex === true && - typeof param.regexEnd === "undefined" + str[j] === "\\" || + str[j] === "*" || + str[j] === "+" || + str[j] === "?" || + str[j] === ":" || + str[j] === "(" || + str[j] === "/" ) { - throw new TypeError(`unterminated group at ${i}`); + break; } - if (param.hasParamName === true) { - param.nameEnd = i; - param.hasParamRegex = param.hasParamRegex === true; - if (param.nameStart !== param.nameEnd) { - params.push(param); + // Escape special characters + path += str[j++]; + continue; + } + tokens.push({ type: "PATH", value: path }); + i = j; + } + + const params = []; + let pathRegex = ""; + let tokenIndex = 0; + while (tokenIndex < tokens.length) { + if (tokens[tokenIndex].type === "DELIMITER") { + if ( + tokens[tokenIndex + 1] && + (tokens[tokenIndex + 1].type === "PARAM" || + tokens[tokenIndex + 1].type === "PARAM_REGEX" || + tokens[tokenIndex + 1].type === "REGEX") + ) { + if ( + tokens[tokenIndex + 2] && + tokens[tokenIndex + 2].type === "MODIFIER" && + tokens[tokenIndex + 2].value === "?" && + (typeof tokens[tokenIndex + 3] === "undefined" || + tokens[tokenIndex + 3].type === "DELIMITER") + ) { + tokenIndex++; + continue; } } - param = {}; + pathRegex += tokens[tokenIndex++].value; + continue; } - pathRegex += char; - } - pathRegex = pathRegex.replace(/(^\/|\/$)/gm, ""); - if (pathRegex === "") { - pathRegex = "^/?$"; - } else { - pathRegex = `^/${pathRegex}/?$`; - } - let allParams = []; - let paramRegexCounter = 0; - for (let e of params) { - let regex = path.slice(e.regexStart, e.regexEnd + 1); - if (regex === "*") { - regex = "(.*)"; + + if ( + tokens[tokenIndex].type === "PARAM" || + tokens[tokenIndex].type === "PARAM_REGEX" || + tokens[tokenIndex].type === "REGEX" + ) { + let param = { + name: tokens[tokenIndex].name, + regex: tokens[tokenIndex].regex, + optional: tokens[tokenIndex + 1] + ? tokens[tokenIndex + 1].type === "MODIFIER" && + tokens[tokenIndex + 1].value === "?" + : false, + }; + + if ( + param.optional === true && + tokens[tokenIndex - 1] && + tokens[tokenIndex - 1].type === "DELIMITER" && + (typeof tokens[tokenIndex + 2] === "undefined" || + tokens[tokenIndex + 2].type === "DELIMITER") + ) { + param.regex = "(?:\\/" + param.regex + ")?"; + tokenIndex = tokenIndex + 2; + } else { + param.regex = param.regex; + tokenIndex++; + } + + params.push(param); + pathRegex += param.regex; + continue; } - let replaceStart = e.nameStart ?? e.regexStart; - let replaceEnd = e.regexEnd ?? e.nameEnd; - let replaceParam = null; - let paramDetails = { - name: path.slice(e.nameStart + 1, e.nameEnd + 1), - paramRegex: regex === "" ? "([^\\/]+?)" : regex, - param: path.slice(replaceStart, replaceEnd + 1), - optional: path[replaceEnd + 1] === "?", - }; + if ( - paramDetails.optional === true && - path[replaceStart - 1] === "/" && - (typeof path[replaceEnd + 2] === "undefined" || - path[replaceEnd + 2] === "/") + tokens[tokenIndex].type === "MODIFIER" && + tokens[tokenIndex].value === "*" ) { - replaceParam = `/${paramDetails.param}`; - paramDetails.paramRegex = `(?:\\/${paramDetails.paramRegex})`; - } else { - replaceParam = paramDetails.param; + let param = { + name: tokens[tokenIndex].name, + regex: tokens[tokenIndex].regex, + optional: false, + }; + + params.push(param); + pathRegex += param.regex; + tokenIndex++; + continue; } - if (paramDetails.name === "") { - paramDetails.name = paramRegexCounter++; + + if (tokens[tokenIndex].type === "ESCAPED_CHAR") { + pathRegex += "\\" + tokens[tokenIndex++].value; + continue; } - if (replaceParam !== null) { - pathRegex = pathRegex.replace(replaceParam, paramDetails.paramRegex); + + if (tokens[tokenIndex].type === "PATH") { + pathRegex += tokens[tokenIndex++].value.replace( + /([.+*?=^!:${}()[\]|/\\])/g, + "\\$1" + ); + continue; } - allParams.push(paramDetails); + pathRegex += tokens[tokenIndex++].value; + } + + pathRegex = pathRegex.replace(/\/$/gm, ""); + if (pathRegex === "") { + pathRegex = "^/?$"; + } else { + pathRegex = `^${pathRegex}/?$`; } let regex = - caseSensitive === true ? new RegExp(pathRegex, "i") : new RegExp(pathRegex); - - // Compile regex to path - function compile(params = {}, options = {}) { - let tmpPath = regex.source; - let validate = options?.validate ?? false; - for (let e of allParams) { - if (params.hasOwnProperty(e.name)) { - let replaceStr = e.paramRegex; - if (e.optional === true && replaceStr.slice(0, 6) === "(?:\\/(") { - tmpPath = tmpPath.replace(replaceStr, `/${params[e.name]}`); - } else { - tmpPath = tmpPath.replace(replaceStr, params[e.name]); + caseSensitive === false + ? new RegExp(pathRegex, "i") + : new RegExp(pathRegex); + + return { + path: str, + params, + regex: regex, + compile(params = {}, options = { validate: false }) { + const validate = options?.validate ?? false; + let compiledPath = ""; + let i = 0; + while (i < tokens.length) { + if (tokens[i].type === "DELIMITER") { + compiledPath += tokens[i++].value; + continue; } - } else { - if (e.optional === true) { - let replaceStr = e.paramRegex; - tmpPath = tmpPath.replace(replaceStr, ""); - } else { - throw new TypeError( - "invalid route parameters, please provide all route parameters" - ); + if (tokens[i].type === "PATH") { + compiledPath += tokens[i++].value; + continue; } - } - } - - let isEscape = false; - let compiledPath = ""; - for (let i = 0; i < tmpPath.length; i++) { - let char = tmpPath[i]; - // Match escape character - if (tmpPath[i] === "\\" && isEscape === false) { - isEscape = true; - continue; - } else if (isEscape === true) { - // Ignore escaped character - isEscape = false; - } else { - // Match params - if (tmpPath[i] === "+") { - char = ""; - } else if (tmpPath[i] === "?") { - char = ""; + if ( + tokens[i].type === "PARAM" || + tokens[i].type === "PARAM_REGEX" || + tokens[i].type === "REGEX" || + (tokens[i].type === "MODIFIER" && tokens[i].value === "*") + ) { + if (params.hasOwnProperty(tokens[i].name)) { + compiledPath += params[tokens[i++].name]; + continue; + } else { + if ( + tokens[i + 1] && + tokens[i + 1].type === "MODIFIER" && + tokens[i + 1].value === "?" + ) { + if (tokens[i + 2] && tokens[i + 2].type === "DELIMITER") { + i = i + 3; + continue; + } + } else { + throw new TypeError( + "invalid route parameters, please provide all route parameters" + ); + } + } } + i++; } - compiledPath += char; - } - compiledPath = compiledPath.replace(/(^\^|\/\$$)/gm, ""); - if (validate === true) { - if (regex.test(compiledPath)) { - return compiledPath; - } else { - throw new TypeError( - "invalid route parameters, please provide all route parameters" - ); - } - } else { return compiledPath; - } - } - - return { - path: path, - params: allParams, - regex, - compile, + }, }; } diff --git a/src/route.cjs b/src/route.cjs index 566df5d..3fe58d7 100644 --- a/src/route.cjs +++ b/src/route.cjs @@ -18,6 +18,7 @@ module.exports = class Route { #regexCache = null; constructor({ host, method, path, name, group, callbacks, caseSensitive }) { + this.caseSensitive = caseSensitive ?? false; if (host && !(typeof host === "string" || host instanceof String)) { throw new TypeError("route host accepts only string as an argument"); } @@ -58,7 +59,6 @@ module.exports = class Route { ? this.#compileMiddlewareRegExp(group) : this.#compileMiddlewareRegExp("/"); - this.caseSensitive = caseSensitive ?? false; this.host = host; this.hostRegexp = hostRegexp?.regexp; this.method = method; diff --git a/src/route.mjs b/src/route.mjs index fbb3a1f..5c8eef6 100644 --- a/src/route.mjs +++ b/src/route.mjs @@ -18,6 +18,7 @@ export default class Route { #regexCache = null; constructor({ host, method, path, name, group, callbacks, caseSensitive }) { + this.caseSensitive = caseSensitive ?? false; if (host && !(typeof host === "string" || host instanceof String)) { throw new TypeError("route host accepts only string as an argument"); } @@ -58,7 +59,6 @@ export default class Route { ? this.#compileMiddlewareRegExp(group) : this.#compileMiddlewareRegExp("/"); - this.caseSensitive = caseSensitive ?? false; this.host = host; this.hostRegexp = hostRegexp?.regexp; this.method = method; diff --git a/src/router.cjs b/src/router.cjs index 2b0d44a..d13ef9c 100644 --- a/src/router.cjs +++ b/src/router.cjs @@ -379,29 +379,29 @@ module.exports = class Router { } handler() { - function requestHandler(request, response) { - var requestHost = request.headers ? request.headers.host : null; - var requestMethod = request.method; - var requestUrl = request.url; + const that = this; + return function requestHandler(request, response) { + let requestHost = request.headers ? request.headers.host : null; + let requestMethod = request.method; + let requestUrl = request.url; - if (!requestHost && "getHeader" in request) { + if (!requestHost && typeof request.getHeader === "function") { requestHost = request.getHeader("host"); } - if (!requestMethod && "getMethod" in request) { + if (!requestMethod && typeof request.getMethod === "function") { requestMethod = request.getMethod(); } - if (!requestUrl && "getUrl" in request) { + if (!requestUrl && typeof request.getUrl === "function") { requestUrl = request.getUrl(); } - this.handle({ + that.handle({ requestHost, requestMethod, requestUrl, request, response, }); - } - return requestHandler.bind(this); + }; } }; diff --git a/src/router.mjs b/src/router.mjs index 3cc235f..5488886 100644 --- a/src/router.mjs +++ b/src/router.mjs @@ -379,29 +379,29 @@ export default class Router { } handler() { - function requestHandler(request, response) { - var requestHost = request.headers ? request.headers.host : null; - var requestMethod = request.method; - var requestUrl = request.url; + const that = this; + return function requestHandler(request, response) { + let requestHost = request.headers ? request.headers.host : null; + let requestMethod = request.method; + let requestUrl = request.url; - if (!requestHost && "getHeader" in request) { + if (!requestHost && typeof request.getHeader === "function") { requestHost = request.getHeader("host"); } - if (!requestMethod && "getMethod" in request) { + if (!requestMethod && typeof request.getMethod === "function") { requestMethod = request.getMethod(); } - if (!requestUrl && "getUrl" in request) { + if (!requestUrl && typeof request.getUrl === "function") { requestUrl = request.getUrl(); } - this.handle({ + that.handle({ requestHost, requestMethod, requestUrl, request, response, }); - } - return requestHandler.bind(this); + }; } } diff --git a/tests/params.test.js b/tests/params.test.js index ce72258..4e1b4b7 100644 --- a/tests/params.test.js +++ b/tests/params.test.js @@ -1,8 +1,8 @@ const request = require("supertest"); const { Router } = require("../index.cjs"); -describe("Route params test", () => { - describe("It should match route params", () => { +describe("Route params test", function () { + describe("It should match route params", function () { test("GET /:name", async () => { const app = new Router(); app.get("/:name", function (req, res) { @@ -45,7 +45,7 @@ describe("Route params test", () => { await request(app.handler()) .get("/string/decode%20and%20capture") - .expect(200, JSON.stringify({ 0: "string/decode and capture" })); + .expect(200, JSON.stringify({ 0: "/string/decode and capture" })); }); test("It should decode correct params", async () => { @@ -55,7 +55,7 @@ describe("Route params test", () => { res.end(String(req.params.name)); }); - await request(app.handler()).get("/foo%2Fbar").expect("foo/bar"); + await request(app.handler()).get("/foo%2Fbar").expect(200, "foo/bar"); }); test("It should not accept params in malformed paths", async () => { @@ -79,7 +79,7 @@ describe("Route params test", () => { res.end(String(req.params.name)); }); - await request(app.handler()).get("/foo+bar").expect("foo+bar"); + await request(app.handler()).get("/foo+bar").expect(200, "foo+bar"); }); test("It should work with unicode", async () => { @@ -89,11 +89,11 @@ describe("Route params test", () => { res.end(String(req.params.name)); }); - await request(app.handler()).get("/%ce%b1").expect("\u03b1"); + await request(app.handler()).get("/%ce%b1").expect(200, "\u03b1"); }); }); - describe("It should match optional params", () => { + describe("It should match optional params", function () { test("GET /:name?/:ext", async () => { const app = new Router(); app.get("/:name?/:ext", function (req, res) { @@ -334,12 +334,7 @@ describe("Route params test", () => { res.writeHead(404).end("Page not found"); }); - await request(app.handler()) - .get("/abc/xyz/testing") - .expect(200) - .then((res) => { - expect(res.text).toBe("abc"); - }); + await request(app.handler()).get("/abc/xyz/testing").expect(200, "abc"); }); test("GET /blog/*", async () => { @@ -378,12 +373,7 @@ describe("Route params test", () => { res.writeHead(404).end("Page not found"); }); - await request(app.handler()) - .get("/blog/12345") - .expect(200) - .then((res) => { - expect(res.text).toBe("12345"); - }); + await request(app.handler()).get("/blog/12345").expect(200, "12345"); }); test("GET /blog/:id(\\d+)", async () => { @@ -422,12 +412,7 @@ describe("Route params test", () => { res.writeHead(404).end("Page not found"); }); - await request(app.handler()) - .get("/blog/12345") - .expect(200) - .then((res) => { - expect(res.text).toBe("12345"); - }); + await request(app.handler()).get("/blog/12345").expect(200, "12345"); }); test("GET /blog/(\\d+)", async () => { @@ -466,12 +451,7 @@ describe("Route params test", () => { res.writeHead(404).end("Page not found"); }); - await request(app.handler()) - .get("/image/cat.png") - .expect(200) - .then((res) => { - expect(res.text).toBe("cat.png"); - }); + await request(app.handler()).get("/image/cat.png").expect(200, "cat.png"); }); test("Param count /image/:name.:ext/(\\d+)/:id(\\d+)", async () => { @@ -480,22 +460,16 @@ describe("Route params test", () => { res.end(`${Object.keys(req.params).length}`); }); - await request(app.handler()) - .get("/image/cat.png/0/1") - .expect(200) - .then((res) => { - expect(res.text).toBe("4"); - }); + await request(app.handler()).get("/image/cat.png/0/1").expect(200, "4"); }); - test("GET /\\(\\)[].:;\\+\\*", async () => { + test("GET /\\(\\)[].\\:;\\+\\*", async () => { const app = new Router(); - app.get("/\\(\\)[].:;\\+\\*", function (req, res) { + app.get("/\\(\\)[].\\:;\\+\\*", function (req, res) { res.end(req.params[0]); }); app.use(function (req, res) { - console.log(req.url); res.writeHead(404).end("Page not found"); }); @@ -537,4 +511,206 @@ describe("Route params test", () => { }); }).toThrow(TypeError); }); + + test("should work with several", async () => { + const app = new Router(); + + app.get("/api/*.*", function (req, res) { + const resource = req.params[0], + format = req.params[1]; + res.end(resource + " as " + format); + }); + + await request(app.handler()) + .get("/api/users/foo.bar.json") + .expect(200, "users/foo.bar as json"); + }); + + test("should work cross-segment", async () => { + const app = new Router(); + + app.get("/api*", function (req, res) { + res.end(req.params[0]); + }); + + await request(app.handler()).get("/api").expect(200, ""); + await request(app.handler()).get("/api/hey").expect(200, "/hey"); + }); + + test("should allow naming", async () => { + const app = new Router(); + + app.get("/api/:resource(.*)", function (req, res) { + const resource = req.params.resource; + res.end(resource); + }); + + await request(app.handler()) + .get("/api/users/0.json") + .expect(200, "users/0.json"); + }); + + test("should not be greedy immediately after param", async () => { + const app = new Router(); + + app.get("/user/:user*", function (req, res) { + res.end(req.params.user); + }); + + await request(app.handler()).get("/user/122").expect(200, "1"); + }); + + test("should eat everything after /", async () => { + const app = new Router(); + + app.get("/user/:user*", function (req, res) { + res.end(req.params.user); + }); + + await request(app.handler()).get("/user/122/aaa").expect(200, "1"); + }); + + test("should span multiple segments", async () => { + const app = new Router(); + + app.get("/file/*", function (req, res) { + res.end(req.params[0]); + }); + + await request(app.handler()) + .get("/file/javascripts/jquery.js") + .expect(200, "javascripts/jquery.js"); + }); + + test("should be optional", async () => { + const app = new Router(); + + app.get("/file/*", function (req, res) { + res.end(req.params[0]); + }); + + await request(app.handler()).get("/file/").expect(200, ""); + }); + + test("should require a preceding /", async () => { + const app = new Router(); + + app.get("/file/*", function (req, res) { + res.end(req.params[0]); + }); + + app.use(function (req, res) { + res.writeHead(404).end("Page Not Found"); + }); + + await request(app.handler()).get("/file").expect(404); + }); + + test("should keep correct parameter indexes", async () => { + const app = new Router(); + + app.get("/*/user/:id", function (req, res) { + res.end(JSON.stringify(req.params)); + }); + + await request(app.handler()) + .get("/1/user/2") + .expect(200, '{"0":"1","id":"2"}'); + }); + + describe(":name", function () { + test("should denote a capture group", async () => { + const app = new Router(); + + app.get("/user/:user", function (req, res) { + res.end(req.params.user); + }); + + await request(app.handler()).get("/user/abc").expect(200, "abc"); + }); + + test("should match a single segment only", async () => { + const app = new Router(); + + app.get("/user/:user", function (req, res) { + res.end(req.params.user); + }); + + app.use(function (req, res) { + res.writeHead(404).end("Page Not Found"); + }); + + await request(app.handler()).get("/user/abc/edit").expect(404); + }); + + test("should allow several capture groups", async () => { + const app = new Router(); + + app.get("/user/:user/:op", function (req, res) { + res.end(req.params.op + "ing " + req.params.user); + }); + + await request(app.handler()) + .get("/user/abc/edit") + .expect(200, "editing abc"); + }); + + test("should work following a partial capture group", async () => { + const app = new Router(); + + app.get("/user(s)?/:user/:op", function (req, res) { + res.end( + req.params.op + + "ing " + + req.params.user + + (req.params[0] ? " (old)" : "") + ); + }); + + await request(app.handler()) + .get("/user/abc/edit") + .expect(200, "editing abc"); + + await request(app.handler()) + .get("/users/abc/edit") + .expect(200, "editing abc (old)"); + }); + + test("should work inside literal parenthesis", async () => { + const app = new Router(); + + app.get("/:user\\(:op\\)", function (req, res) { + res.end(req.params.op + "ing " + req.params.user); + }); + + await request(app.handler()).get("/abc(edit)").expect(200, "editing abc"); + }); + }); + + describe("case sensitivity", function () { + test("should be disabled by default", async () => { + const app = new Router(); + + app.get("/user", function (req, res) { + res.end("Ok"); + }); + + await request(app.handler()).get("/USER").expect(200, "Ok"); + }); + + test("should match identical casing", async () => { + const app = new Router({ caseSensitive: true }); + + app.get("/uSer", function (req, res) { + res.end("Ok"); + }); + + app.use(function (req, res) { + res.writeHead(404).end("Page Not Found"); + }); + + await request(app.handler()).get("/uSer").expect(200, "Ok"); + await request(app.handler()).get("/user").expect(404); + }); + }); }); diff --git a/tests/routing.test.js b/tests/routing.test.js index 72854ad..1835406 100644 --- a/tests/routing.test.js +++ b/tests/routing.test.js @@ -2,12 +2,29 @@ const request = require("supertest"); const { Router } = require("../index.cjs"); describe("Routing test", () => { - test("GET /", async () => { + test("routing should match routes", async () => { const app = new Router(); + const blog = new Router(); + const group = new Router(); + + blog.get("/", function (req, res) { + res.end("blog"); + }); + + blog.get("/:slug", function (req, res) { + res.end(req.params.slug); + }); + + group.get("/", function (req, res) { + res.end("group"); + }); + app .use(function (req, res, next) { next(); }) + .use("/blog", blog) + .use("/group", group) .get("/", function (req, res) { res.end("GET"); }) @@ -17,12 +34,12 @@ describe("Routing test", () => { .use(function (req, res) { res.writeHead(404).end("Page not found"); }); - await request(app.handler()) - .get("/") - .expect(200) - .then((res) => { - expect(res.text).toBe("GET"); - }); + + await request(app.handler()).get("/").expect(200, "GET"); + await request(app.handler()).get("/blog").expect(200, "blog"); + await request(app.handler()).get("/group").expect(200, "group"); + await request(app.handler()).get("/blog/abc").expect(200, "abc"); + await request(app.handler()).get("/group/abc").expect(404); }); test("POST /", async () => { @@ -34,12 +51,8 @@ describe("Routing test", () => { .post("/", function (req, res) { res.end("POST"); }); - await request(app.handler()) - .post("/") - .expect(200) - .then((res) => { - expect(res.text).toBe("POST"); - }); + + await request(app.handler()).post("/").expect(200, "POST"); }); test("PUT /", async () => { @@ -51,12 +64,8 @@ describe("Routing test", () => { .put("/", function (req, res) { res.end("PUT"); }); - await request(app.handler()) - .put("/") - .expect(200) - .then((res) => { - expect(res.text).toBe("PUT"); - }); + + await request(app.handler()).put("/").expect(200, "PUT"); }); test("DELETE /", async () => { @@ -68,12 +77,8 @@ describe("Routing test", () => { .delete("/", function (req, res) { res.end("DELETE"); }); - await request(app.handler()) - .delete("/") - .expect(200) - .then((res) => { - expect(res.text).toBe("DELETE"); - }); + + await request(app.handler()).delete("/").expect(200, "DELETE"); }); test("Not found /", async () => { @@ -100,12 +105,8 @@ describe("Routing test", () => { .get("/:name", function (req, res) { res.end(`${req.params.name}`); }); - await request(app.handler()) - .get("/abc") - .expect(200) - .then((res) => { - expect(res.text).toBe("abc"); - }); + + await request(app.handler()).get("/abc").expect(200, "abc"); }); test("Route name", async () => { @@ -115,12 +116,8 @@ describe("Routing test", () => { res.end(app.route("name")); }) .setName("name"); - await request(app.handler()) - .get("/test") - .expect(200) - .then((res) => { - expect(res.text).toBe("/test"); - }); + + await request(app.handler()).get("/test").expect(200, "/test"); }); test("Route name", async () => { @@ -130,12 +127,8 @@ describe("Routing test", () => { res.end(app.route("name", { name: "abc" })); }) .setName("name"); - await request(app.handler()) - .get("/test") - .expect(200) - .then((res) => { - expect(res.text).toBe("/abc"); - }); + + await request(app.handler()).get("/test").expect(200, "/abc"); }); test("Route name", async () => { @@ -145,12 +138,8 @@ describe("Routing test", () => { res.end(app.route("name", { name: "image" })); }) .setName("name"); - await request(app.handler()) - .get("/image.") - .expect(200) - .then((res) => { - expect(res.text).toBe("/image."); - }); + + await request(app.handler()).get("/image.").expect(200, "/image."); }); test("Route name", async () => { @@ -184,6 +173,7 @@ describe("Routing test", () => { .get("/image/png/file") .expect(200, "/image/png/file"); }); + test("Route name", async () => { const app = new Router(); app @@ -213,12 +203,10 @@ describe("Routing test", () => { ); }) .setName("name"); + await request(app.handler()) .get("/abc/user/123/10/test/abc") - .expect(200) - .then((res) => { - expect(res.text).toBe("/abc/user/123/10/test/abc"); - }); + .expect(200, "/abc/user/123/10/test/abc"); }); test("Route name throw error", async () => { @@ -233,12 +221,11 @@ describe("Routing test", () => { }); await request(app.handler()) .get("/abc/user/123/10/test/abc") - .expect(500) - .then((res) => { - expect(res.text).toBe( - "invalid route parameters, please provide all route parameters" - ); - }); + + .expect( + 500, + "invalid route parameters, please provide all route parameters" + ); }); test("GET /any", async () => { @@ -246,12 +233,8 @@ describe("Routing test", () => { app.any(["get", "post"], "/any", function (req, res) { res.end("any"); }); - await request(app.handler()) - .get("/any") - .expect(200) - .then((res) => { - expect(res.text).toBe("any"); - }); + + await request(app.handler()).get("/any").expect(200, "any"); }); test("GET /all", async () => { @@ -259,11 +242,7 @@ describe("Routing test", () => { app.all("/all", function (req, res) { res.end("all"); }); - await request(app.handler()) - .post("/all") - .expect(200) - .then((res) => { - expect(res.text).toBe("all"); - }); + + await request(app.handler()).post("/all").expect(200, "all"); }); }); diff --git a/tests/url.test.js b/tests/url.test.js index 4fbdbf3..2a69b12 100644 --- a/tests/url.test.js +++ b/tests/url.test.js @@ -19,12 +19,8 @@ describe("Routing test", () => { }), ]; app.use(urls); - await request(app.handler()) - .get("/") - .expect(200) - .then((res) => { - expect(res.text).toBe("GET"); - }); + + await request(app.handler()).get("/").expect(200, "GET"); }); test("POST /", async () => { @@ -38,12 +34,8 @@ describe("Routing test", () => { }), ]; app.use(urls); - await request(app.handler()) - .post("/") - .expect(200) - .then((res) => { - expect(res.text).toBe("POST"); - }); + + await request(app.handler()).post("/").expect(200, "POST"); }); test("PUT /", async () => { @@ -57,12 +49,8 @@ describe("Routing test", () => { }), ]; app.use(urls); - await request(app.handler()) - .put("/") - .expect(200) - .then((res) => { - expect(res.text).toBe("PUT"); - }); + + await request(app.handler()).put("/").expect(200, "PUT"); }); test("DELETE /", async () => { @@ -76,12 +64,8 @@ describe("Routing test", () => { }), ]; app.use(urls); - await request(app.handler()) - .delete("/") - .expect(200) - .then((res) => { - expect(res.text).toBe("DELETE"); - }); + + await request(app.handler()).delete("/").expect(200, "DELETE"); }); test("Not found /", async () => { @@ -98,6 +82,7 @@ describe("Routing test", () => { }), ]; app.use(urls); + await request(app.handler()).delete("/").expect(404); }); @@ -112,12 +97,8 @@ describe("Routing test", () => { }), ]; app.use(urls); - await request(app.handler()) - .get("/abc") - .expect(200) - .then((res) => { - expect(res.text).toBe("abc"); - }); + + await request(app.handler()).get("/abc").expect(200, "abc"); }); test("Route name", async () => { @@ -128,12 +109,8 @@ describe("Routing test", () => { }).setName("name"), ]; app.use(urls); - await request(app.handler()) - .get("/test") - .expect(200) - .then((res) => { - expect(res.text).toBe("/test"); - }); + + await request(app.handler()).get("/test").expect(200, "/test"); }); test("Route name", async () => { @@ -144,12 +121,8 @@ describe("Routing test", () => { }).setName("name"), ]; app.use(urls); - await request(app.handler()) - .get("/test") - .expect(200) - .then((res) => { - expect(res.text).toBe("/abc"); - }); + + await request(app.handler()).get("/test").expect(200, "/abc"); }); test("Route name", async () => { @@ -162,12 +135,10 @@ describe("Routing test", () => { }).setName("name"), ]; app.use(urls); + await request(app.handler()) .get("/abc/user/123/10/test/abc") - .expect(200) - .then((res) => { - expect(res.text).toBe("/abc/user/123/10/test/abc"); - }); + .expect(200, "/abc/user/123/10/test/abc"); }); test("Route name throw error", async () => { @@ -181,14 +152,13 @@ describe("Routing test", () => { }), ]; app.use(urls); + await request(app.handler()) .get("/abc/user/123/10/test/abc") - .expect(500) - .then((res) => { - expect(res.text).toBe( - "invalid route parameters, please provide all route parameters" - ); - }); + .expect( + 500, + "invalid route parameters, please provide all route parameters" + ); }); test("GET /any", async () => { @@ -199,12 +169,8 @@ describe("Routing test", () => { }), ]; app.use(urls); - await request(app.handler()) - .get("/any") - .expect(200) - .then((res) => { - expect(res.text).toBe("any"); - }); + + await request(app.handler()).get("/any").expect(200, "any"); }); test("GET /all", async () => { @@ -215,11 +181,7 @@ describe("Routing test", () => { }), ]; app.use(urls); - await request(app.handler()) - .post("/all") - .expect(200) - .then((res) => { - expect(res.text).toBe("all"); - }); + + await request(app.handler()).post("/all").expect(200, "all"); }); });