hono 0.0.2 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,37 @@
1
- # hono
1
+ # Hono
2
2
 
3
- Minimal web framework for Cloudflare Workers.
3
+ Hono [炎] - Tiny web framework for Cloudflare Workers and others.
4
+
5
+ ```js
6
+ const Hono = require('Hono')
7
+ const app = Hono()
8
+
9
+ app.get('/', () => new Response('Hono!!'))
10
+
11
+ app.fire()
12
+ ```
13
+
14
+ ## Feature
15
+
16
+ - Fast - the router is implemented with Trie-Tree structure.
17
+ - Tiny - use only standard API.
18
+ - Portable - zero dependencies.
19
+ - Flexible - you can make your own middlewares.
20
+ - Optimized - for Cloudflare Workers and Fastly Compute@Edge.
21
+
22
+ ## Benchmark
23
+
24
+ ```
25
+ hono x 813,001 ops/sec ±2.96% (75 runs sampled)
26
+ itty-router x 160,415 ops/sec ±3.31% (85 runs sampled)
27
+ sunder x 307,438 ops/sec ±4.77% (73 runs sampled)
28
+ Fastest is hono
29
+ ✨ Done in 37.03s.
30
+ ```
4
31
 
5
32
  ## Install
6
33
 
7
- ```sh
34
+ ```
8
35
  $ yarn add hono
9
36
  ```
10
37
 
@@ -14,18 +41,132 @@ or
14
41
  $ npm install hono
15
42
  ```
16
43
 
17
- ## Hello hono!
44
+ ## Methods
45
+
46
+ - app.**HTTP_METHOD**(path, callback)
47
+ - app.**all**(path, callback)
48
+ - app.**route**(path)
49
+ - app.**use**(path, middleware)
50
+
51
+ ## Routing
52
+
53
+ ### Basic
54
+
55
+ `app.HTTP_METHOD`
56
+
57
+ ```js
58
+ // HTTP Methods
59
+ app.get('/', () => new Response('GET /'))
60
+ app.post('/', () => new Response('POST /'))
61
+
62
+ // Wildcard
63
+ app.get('/wild/*/card', () => {
64
+ return new Response('GET /wild/*/card')
65
+ })
66
+ ```
67
+
68
+ `app.all`
69
+
70
+ ```js
71
+ // Any HTTP methods
72
+ app.all('/hello', () => new Response('ALL Method /hello'))
73
+ ```
74
+
75
+ ### Named Parameter
76
+
77
+ ```js
78
+ app.get('/user/:name', (c) => {
79
+ const name = c.req.params('name')
80
+ ...
81
+ })
82
+ ```
83
+
84
+ ### Regexp
85
+
86
+ ```js
87
+ app.get('/post/:date{[0-9]+}/:title{[a-z]+}', (c) => {
88
+ const date = c.req.params('date')
89
+ const title = c.req.params('title')
90
+ ...
91
+ ```
92
+
93
+ ### Chained Route
18
94
 
19
95
  ```js
20
- const Hono = require("hono");
21
- const app = Hono();
96
+ app
97
+ .route('/api/book')
98
+ .get(() => {...})
99
+ .post(() => {...})
100
+ .put(() => {...})
101
+ ```
22
102
 
23
- app.get("/", () => new Response("Hono!!"));
103
+ ## Middleware
104
+
105
+ ```js
106
+ const logger = (c, next) => {
107
+ console.log(`[${c.req.method}] ${c.req.url}`)
108
+ next()
109
+ }
24
110
 
25
- app.fire(); // call `addEventListener`
111
+ const addHeader = (c, next) => {
112
+ next()
113
+ c.res.headers.add('x-message', 'This is middleware!')
114
+ }
115
+
116
+ app = app.use('*', logger)
117
+ app = app.use('/message/*', addHeader)
118
+
119
+ app.get('/message/hello', () => 'Hello Middleware!')
120
+ ```
121
+
122
+ ## Context
123
+
124
+ ### req
125
+
126
+ ```js
127
+ app.get('/hello', (c) => {
128
+ const userAgent = c.req.headers('User-Agent')
129
+ ...
130
+ })
26
131
  ```
27
132
 
28
- Then, run `wrangler dev`.
133
+ ### res
134
+
135
+ ```js
136
+ app.use('/', (c, next) => {
137
+ next()
138
+ c.res.headers.append('X-Debug', 'Debug message')
139
+ })
140
+ ```
141
+
142
+ ## Request
143
+
144
+ ### query
145
+
146
+ ```js
147
+ app.get('/search', (c) => {
148
+ const query = c.req.query('q')
149
+ ...
150
+ })
151
+ ```
152
+
153
+ ### params
154
+
155
+ ```js
156
+ app.get('/entry/:id', (c) => {
157
+ const id = c.req.params('id')
158
+ ...
159
+ })
160
+ ```
161
+
162
+ ## Related projects
163
+
164
+ - koa <https://github.com/koajs/koa>
165
+ - express <https://github.com/expressjs/express>
166
+ - oak <https://github.com/oakserver/oak>
167
+ - itty-router <https://github.com/kwhitley/itty-router>
168
+ - Sunder <https://github.com/SunderJS/sunder>
169
+ - goblin <https://github.com/bmf-san/goblin>
29
170
 
30
171
  ## Author
31
172
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "hono",
3
- "version": "0.0.2",
3
+ "version": "0.0.6",
4
4
  "description": "Minimal web framework for Cloudflare Workers",
5
5
  "main": "src/hono.js",
6
6
  "scripts": {
7
- "test": "jest --verbose"
7
+ "test": "jest"
8
8
  },
9
9
  "author": "Yusuke Wada <yusuke@kamawada.com> (https://github.com/yusukebe)",
10
10
  "license": "MIT",
package/src/compose.js ADDED
@@ -0,0 +1,22 @@
1
+ // Based on the code in the MIT licensed `koa-compose` package.
2
+ const compose = (middleware) => {
3
+ return function (context, next) {
4
+ let index = -1
5
+ return dispatch(0)
6
+ function dispatch(i) {
7
+ if (i <= index)
8
+ return Promise.reject(new Error('next() called multiple times'))
9
+ index = i
10
+ let fn = middleware[i]
11
+ if (i === middleware.length) fn = next
12
+ if (!fn) return Promise.resolve()
13
+ try {
14
+ return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
15
+ } catch (err) {
16
+ return Promise.reject(err)
17
+ }
18
+ }
19
+ }
20
+ }
21
+
22
+ module.exports = compose
@@ -0,0 +1,42 @@
1
+ const compose = require('./compose')
2
+
3
+ describe('compose middleware', () => {
4
+ const middleware = []
5
+
6
+ const a = (c, next) => {
7
+ c.req['log'] = 'log'
8
+ next()
9
+ }
10
+ middleware.push(a)
11
+
12
+ const b = (c, next) => {
13
+ next()
14
+ c.res['header'] = `${c.res.header}-custom-header`
15
+ }
16
+ middleware.push(b)
17
+
18
+ const handler = (c, next) => {
19
+ c.req['log'] = `${c.req.log} message`
20
+ next()
21
+ c.res = { message: 'new response' }
22
+ }
23
+ middleware.push(handler)
24
+
25
+ const request = {}
26
+ const response = {}
27
+
28
+ it('Request', () => {
29
+ const c = { req: request, res: response }
30
+ const composed = compose(middleware)
31
+ composed(c)
32
+ expect(c.req['log']).not.toBeNull()
33
+ expect(c.req['log']).toBe('log message')
34
+ })
35
+ it('Response', () => {
36
+ const c = { req: request, res: response }
37
+ const composed = compose(middleware)
38
+ composed(c)
39
+ expect(c.res['header']).not.toBeNull()
40
+ expect(c.res['message']).toBe('new response')
41
+ })
42
+ })
package/src/hono.js CHANGED
@@ -1,77 +1,141 @@
1
- const Router = require('./router')
1
+ 'use strict'
2
2
 
3
- class Route {
4
- constructor(method, handler) {
5
- this.method = method;
6
- this.handler = handler;
3
+ const Node = require('./node')
4
+ const compose = require('./compose')
5
+ const defaultFilter = require('./middleware/defaultFilter')
6
+
7
+ const METHOD_NAME_OF_ALL = 'ALL'
8
+
9
+ class Router {
10
+ constructor() {
11
+ this.node = new Node()
12
+ this.tempPath = '/'
7
13
  }
14
+
15
+ add(method, path, ...handlers) {
16
+ this.node.insert(method, path, handlers)
17
+ }
18
+
19
+ match(method, path) {
20
+ return this.node.search(method, path)
21
+ }
22
+ }
23
+
24
+ const getPathFromURL = (url) => {
25
+ // XXX
26
+ const match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/)
27
+ return match[5]
8
28
  }
9
29
 
10
- class App {
30
+ const proxyHandler = {
31
+ get:
32
+ (target, prop) =>
33
+ (...args) => {
34
+ if (target.constructor.prototype.hasOwnProperty(prop)) {
35
+ return target[prop](...args)
36
+ } else {
37
+ return target.addRoute(prop, ...args)
38
+ }
39
+ },
40
+ }
41
+
42
+ class Hono {
11
43
  constructor() {
12
- this.router = new Router();
44
+ this.router = new Router()
45
+ this.middlewareRouter = new Router()
46
+ this.middlewareRouters = []
13
47
  }
14
48
 
15
- addRoute(method, path, handler) {
16
- this.router.add(path, new Route(method, handler))
49
+ getRouter() {
50
+ return this.router
51
+ }
52
+
53
+ addRoute(method, ...args) {
54
+ method = method.toUpperCase()
55
+ if (args.length === 1) {
56
+ this.router.add(method, this.router.tempPath, ...args)
57
+ } else {
58
+ this.router.add(method, ...args)
59
+ }
60
+ return WrappedApp(this)
17
61
  }
18
62
 
19
- handle(event) {
20
- const response = this.dispatch(event.request)
21
- return event.respondWith(response)
63
+ route(path) {
64
+ this.router.tempPath = path
65
+ return WrappedApp(this)
22
66
  }
23
67
 
24
- dispatch(request) {
25
- const url = new URL(request.url)
26
- const path = url.pathname
27
- const match = this.router.match(path)
68
+ use(path, middleware) {
69
+ const router = new Router()
70
+ router.add(METHOD_NAME_OF_ALL, path, middleware)
71
+ this.middlewareRouters.push(router)
72
+ }
28
73
 
29
- if (!match) {
30
- return this.notFound()
31
- }
74
+ async matchRoute(method, path) {
75
+ return this.router.match(method, path)
76
+ }
32
77
 
33
- const method = request.method.toLowerCase()
34
- const route = match[0]
35
- if (route.method == method) {
36
- const handler = route.handler
37
- return handler(request)
78
+ // XXX
79
+ async createContext(req, res) {
80
+ return {
81
+ req: req,
82
+ res: res,
83
+ newResponse: (params) => {
84
+ return new Response(params)
85
+ },
38
86
  }
39
- return this.notFound()
40
87
  }
41
88
 
42
- notFound() {
43
- return new Response('Not Found', {
44
- status: 404,
45
- headers: {
46
- 'content-type': 'text/plain'
89
+ async dispatch(request, response) {
90
+ const [method, path] = [request.method, getPathFromURL(request.url)]
91
+
92
+ const result = await this.matchRoute(method, path)
93
+ if (!result) return this.notFound()
94
+
95
+ request.params = (key) => result.params[key]
96
+
97
+ let handler = result.handler[0] // XXX
98
+
99
+ const middleware = [defaultFilter] // add defaultFilter later
100
+
101
+ for (const mr of this.middlewareRouters) {
102
+ const mwResult = mr.match(METHOD_NAME_OF_ALL, path)
103
+ if (mwResult) {
104
+ middleware.push(...mwResult.handler)
47
105
  }
48
- })
106
+ }
107
+
108
+ let wrappedHandler = async (context, next) => {
109
+ context.res = handler(context)
110
+ next()
111
+ }
112
+
113
+ middleware.push(wrappedHandler)
114
+ const composed = compose(middleware)
115
+ const c = await this.createContext(request, response)
116
+
117
+ composed(c)
118
+
119
+ return c.res
120
+ }
121
+
122
+ async handleEvent(event) {
123
+ return this.dispatch(event.request, {}) // XXX
49
124
  }
50
125
 
51
126
  fire() {
52
- addEventListener("fetch", (event) => {
53
- this.handle(event)
127
+ addEventListener('fetch', (event) => {
128
+ event.respondWith(this.handleEvent(event))
54
129
  })
55
130
  }
56
- }
57
131
 
58
- const proxyHandler = {
59
- get: (target, prop) => (...args) => {
60
- if (target.constructor.prototype.hasOwnProperty(prop)) {
61
- return target[prop](args[0])
62
- } else {
63
- target.addRoute(prop, args[0], args[1])
64
- return
65
- }
132
+ notFound() {
133
+ return new Response('Not Found', { status: 404 })
66
134
  }
67
135
  }
68
136
 
69
- const app = new App()
70
-
71
- function Hono() {
72
- return new Proxy(
73
- app, proxyHandler
74
- )
137
+ const WrappedApp = (hono = new Hono()) => {
138
+ return new Proxy(hono, proxyHandler)
75
139
  }
76
140
 
77
- module.exports = Hono
141
+ module.exports = WrappedApp
package/src/hono.test.js CHANGED
@@ -1,29 +1,114 @@
1
1
  const Hono = require('./hono')
2
2
  const fetch = require('node-fetch')
3
3
 
4
- const app = new Hono()
5
-
6
- describe('GET match', () => {
4
+ describe('GET Request', () => {
5
+ const app = Hono()
7
6
  app.get('/hello', () => {
8
7
  return new fetch.Response('hello', {
9
- status: 200
8
+ status: 200,
10
9
  })
11
10
  })
12
11
  app.notFound = () => {
13
12
  return new fetch.Response('not found', {
14
- status: 404
13
+ status: 404,
15
14
  })
16
15
  }
17
- it('GET /hello is ok', () => {
16
+ it('GET /hello is ok', async () => {
18
17
  let req = new fetch.Request('https://example.com/hello')
19
- let res = app.dispatch(req)
18
+ let res = await app.dispatch(req, new fetch.Response())
20
19
  expect(res).not.toBeNull()
21
20
  expect(res.status).toBe(200)
21
+ expect(await res.text()).toBe('hello')
22
22
  })
23
- it('GET / is not found', () => {
23
+ it('GET / is not found', async () => {
24
24
  let req = new fetch.Request('https://example.com/')
25
- let res = app.dispatch(req)
25
+ let res = await app.dispatch(req, new fetch.Response())
26
26
  expect(res).not.toBeNull()
27
27
  expect(res.status).toBe(404)
28
28
  })
29
29
  })
30
+
31
+ describe('params and query', () => {
32
+ const app = Hono()
33
+
34
+ app.get('/entry/:id', async (c) => {
35
+ const id = await c.req.params('id')
36
+ return new fetch.Response(`id is ${id}`, {
37
+ status: 200,
38
+ })
39
+ })
40
+
41
+ app.get('/search', async (c) => {
42
+ const name = await c.req.query('name')
43
+ return new fetch.Response(`name is ${name}`, {
44
+ status: 200,
45
+ })
46
+ })
47
+
48
+ it('params of /entry/:id is found', async () => {
49
+ let req = new fetch.Request('https://example.com/entry/123')
50
+ let res = await app.dispatch(req)
51
+ expect(res.status).toBe(200)
52
+ expect(await res.text()).toBe('id is 123')
53
+ })
54
+ it('query of /search?name=sam is found', async () => {
55
+ let req = new fetch.Request('https://example.com/search?name=sam')
56
+ let res = await app.dispatch(req)
57
+ expect(res.status).toBe(200)
58
+ expect(await res.text()).toBe('name is sam')
59
+ })
60
+ })
61
+
62
+ describe('Middleware', () => {
63
+ const app = Hono()
64
+
65
+ const logger = async (c, next) => {
66
+ console.log(`${c.req.method} : ${c.req.url}`)
67
+ next()
68
+ }
69
+
70
+ const rootHeader = async (c, next) => {
71
+ next()
72
+ await c.res.headers.append('x-custom', 'root')
73
+ }
74
+
75
+ const customHeader = async (c, next) => {
76
+ next()
77
+ await c.res.headers.append('x-message', 'custom-header')
78
+ }
79
+ const customHeader2 = async (c, next) => {
80
+ next()
81
+ await c.res.headers.append('x-message-2', 'custom-header-2')
82
+ }
83
+
84
+ app.use('*', logger)
85
+ app.use('*', rootHeader)
86
+ app.use('/hello', customHeader)
87
+ app.use('/hello/*', customHeader2)
88
+ app.get('/hello', () => {
89
+ return new fetch.Response('hello')
90
+ })
91
+ app.get('/hello/:message', (c) => {
92
+ const message = c.req.params('message')
93
+ return new fetch.Response(`${message}`)
94
+ })
95
+
96
+ it('logging and custom header', async () => {
97
+ let req = new fetch.Request('https://example.com/hello')
98
+ let res = await app.dispatch(req)
99
+ expect(res.status).toBe(200)
100
+ expect(await res.text()).toBe('hello')
101
+ expect(await res.headers.get('x-custom')).toBe('root')
102
+ expect(await res.headers.get('x-message')).toBe('custom-header')
103
+ expect(await res.headers.get('x-message-2')).toBe('custom-header-2')
104
+ })
105
+
106
+ it('logging and custom header with named params', async () => {
107
+ let req = new fetch.Request('https://example.com/hello/message')
108
+ let res = await app.dispatch(req)
109
+ expect(res.status).toBe(200)
110
+ expect(await res.text()).toBe('message')
111
+ expect(await res.headers.get('x-custom')).toBe('root')
112
+ expect(await res.headers.get('x-message-2')).toBe('custom-header-2')
113
+ })
114
+ })
@@ -0,0 +1,19 @@
1
+ const defaultFilter = (c, next) => {
2
+ c.req.query = (key) => {
3
+ const url = new URL(c.req.url)
4
+ return url.searchParams.get(key)
5
+ }
6
+
7
+ next()
8
+
9
+ if (typeof c.res === 'string') {
10
+ c.res = new Reponse(c.res, {
11
+ status: 200,
12
+ headers: {
13
+ 'Conten-Type': 'text/plain',
14
+ },
15
+ })
16
+ }
17
+ }
18
+
19
+ module.exports = defaultFilter
package/src/node.js ADDED
@@ -0,0 +1,97 @@
1
+ 'use strict'
2
+
3
+ const { splitPath, getPattern } = require('./util')
4
+
5
+ const METHOD_NAME_OF_ALL = 'ALL'
6
+
7
+ const createResult = (handler, params) => {
8
+ return { handler: handler, params: params }
9
+ }
10
+
11
+ const noRoute = () => {
12
+ return null
13
+ }
14
+
15
+ function Node(method, handler, children) {
16
+ this.children = children || {}
17
+ this.method = {}
18
+ if (method && handler) {
19
+ this.method[method] = handler
20
+ }
21
+ this.middlewares = []
22
+ }
23
+
24
+ Node.prototype.insert = function (method, path, handler) {
25
+ let curNode = this
26
+ const parts = splitPath(path)
27
+ for (let i = 0; i < parts.length; i++) {
28
+ const p = parts[i]
29
+ if (Object.keys(curNode.children).includes(p)) {
30
+ curNode = curNode.children[p]
31
+ continue
32
+ }
33
+ curNode.children[p] = new Node(method, handler)
34
+ curNode = curNode.children[p]
35
+ }
36
+ curNode.method[method] = handler
37
+ return curNode
38
+ }
39
+
40
+ Node.prototype.search = function (method, path) {
41
+ let curNode = this
42
+
43
+ const params = {}
44
+ const parts = splitPath(path)
45
+
46
+ for (let i = 0; i < parts.length; i++) {
47
+ const p = parts[i]
48
+ const nextNode = curNode.children[p]
49
+ if (nextNode) {
50
+ curNode = nextNode
51
+ continue
52
+ }
53
+
54
+ let isWildcard = false
55
+ let isParamMatch = false
56
+ const keys = Object.keys(curNode.children)
57
+ for (let j = 0; j < keys.length; j++) {
58
+ const key = keys[j]
59
+ // Wildcard
60
+ if (key === '*') {
61
+ curNode = curNode.children['*']
62
+ isWildcard = true
63
+ break
64
+ }
65
+ const pattern = getPattern(key)
66
+ if (pattern) {
67
+ const match = p.match(new RegExp(pattern[1]))
68
+ if (match) {
69
+ const k = pattern[0]
70
+ params[k] = match[1]
71
+ curNode = curNode.children[key]
72
+ isParamMatch = true
73
+ break
74
+ }
75
+ return noRoute()
76
+ }
77
+ }
78
+
79
+ if (isWildcard) {
80
+ break
81
+ }
82
+
83
+ if (isParamMatch === false) {
84
+ return noRoute()
85
+ }
86
+ }
87
+
88
+ const handler = curNode.method[METHOD_NAME_OF_ALL] || curNode.method[method]
89
+
90
+ if (!handler) {
91
+ return noRoute()
92
+ }
93
+
94
+ return createResult(handler, params)
95
+ }
96
+
97
+ module.exports = Node
@@ -0,0 +1,135 @@
1
+ const Node = require('./node')
2
+
3
+ describe('Root Node', () => {
4
+ const node = new Node()
5
+ node.insert('get', '/', 'get root')
6
+ it('get /', () => {
7
+ let res = node.search('get', '/')
8
+ expect(res).not.toBeNull()
9
+ expect(res.handler).toBe('get root')
10
+ expect(node.search('get', '/hello')).toBeNull()
11
+ })
12
+ })
13
+
14
+ describe('Root Node id not defined', () => {
15
+ const node = new Node()
16
+ node.insert('get', '/hello', 'get hello')
17
+ it('get /', () => {
18
+ expect(node.search('get', '/')).toBeNull()
19
+ })
20
+ })
21
+
22
+ describe('All with *', () => {
23
+ const node = new Node()
24
+ node.insert('get', '*', 'get all')
25
+ it('get /', () => {
26
+ expect(node.search('get', '/')).not.toBeNull()
27
+ expect(node.search('get', '/hello')).not.toBeNull()
28
+ })
29
+ })
30
+
31
+ describe('Basic Usage', () => {
32
+ const node = new Node()
33
+ node.insert('get', '/hello', 'get hello')
34
+ node.insert('post', '/hello', 'post hello')
35
+ node.insert('get', '/hello/foo', 'get hello foo')
36
+
37
+ it('get, post /hello', () => {
38
+ expect(node.search('get', '/')).toBeNull()
39
+ expect(node.search('post', '/')).toBeNull()
40
+
41
+ expect(node.search('get', '/hello').handler).toBe('get hello')
42
+ expect(node.search('post', '/hello').handler).toBe('post hello')
43
+ expect(node.search('put', '/hello')).toBeNull()
44
+ })
45
+ it('get /nothing', () => {
46
+ expect(node.search('get', '/nothing')).toBeNull()
47
+ })
48
+ it('/hello/foo, /hello/bar', () => {
49
+ expect(node.search('get', '/hello/foo').handler).toBe('get hello foo')
50
+ expect(node.search('post', '/hello/foo')).toBeNull()
51
+ expect(node.search('get', '/hello/bar')).toBeNull()
52
+ })
53
+ it('/hello/foo/bar', () => {
54
+ expect(node.search('get', '/hello/foo/bar')).toBeNull()
55
+ })
56
+ })
57
+
58
+ describe('Name path', () => {
59
+ const node = new Node()
60
+ it('get /entry/123', () => {
61
+ node.insert('get', '/entry/:id', 'get entry')
62
+ let res = node.search('get', '/entry/123')
63
+ expect(res).not.toBeNull()
64
+ expect(res.handler).toBe('get entry')
65
+ expect(res.params).not.toBeNull()
66
+ expect(res.params['id']).toBe('123')
67
+ expect(res.params['id']).not.toBe('1234')
68
+ })
69
+
70
+ it('get /entry/456/comment', () => {
71
+ node.insert('get', '/entry/:id', 'get entry')
72
+ let res = node.search('get', '/entry/456/comment')
73
+ expect(res).toBeNull()
74
+ })
75
+
76
+ it('get /entry/789/comment/123', () => {
77
+ node.insert('get', '/entry/:id/comment/:comment_id', 'get comment')
78
+ let res = node.search('get', '/entry/789/comment/123')
79
+ expect(res).not.toBeNull()
80
+ expect(res.handler).toBe('get comment')
81
+ expect(res.params['id']).toBe('789')
82
+ expect(res.params['comment_id']).toBe('123')
83
+ })
84
+
85
+ it('get /map/:location/events', () => {
86
+ node.insert('get', '/map/:location/events', 'get events')
87
+ let res = node.search('get', '/map/yokohama/events')
88
+ expect(res).not.toBeNull()
89
+ expect(res.handler).toBe('get events')
90
+ expect(res.params['location']).toBe('yokohama')
91
+ })
92
+ })
93
+
94
+ describe('Wildcard', () => {
95
+ const node = new Node()
96
+ it('/wildcard-abc/xxxxxx/wildcard-efg', () => {
97
+ node.insert('get', '/wildcard-abc/*/wildcard-efg', 'wildcard')
98
+ let res = node.search('get', '/wildcard-abc/xxxxxx/wildcard-efg')
99
+ expect(res).not.toBeNull()
100
+ expect(res.handler).toBe('wildcard')
101
+ })
102
+ })
103
+
104
+ describe('Regexp', () => {
105
+ const node = new Node()
106
+ node.insert('get', '/regex-abc/:id{[0-9]+}/comment/:comment_id{[a-z]+}', 'regexp')
107
+ it('/regexp-abc/123/comment/abc', () => {
108
+ res = node.search('get', '/regex-abc/123/comment/abc')
109
+ expect(res).not.toBeNull()
110
+ expect(res.handler).toBe('regexp')
111
+ expect(res.params['id']).toBe('123')
112
+ expect(res.params['comment_id']).toBe('abc')
113
+ })
114
+ it('/regexp-abc/abc', () => {
115
+ res = node.search('get', '/regex-abc/abc')
116
+ expect(res).toBeNull()
117
+ })
118
+ it('/regexp-abc/123/comment/123', () => {
119
+ res = node.search('get', '/regex-abc/123/comment/123')
120
+ expect(res).toBeNull()
121
+ })
122
+ })
123
+
124
+ describe('All', () => {
125
+ const node = new Node()
126
+ node.insert('ALL', '/all-methods', 'all methods') // ALL
127
+ it('/all-methods', () => {
128
+ res = node.search('get', '/all-methods')
129
+ expect(res).not.toBeNull()
130
+ expect(res.handler).toBe('all methods')
131
+ res = node.search('put', '/all-methods')
132
+ expect(res).not.toBeNull()
133
+ expect(res.handler).toBe('all methods')
134
+ })
135
+ })
@@ -1,66 +1,88 @@
1
- const Router = require('./router')
2
-
3
- let router = new Router()
4
-
5
- describe('root match', () => {
6
- it('/ match', () => {
7
- router.add('/', 'root')
8
- let match = router.match('/')
9
- expect(match).not.toBeNull()
10
- expect(match[0]).toBe('root')
11
- match = router.match('/foo')
12
- expect(match).toBeNull()
13
- })
1
+ const App = require('./hono')
2
+
3
+ describe('Basic Usage', () => {
4
+ const router = App()
5
+
6
+ it('get, post hello', async () => {
7
+ router.get('/hello', 'get hello')
8
+ router.post('/hello', 'post hello')
9
+
10
+ let res = await router.matchRoute('GET', '/hello')
11
+ expect(res).not.toBeNull()
12
+ expect(res.handler[0]).toBe('get hello')
14
13
 
15
- it('/hello match', () => {
16
- router.add('/hello', 'hello')
17
- match = router.match('/foo')
18
- expect(match).toBeNull()
19
- match = router.match('/hello')
20
- expect(match[0]).toBe('hello')
14
+ res = await router.matchRoute('POST', '/hello')
15
+ expect(res).not.toBeNull()
16
+ expect(res.handler[0]).toBe('post hello')
17
+
18
+ res = await router.matchRoute('PUT', '/hello')
19
+ expect(res).toBeNull()
20
+
21
+ res = await router.matchRoute('GET', '/')
22
+ expect(res).toBeNull()
21
23
  })
22
24
  })
23
25
 
24
- describe('path match', () => {
25
- router.add('/entry/:id', 'entry-id')
26
- router.add('/entry/:id/:comment', 'entry-id-comment')
27
- router.add('/year/:year{[0-9]{4}}/:month{[0-9]{2}}', 'date-regex')
26
+ describe('Complex', () => {
27
+ let router = App()
28
28
 
29
- it('entry id match', () => {
30
- const match = router.match('/entry/123')
31
- expect(match[0]).toBe('entry-id')
32
- expect(match[1]['id']).toBe('123')
29
+ it('Named Param', async () => {
30
+ router.get('/entry/:id', 'get entry')
31
+ res = await router.matchRoute('GET', '/entry/123')
32
+ expect(res).not.toBeNull()
33
+ expect(res.handler[0]).toBe('get entry')
34
+ expect(res.params['id']).toBe('123')
33
35
  })
34
36
 
35
- it('entry id and comment match', () => {
36
- const match = router.match('/entry/123/45678')
37
- expect(match[0]).toBe('entry-id-comment')
38
- expect(match[1]['id']).toBe('123')
39
- expect(match[1]['comment']).toBe('45678')
37
+ it('Wildcard', async () => {
38
+ router.get('/wild/*/card', 'get wildcard')
39
+ res = await router.matchRoute('GET', '/wild/xxx/card')
40
+ expect(res).not.toBeNull()
41
+ expect(res.handler[0]).toBe('get wildcard')
40
42
  })
41
43
 
42
- it('date-regex', () => {
43
- const match = router.match('/year/2021/12')
44
- expect(match[0]).toBe('date-regex')
45
- expect(match[1]['year']).toBe('2021')
46
- expect(match[1]['month']).toBe('12')
44
+ it('Regexp', async () => {
45
+ router.get('/post/:date{[0-9]+}/:title{[a-z]+}', 'get post')
46
+ res = await router.matchRoute('GET', '/post/20210101/hello')
47
+ expect(res).not.toBeNull()
48
+ expect(res.handler[0]).toBe('get post')
49
+ expect(res.params['date']).toBe('20210101')
50
+ expect(res.params['title']).toBe('hello')
51
+ res = await router.matchRoute('GET', '/post/onetwothree')
52
+ expect(res).toBeNull()
53
+ res = await router.matchRoute('GET', '/post/123/123')
54
+ expect(res).toBeNull()
47
55
  })
56
+ })
48
57
 
49
- it('not match', () => {
50
- let match = router.match('/foo')
51
- expect(match).toBeNull()
52
- match = router.match('/year/abc')
53
- expect(match).toBeNull()
58
+ describe('Chained Route', () => {
59
+ let router = App()
60
+
61
+ it('Return rooter object', async () => {
62
+ router = router.patch('/hello', 'patch hello')
63
+ expect(router).not.toBeNull()
64
+ router = router.delete('/hello', 'delete hello')
65
+ res = await router.matchRoute('DELETE', '/hello')
66
+ expect(res).not.toBeNull()
67
+ expect(res.handler[0]).toBe('delete hello')
54
68
  })
55
- })
56
69
 
57
- describe('wildcard', () => {
58
- it('match', () => {
59
- router = new Router()
60
- router.add('/abc/*/def')
61
- let match = router.match('/abc/xxx/def')
62
- expect(match).not.toBeNull()
63
- match = router.match('/abc/xxx/abc')
64
- expect(match).toBeNull()
70
+ it('Chain with route method', async () => {
71
+ router.route('/api/book').get('get book').post('post book').put('put book')
72
+
73
+ res = await router.matchRoute('GET', '/api/book')
74
+ expect(res).not.toBeNull()
75
+ expect(res.handler[0]).toBe('get book')
76
+
77
+ res = await router.matchRoute('POST', '/api/book')
78
+ expect(res).not.toBeNull()
79
+ expect(res.handler[0]).toBe('post book')
80
+
81
+ res = await router.matchRoute('PUT', '/api/book')
82
+ expect(res).not.toBeNull()
83
+ expect(res.handler[0]).toBe('put book')
84
+
85
+ res = await router.matchRoute('DELETE', '/api/book')
86
+ expect(res).toBeNull()
65
87
  })
66
88
  })
package/src/util.js ADDED
@@ -0,0 +1,26 @@
1
+ const splitPath = (path) => {
2
+ path = path.split(/\//) // faster than path.split('/')
3
+ if (path[0] === '') {
4
+ path.shift()
5
+ }
6
+ return path
7
+ }
8
+
9
+ const getPattern = (label) => {
10
+ // :id{[0-9]+} => ([0-9]+)
11
+ // :id => (.+)
12
+ //const name = ''
13
+ const match = label.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/)
14
+ if (match) {
15
+ if (match[2]) {
16
+ return [match[1], '(' + match[2] + ')']
17
+ } else {
18
+ return [match[1], '(.+)']
19
+ }
20
+ }
21
+ }
22
+
23
+ module.exports = {
24
+ splitPath,
25
+ getPattern,
26
+ }
@@ -0,0 +1,29 @@
1
+ const { splitPath, getPattern } = require('./util')
2
+
3
+ describe('Utility methods', () => {
4
+ it('splitPath', () => {
5
+ let ps = splitPath('/')
6
+ expect(ps[0]).toBe('')
7
+ ps = splitPath('/hello')
8
+ expect(ps[0]).toBe('hello')
9
+ ps = splitPath('*')
10
+ expect(ps[0]).toBe('*')
11
+ ps = splitPath('/wildcard-abc/*/wildcard-efg')
12
+ expect(ps[0]).toBe('wildcard-abc')
13
+ expect(ps[1]).toBe('*')
14
+ expect(ps[2]).toBe('wildcard-efg')
15
+ ps = splitPath('/map/:location/events')
16
+ expect(ps[0]).toBe('map')
17
+ expect(ps[1]).toBe(':location')
18
+ expect(ps[2]).toBe('events')
19
+ })
20
+
21
+ it('getPattern', () => {
22
+ let res = getPattern(':id')
23
+ expect(res[0]).toBe('id')
24
+ expect(res[1]).toBe('(.+)')
25
+ res = getPattern(':id{[0-9]+}')
26
+ expect(res[0]).toBe('id')
27
+ expect(res[1]).toBe('([0-9]+)')
28
+ })
29
+ })
package/src/router.js DELETED
@@ -1,116 +0,0 @@
1
- // Ref: https://github.com/bmf-san/goblin
2
-
3
- class Router {
4
- constructor() {
5
- this.node = new Node({ label: "/" })
6
- }
7
- add(path, stuff) {
8
- this.node.insert(path, stuff);
9
- }
10
- match(path) {
11
- return this.node.search(path);
12
- }
13
- }
14
-
15
- class Node {
16
- constructor({ label, stuff, children } = {}) {
17
- this.label = label || "";
18
- this.stuff = stuff || {};
19
- this.children = children || [];
20
- }
21
-
22
- insert(path, stuff) {
23
- let curNode = this
24
- if (path == '/') {
25
- curNode.label = path
26
- curNode.stuff = stuff
27
- }
28
- const ps = this.splitPath(path)
29
- for (const p of ps) {
30
- let nextNode = curNode.children[p]
31
- if (nextNode) {
32
- curNode = nextNode
33
- } else {
34
- curNode.children[p] = new Node({ label: p, stuff: stuff, children: [] })
35
- curNode = curNode.children[p]
36
- }
37
- }
38
- }
39
-
40
- splitPath(path) {
41
- const ps = []
42
- for (const p of path.split('/')) {
43
- if (p) {
44
- ps.push(p)
45
- }
46
- }
47
- return ps
48
- }
49
-
50
- getPattern(label) {
51
- // :id{[0-9]+} → [0-9]+$
52
- // :id → (.+)
53
- const match = label.match(/^\:.+?\{(.+)\}$/)
54
- if (match) {
55
- return '(' + match[1] + ')'
56
- }
57
- return '(.+)'
58
- }
59
-
60
- getParamName(label) {
61
- const match = label.match(/^\:([^\{\}]+)/)
62
- if (match) {
63
- return match[1]
64
- }
65
- }
66
-
67
- noRoute() {
68
- return null
69
- }
70
-
71
- search(path) {
72
-
73
- let curNode = this
74
- const params = {}
75
-
76
- for (const p of this.splitPath(path)) {
77
- const nextNode = curNode.children[p]
78
- if (nextNode) {
79
- curNode = nextNode
80
- continue
81
- }
82
- if (Object.keys(curNode.children).length == 0) {
83
- if (curNode.label != p) {
84
- return this.noRoute()
85
- }
86
- break
87
- }
88
- let isParamMatch = false
89
- for (const key in curNode.children) {
90
- if (key == "*") { // Wildcard
91
- curNode = curNode.children[key]
92
- isParamMatch = true
93
- break
94
- }
95
- if (key.match(/^:/)) {
96
- const pattern = this.getPattern(key)
97
- const match = p.match(new RegExp(pattern))
98
- if (match) {
99
- const k = this.getParamName(key)
100
- params[k] = match[0]
101
- curNode = curNode.children[key]
102
- isParamMatch = true
103
- break
104
- }
105
- return this.noRoute()
106
- }
107
- }
108
- if (isParamMatch == false) {
109
- return this.noRoute()
110
- }
111
- }
112
- return [curNode.stuff, params]
113
- }
114
- }
115
-
116
- module.exports = Router