@jnode/server
Version:
Simple web server package for Node.js.
271 lines (228 loc) • 8.68 kB
JavaScript
/*
@jnode/server/routers.js
v2
Simple web server package for Node.js.
by JustApple
*/
// path router: the most important router, routes the request by path
class PathRouter {
constructor(end = 404, map = {}) {
this.end = end;
// parse map
this.map = {};
for (let [key, value] of Object.entries(map)) {
// any path segment
if (key === '*') {
this.map['*'] = value;
continue;
}
key = key.trimStart();
const firstSlashIndex = key.indexOf('/');
if (firstSlashIndex === -1) continue;
// key format: '[@ (path end check)][METHOD (method check)]/path/segments'
// example: '@ GET /cat/names', 'POST /cats', '/api'
const routeEnd = key.startsWith('@');
const routeMethod = key.substring(routeEnd ? 1 : 0, firstSlashIndex).trim();
const routePath = key.substring(firstSlashIndex).split('/').slice(1);
// expand map
let current = this.map;
let args = [];
for (let segment of routePath) {
if (segment.startsWith('%:')) {
args.push(segment.substring(2));
if (!current[':']) current[':'] = {};
current = current[':'];
continue;
}
segment = '/' + decodeURIComponent(segment);
if (!current[segment]) current[segment] = {};
current = current[segment];
}
// '*' for non-end check, '@' for end check
// '*' for any method, 'METHOD' for specific method
if (!current[routeEnd ? '@' : '*']) current[routeEnd ? '@' : '*'] = {};
current[routeEnd ? '@' : '*'][routeMethod || '*'] = value;
if (args.length > 0) current[routeEnd ? '@' : '*']['::' + (routeMethod || '*')] = args;
}
}
route(env, ctx) {
if (env.pathPointer >= env.path.length) return this.end;
let result = this.map['*'];
let resultPointer = env.pathPointer;
let current = this.map;
let currentArgs = [];
let resultArgNames;
while (env.pathPointer < env.path.length) {
let segment = '/' + env.path[env.pathPointer];
if (!current[segment] && !current[':']) break;
if (!current[segment]) {
segment = ':';
currentArgs.push(env.path[env.pathPointer]);
}
// prepare fallback
if (current[segment]['*']?.['*'] || current[segment]['*']?.[ctx.method]) {
result = current[segment]['*'][ctx.method] ?? current[segment]['*']['*'];
resultPointer = env.pathPointer + 1;
resultArgNames = current[segment]['*']['::' + ctx.method] ?? current[segment]['*']['::*'];
}
current = current[segment];
env.pathPointer++;
// ends
if (env.pathPointer >= env.path.length && (current['@']?.['*'] || current['@']?.[ctx.method])) {
result = current['@'][ctx.method] ?? current['@']['*'];
resultPointer = env.pathPointer;
resultArgNames = current['@']['::' + ctx.method] ?? current['@']['::*'];
}
}
env.pathPointer = resultPointer;
if (resultArgNames) {
const len = resultArgNames.length;
for (let i = 0; i < len; i++) {
ctx.params[resultArgNames[i]] = currentArgs[i];
}
}
return result;
}
}
// host router: routes the request by host
class HostRouter {
constructor(end = 404, map = {}) {
this.end = end;
// parse map
this.map = {};
for (let [key, value] of Object.entries(map)) {
// any path segment
if (key === '*') {
this.map['*'] = value;
continue;
}
key = key.trimStart();
const firstDotIndex = key.indexOf('.');
if (firstDotIndex === -1) continue;
// key format: '[@ (domain end check)].domain.segments'
// example: '@ .com.example', '.com.example', '.localhost', '.1.0.0.127' (yes, that's how it works)
const routeEnd = key.startsWith('@');
const routeDomain = key.substring(firstDotIndex).split('.').slice(1);
// expand map
let current = this.map;
let args = [];
for (const segment of routeDomain) {
if (segment.startsWith('%:')) {
args.push(segment.substring(2));
if (!current[':']) current[':'] = {};
current = current[':'];
continue;
}
if (!current['.' + segment]) current['.' + segment] = {};
current = current['.' + segment];
}
// '*' for non-end check, '@' for end check
current[routeEnd ? '@' : '*'] = value;
if (args.length > 0) current['::'] = args;
}
}
route(env, ctx) {
if (env.hostPointer >= env.host.length) return this.end;
let result = this.map['*'];
let resultPointer = env.hostPointer;
let current = this.map;
let currentArgs = [];
let resultArgNames;
while (env.hostPointer < env.host.length) {
let segment = '.' + env.host[env.hostPointer];
if (!current[segment] && !current[':']) break;
if (!current[segment]) {
segment = ':';
currentArgs.push(env.host[env.hostPointer]);
}
// prepare fallback
if (current[segment]['*']) {
result = current[segment]['*'];
resultPointer = env.hostPointer + 1;
resultArgNames = current[segment]['::'];
}
current = current[segment];
env.hostPointer++;
// ends
if (env.hostPointer >= env.host.length && current['@']) {
result = current['@'];
resultPointer = env.hostPointer;
}
}
env.hostPointer = resultPointer;
if (resultArgNames) {
const len = resultArgNames.length;
for (let i = 0; i < len; i++) {
ctx.params[resultArgNames[i]] = currentArgs[i];
}
}
return result;
}
}
// method router: routes the request by method
class MethodRouter {
constructor(methodMap = {}) {
this.methodMap = methodMap;
}
route(env, ctx) {
return this.methodMap[ctx.method] || this.methodMap['*'] || 405;
}
}
// function router: a simple router that allows you to make custom routing logic
class FunctionRouter {
constructor(fn, ext) {
this.fn = fn;
this.ext = ext;
}
route(env, ctx) {
return this.fn(env, ctx, this.ext);
}
}
// path argument router: collects a path segment and save to `ctx.params`
class PathArgRouter {
constructor(paramName, next) {
this.paramName = paramName;
this.next = next;
}
route(env, ctx) {
ctx.params[this.paramName] = env.path[env.pathPointer];
env.pathPointer++;
return this.next;
}
}
// host argument router: collects a host segment and save to `ctx.params`
class HostArgRouter {
constructor(paramName, next) {
this.paramName = paramName;
this.next = next;
}
route(env, ctx) {
ctx.params[this.paramName] = env.host[env.hostPointer];
env.hostPointer++;
return this.next;
}
}
// set code router: set the code handler for specific status code
class SetCodeRouter {
constructor(codeHandlers, next) {
this.codeHandlers = codeHandlers;
this.next = next;
}
route(env, ctx) {
env.codeHandlers = Object.assign({}, env.codeHandlers, this.codeHandlers);
return this.next;
}
}
// export
module.exports = {
PathRouter, HostRouter, MethodRouter, FunctionRouter, PathArgRouter, HostArgRouter, SetCodeRouter,
routerConstructors: {
Path: (end, map) => new PathRouter(end, map),
Host: (end, map) => new HostRouter(end, map),
Method: (methodMap) => new MethodRouter(methodMap),
Function: (fn, ext) => new FunctionRouter(fn, ext),
PathArg: (name, next) => new PathArgRouter(name, next),
HostArg: (name, next) => new HostArgRouter(name, next),
SetCode: (handlers, next) => new SetCodeRouter(handlers, next)
}
};