UNPKG

@loopback/rest

Version:

Expose controllers as REST endpoints and route REST API requests to controller methods

234 lines (210 loc) 5.92 kB
// Copyright IBM Corp. and LoopBack contributors 2018,2019. All Rights Reserved. // Node module: @loopback/rest // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import {Key, pathToRegexp} from 'path-to-regexp'; import {PathParameterValues} from '../types'; import {toExpressPath} from './openapi-path'; /** * A Node in the trie */ export interface Node<T> { /** * Key of the node */ key: string; /** * Value of the node */ value?: T; /** * Children of the node */ readonly children: {[key: string]: Node<T>}; /** * Regular expression for the template */ regexp?: RegExp; /** * Names of the node if it contains named parameters */ names?: string[]; } export type NodeWithValue<T> = Node<T> & {value: T}; export interface ResolvedNode<T> { node: Node<T>; params?: PathParameterValues; } /** * An implementation of trie for routes. The key hierarchy is built with parts * of the route path delimited by `/` */ export class Trie<T> { readonly root: Node<T> = {key: '', children: {}}; /** * Create a node for a given path template * @param pathTemplate - The path template, * @param value - Value of the route */ create(routeTemplate: string, value: T) { const keys = routeTemplate.split('/').filter(Boolean); return createNode(keys, 0, value, this.root); } /** * Match a route path against the trie * @param path - The route path, such as `/customers/c01` */ match( path: string, ): (ResolvedNode<T> & {node: NodeWithValue<T>}) | undefined { const keys = path.split('/').filter(Boolean); const params = {}; const resolved = search(keys, 0, params, this.root); if (resolved == null || !isNodeWithValue(resolved.node)) return undefined; return { node: resolved.node, params: resolved.params, }; } /** * List all nodes with value of the trie */ list(): NodeWithValue<T>[] { const nodes: NodeWithValue<T>[] = []; traverse(this.root, node => { nodes.push(node); }); return nodes; } } function isNodeWithValue<T>(node: Node<T>): node is NodeWithValue<T> { return node.value != null; } /** * Use depth-first preorder traversal to list all nodes with values * @param root - Root node * @param visitor - A function to process nodes with values */ function traverse<T>(root: Node<T>, visitor: (node: NodeWithValue<T>) => void) { if (isNodeWithValue(root)) visitor(root); for (const k in root.children) { traverse(root.children[k], visitor); } } /** * Match the given key to one or more children of the parent node * @param key - Key * @param parent - Parent node */ function matchChildren<T>(key: string, parent: Node<T>): ResolvedNode<T>[] { const resolvedNodes: ResolvedNode<T>[] = []; // Match key literal first let child = parent.children[key]; if (child) { resolvedNodes.push({ node: child, }); return resolvedNodes; } // Match templates for (const k in parent.children) { child = parent.children[k]; if (!child.names || !child.regexp) continue; const match = child.regexp.exec(key); if (match) { const resolved: ResolvedNode<T> = {params: {}, node: child}; let i = 0; for (const n of child.names) { const val = match[++i]; resolved.params![n] = decodeURIComponent(val); } resolvedNodes.push(resolved); } } return resolvedNodes; } /** * Search a sub list of keys against the parent node * @param keys - An array of keys * @param index - Starting index of the key list * @param params - An object to receive resolved parameter values * @param parent - Parent node */ function search<T>( keys: string[], index: number, // eslint-disable-next-line @typescript-eslint/no-explicit-any params: {[name: string]: any}, parent: Node<T>, ): ResolvedNode<T> | undefined { const key = keys[index]; const resolved: ResolvedNode<T> = {node: parent, params}; if (key === undefined) return resolved; const children = matchChildren(key, parent); if (children.length === 0) return undefined; // There might be multiple matches, such as `/users/{id}` and `/users/{userId}` for (const child of children) { const result = search(keys, index + 1, params, child.node); if (result && isNodeWithValue(result.node)) { Object.assign(params, child.params); return result; } } // no matches found return undefined; } /** * Create a node for a sub list of keys against the parent node * @param keys - An array of keys * @param index - Starting index of the key list * @param value - Value of the node * @param parent - Parent node */ function createNode<T>( keys: string[], index: number, value: T, parent: Node<T>, ): Node<T> { const key = keys[index]; if (key === undefined) return parent; const isLast = keys.length - 1 === index; let child = parent.children[key]; if (child != null) { // Found an existing node if (isLast) { if (child.value == null) { child.value = value; } else { if (child.value !== value) { throw new Error( 'Duplicate key found with different value: ' + keys.join('/'), ); } } } return createNode(keys, index + 1, value, child); } /** * Build a new node */ child = { children: {}, key: key, }; if (isLast) { child.value = value; } // Check if the key has variables such as `{var}` const path = toExpressPath(key); const params: Key[] = []; const re = pathToRegexp(path, params); if (params.length) { child.names = params.map(p => `${p.name}`); child.regexp = re; } // Add the node to the parent parent.children[key] = child; // Create nodes for rest of the keys return createNode(keys, index + 1, value, child); }