UNPKG

@iopa/router

Version:

Lightweight and fast router for IOPA applications

268 lines (225 loc) 6.74 kB
import type { IRouterResult, UrlPattern } from '@iopa/types' import { util } from 'iopa' import { METHOD_NAME_ALL } from '../constants' const { url: { splitPath, getPattern } } = util interface INext<T> { nodes: Node<T>[] handlerSets: IHandlerSet<T>[] params: Record<string, string> } interface IHandlerSet<T> { handler: T score: number name: string // For debug } function findParam<T>(node: Node<T>, name: string): boolean { for (let i = 0, len = node.patterns.length; i < len; i++) { if (typeof node.patterns[i] === 'object' && node.patterns[i][1] === name) { return true } } const nodes = Object.values(node.children) for (let i = 0, len = nodes.length; i < len; i++) { if (findParam(nodes[i], name)) { return true } } return false } export class Node<T> { public methods: Record<string, IHandlerSet<T>>[] public children: Record<string, Node<T>> public patterns: UrlPattern[] public order: number = 0 public name: string public constructor( method?: string, handler?: T, children?: Record<string, Node<T>> ) { this.children = children || {} this.methods = [] if (method && handler) { const m: Record<string, IHandlerSet<T>> = {} m[method] = { handler: handler, score: 0, name: this.name } this.methods = [m] } this.patterns = [] } public insert(method: string, path: string, handler: T): Node<T> { this.name = `${method} ${path}` this.order = ++this.order // eslint-disable-next-line @typescript-eslint/no-this-alias let curNode: Node<T> = this const parts = splitPath(path) const parentPatterns: UrlPattern[] = [] const errorMessage = (name: string): string => { return `Duplicate param name, use another name instead of '${name}' - ${method} ${path} <--- '${name}'` } for (let i = 0, len = parts.length; i < len; i++) { const p: string = parts[i] if (Object.keys(curNode.children).includes(p)) { parentPatterns.push(...curNode.patterns) curNode = curNode.children[p] continue } curNode.children[p] = new Node() const pattern = getPattern(p) if (pattern) { if (typeof pattern === 'object') { for (let j = 0, len = parentPatterns.length; j < len; j++) { if ( typeof parentPatterns[j] === 'object' && parentPatterns[j][1] === pattern[1] ) { throw new Error(errorMessage(pattern[1])) } } if ( Object.values(curNode.children).some((n) => findParam(n, pattern[1]) ) ) { throw new Error(errorMessage(pattern[1])) } } curNode.patterns.push(pattern) parentPatterns.push(...curNode.patterns) } parentPatterns.push(...curNode.patterns) curNode = curNode.children[p] } let score = 1 if (path === '*') { score = score + this.order * 0.01 } else { score = parts.length + this.order * 0.01 } if (!curNode.methods.length) { curNode.methods = [] } const m: Record<string, IHandlerSet<T>> = {} const handlerSet: IHandlerSet<T> = { handler: handler, name: this.name, score: score } m[method] = handlerSet curNode.methods.push(m) return curNode } private _getHandlerSets( node: Node<T>, method: string, wildcard?: boolean ): IHandlerSet<T>[] { const handlerSets: IHandlerSet<T>[] = [] node.methods.map((m) => { const handlerSet = m[method] || m[METHOD_NAME_ALL] if (handlerSet !== undefined) { const hs = { ...handlerSet } if (wildcard) { hs.score = handlerSet.score - 1 } handlerSets.push(hs) return } }) return handlerSets } private _next( node: Node<T>, part: string, method: string, isLast: boolean ): INext<T> { const handlerSets: IHandlerSet<T>[] = [] const nextNodes: Node<T>[] = [] const params: Record<string, string> = {} for (let j = 0, len = node.patterns.length; j < len; j++) { const pattern = node.patterns[j] // Wildcard // '/hello/*/foo' => match /hello/bar/foo if (pattern === '*') { const astNode = node.children['*'] if (astNode) { handlerSets.push(...this._getHandlerSets(astNode, method)) nextNodes.push(astNode) } } if (part === '') continue // Named match // `/posts/:id` => match /posts/123 const [key, name, matcher] = pattern if ( matcher === true || (matcher instanceof RegExp && matcher.test(part)) ) { if (typeof key === 'string') { if (isLast === true) { handlerSets.push( ...this._getHandlerSets(node.children[key], method) ) } nextNodes.push(node.children[key]) } if (typeof name === 'string') { params[name] = part } } } const nextNode = node.children[part] if (nextNode) { if (isLast === true) { // '/hello/*' => match '/hello' if (nextNode.children['*']) { handlerSets.push( ...this._getHandlerSets(nextNode.children['*'], method, true) ) } handlerSets.push(...this._getHandlerSets(nextNode, method)) } nextNodes.push(nextNode) } const next: INext<T> = { nodes: nextNodes, handlerSets: handlerSets, params: params } return next } public search(method: string, path: string): IRouterResult<T> | null { const handlerSets: IHandlerSet<T>[] = [] let params: Record<string, string> = {} // eslint-disable-next-line @typescript-eslint/no-this-alias const curNode: Node<T> = this let curNodes = [curNode] const parts = splitPath(path) for (let i = 0, len = parts.length; i < len; i++) { const p: string = parts[i] const isLast = i === len - 1 const tempNodes: Node<T>[] = [] for (let j = 0, len2 = curNodes.length; j < len2; j++) { const res = this._next(curNodes[j], p, method, isLast) if (res.nodes.length === 0) { continue } handlerSets.push(...res.handlerSets) params = Object.assign(params, res.params) tempNodes.push(...res.nodes) } curNodes = tempNodes } if (handlerSets.length <= 0) return null const handlers = handlerSets .sort((a, b) => { return a.score - b.score }) .map((s) => { return s.handler }) return { handlers, params } } }