galbe 0.2.0 → 0.4.1

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.
@@ -1,18 +1,9 @@
1
1
  import { expect, test, describe, beforeAll } from 'bun:test'
2
2
  import { Galbe, $T } from '../src'
3
- import {
4
- formdata,
5
- type Case,
6
- fileHash,
7
- schema_objectBase,
8
- schema_object,
9
- handleBody,
10
- isAsyncIterator,
11
- handleUrlFormStream
12
- } from './test.utils'
3
+ import { formdata, type Case, fileHash, handleBody, isAsyncIterator } from './test.utils'
13
4
 
14
5
  const port = 7358
15
- const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options']
6
+ const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head']
16
7
 
17
8
  describe('requests', () => {
18
9
  beforeAll(async () => {
@@ -24,6 +15,7 @@ describe('requests', () => {
24
15
  galbe.patch('/test', () => {})
25
16
  galbe.delete('/test', () => {})
26
17
  galbe.options('/test', () => {})
18
+ galbe.head('/test', () => {})
27
19
 
28
20
  galbe.get('/headers', ctx => {
29
21
  return ctx.headers
@@ -76,7 +68,8 @@ describe('requests', () => {
76
68
  p2: $T.number(),
77
69
  p3: $T.boolean(),
78
70
  p4: $T.union([$T.number(), $T.boolean()]),
79
- p5: $T.optional($T.string())
71
+ p5: $T.optional($T.string()),
72
+ p6: $T.array($T.union([$T.string()]))
80
73
  }
81
74
  },
82
75
  ctx => {
@@ -35,4 +35,24 @@ export default (g: Galbe) => {
35
35
  * @other Hello Mom!
36
36
  */
37
37
  g.put('/test', { body: $T.object({ foo: $T.string() }) }, [() => {}], _ => {})
38
+
39
+ /**
40
+ * patch method
41
+ */
42
+ g.patch('/test', _ => {})
43
+
44
+ /**
45
+ * options method
46
+ */
47
+ g.options('/test', _ => {})
48
+
49
+ /**
50
+ * delete method
51
+ */
52
+ g.delete('/test', _ => {})
53
+
54
+ /**
55
+ * head method
56
+ */
57
+ g.head('/test', _ => {})
38
58
  }
@@ -21,12 +21,14 @@ const rsTxt = (text: string) => {
21
21
  })
22
22
  }
23
23
 
24
+ const handleResp = ctx => ctx.body
25
+
24
26
  describe('responses', () => {
25
27
  beforeAll(async () => {
26
28
  const galbe = new Galbe()
27
29
 
28
30
  galbe.post(
29
- '/response',
31
+ '/none',
30
32
  {
31
33
  body: $T.optional($T.object($T.any())),
32
34
  query: { text: $T.optional($T.string()), stream: $T.optional($T.string()) }
@@ -42,20 +44,29 @@ describe('responses', () => {
42
44
  }
43
45
  )
44
46
 
45
- galbe.get('/error', _ => {
46
- return { next: () => {} }
47
- })
47
+ galbe.post('', { response: { test: '' } }, () => {})
48
48
 
49
- galbe.onError(_ => {
50
- throw new Error()
51
- })
49
+ galbe.post('/ba', { response: { 200: $T.byteArray() } }, handleResp)
50
+ galbe.post('/bool', { response: { 200: $T.boolean() } }, handleResp)
51
+ galbe.post('/num', { response: { 200: $T.number() } }, handleResp)
52
+ galbe.post('/str', { response: { 200: $T.string() } }, handleResp)
53
+ galbe.post('/json/bool', { response: { 200: $T.json($T.boolean()) } }, handleResp)
54
+ galbe.post('/json/num', { response: { 200: $T.json($T.number()) } }, handleResp)
55
+ galbe.post('/json/str', { response: { 200: $T.json($T.string()) } }, handleResp)
56
+ galbe.post('/json/obj', { response: { 200: $T.json($T.object()) } }, handleResp)
57
+ galbe.post('/arr', { response: { 200: $T.array() } }, handleResp)
58
+ galbe.post('/obj', { response: { 200: $T.object($T.any()) } }, handleResp)
59
+ galbe.post('/stream/ba', { response: { 200: $T.stream($T.byteArray()) } }, ctx =>
60
+ ctx.body ? genTxt(ctx.body) : ''
61
+ )
62
+ galbe.post('/stream/str', { response: { 200: $T.stream($T.string()) } }, ctx => (ctx.body ? genTxt(ctx.body) : 42))
52
63
 
53
64
  await galbe.listen(port)
54
65
  })
55
66
 
56
- test('response, string', async () => {
67
+ test('response, no schema, string', async () => {
57
68
  const reqTxt = 'Hello Mom!'
58
- let resp = await fetch(`http://localhost:${port}/response?text=${reqTxt}`, {
69
+ let resp = await fetch(`http://localhost:${port}/none?text=${reqTxt}`, {
59
70
  method: 'POST'
60
71
  })
61
72
 
@@ -66,9 +77,9 @@ describe('responses', () => {
66
77
  expect(body).toBe(reqTxt)
67
78
  })
68
79
 
69
- test('response, object', async () => {
80
+ test('response, no schema, object', async () => {
70
81
  const reqBody = { foo: 'bar' }
71
- let resp = await fetch(`http://localhost:${port}/response`, {
82
+ let resp = await fetch(`http://localhost:${port}/none`, {
72
83
  method: 'POST',
73
84
  body: JSON.stringify(reqBody),
74
85
  headers: {
@@ -83,12 +94,12 @@ describe('responses', () => {
83
94
  expect(body).toEqual(reqBody)
84
95
  })
85
96
 
86
- test('response, stream', async () => {
97
+ test('response, no schema, stream', async () => {
87
98
  const reqTxt = 'Hello Mom!'
88
99
  const cases = [{ stream: 'generatorFunction' }, { stream: 'readableStream' }]
89
100
 
90
101
  for (const c of cases) {
91
- let resp = await fetch(`http://localhost:${port}/response?text=${reqTxt}&stream=${c.stream}`, {
102
+ let resp = await fetch(`http://localhost:${port}/none?text=${reqTxt}&stream=${c.stream}`, {
92
103
  method: 'POST',
93
104
  headers: {
94
105
  'content-type': 'application/json'
@@ -106,4 +117,366 @@ describe('responses', () => {
106
117
  expect(body).toMatch(new RegExp(`id:${UUID_RGX}\ndata:Hello\n\nid:${UUID_RGX}\ndata:Mom!\n\n`))
107
118
  }
108
119
  })
120
+
121
+ test('response, ba, validation OK', async () => {
122
+ let bodyStr = 'Hello mom!'
123
+ let reqBody = new TextEncoder().encode(bodyStr)
124
+
125
+ let resp = await fetch(`http://localhost:${port}/ba`, {
126
+ method: 'POST',
127
+ body: bodyStr,
128
+ headers: {
129
+ 'content-type': 'application/octet-stream'
130
+ }
131
+ })
132
+
133
+ const reader = resp.body?.getReader()
134
+ let body = new Uint8Array()
135
+ while (reader) {
136
+ const { value, done } = await reader.read()
137
+ if (done) break
138
+ let buff = new Uint8Array(body.length + value.length)
139
+ buff.set(body)
140
+ buff.set(value, body.length)
141
+ body = buff
142
+ }
143
+
144
+ expect(resp.status).toBe(200)
145
+ expect(resp.headers.get('content-type')).toBe('application/octet-stream')
146
+ expect(body).toEqual(reqBody)
147
+ })
148
+
149
+ test('response, ba, validation error', async () => {
150
+ let bodyStr = 'Hello mom!'
151
+
152
+ let resp = await fetch(`http://localhost:${port}/ba`, {
153
+ method: 'POST',
154
+ body: bodyStr,
155
+ headers: {
156
+ 'content-type': 'text/plain'
157
+ }
158
+ })
159
+
160
+ expect(resp.status).toBe(500)
161
+ })
162
+
163
+ test('response, bool, validation OK', async () => {
164
+ let bodyStr = 'true'
165
+ let reqBody = true
166
+
167
+ let resp = await fetch(`http://localhost:${port}/bool`, {
168
+ method: 'POST',
169
+ body: bodyStr,
170
+ headers: {
171
+ 'content-type': 'application/json'
172
+ }
173
+ })
174
+
175
+ const body = await resp.json()
176
+
177
+ expect(resp.status).toBe(200)
178
+ expect(resp.headers.get('content-type')).toBe('application/json')
179
+ expect(body).toEqual(reqBody)
180
+ })
181
+
182
+ test('response, bool, validation error', async () => {
183
+ let bodyStr = 'true'
184
+
185
+ let resp = await fetch(`http://localhost:${port}/bool`, {
186
+ method: 'POST',
187
+ body: bodyStr,
188
+ headers: {
189
+ 'content-type': 'text/plain'
190
+ }
191
+ })
192
+
193
+ expect(resp.status).toBe(500)
194
+ })
195
+
196
+ test('response, num, validation OK', async () => {
197
+ let bodyStr = '42'
198
+ let reqBody = 42
199
+
200
+ let resp = await fetch(`http://localhost:${port}/num`, {
201
+ method: 'POST',
202
+ body: bodyStr,
203
+ headers: {
204
+ 'content-type': 'application/json'
205
+ }
206
+ })
207
+
208
+ const body = await resp.json()
209
+
210
+ expect(resp.status).toBe(200)
211
+ expect(resp.headers.get('content-type')).toBe('application/json')
212
+ expect(body).toEqual(reqBody)
213
+ })
214
+
215
+ test('response, num, validation error', async () => {
216
+ let bodyStr = '"test"'
217
+
218
+ let resp = await fetch(`http://localhost:${port}/num`, {
219
+ method: 'POST',
220
+ body: bodyStr,
221
+ headers: {
222
+ 'content-type': 'application/json'
223
+ }
224
+ })
225
+
226
+ expect(resp.status).toBe(500)
227
+ })
228
+
229
+ test('response, str, validation OK', async () => {
230
+ let bodyStr = 'Hello Mom!'
231
+
232
+ let resp = await fetch(`http://localhost:${port}/str`, {
233
+ method: 'POST',
234
+ body: bodyStr,
235
+ headers: {
236
+ 'content-type': 'text/plain'
237
+ }
238
+ })
239
+
240
+ const body = await resp.text()
241
+
242
+ expect(resp.status).toBe(200)
243
+ expect(resp.headers.get('content-type')).toBe('text/plain')
244
+ expect(body).toEqual(bodyStr)
245
+ })
246
+
247
+ test('response, json bool, validation OK', async () => {
248
+ let bodyStr = 'false'
249
+ let reqBody = false
250
+
251
+ let resp = await fetch(`http://localhost:${port}/json/bool`, {
252
+ method: 'POST',
253
+ body: bodyStr,
254
+ headers: {
255
+ 'content-type': 'application/json'
256
+ }
257
+ })
258
+
259
+ const body = await resp.json()
260
+
261
+ expect(resp.status).toBe(200)
262
+ expect(resp.headers.get('content-type')).toBe('application/json')
263
+ expect(body).toEqual(reqBody)
264
+ })
265
+
266
+ test('response, json bool, validation error', async () => {
267
+ let bodyStr = '3.14'
268
+
269
+ let resp = await fetch(`http://localhost:${port}/json/bool`, {
270
+ method: 'POST',
271
+ body: bodyStr,
272
+ headers: {
273
+ 'content-type': 'application/json'
274
+ }
275
+ })
276
+
277
+ expect(resp.status).toBe(500)
278
+ })
279
+
280
+ test('response, json num, validation OK', async () => {
281
+ let bodyStr = '42'
282
+ let reqBody = 42
283
+
284
+ let resp = await fetch(`http://localhost:${port}/json/num`, {
285
+ method: 'POST',
286
+ body: bodyStr,
287
+ headers: {
288
+ 'content-type': 'application/json'
289
+ }
290
+ })
291
+
292
+ const body = await resp.json()
293
+
294
+ expect(resp.status).toBe(200)
295
+ expect(resp.headers.get('content-type')).toBe('application/json')
296
+ expect(body).toEqual(reqBody)
297
+ })
298
+
299
+ test('response, json num, validation error', async () => {
300
+ let bodyStr = '"Hi"'
301
+
302
+ let resp = await fetch(`http://localhost:${port}/json/bool`, {
303
+ method: 'POST',
304
+ body: bodyStr,
305
+ headers: {
306
+ 'content-type': 'application/json'
307
+ }
308
+ })
309
+
310
+ expect(resp.status).toBe(500)
311
+ })
312
+
313
+ test('response, json str, validation OK', async () => {
314
+ let bodyStr = '"Hello Mom!"'
315
+ let reqBody = 'Hello Mom!'
316
+
317
+ let resp = await fetch(`http://localhost:${port}/json/str`, {
318
+ method: 'POST',
319
+ body: bodyStr,
320
+ headers: {
321
+ 'content-type': 'application/json'
322
+ }
323
+ })
324
+
325
+ const body = await resp.json()
326
+
327
+ expect(resp.status).toBe(200)
328
+ expect(resp.headers.get('content-type')).toBe('application/json')
329
+ expect(body).toEqual(reqBody)
330
+ })
331
+
332
+ test('response, json str, validation error', async () => {
333
+ let bodyStr = '3.14'
334
+
335
+ let resp = await fetch(`http://localhost:${port}/json/str`, {
336
+ method: 'POST',
337
+ body: bodyStr,
338
+ headers: {
339
+ 'content-type': 'application/json'
340
+ }
341
+ })
342
+
343
+ expect(resp.status).toBe(500)
344
+ })
345
+
346
+ test('response, array, validation OK', async () => {
347
+ let bodyStr = '[false, "one", 2]'
348
+ let reqBody = [false, 'one', 2]
349
+
350
+ let resp = await fetch(`http://localhost:${port}/arr`, {
351
+ method: 'POST',
352
+ body: bodyStr,
353
+ headers: {
354
+ 'content-type': 'application/json'
355
+ }
356
+ })
357
+
358
+ const body = await resp.json()
359
+
360
+ expect(resp.status).toBe(200)
361
+ expect(resp.headers.get('content-type')).toBe('application/json')
362
+ expect(body).toEqual(reqBody)
363
+ })
364
+
365
+ test('response, array, validation error', async () => {
366
+ let bodyStr = '0'
367
+
368
+ let resp = await fetch(`http://localhost:${port}/arr`, {
369
+ method: 'POST',
370
+ body: bodyStr,
371
+ headers: {
372
+ 'content-type': 'application/json'
373
+ }
374
+ })
375
+
376
+ expect(resp.status).toBe(500)
377
+ })
378
+
379
+ test('response, object, validation OK', async () => {
380
+ let bodyStr = '{"str":"str", "num":0, "arr":[1,2,3], "nested": {"foo":"bar"}}'
381
+ let reqBody = { str: 'str', num: 0, arr: [1, 2, 3], nested: { foo: 'bar' } }
382
+
383
+ let resp = await fetch(`http://localhost:${port}/obj`, {
384
+ method: 'POST',
385
+ body: bodyStr,
386
+ headers: {
387
+ 'content-type': 'application/json'
388
+ }
389
+ })
390
+
391
+ const body = await resp.json()
392
+
393
+ expect(resp.status).toBe(200)
394
+ expect(resp.headers.get('content-type')).toBe('application/json')
395
+ expect(body).toEqual(reqBody)
396
+ })
397
+
398
+ test('response, object, validation error', async () => {
399
+ let bodyStr = '"This"'
400
+
401
+ let resp = await fetch(`http://localhost:${port}/obj`, {
402
+ method: 'POST',
403
+ body: bodyStr,
404
+ headers: {
405
+ 'content-type': 'application/json'
406
+ }
407
+ })
408
+
409
+ expect(resp.status).toBe(500)
410
+ })
411
+
412
+ test('response, stream ba, validation OK', async () => {
413
+ let bodyStr = 'Hello Mom!'
414
+
415
+ let resp = await fetch(`http://localhost:${port}/stream/ba`, {
416
+ method: 'POST',
417
+ body: bodyStr,
418
+ headers: {
419
+ 'content-type': 'text/plain'
420
+ }
421
+ })
422
+
423
+ const reader = resp.body?.getReader()
424
+ let body = ''
425
+ while (reader) {
426
+ const { value, done } = await reader.read()
427
+ if (done) break
428
+ body += decoder.decode(value)
429
+ }
430
+ expect(resp.status).toBe(200)
431
+ expect(resp.headers.get('content-type')).toBe('text/event-stream')
432
+ expect(body).toMatch(new RegExp(`id:${UUID_RGX}\ndata:Hello\n\nid:${UUID_RGX}\ndata:Mom!\n\n`))
433
+ })
434
+
435
+ test('response, stream ba, validation error', async () => {
436
+ let bodyStr = ''
437
+
438
+ let resp = await fetch(`http://localhost:${port}/stream/ba`, {
439
+ method: 'POST',
440
+ body: bodyStr
441
+ })
442
+
443
+ expect(resp.status).toBe(500)
444
+ })
445
+
446
+ test('response, stream str, validation OK', async () => {
447
+ let bodyStr = 'Hello Mom!'
448
+
449
+ let resp = await fetch(`http://localhost:${port}/stream/str`, {
450
+ method: 'POST',
451
+ body: bodyStr,
452
+ headers: {
453
+ 'content-type': 'text/plain'
454
+ }
455
+ })
456
+
457
+ const reader = resp.body?.getReader()
458
+ let body = ''
459
+ while (reader) {
460
+ const { value, done } = await reader.read()
461
+ if (done) break
462
+ body += decoder.decode(value)
463
+ }
464
+ expect(resp.status).toBe(200)
465
+ expect(resp.headers.get('content-type')).toBe('text/event-stream')
466
+ expect(body).toMatch(new RegExp(`id:${UUID_RGX}\ndata:Hello\n\nid:${UUID_RGX}\ndata:Mom!\n\n`))
467
+ })
468
+
469
+ test('response, stream str, validation error', async () => {
470
+ let bodyStr = '0'
471
+
472
+ let resp = await fetch(`http://localhost:${port}/stream/str`, {
473
+ method: 'POST',
474
+ body: bodyStr,
475
+ headers: {
476
+ 'content-type': 'application/json'
477
+ }
478
+ })
479
+
480
+ expect(resp.status).toBe(500)
481
+ })
109
482
  })
@@ -49,6 +49,18 @@ describe('routeFiles', () => {
49
49
  put: {
50
50
  tags: 'tag1, tag2',
51
51
  other: 'Hello Mom!'
52
+ },
53
+ patch: {
54
+ head: 'patch method'
55
+ },
56
+ options: {
57
+ head: 'options method'
58
+ },
59
+ delete: {
60
+ head: 'delete method'
61
+ },
62
+ head: {
63
+ head: 'head method'
52
64
  }
53
65
  }
54
66
  }
@@ -59,12 +71,7 @@ describe('routeFiles', () => {
59
71
  const k = new Galbe()
60
72
  await defineRoutes({}, k)
61
73
  expect(k.router.routes).toEqual({
62
- GET: {},
63
- POST: {},
64
- PUT: {},
65
- PATCH: {},
66
- DELETE: {},
67
- OPTIONS: {}
74
+ routes: {}
68
75
  })
69
76
  })
70
77
 
@@ -72,12 +79,7 @@ describe('routeFiles', () => {
72
79
  const k = new Galbe({ routes: false })
73
80
  await defineRoutes({}, k)
74
81
  expect(k.router.routes).toEqual({
75
- GET: {},
76
- POST: {},
77
- PUT: {},
78
- PATCH: {},
79
- DELETE: {},
80
- OPTIONS: {}
82
+ routes: {}
81
83
  })
82
84
  })
83
85
 
@@ -85,12 +87,7 @@ describe('routeFiles', () => {
85
87
  const k = new Galbe()
86
88
  await defineRoutes({ routes: 'unexisting_route' }, k)
87
89
  expect(k.router.routes).toEqual({
88
- GET: {},
89
- POST: {},
90
- PUT: {},
91
- PATCH: {},
92
- DELETE: {},
93
- OPTIONS: {}
90
+ routes: {}
94
91
  })
95
92
  })
96
93
 
@@ -100,17 +97,17 @@ describe('routeFiles', () => {
100
97
  await defineRoutes({ routes: 'test/resources/test.route.empty.ts' }, k)
101
98
 
102
99
  const r = k.router.routes
103
- expect(r.GET?.children?.one?.route).toMatchObject({
100
+ expect(r?.children?.one?.routes.get).toMatchObject({
104
101
  method: 'get',
105
102
  path: '/one',
106
103
  handler: () => {}
107
104
  })
108
- expect(r.POST?.children?.two?.route).toMatchObject({
105
+ expect(r?.children?.two?.routes.post).toMatchObject({
109
106
  method: 'post',
110
107
  path: '/two',
111
108
  handler: () => {}
112
109
  })
113
- expect(r.PUT?.children?.three?.route).toMatchObject({
110
+ expect(r?.children?.three?.routes.put).toMatchObject({
114
111
  method: 'put',
115
112
  path: '/three',
116
113
  handler: () => {}
@@ -140,36 +137,56 @@ describe('routeFiles', () => {
140
137
  await defineRoutes({ routes: ['test/resources/test.route.*.ts'] }, k)
141
138
 
142
139
  const r = k.router.routes
143
- expect(r.GET?.children?.one?.route).toMatchObject({
140
+ expect(r?.children?.one?.routes.get).toMatchObject({
144
141
  method: 'get',
145
142
  path: '/one',
146
143
  handler: () => {}
147
144
  })
148
- expect(r.POST?.children?.two?.route).toMatchObject({
145
+ expect(r?.children?.two?.routes.post).toMatchObject({
149
146
  method: 'post',
150
147
  path: '/two',
151
148
  handler: () => {}
152
149
  })
153
- expect(r.PUT?.children?.three?.route).toMatchObject({
150
+ expect(r?.children?.three?.routes.put).toMatchObject({
154
151
  method: 'put',
155
152
  path: '/three',
156
153
  handler: () => {}
157
154
  })
158
- expect(r.GET?.children?.test?.param?.route).toMatchObject({
155
+ expect(r?.children?.test?.param?.routes.get).toMatchObject({
159
156
  method: 'get',
160
157
  path: '/test/:param1',
161
158
  handler: () => {}
162
159
  })
163
- expect(r.POST?.children?.test?.route).toMatchObject({
160
+ expect(r?.children?.test?.routes.post).toMatchObject({
164
161
  method: 'post',
165
162
  path: '/test',
166
163
  handler: () => {}
167
164
  })
168
- expect(r.PUT?.children?.test?.route).toMatchObject({
165
+ expect(r?.children?.test?.routes.put).toMatchObject({
169
166
  method: 'put',
170
167
  path: '/test',
171
168
  handler: () => {}
172
169
  })
170
+ expect(r?.children?.test?.routes.patch).toMatchObject({
171
+ method: 'patch',
172
+ path: '/test',
173
+ handler: () => {}
174
+ })
175
+ expect(r?.children?.test?.routes.options).toMatchObject({
176
+ method: 'options',
177
+ path: '/test',
178
+ handler: () => {}
179
+ })
180
+ expect(r?.children?.test?.routes.delete).toMatchObject({
181
+ method: 'delete',
182
+ path: '/test',
183
+ handler: () => {}
184
+ })
185
+ expect(r?.children?.test?.routes.head).toMatchObject({
186
+ method: 'head',
187
+ path: '/test',
188
+ handler: () => {}
189
+ })
173
190
  expect(k.meta?.sort((a, b) => (a.file < b.file ? 1 : -1))).toMatchObject([
174
191
  {
175
192
  header: {},