@loopback/rest
Version:
Expose controllers as REST endpoints and route REST API requests to controller methods
234 lines (210 loc) • 5.92 kB
text/typescript
// 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);
}