@zeushq/nextjs-zapi
Version:
Next.js SDK for creating a Zeus API
228 lines (176 loc) • 5.49 kB
text/typescript
import createDebug from '../zapi/utils/debug';
import { NextApiHandler } from 'next';
const debug = createDebug('router');
export enum TreeNodeType {
HTTP_METHOD,
STATIC,
PARAM
}
export type Tree = { [method: string]: TreeNode };
export interface Route {
method: string;
path: string;
}
export interface TreeNode {
value: string;
type: TreeNodeType;
children: { [key: string]: TreeNode };
handler?: NextApiHandler;
}
export interface RouterOptions {
caseSensitive?: boolean;
}
export interface RouteLookupResult {
params: Record<string, unknown>;
handler: NextApiHandler;
}
export class Router {
routes = [] as Route[];
tree = {} as Tree;
caseSensitive = false;
public constructor(options?: RouterOptions) {
if (options !== undefined) {
if (options.caseSensitive !== undefined) this.caseSensitive = options.caseSensitive;
}
}
public print(): string | undefined {
// const methods = Object.keys(this.tree);
let out = '';
for (let i = 0; i < this.routes.length; i++) {
const r = this.routes[i];
out += `${r.method.padStart(7, ' ')} ${r.path}\n`;
}
// for (let i = 0; i < methods.length; i++) {
// const method = methods[i];
// const currNode = this.tree[method];
// // out += (`${method}:\n`);
// out = this._print(out, currNode, 1);
// }
// debug(out);
debug(out);
return out;
}
public _print(out: string, node: TreeNode, depth = 1): string {
const keys = Object.keys(node.children);
out += `${Array(depth).join(' ')} - ${node.value}\n`;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const currNode = node.children[key];
out = this._print(out, currNode, depth + 1);
}
return out;
}
public on(method: string | string[], path: string, handler: NextApiHandler): void {
if (Array.isArray(method)) {
for (let i = 0; i < method.length; i++) {
this.on(method[i], path, handler);
}
return;
}
let methodNode = this.tree[method];
if (methodNode === undefined) {
methodNode = this.tree[method] = {
value: method,
type: TreeNodeType.HTTP_METHOD,
children: {}
};
}
let pathElements = path.split('/');
if (pathElements[0] === '') pathElements = pathElements.splice(1, pathElements.length - 1);
this._on(methodNode, pathElements, handler);
this.routes.push({ method, path });
}
private _on(node: TreeNode, pathElements: string[], handler: NextApiHandler): void {
const nextPathEl = pathElements.shift();
if (nextPathEl === undefined) return;
const isParam = nextPathEl.startsWith(':');
const elName = isParam ? nextPathEl.substring(1) : nextPathEl;
let nextNode = node.children[elName];
if (!nextNode) {
nextNode = node.children[nextPathEl] = {
value: elName,
type: isParam ? TreeNodeType.PARAM : TreeNodeType.STATIC,
children: {},
handler: pathElements.length === 0 ? handler : undefined
};
}
if (pathElements.length > 0) {
this._on(nextNode, pathElements, handler);
}
}
public lookup(method: string | undefined, path: string | undefined): RouteLookupResult | undefined {
if (!method) return undefined;
if (!path) return undefined;
const methodNode = this.tree[method];
if (!methodNode) return undefined;
let pathElements = path.split('/');
if (pathElements[0] === '') pathElements = pathElements.splice(1, pathElements.length - 1);
const params = {};
const handler = this._lookup(methodNode, pathElements, params);
if (!handler) return undefined;
return { params, handler };
}
private parseInt(value: string) {
for (let i = 0; i < value.length; i++) {
if (value.charAt(i) < '0' || value.charAt(i) > '9') return undefined;
}
return parseInt(value);
}
private parseFloat(value: string) {
try {
return parseFloat(value);
} catch {
return undefined;
}
}
private _transform(value: string) {
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
const intVal = this.parseInt(value);
if (!!intVal) {
return intVal;
}
const floatVal = this.parseFloat(value);
if (!!floatVal) {
return floatVal;
}
return value;
}
public _lookup(
node: TreeNode,
pathElements: string[],
params: { [key: string]: string | number | boolean | null }
): NextApiHandler | undefined {
const nextPathEl = pathElements.shift();
if (nextPathEl === undefined) return undefined;
const nPathLeft = pathElements.length;
// Match static
const nextNode = node.children[nextPathEl];
if (nextNode !== undefined) {
switch (nPathLeft) {
case 0:
return nextNode.handler;
default:
return this._lookup(nextNode, pathElements, params);
}
}
// Match dynamic
const childrenKeys = Object.keys(node.children);
for (let i = 0; i < childrenKeys.length; i++) {
const key = childrenKeys[i];
const nextNode = node.children[key];
if (nextNode.type !== TreeNodeType.PARAM) continue;
params[nextNode.value] = this._transform(nextPathEl);
if (nPathLeft === 0) {
return nextNode.handler;
} else {
return this._lookup(nextNode, pathElements, params);
}
}
return undefined;
}
}