2023-04-27 09:32:31 +02:00
|
|
|
import type { Router, Result } from '../../router.ts'
|
|
|
|
import { METHOD_NAME_ALL, UnsupportedPathError } from '../../router.ts'
|
|
|
|
|
2023-05-17 17:05:28 +02:00
|
|
|
type RegExpMatchArrayWithIndices = RegExpMatchArray & { indices: [number, number][] }
|
2023-04-27 09:32:31 +02:00
|
|
|
|
|
|
|
const splitPathRe = /\/(:\w+(?:{[^}]+})?)|\/[^\/\?]+|(\?)/g
|
|
|
|
const splitByStarRe = /\*/
|
|
|
|
export class LinearRouter<T> implements Router<T> {
|
2023-05-17 17:05:28 +02:00
|
|
|
name: string = 'LinearRouter'
|
2023-04-27 09:32:31 +02:00
|
|
|
routes: [string, string, T][] = []
|
|
|
|
|
|
|
|
add(method: string, path: string, handler: T) {
|
|
|
|
if (path.charCodeAt(path.length - 1) === 63) {
|
|
|
|
// /path/to/:label? means /path/to/:label or /path/to
|
|
|
|
this.routes.push([method, path.slice(0, -1), handler])
|
|
|
|
this.routes.push([method, path.replace(/\/[^/]+$/, ''), handler])
|
|
|
|
} else {
|
|
|
|
this.routes.push([method, path, handler])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
match(method: string, path: string): Result<T> | null {
|
|
|
|
const handlers: T[] = []
|
|
|
|
const params: Record<string, string> = {}
|
|
|
|
ROUTES_LOOP: for (let i = 0; i < this.routes.length; i++) {
|
|
|
|
const [routeMethod, routePath, handler] = this.routes[i]
|
|
|
|
if (routeMethod !== method && routeMethod !== METHOD_NAME_ALL) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if (routePath === '*' || routePath === '/*') {
|
|
|
|
handlers.push(handler)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasStar = routePath.indexOf('*') !== -1
|
|
|
|
const hasLabel = routePath.indexOf(':') !== -1
|
|
|
|
if (!hasStar && !hasLabel) {
|
|
|
|
if (routePath === path || routePath + '/' === path) {
|
|
|
|
handlers.push(handler)
|
|
|
|
}
|
|
|
|
} else if (hasStar && !hasLabel) {
|
|
|
|
const endsWithStar = routePath.charCodeAt(routePath.length - 1) === 42
|
|
|
|
const parts = (endsWithStar ? routePath.slice(0, -2) : routePath).split(splitByStarRe)
|
|
|
|
|
|
|
|
const lastIndex = parts.length - 1
|
|
|
|
for (let j = 0, pos = 0; j < parts.length; j++) {
|
|
|
|
const part = parts[j]
|
|
|
|
const index = path.indexOf(part, pos)
|
|
|
|
if (index !== pos) {
|
|
|
|
continue ROUTES_LOOP
|
|
|
|
}
|
|
|
|
pos += part.length
|
|
|
|
if (j === lastIndex) {
|
|
|
|
if (
|
|
|
|
!endsWithStar &&
|
|
|
|
pos !== path.length &&
|
|
|
|
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
|
|
|
|
) {
|
|
|
|
continue ROUTES_LOOP
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const index = path.indexOf('/', pos)
|
|
|
|
if (index === -1) {
|
|
|
|
continue ROUTES_LOOP
|
|
|
|
}
|
|
|
|
pos = index
|
|
|
|
}
|
|
|
|
}
|
|
|
|
handlers.push(handler)
|
|
|
|
} else if (hasLabel && !hasStar) {
|
|
|
|
const localParams: Record<string, string> = {}
|
|
|
|
const parts = routePath.match(splitPathRe) as string[]
|
|
|
|
|
|
|
|
const lastIndex = parts.length - 1
|
|
|
|
for (let j = 0, pos = 0; j < parts.length; j++) {
|
|
|
|
if (pos === -1 || pos >= path.length) {
|
|
|
|
continue ROUTES_LOOP
|
|
|
|
}
|
|
|
|
|
|
|
|
const part = parts[j]
|
|
|
|
if (part.charCodeAt(1) === 58) {
|
|
|
|
// /:label
|
|
|
|
let name = part.slice(2)
|
|
|
|
let value
|
|
|
|
|
|
|
|
if (name.charCodeAt(name.length - 1) === 125) {
|
|
|
|
// :label{pattern}
|
|
|
|
const openBracePos = name.indexOf('{')
|
|
|
|
const pattern = name.slice(openBracePos + 1, -1)
|
|
|
|
const restPath = path.slice(pos + 1)
|
|
|
|
const match = new RegExp(pattern, 'd').exec(restPath) as RegExpMatchArrayWithIndices
|
|
|
|
if (!match || match.indices[0][0] !== 0 || match.indices[0][1] === 0) {
|
|
|
|
continue ROUTES_LOOP
|
|
|
|
}
|
|
|
|
name = name.slice(0, openBracePos)
|
|
|
|
value = restPath.slice(...match.indices[0])
|
|
|
|
pos += match.indices[0][1] + 1
|
|
|
|
} else {
|
|
|
|
let endValuePos = path.indexOf('/', pos + 1)
|
|
|
|
if (endValuePos === -1) {
|
|
|
|
if (pos + 1 === path.length) {
|
|
|
|
continue ROUTES_LOOP
|
|
|
|
}
|
|
|
|
endValuePos = path.length
|
|
|
|
}
|
|
|
|
value = path.slice(pos + 1, endValuePos)
|
|
|
|
pos = endValuePos
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
(params[name] && params[name] !== value) ||
|
|
|
|
(localParams[name] && localParams[name] !== value)
|
|
|
|
) {
|
|
|
|
throw new Error('Duplicate param name')
|
|
|
|
}
|
|
|
|
localParams[name] = value as string
|
|
|
|
} else {
|
|
|
|
const index = path.indexOf(part, pos)
|
|
|
|
if (index !== pos) {
|
|
|
|
continue ROUTES_LOOP
|
|
|
|
}
|
|
|
|
pos += part.length
|
|
|
|
}
|
|
|
|
|
|
|
|
if (j === lastIndex) {
|
|
|
|
if (pos !== path.length && !(pos === path.length - 1 && path.charCodeAt(pos) === 47)) {
|
|
|
|
continue ROUTES_LOOP
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Object.assign(params, localParams)
|
|
|
|
handlers.push(handler)
|
|
|
|
} else if (hasLabel && hasStar) {
|
|
|
|
throw new UnsupportedPathError()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return handlers.length
|
|
|
|
? {
|
|
|
|
handlers,
|
|
|
|
params,
|
|
|
|
}
|
|
|
|
: null
|
|
|
|
}
|
|
|
|
}
|