mirror of
https://github.com/cupcakearmy/koa-router.git
synced 2024-12-22 16:16:29 +00:00
Initial Commit
This commit is contained in:
parent
b99e082ae1
commit
21119ffa4f
159
README.md
Normal file
159
README.md
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# Koa Router
|
||||||
|
Koa Router with support for recursive nesting and regexp and dynamic urls. No dependecies and lightweight code.
|
||||||
|
|
||||||
|
### Simple Example
|
||||||
|
```javascript
|
||||||
|
const
|
||||||
|
Koa = require('koa'),
|
||||||
|
router = require('cca-koa-router')
|
||||||
|
|
||||||
|
const
|
||||||
|
app = new Koa(),
|
||||||
|
port = 3001
|
||||||
|
|
||||||
|
app.use(router(_ => {
|
||||||
|
_.get('/user/:user/', (c, n) => {
|
||||||
|
c.body = c.request.param['user']
|
||||||
|
// GET /user/foo/ => 'foo'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
app.listen(port)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Options](#options)
|
||||||
|
- [Modes](#modes)
|
||||||
|
- [Nesting](#nesting)
|
||||||
|
- [Methods](#methods)
|
||||||
|
- [Parameters](#params)
|
||||||
|
|
||||||
|
##### ~ `router(options, builder)`
|
||||||
|
|
||||||
|
##### Options:
|
||||||
|
|
||||||
|
- `prefix` Prefix for the paths
|
||||||
|
- `end` If trailing paths sould be counted
|
||||||
|
- `case` Case sentitive
|
||||||
|
|
||||||
|
###### Default
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
prefix: '',
|
||||||
|
end: false,
|
||||||
|
case: false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Modes:
|
||||||
|
1. `router(builder)` No options specified, use default
|
||||||
|
2. `router('string', builder)` String will be taken as the prefix
|
||||||
|
3. `router({}, builder)` Specify custom options
|
||||||
|
|
||||||
|
###### Example
|
||||||
|
```javascript
|
||||||
|
// 1
|
||||||
|
app.use(router(_ => {
|
||||||
|
_.get('/mypath', (c, n) => {
|
||||||
|
// GET /mypath
|
||||||
|
c.body = 'Some Response'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 2
|
||||||
|
app.use(router('/myprefix', _ => {
|
||||||
|
_.get('/mypath', (c, n) => {
|
||||||
|
// GET /myprefix/mypath
|
||||||
|
c.body = 'Some Response'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 3
|
||||||
|
app.use(router({
|
||||||
|
prefix: '/myprefix',
|
||||||
|
case: true
|
||||||
|
}, _ => {
|
||||||
|
_.get('/myPath', (c, n) => {
|
||||||
|
// GET /myprefix/myPath
|
||||||
|
c.body = 'Some Response'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
````
|
||||||
|
|
||||||
|
##### Nesting
|
||||||
|
You can nest recursively `routers`. Each can have its own `options`.
|
||||||
|
|
||||||
|
###### Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
app.use(router(_ => {
|
||||||
|
_.nest(router('/user', _ => {
|
||||||
|
_.get('/view', (c, n) => {
|
||||||
|
c.body = 'View User'
|
||||||
|
})
|
||||||
|
|
||||||
|
_.get('/edit', (c, n) => {
|
||||||
|
c.body = 'Edit User'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
_.get('/', c => {
|
||||||
|
c.body = 'Root'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
/*
|
||||||
|
GET / => 'Root'
|
||||||
|
GET /user/view => 'View User'
|
||||||
|
GET /user/edit => 'Edit User'
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Methods
|
||||||
|
Supported methods:
|
||||||
|
- `GET`
|
||||||
|
- `POST`
|
||||||
|
- `PUT`
|
||||||
|
- `PATCH`
|
||||||
|
- `DELETE`
|
||||||
|
|
||||||
|
Special "methods":
|
||||||
|
- `ALL` Used if none other method is defined
|
||||||
|
- `NEST` Used to nest layers of the router
|
||||||
|
|
||||||
|
###### Example
|
||||||
|
```javascript
|
||||||
|
app.use(router(_ => {
|
||||||
|
_.get('/path', c => {
|
||||||
|
c.body = 'GET'
|
||||||
|
})
|
||||||
|
_.post('/path', c => {
|
||||||
|
c.body = 'POST'
|
||||||
|
})
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
_.delete('/path', c => {
|
||||||
|
c.body = 'DELETE'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Params
|
||||||
|
The `router` suppors parametrs in the url/path. Parameters will be stored in the `ctx.request.params` object
|
||||||
|
|
||||||
|
###### Example
|
||||||
|
```javascript
|
||||||
|
app.use(router(_ => {
|
||||||
|
_.get('/user/:user/:id/view/:type', (c, n) => {
|
||||||
|
c.body = c.request.params
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
/*
|
||||||
|
GET /user/foo/123/view/active
|
||||||
|
=>
|
||||||
|
{"user":"foo","id":"123","type":"active"}
|
||||||
|
*/
|
||||||
|
```
|
194
Router.js
Normal file
194
Router.js
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
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 = () => {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
path = options.prefix + path
|
||||||
|
|
||||||
|
// Test Path & Prefix
|
||||||
|
assert(reg_url.test(path), 'Invalid Path')
|
||||||
|
assert(options.prefix === '' || reg_prefix.test(options.prefix), 'Invalid Prefix')
|
||||||
|
|
||||||
|
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) {
|
||||||
|
let params = new Map()
|
||||||
|
let i = 0
|
||||||
|
for (const seg of path.split('/').slice(1)) {
|
||||||
|
if (seg[0] === ':')
|
||||||
|
params.set(seg.slice(1), i)
|
||||||
|
++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) {
|
||||||
|
|
||||||
|
let routes = new Map()
|
||||||
|
let routesKeys = new Map()
|
||||||
|
|
||||||
|
// This object will process the builder function
|
||||||
|
let routesMaker = {
|
||||||
|
nest: function () {
|
||||||
|
// Join the lower paths with the current ones
|
||||||
|
routes = new Map([...routes, ...arguments[0](options.prefix)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the other methods to the routesMaker object
|
||||||
|
for (const method of METHODS)
|
||||||
|
routesMaker[method.toLowerCase()] = function () {
|
||||||
|
let
|
||||||
|
key = pathToReg(arguments[0], options),
|
||||||
|
data = {
|
||||||
|
fn: arguments[1],
|
||||||
|
params: pathToParams(arguments[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the same regex already exits, grab the object
|
||||||
|
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) || {}
|
||||||
|
let obj = {}
|
||||||
|
obj[method] = data
|
||||||
|
routes.set(key, Object.assign(current, obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// TODO route chooser
|
||||||
|
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]
|
||||||
|
fn[0](c, n)
|
||||||
|
}
|
||||||
|
}
|
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "cca-koa-router",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Koa Router",
|
||||||
|
"main": "Router.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/CupCakeArmy/koa-router.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Koa",
|
||||||
|
"Router",
|
||||||
|
"Middleware",
|
||||||
|
"Regexp"
|
||||||
|
],
|
||||||
|
"author": "Niccolo Borgioli",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/CupCakeArmy/koa-router/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/CupCakeArmy/koa-router#readme"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user