hono 0.0.1 → 0.0.5

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,4 +1,172 @@
1
- ## hono
1
+ # Hono
2
+
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
+ sundar x 307,438 ops/sec ±4.77% (73 runs sampled)
28
+ Fastest is hono
29
+ ✨ Done in 37.03s.
30
+ ```
31
+
32
+ ## Install
33
+
34
+ ```
35
+ $ yarn add hono
36
+ ```
37
+
38
+ or
39
+
40
+ ```sh
41
+ $ npm install hono
42
+ ```
43
+
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
94
+
95
+ ```js
96
+ app
97
+ .route('/api/book')
98
+ .get(() => {...})
99
+ .post(() => {...})
100
+ .put(() => {...})
101
+ ```
102
+
103
+ ## Middleware
104
+
105
+ ```js
106
+ const logger = (c, next) => {
107
+ console.log(`[${c.req.method}] ${c.req.url}`)
108
+ next()
109
+ }
110
+
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
+ })
131
+ ```
132
+
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>
2
170
 
3
171
  ## Author
4
172
 
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "hono",
3
- "version": "0.0.1",
3
+ "version": "0.0.5",
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",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/yusukebe/hono.git"
14
+ },
15
+ "homepage": "https://github.com/yusukebe/hono",
11
16
  "devDependencies": {
12
17
  "jest": "^27.4.5",
13
18
  "node-fetch": "^2.6.6"
14
19
  }
15
- }
20
+ }
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,144 @@
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
+ class Router {
8
+ constructor() {
9
+ this.node = new Node()
10
+ this.tempPath = '/'
11
+ }
12
+
13
+ add(method, path, ...handlers) {
14
+ this.node.insert(method, path, handlers)
15
+ }
16
+
17
+ match(method, path) {
18
+ return this.node.search(method, path)
7
19
  }
8
20
  }
9
21
 
10
- class App {
22
+ const getPathFromURL = (url) => {
23
+ // XXX
24
+ const match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/)
25
+ return match[5]
26
+ }
27
+
28
+ class Hono {
11
29
  constructor() {
12
- this.router = new Router();
30
+ this.router = new Router()
31
+ this.middlewareRouter = new Router()
32
+ this.middlewareRouters = []
13
33
  }
14
34
 
15
- addRoute(method, path, handler) {
16
- this.router.add(path, new Route(method, handler))
35
+ get(...args) {
36
+ return this.addRoute('GET', ...args)
37
+ }
38
+ post(...args) {
39
+ return this.addRoute('POST', ...args)
40
+ }
41
+ put(...args) {
42
+ return this.addRoute('PUT', ...args)
43
+ }
44
+ delete(...args) {
45
+ return this.addRoute('DELETE', ...args)
46
+ }
47
+ patch(...args) {
48
+ return this.addRoute('PATCH', ...args)
17
49
  }
18
50
 
19
- handle(event) {
20
- const response = this.dispatch(event.request)
21
- return event.respondWith(response)
51
+ addRoute(method, ...args) {
52
+ method = method.toUpperCase()
53
+ if (args.length === 1) {
54
+ this.router.add(method, this.router.tempPath, ...args)
55
+ } else {
56
+ this.router.add(method, ...args)
57
+ }
58
+ return this
22
59
  }
23
60
 
24
- dispatch(request) {
25
- const url = new URL(request.url)
26
- const path = url.pathname
27
- const match = this.router.match(path)
61
+ getRouter() {
62
+ return this.router
63
+ }
28
64
 
29
- if (!match) {
30
- return this.notFound()
31
- }
65
+ route(path) {
66
+ this.router.tempPath = path
67
+ return this
68
+ }
69
+
70
+ use(path, middleware) {
71
+ const router = new Router()
72
+ router.add('all', path, middleware)
73
+ this.middlewareRouters.push(router)
74
+ }
75
+
76
+ async matchRoute(method, path) {
77
+ const res = this.router.match(method, path)
78
+ return res
79
+ }
32
80
 
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)
81
+ // XXX
82
+ async createContext(req, res) {
83
+ return {
84
+ req: req,
85
+ res: res,
86
+ newResponse: (params) => {
87
+ return new Response(params)
88
+ },
38
89
  }
39
- return this.notFound()
40
90
  }
41
91
 
42
- notFound() {
43
- return new Response('Not Found', {
44
- status: 404,
45
- headers: {
46
- 'content-type': 'text/plain'
92
+ async dispatch(request, response) {
93
+ const [method, path] = [request.method, getPathFromURL(request.url)]
94
+
95
+ const result = await this.matchRoute(method, path)
96
+ if (!result) return this.notFound()
97
+
98
+ request.params = (key) => result.params[key]
99
+
100
+ let handler = result.handler[0] // XXX
101
+
102
+ const middleware = [defaultFilter] // add defaultFilter later
103
+
104
+ for (const mr of this.middlewareRouters) {
105
+ const mwResult = mr.match('all', path)
106
+ if (mwResult) {
107
+ middleware.push(...mwResult.handler)
47
108
  }
48
- })
109
+ }
110
+
111
+ let wrappedHandler = async (context, next) => {
112
+ context.res = handler(context)
113
+ next()
114
+ }
115
+
116
+ middleware.push(wrappedHandler)
117
+ const composed = compose(middleware)
118
+ const c = await this.createContext(request, response)
119
+
120
+ composed(c)
121
+
122
+ return c.res
123
+ }
124
+
125
+ async handleEvent(event) {
126
+ return this.dispatch(event.request, {}) // XXX
49
127
  }
50
128
 
51
129
  fire() {
52
- addEventListener("fetch", (event) => {
53
- this.handle(event)
130
+ addEventListener('fetch', (event) => {
131
+ event.respondWith(this.handleEvent(event))
54
132
  })
55
133
  }
56
- }
57
134
 
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
- }
135
+ notFound() {
136
+ return new Response('Not Found', { status: 404 })
66
137
  }
67
138
  }
68
139
 
69
- const app = new App()
70
-
71
- function Hono() {
72
- return new Proxy(
73
- app, proxyHandler
74
- )
140
+ const CreateApp = () => {
141
+ return new Hono()
75
142
  }
76
143
 
77
- module.exports = Hono
144
+ module.exports = CreateApp
package/src/hono.test.js CHANGED
@@ -1,29 +1,88 @@
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 = (c, next) => {
66
+ console.log(`${c.req.method} : ${c.req.url}`)
67
+ next()
68
+ }
69
+
70
+ const customHeader = (c, next) => {
71
+ next()
72
+ c.res.headers.append('x-message', 'custom-header')
73
+ }
74
+
75
+ app.use('*', logger)
76
+ app.use('/hello', customHeader)
77
+ app.get('/hello', () => {
78
+ return new fetch.Response('hello')
79
+ })
80
+
81
+ it('logging and custom header', async () => {
82
+ let req = new fetch.Request('https://example.com/hello')
83
+ let res = await app.dispatch(req)
84
+ expect(res.status).toBe(200)
85
+ expect(await res.text()).toBe('hello')
86
+ expect(await res.headers.get('x-message')).toBe('custom-header')
87
+ })
88
+ })
@@ -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,91 @@
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
+ }
22
+
23
+ Node.prototype.insert = function (method, path, handler) {
24
+ let curNode = this
25
+ const parts = splitPath(path)
26
+ for (let i = 0; i < parts.length; i++) {
27
+ const p = parts[i]
28
+ if (Object.keys(curNode.children).includes(p)) {
29
+ curNode = curNode.children[p]
30
+ continue
31
+ }
32
+ curNode.children[p] = new Node(method, handler)
33
+ curNode = curNode.children[p]
34
+ }
35
+ curNode.method[method] = handler
36
+ return curNode
37
+ }
38
+
39
+ Node.prototype.search = function (method, path) {
40
+ let curNode = this
41
+
42
+ const params = {}
43
+ const parts = splitPath(path)
44
+
45
+ for (let i = 0; i < parts.length; i++) {
46
+ const p = parts[i]
47
+ const nextNode = curNode.children[p]
48
+ if (nextNode) {
49
+ curNode = nextNode
50
+ continue
51
+ }
52
+
53
+ let isParamMatch = false
54
+ const keys = Object.keys(curNode.children)
55
+ for (let j = 0; j < keys.length; j++) {
56
+ const key = keys[j]
57
+ // Wildcard
58
+ if (key === '*') {
59
+ curNode = curNode.children['*']
60
+ isParamMatch = true
61
+ break
62
+ }
63
+ const pattern = getPattern(key)
64
+ if (pattern) {
65
+ const match = p.match(new RegExp(pattern[1]))
66
+ if (match) {
67
+ const k = pattern[0]
68
+ params[k] = match[1]
69
+ curNode = curNode.children[key]
70
+ isParamMatch = true
71
+ break
72
+ }
73
+ return noRoute()
74
+ }
75
+ }
76
+
77
+ if (isParamMatch === false) {
78
+ return noRoute()
79
+ }
80
+ }
81
+
82
+ const handler = curNode.method[METHOD_NAME_OF_ALL] || curNode.method[method]
83
+
84
+ if (!handler) {
85
+ return noRoute()
86
+ }
87
+
88
+ return createResult(handler, params)
89
+ }
90
+
91
+ 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')
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, getParamName } = 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