mirror of
https://github.com/cupcakearmy/koa-router.git
synced 2024-12-22 08:06:27 +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