galbe 0.1.0
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/.github/workflows/build_test.yml +19 -0
- package/.github/workflows/deploy_website.yml +20 -0
- package/.prettierrc +10 -0
- package/README.md +39 -0
- package/bin/cli.ts +114 -0
- package/bun.lockb +0 -0
- package/docs/Getting Started/basic.md +1 -0
- package/docs/Guides/routes.md +1 -0
- package/docs/getting-started.md +5 -0
- package/package.json +49 -0
- package/scripts/build.ts +14 -0
- package/scripts/hooks/prepare-commit-msg +10 -0
- package/scripts/postinstall.ts +9 -0
- package/src/index.ts +353 -0
- package/src/parser.ts +656 -0
- package/src/router.ts +100 -0
- package/src/routes.ts +157 -0
- package/src/server.ts +174 -0
- package/src/types.ts +279 -0
- package/src/util.ts +12 -0
- package/src/validator.ts +117 -0
- package/test/hooks.test.ts +177 -0
- package/test/parser.test.ts +1198 -0
- package/test/plugins.test.ts +239 -0
- package/test/requests.test.ts +909 -0
- package/test/resources/image.png +0 -0
- package/test/resources/object.badSyntax.json +8 -0
- package/test/resources/object.json +8 -0
- package/test/resources/object.missing.json +6 -0
- package/test/resources/test.route.comment.ts +38 -0
- package/test/resources/test.route.empty.ts +9 -0
- package/test/responses.test.ts +109 -0
- package/test/routeFiles.test.ts +209 -0
- package/test/router.test.ts +207 -0
- package/test/test.utils.ts +102 -0
- package/tsconfig.json +23 -0
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Galbe } from '../../src'
|
|
2
|
+
import { T } from '../../src'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* description
|
|
6
|
+
* multiline
|
|
7
|
+
* @tag test
|
|
8
|
+
* ok
|
|
9
|
+
*/
|
|
10
|
+
export default (g: Galbe) => {
|
|
11
|
+
/**
|
|
12
|
+
* This part
|
|
13
|
+
* here
|
|
14
|
+
* @tags tag1, tag2, tag3
|
|
15
|
+
* @summary short summary
|
|
16
|
+
* @description longer description example
|
|
17
|
+
* @deprecated
|
|
18
|
+
* @param {path} param1 description
|
|
19
|
+
* @param {query} param2 description
|
|
20
|
+
*/
|
|
21
|
+
g.get('/test/:param1', _ => {})
|
|
22
|
+
|
|
23
|
+
// Comment between enpoints
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @tags tag1, tag2, tag3
|
|
27
|
+
* @summary short summary
|
|
28
|
+
* @description longer description example
|
|
29
|
+
* @body body descripton
|
|
30
|
+
*/
|
|
31
|
+
g.post('/test', { body: T.Object({ foo: T.String() }) }, _ => {})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @tags tag1, tag2
|
|
35
|
+
* @other Hello Mom!
|
|
36
|
+
*/
|
|
37
|
+
g.put('/test', { body: T.Object({ foo: T.String() }) }, [() => {}], _ => {})
|
|
38
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { expect, test, describe, beforeAll } from 'bun:test'
|
|
2
|
+
import { Galbe, T } from '../src'
|
|
3
|
+
import { decoder } from './test.utils'
|
|
4
|
+
|
|
5
|
+
const port = 7359
|
|
6
|
+
|
|
7
|
+
const UUID_RGX = '[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'
|
|
8
|
+
|
|
9
|
+
function* genTxt(text: string) {
|
|
10
|
+
const words = text.split(' ')
|
|
11
|
+
for (const w of words) yield w
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const rsTxt = (text: string) => {
|
|
15
|
+
return new ReadableStream({
|
|
16
|
+
async start(controller) {
|
|
17
|
+
const words = text.split(' ')
|
|
18
|
+
for (const w of words) controller.enqueue(w)
|
|
19
|
+
controller.close()
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('responses', () => {
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
const galbe = new Galbe()
|
|
27
|
+
|
|
28
|
+
galbe.post(
|
|
29
|
+
'/response',
|
|
30
|
+
{
|
|
31
|
+
body: T.Optional(T.Object(T.Any())),
|
|
32
|
+
query: { text: T.Optional(T.String()), stream: T.Optional(T.String()) }
|
|
33
|
+
},
|
|
34
|
+
ctx => {
|
|
35
|
+
if (ctx.query.text) {
|
|
36
|
+
if (ctx.query.stream === 'generatorFunction') return genTxt(ctx.query.text)
|
|
37
|
+
if (ctx.query.stream === 'readableStream') return rsTxt(ctx.query.text)
|
|
38
|
+
else return ctx.query.text
|
|
39
|
+
} else if (ctx.body) return ctx.body
|
|
40
|
+
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
galbe.get('/error', _ => {
|
|
46
|
+
return { next: () => {} }
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
galbe.onError(_ => {
|
|
50
|
+
throw new Error()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
await galbe.listen(port)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('response, string', async () => {
|
|
57
|
+
const reqTxt = 'Hello Mom!'
|
|
58
|
+
let resp = await fetch(`http://localhost:${port}/response?text=${reqTxt}`, {
|
|
59
|
+
method: 'POST'
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const body = await resp.text()
|
|
63
|
+
|
|
64
|
+
expect(resp.status).toBe(200)
|
|
65
|
+
expect(resp.headers.get('content-type')).toBe('text/plain')
|
|
66
|
+
expect(body).toBe(reqTxt)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('response, object', async () => {
|
|
70
|
+
const reqBody = { foo: 'bar' }
|
|
71
|
+
let resp = await fetch(`http://localhost:${port}/response`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: JSON.stringify(reqBody),
|
|
74
|
+
headers: {
|
|
75
|
+
'content-type': 'application/json'
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const body = await resp.json()
|
|
80
|
+
|
|
81
|
+
expect(resp.status).toBe(200)
|
|
82
|
+
expect(resp.headers.get('content-type')).toBe('application/json')
|
|
83
|
+
expect(body).toEqual(reqBody)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('response, stream', async () => {
|
|
87
|
+
const reqTxt = 'Hello Mom!'
|
|
88
|
+
const cases = [{ stream: 'generatorFunction' }, { stream: 'readableStream' }]
|
|
89
|
+
|
|
90
|
+
for (const c of cases) {
|
|
91
|
+
let resp = await fetch(`http://localhost:${port}/response?text=${reqTxt}&stream=${c.stream}`, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'content-type': 'application/json'
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
const reader = resp.body?.getReader()
|
|
98
|
+
let body = ''
|
|
99
|
+
while (reader) {
|
|
100
|
+
const { value, done } = await reader.read()
|
|
101
|
+
if (done) break
|
|
102
|
+
body += decoder.decode(value)
|
|
103
|
+
}
|
|
104
|
+
expect(resp.status).toBe(200)
|
|
105
|
+
expect(resp.headers.get('content-type')).toBe('text/event-stream')
|
|
106
|
+
expect(body).toMatch(new RegExp(`id:${UUID_RGX}\ndata:Hello\n\nid:${UUID_RGX}\ndata:Mom!\n\n`))
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
})
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { expect, test, describe } from 'bun:test'
|
|
2
|
+
import { defineRoutes, metaAnalysis } from '../src/routes'
|
|
3
|
+
import { Galbe } from '../src'
|
|
4
|
+
|
|
5
|
+
describe('routeFiles', () => {
|
|
6
|
+
test('meta analysis, empty', async () => {
|
|
7
|
+
let meta = await metaAnalysis('./test/resources/test.route.empty.ts')
|
|
8
|
+
expect(meta).toEqual({
|
|
9
|
+
header: {},
|
|
10
|
+
routes: {
|
|
11
|
+
'/one': {
|
|
12
|
+
get: {}
|
|
13
|
+
},
|
|
14
|
+
'/two': {
|
|
15
|
+
post: {}
|
|
16
|
+
},
|
|
17
|
+
'/three': {
|
|
18
|
+
put: {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('meta analysis, comments', async () => {
|
|
25
|
+
let meta = await metaAnalysis('./test/resources/test.route.comment.ts')
|
|
26
|
+
expect(meta).toEqual({
|
|
27
|
+
header: {
|
|
28
|
+
head: 'description\nmultiline',
|
|
29
|
+
tag: 'test'
|
|
30
|
+
},
|
|
31
|
+
routes: {
|
|
32
|
+
'/test/:param1': {
|
|
33
|
+
get: {
|
|
34
|
+
head: 'This part\nhere',
|
|
35
|
+
tags: 'tag1, tag2, tag3',
|
|
36
|
+
summary: 'short summary',
|
|
37
|
+
description: 'longer description example',
|
|
38
|
+
deprecated: true,
|
|
39
|
+
param: ['{path} param1 description', '{query} param2 description']
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
'/test': {
|
|
43
|
+
post: {
|
|
44
|
+
tags: 'tag1, tag2, tag3',
|
|
45
|
+
summary: 'short summary',
|
|
46
|
+
description: 'longer description example',
|
|
47
|
+
body: 'body descripton'
|
|
48
|
+
},
|
|
49
|
+
put: {
|
|
50
|
+
tags: 'tag1, tag2',
|
|
51
|
+
other: 'Hello Mom!'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('define routes, no route', async () => {
|
|
59
|
+
const k = new Galbe()
|
|
60
|
+
await defineRoutes({}, k)
|
|
61
|
+
expect(k.router.routes).toEqual({
|
|
62
|
+
GET: {},
|
|
63
|
+
POST: {},
|
|
64
|
+
PUT: {},
|
|
65
|
+
PATCH: {},
|
|
66
|
+
DELETE: {},
|
|
67
|
+
OPTIONS: {}
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('define routes, no route found', async () => {
|
|
72
|
+
const k = new Galbe()
|
|
73
|
+
await defineRoutes({ routes: 'unexisting_route' }, k)
|
|
74
|
+
expect(k.router.routes).toEqual({
|
|
75
|
+
GET: {},
|
|
76
|
+
POST: {},
|
|
77
|
+
PUT: {},
|
|
78
|
+
PATCH: {},
|
|
79
|
+
DELETE: {},
|
|
80
|
+
OPTIONS: {}
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('define routes, route.empty', async () => {
|
|
85
|
+
const k = new Galbe()
|
|
86
|
+
|
|
87
|
+
await defineRoutes({ routes: 'test/resources/test.route.empty.ts' }, k)
|
|
88
|
+
|
|
89
|
+
const r = k.router.routes
|
|
90
|
+
expect(r.GET?.children?.one?.route).toMatchObject({
|
|
91
|
+
method: 'get',
|
|
92
|
+
path: '/one',
|
|
93
|
+
handler: () => {}
|
|
94
|
+
})
|
|
95
|
+
expect(r.POST?.children?.two?.route).toMatchObject({
|
|
96
|
+
method: 'post',
|
|
97
|
+
path: '/two',
|
|
98
|
+
handler: () => {}
|
|
99
|
+
})
|
|
100
|
+
expect(r.PUT?.children?.three?.route).toMatchObject({
|
|
101
|
+
method: 'put',
|
|
102
|
+
path: '/three',
|
|
103
|
+
handler: () => {}
|
|
104
|
+
})
|
|
105
|
+
expect(k.meta).toMatchObject([
|
|
106
|
+
{
|
|
107
|
+
header: {},
|
|
108
|
+
routes: {
|
|
109
|
+
'/one': {
|
|
110
|
+
get: {}
|
|
111
|
+
},
|
|
112
|
+
'/two': {
|
|
113
|
+
post: {}
|
|
114
|
+
},
|
|
115
|
+
'/three': {
|
|
116
|
+
put: {}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
])
|
|
121
|
+
expect(k.meta?.[0].file).toMatch(/test\.route\.empty\.ts$/)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('define routes, all', async () => {
|
|
125
|
+
const k = new Galbe()
|
|
126
|
+
|
|
127
|
+
await defineRoutes({ routes: ['test/resources/test.route.*.ts'] }, k)
|
|
128
|
+
|
|
129
|
+
const r = k.router.routes
|
|
130
|
+
expect(r.GET?.children?.one?.route).toMatchObject({
|
|
131
|
+
method: 'get',
|
|
132
|
+
path: '/one',
|
|
133
|
+
handler: () => {}
|
|
134
|
+
})
|
|
135
|
+
expect(r.POST?.children?.two?.route).toMatchObject({
|
|
136
|
+
method: 'post',
|
|
137
|
+
path: '/two',
|
|
138
|
+
handler: () => {}
|
|
139
|
+
})
|
|
140
|
+
expect(r.PUT?.children?.three?.route).toMatchObject({
|
|
141
|
+
method: 'put',
|
|
142
|
+
path: '/three',
|
|
143
|
+
handler: () => {}
|
|
144
|
+
})
|
|
145
|
+
expect(r.GET?.children?.test?.param?.route).toMatchObject({
|
|
146
|
+
method: 'get',
|
|
147
|
+
path: '/test/:param1',
|
|
148
|
+
handler: () => {}
|
|
149
|
+
})
|
|
150
|
+
expect(r.POST?.children?.test?.route).toMatchObject({
|
|
151
|
+
method: 'post',
|
|
152
|
+
path: '/test',
|
|
153
|
+
handler: () => {}
|
|
154
|
+
})
|
|
155
|
+
expect(r.PUT?.children?.test?.route).toMatchObject({
|
|
156
|
+
method: 'put',
|
|
157
|
+
path: '/test',
|
|
158
|
+
handler: () => {}
|
|
159
|
+
})
|
|
160
|
+
expect(k.meta?.sort((a, b) => (a.file < b.file ? 1 : -1))).toMatchObject([
|
|
161
|
+
{
|
|
162
|
+
header: {},
|
|
163
|
+
routes: {
|
|
164
|
+
'/one': {
|
|
165
|
+
get: {}
|
|
166
|
+
},
|
|
167
|
+
'/two': {
|
|
168
|
+
post: {}
|
|
169
|
+
},
|
|
170
|
+
'/three': {
|
|
171
|
+
put: {}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
header: {
|
|
177
|
+
head: 'description\nmultiline',
|
|
178
|
+
tag: 'test'
|
|
179
|
+
},
|
|
180
|
+
routes: {
|
|
181
|
+
'/test/:param1': {
|
|
182
|
+
get: {
|
|
183
|
+
head: 'This part\nhere',
|
|
184
|
+
tags: 'tag1, tag2, tag3',
|
|
185
|
+
summary: 'short summary',
|
|
186
|
+
description: 'longer description example',
|
|
187
|
+
deprecated: true,
|
|
188
|
+
param: ['{path} param1 description', '{query} param2 description']
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
'/test': {
|
|
192
|
+
post: {
|
|
193
|
+
tags: 'tag1, tag2, tag3',
|
|
194
|
+
summary: 'short summary',
|
|
195
|
+
description: 'longer description example',
|
|
196
|
+
body: 'body descripton'
|
|
197
|
+
},
|
|
198
|
+
put: {
|
|
199
|
+
tags: 'tag1, tag2',
|
|
200
|
+
other: 'Hello Mom!'
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
])
|
|
206
|
+
expect(k.meta?.[0].file).toMatch(/test\.route\..*$/)
|
|
207
|
+
expect(k.meta?.[1].file).toMatch(/test\.route\..*$/)
|
|
208
|
+
})
|
|
209
|
+
})
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { expect, test, describe } from 'bun:test'
|
|
2
|
+
import { Galbe, NotFoundError, type RouteNode } from '../src'
|
|
3
|
+
|
|
4
|
+
describe('router', () => {
|
|
5
|
+
test('empty', async () => {
|
|
6
|
+
const galbe = new Galbe()
|
|
7
|
+
const router = galbe.router
|
|
8
|
+
|
|
9
|
+
expect(router.prefix).toBe('')
|
|
10
|
+
expect(router.routes.GET).toEqual({})
|
|
11
|
+
expect(router.routes.POST).toEqual({})
|
|
12
|
+
expect(router.routes.PUT).toEqual({})
|
|
13
|
+
expect(router.routes.PATCH).toEqual({})
|
|
14
|
+
expect(router.routes.DELETE).toEqual({})
|
|
15
|
+
expect(router.routes.OPTIONS).toEqual({})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('routes, bad syntax', async () => {
|
|
19
|
+
const galbe = new Galbe()
|
|
20
|
+
|
|
21
|
+
const invalidPaths = [
|
|
22
|
+
'.',
|
|
23
|
+
'/@',
|
|
24
|
+
'/-ta',
|
|
25
|
+
'/test/-ta',
|
|
26
|
+
'/test/my.path',
|
|
27
|
+
'/hell@/w0rld',
|
|
28
|
+
'/last-',
|
|
29
|
+
'../',
|
|
30
|
+
'./x',
|
|
31
|
+
'/hello?'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
for (const p of invalidPaths) {
|
|
35
|
+
try {
|
|
36
|
+
galbe.get(p, () => {})
|
|
37
|
+
expect.unreachable()
|
|
38
|
+
} catch (err) {
|
|
39
|
+
expect(err).toBeInstanceOf(SyntaxError)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('chaining routes', async () => {
|
|
45
|
+
const galbe = new Galbe()
|
|
46
|
+
const router = galbe.router
|
|
47
|
+
|
|
48
|
+
expect(router.routes.GET).toEqual({})
|
|
49
|
+
|
|
50
|
+
galbe.get('/', () => {})
|
|
51
|
+
let r: RouteNode | undefined = router.routes.GET
|
|
52
|
+
|
|
53
|
+
expect(r?.route?.method).toBe('get')
|
|
54
|
+
expect(r?.route?.path).toBe('/')
|
|
55
|
+
expect(r?.param).toBeUndefined()
|
|
56
|
+
expect(r?.children).toBeUndefined()
|
|
57
|
+
|
|
58
|
+
const mockHandler = () => {}
|
|
59
|
+
galbe.get('/test', mockHandler)
|
|
60
|
+
r = r?.children?.test
|
|
61
|
+
|
|
62
|
+
expect(r?.route?.method).toBe('get')
|
|
63
|
+
expect(r?.route?.path).toBe('/test')
|
|
64
|
+
expect(r?.param).toBeUndefined()
|
|
65
|
+
expect(r?.children).toBeUndefined()
|
|
66
|
+
expect(r?.route?.handler).toBe(mockHandler)
|
|
67
|
+
|
|
68
|
+
const mockHandler2 = () => {}
|
|
69
|
+
galbe.get('/test/:foo', mockHandler2)
|
|
70
|
+
r = r?.param
|
|
71
|
+
|
|
72
|
+
expect(r?.route?.method).toBe('get')
|
|
73
|
+
expect(r?.route?.path).toBe('/test/:foo')
|
|
74
|
+
expect(r?.param).toBeUndefined()
|
|
75
|
+
expect(r?.children).toBeUndefined()
|
|
76
|
+
expect(r?.route?.handler).toBe(mockHandler2)
|
|
77
|
+
|
|
78
|
+
const mockHandler3 = () => {}
|
|
79
|
+
galbe.get('/test/:foo/bar', mockHandler3)
|
|
80
|
+
|
|
81
|
+
expect(r?.children).toHaveProperty('bar')
|
|
82
|
+
expect(r?.param).toBeUndefined()
|
|
83
|
+
expect(r?.children?.bar?.route?.handler).toBe(mockHandler3)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('redefining root', async () => {
|
|
87
|
+
const galbe = new Galbe()
|
|
88
|
+
const router = galbe.router
|
|
89
|
+
|
|
90
|
+
const [h1, h2, h3] = [() => {}, () => {}, () => {}]
|
|
91
|
+
|
|
92
|
+
galbe.get('/', h1)
|
|
93
|
+
galbe.get('/test', h2)
|
|
94
|
+
galbe.get('/', h3)
|
|
95
|
+
|
|
96
|
+
let r: RouteNode | undefined = router.routes.GET
|
|
97
|
+
expect(router.prefix).toBe('')
|
|
98
|
+
expect(r?.route?.method).toBe('get')
|
|
99
|
+
expect(r?.route?.path).toBe('/')
|
|
100
|
+
expect(r?.route?.handler).toBe(h3)
|
|
101
|
+
expect(r?.children?.test?.route?.method).toBe('get')
|
|
102
|
+
expect(r?.children?.test?.route?.handler).toBe(h2)
|
|
103
|
+
|
|
104
|
+
const [h4, h5] = [() => {}, () => {}]
|
|
105
|
+
|
|
106
|
+
galbe.get('/foo/bar', h4)
|
|
107
|
+
galbe.get('/foo', h5)
|
|
108
|
+
|
|
109
|
+
r = router?.routes?.GET?.children?.foo
|
|
110
|
+
expect(r?.route?.method).toBe('get')
|
|
111
|
+
expect(r?.route?.path).toBe('/foo')
|
|
112
|
+
expect(r?.route?.handler).toBe(h5)
|
|
113
|
+
expect(r?.children?.bar?.route?.method).toBe('get')
|
|
114
|
+
expect(r?.children?.bar?.route?.path).toBe('/foo/bar')
|
|
115
|
+
expect(r?.children?.bar?.route?.handler).toBe(h4)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('find route', async () => {
|
|
119
|
+
const galbe = new Galbe()
|
|
120
|
+
const router = galbe.router
|
|
121
|
+
|
|
122
|
+
const [h1, h3, h4] = [() => {}, () => {}, () => {}, () => {}]
|
|
123
|
+
|
|
124
|
+
galbe.get('/', h1)
|
|
125
|
+
galbe.get('/test/foo', h3)
|
|
126
|
+
galbe.get('/test/foo/bar', h4)
|
|
127
|
+
|
|
128
|
+
let r1 = router.find('GET', '/')
|
|
129
|
+
expect(r1.path).toBe('/')
|
|
130
|
+
expect(r1.handler).toBe(h1)
|
|
131
|
+
let r2 = router.find('GET', '/test/foo')
|
|
132
|
+
expect(r2.path).toBe('/test/foo')
|
|
133
|
+
expect(r2.handler).toBe(h3)
|
|
134
|
+
let r3 = router.find('GET', '/test/foo/bar')
|
|
135
|
+
expect(r3.path).toBe('/test/foo/bar')
|
|
136
|
+
expect(r3.handler).toBe(h4)
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
router.find('GET', '/test')
|
|
140
|
+
expect.unreachable()
|
|
141
|
+
} catch (err: any) {
|
|
142
|
+
expect(err).toBeInstanceOf(NotFoundError)
|
|
143
|
+
expect(err.status).toBe(404)
|
|
144
|
+
expect(err.error).toBe('Not found')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
router.find('GET', '/test/bar/bar')
|
|
149
|
+
expect.unreachable()
|
|
150
|
+
} catch (err: any) {
|
|
151
|
+
expect(err).toBeInstanceOf(NotFoundError)
|
|
152
|
+
expect(err.status).toBe(404)
|
|
153
|
+
expect(err.error).toBe('Not found')
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
test('find route param', async () => {
|
|
158
|
+
const galbe = new Galbe()
|
|
159
|
+
const router = galbe.router
|
|
160
|
+
|
|
161
|
+
const [h1, h2] = [() => {}, () => {}]
|
|
162
|
+
|
|
163
|
+
galbe.get('/test/:foo', h1)
|
|
164
|
+
galbe.get('/test/test', h2)
|
|
165
|
+
|
|
166
|
+
let r1 = router.find('GET', '/test/42')
|
|
167
|
+
expect(r1.path).toBe('/test/:foo')
|
|
168
|
+
expect(r1.handler).toBe(h1)
|
|
169
|
+
|
|
170
|
+
let r2 = router.find('GET', '/test/test')
|
|
171
|
+
expect(r2.path).toBe('/test/test')
|
|
172
|
+
expect(r2.handler).toBe(h2)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('wildcard routes', async () => {
|
|
176
|
+
const galbe = new Galbe()
|
|
177
|
+
const router = galbe.router
|
|
178
|
+
|
|
179
|
+
const [h1, h2, h3, h4] = [() => {}, () => {}, () => {}, () => {}]
|
|
180
|
+
|
|
181
|
+
galbe.get('/test/foo/*', h1)
|
|
182
|
+
galbe.get('/test/foo/bar', h2)
|
|
183
|
+
galbe.get('/test/foo/:p/bar', h3)
|
|
184
|
+
|
|
185
|
+
galbe.get('/test/foo/*/lol', h4)
|
|
186
|
+
|
|
187
|
+
let r1 = router.find('GET', '/test/foo/bar/42')
|
|
188
|
+
expect(r1.path).toBe('/test/foo/*')
|
|
189
|
+
expect(r1.handler).toBe(h1)
|
|
190
|
+
|
|
191
|
+
let r2 = router.find('GET', '/test//foo/bar')
|
|
192
|
+
expect(r2.path).toBe('/test/foo/bar')
|
|
193
|
+
expect(r2.handler).toBe(h2)
|
|
194
|
+
|
|
195
|
+
let r3 = router.find('GET', '/test/foo/bar/bar')
|
|
196
|
+
expect(r3.path).toBe('/test/foo/:p/bar')
|
|
197
|
+
expect(r3.handler).toBe(h3)
|
|
198
|
+
|
|
199
|
+
let r4 = router.find('GET', '/test/foo/foo')
|
|
200
|
+
expect(r4.path).toBe('/test/foo/*')
|
|
201
|
+
expect(r4.handler).toBe(h1)
|
|
202
|
+
|
|
203
|
+
let r5 = router.find('GET', '/test/foo/toto/lol')
|
|
204
|
+
expect(r5.path).toBe('/test/foo/*/lol')
|
|
205
|
+
expect(r5.handler).toBe(h4)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { T, type Context } from '../src'
|
|
2
|
+
|
|
3
|
+
export type Case = {
|
|
4
|
+
body?: any
|
|
5
|
+
type?: string
|
|
6
|
+
schema: string
|
|
7
|
+
expected: { status: number; type?: string; resp?: any }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const formdata = (data: Record<string, string | string[] | Blob>): FormData => {
|
|
11
|
+
const form = new FormData()
|
|
12
|
+
for (const [k, v] of Object.entries(data)) {
|
|
13
|
+
if (Array.isArray(v)) for (const v2 of v) form.append(k, v2)
|
|
14
|
+
else form.append(k, v)
|
|
15
|
+
}
|
|
16
|
+
return form
|
|
17
|
+
}
|
|
18
|
+
export const fileHash = async (ba: any) =>
|
|
19
|
+
Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', ba)))
|
|
20
|
+
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
21
|
+
.join('')
|
|
22
|
+
export const decoder = new TextDecoder()
|
|
23
|
+
|
|
24
|
+
export const schema_objectBase = {
|
|
25
|
+
ba: T.ByteArray(),
|
|
26
|
+
string: T.String(),
|
|
27
|
+
number: T.Number(),
|
|
28
|
+
bool: T.Boolean(),
|
|
29
|
+
any: T.Any(),
|
|
30
|
+
optional: T.Optional(T.Any())
|
|
31
|
+
}
|
|
32
|
+
export const schema_object = {
|
|
33
|
+
...schema_objectBase,
|
|
34
|
+
object: T.Object(T.Any()),
|
|
35
|
+
array: T.Array(T.Any())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const isAsyncIterator = (obj: any) => {
|
|
39
|
+
if (Object(obj) !== obj) return false
|
|
40
|
+
const method = obj[Symbol.asyncIterator]
|
|
41
|
+
if (typeof method != 'function') return false
|
|
42
|
+
const aIter = method.call(obj)
|
|
43
|
+
return aIter === obj
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parseAsyncIterator = async (body: any): Promise<any> => {
|
|
47
|
+
const chunks = []
|
|
48
|
+
let type
|
|
49
|
+
for await (const chunk of body) {
|
|
50
|
+
if (chunk instanceof Uint8Array) type = 'ByteArray'
|
|
51
|
+
else if (typeof chunk === 'string') type = 'string'
|
|
52
|
+
chunks.push(chunk)
|
|
53
|
+
}
|
|
54
|
+
// @ts-ignore
|
|
55
|
+
if (type === 'ByteArray') return new Uint8Array(chunks.map(c => Array.from(c)).flat())
|
|
56
|
+
if (type === 'string') return chunks.join('')
|
|
57
|
+
return chunks
|
|
58
|
+
}
|
|
59
|
+
const parseBody = async (body: any): Promise<any> =>
|
|
60
|
+
typeof body === 'object' && body !== null && !Array.isArray(body)
|
|
61
|
+
? Object.fromEntries(
|
|
62
|
+
await Promise.all(
|
|
63
|
+
Object.entries(body).map(async ([k, v]) => {
|
|
64
|
+
if (v instanceof Uint8Array) return [k, await fileHash(v)]
|
|
65
|
+
else return [k, await parseBody(v)]
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
: body
|
|
70
|
+
export const handleBody = async (ctx: any) => {
|
|
71
|
+
if (ctx.body === undefined) {
|
|
72
|
+
return { type: 'undefined' }
|
|
73
|
+
}
|
|
74
|
+
if (ctx?.body instanceof ReadableStream) {
|
|
75
|
+
let content = ''
|
|
76
|
+
for await (const chunk of ctx.body) {
|
|
77
|
+
if (typeof chunk === 'string') content += chunk
|
|
78
|
+
else content += decoder.decode(chunk)
|
|
79
|
+
}
|
|
80
|
+
return { type: 'ReadableStream', content }
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(ctx.body)) {
|
|
83
|
+
return { type: 'array', content: ctx.body }
|
|
84
|
+
}
|
|
85
|
+
if (ctx?.body instanceof Uint8Array) {
|
|
86
|
+
return { type: 'ByteArray', content: decoder.decode(ctx.body) }
|
|
87
|
+
}
|
|
88
|
+
if (isAsyncIterator(ctx.body)) {
|
|
89
|
+
return { type: 'AsyncIterator', content: await parseAsyncIterator(ctx.body) }
|
|
90
|
+
} else {
|
|
91
|
+
return { type: typeof ctx.body, content: await parseBody(ctx.body) }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export const handleUrlFormStream = async (ctx: Context) => {
|
|
95
|
+
let resp: Record<string, any> = {}
|
|
96
|
+
for await (const [k, v] of ctx.body) {
|
|
97
|
+
if (k in resp) {
|
|
98
|
+
resp[k] = Array.isArray(resp[k]) ? [...resp[k], v] : [resp[k], v]
|
|
99
|
+
} else resp[k] = v
|
|
100
|
+
}
|
|
101
|
+
return { type: 'object', content: resp }
|
|
102
|
+
}
|