koa-router/Router.js

215 lines
4.9 KiB
JavaScript

const
assert = require('assert')
const
// Regex for the different parts
reg_segment = new RegExp(`([A-z]|[0-9]|-|_|\\.|:)+`),
reg_prefix = new RegExp(`^(\/${reg_segment.source})+$`),
reg_url = new RegExp(`^(\/${reg_segment.source})+`),
// Allowed methods
METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'ALL'],
// Default Response
defaultResponse = [async(c, n) => {
await n()
}, {}]
/**
* Defaults an options object
* @param {*} options
*/
function defaultOptions(options) {
return Object.assign({
prefix: '',
end: false,
case: false
}, options)
}
/**
* Creates a regex from a path.
* Options:
* end
* prefix
* case
* @param {String} path
* @param {*} options
*/
function pathToReg(path, options) {
if (path instanceof RegExp)
return path
assert(typeof path === 'string', 'Path must be a String or RegExp')
options = defaultOptions(options)
// Test Path & Prefix
assert(reg_url.test(path) || path === '/', 'Invalid Path')
assert(reg_prefix.test(options.prefix) || options.prefix === '', 'Invalid Prefix')
path = options.prefix + path
let ret = '^'
for (const seg of path.split('/').slice(1))
// If segment starts with a ':' make it wildcard
ret += '/' + (seg[0] === ':' ? reg_segment.source : seg)
if (options.end)
ret += '$'
return new RegExp(ret, options.case ? '' : 'i')
}
/**
* Gets the position of each parameter type and returns a map with it
* @param {*} path
*/
function pathToParams(path, options) {
const
offset = options.prefix.split('/').slice(1).length,
params = new Map()
let i = 0
for (const seg of path.split('/').slice(1)) {
if (seg[0] === ':')
params.set(seg.slice(1), i + offset)
++i
}
return params
}
/**
* Takes the builder function and creates a map with all the routes in regex
* @param {*} options
* @param {*} builder provided by user
*/
function mkRouter(options, builder) {
const
routes = new Map(),
routesKeys = new Map()
// This object (routesMaker) will process the builder function
let routesMaker = {
nest: function () {
// Join the lower paths with the current ones
const nested = arguments[0](options.prefix)
for (const key of nested.keys())
routes.set(key, nested.get(key))
}
}
// Add the other methods to the routesMaker object
for (const method of METHODS)
routesMaker[method.toLowerCase()] = function () {
const data = {
fn: arguments[1],
params: pathToParams(arguments[0], options)
}
// If the same regex already exits, grab the object
let key = pathToReg(arguments[0], options)
if (routesKeys.has(key.source))
key = routesKeys.get(key.source)
else
routesKeys.set(key.source, key)
// Add the value into the object of GET, POST, etc.
const current = routes.get(key) || {}
routes.set(key, Object.assign(current, {
[method]: data
}))
}
// Build the routes, including the nested ones
builder(routesMaker)
return routes
}
/**
* Chooses the right function for given path and method
* @param {*} routes
* @param {*} path
* @param {*} method
*/
function chooseFn(routes, path, method) {
const
candidates = new Map(),
pathArr = path.split('/').slice(1),
paramObj = {}
for (const reg of routes.keys())
if (reg.test(path))
candidates.set(reg, routes.get(reg))
// Choose the route
let route = null
if (candidates.size === 1)
route = candidates.entries().next().value[1]
else if (candidates.size > 1) {
// If more than 1 route matches, choose the most logical fit
let testPath = ''
for (let i = 0; i < path.length; ++i) {
if (candidates.size < 2)
// Only 1 route left
break
testPath += path[i]
for (const entry of candidates)
if (entry[0].test(testPath))
candidates.delete(entry[0])
}
route = candidates.entries().next().value[1]
}
if (route === null)
return defaultResponse
// Choose method or ALL if specific method is not set, but ALL is
let fn = route['ALL'] === undefined ? null : route['ALL']
if (route[method.toUpperCase()] !== undefined)
fn = route[method.toUpperCase()]
if (fn === null)
return defaultResponse
// Get the parameters
for (const key of fn.params.keys())
paramObj[key] = pathArr[fn.params.get(key)]
return [fn.fn, paramObj]
}
module.exports = function (options, builder) {
// If only one argument was given
if (typeof options === 'function' && builder === undefined) {
builder = options
options = {}
}
if (typeof options === 'string')
options = {
prefix: options
}
assert(options instanceof Object, 'Options can only be a string or object')
options = defaultOptions(options)
// Build the routes
const routes = mkRouter(options, builder)
return function (c, n) {
// For building nested routes
if (typeof c === 'string') {
options.prefix = c + options.prefix
return mkRouter(options, builder)
}
const fn = chooseFn(routes, c.request.url, c.request.method)
c.request.params = fn[1]
if (typeof fn[0] !== 'function')
fn[0] = defaultResponse[0]
return fn[0](c, n)
}
}