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/src/parser.ts ADDED
@@ -0,0 +1,656 @@
1
+ import type { Static, TObject, TProperties } from '@sinclair/typebox'
2
+ import type {
3
+ MultipartFormData,
4
+ MaybeArray,
5
+ TBody,
6
+ TMultipartForm,
7
+ TUrlForm,
8
+ TUrlFormParam,
9
+ TMultipartFormParam,
10
+ TStream,
11
+ Context
12
+ } from './index'
13
+
14
+ import { Kind, Optional } from '@sinclair/typebox'
15
+ import { RequestError, Stream, T } from './index'
16
+ import { validate } from './validator'
17
+ import { readableStreamToArrayBuffer } from 'bun'
18
+
19
+ const textDecoder = new TextDecoder()
20
+ const textEncoder = new TextEncoder()
21
+
22
+ async function* rsToAsyncIterator(readableStream: ReadableStream) {
23
+ try {
24
+ for await (const chunk of readableStream) yield chunk
25
+ } finally {
26
+ readableStream.cancel()
27
+ }
28
+ }
29
+
30
+ const BA_HEADER = 'application/octet-stream'
31
+ const JSON_HEADER = 'application/json'
32
+ const TXT_HEADER_RX = /^text\//
33
+ const FORM_HEADER_RX = /^application\/x-www-form-urlencoded/
34
+ const MP_HEADER_RX = /^multipart\/form-data/
35
+
36
+ export const requestBodyParser = async (
37
+ body: ReadableStream | null,
38
+ headers: { 'content-type'?: string; 'content-length'?: string },
39
+ schema?: TBody
40
+ ) => {
41
+ const { 'content-type': contentType, 'content-length': _contentLength } = headers
42
+ const kind = schema?.[Kind]
43
+ const isStream = schema && Stream in schema
44
+ try {
45
+ if (body === null) {
46
+ if (schema) {
47
+ if (kind === 'ByteArray') {
48
+ if (Stream in schema) {
49
+ return new ReadableStream({
50
+ start(controller) {
51
+ controller.enqueue(new Uint8Array())
52
+ controller.close()
53
+ }
54
+ })
55
+ } else return new Uint8Array()
56
+ } else if (schema?.[Optional]) {
57
+ return null
58
+ } else {
59
+ throw new RequestError({ status: 400, error: { body: `Not a valid ${kind}` } })
60
+ }
61
+ } else {
62
+ if (contentType === BA_HEADER) {
63
+ return new Uint8Array()
64
+ } else if (contentType === JSON_HEADER) {
65
+ throw new RequestError({ status: 400, error: { body: 'Not a valid json' } })
66
+ } else if (contentType?.match(TXT_HEADER_RX)) {
67
+ throw new RequestError({ status: 400, error: { body: 'Not a valid text' } })
68
+ } else if (contentType?.match(FORM_HEADER_RX)) {
69
+ throw new RequestError({ status: 400, error: { body: 'Not a valid form' } })
70
+ } else if (contentType?.match(MP_HEADER_RX)) {
71
+ throw new RequestError({ status: 400, error: { body: 'Not a valid multipart form' } })
72
+ } else return null
73
+ }
74
+ } else {
75
+ if (kind === 'ByteArray') {
76
+ if (isStream) return rsToAsyncIterator(body)
77
+ const bytes = await readableStreamToArrayBuffer(body)
78
+ return new Uint8Array(bytes)
79
+ } else if (kind === 'String' && isStream) {
80
+ return $streamToString(body)
81
+ } else if (
82
+ (!contentType?.match(FORM_HEADER_RX) && kind === 'UrlForm') ||
83
+ (!contentType?.match(MP_HEADER_RX) && kind === 'MultipartForm')
84
+ ) {
85
+ let received = contentType?.match(FORM_HEADER_RX)
86
+ ? 'UrlForm'
87
+ : contentType?.match(MP_HEADER_RX)
88
+ ? 'MultipartForm'
89
+ : contentType
90
+ throw new RequestError({ status: 400, error: { body: `Expected ${kind}, received ${received}` } })
91
+ } else if (!contentType || contentType === BA_HEADER) {
92
+ if (!schema) {
93
+ if (isStream) return rsToAsyncIterator(body)
94
+ const bytes = []
95
+ for await (const b of body) bytes.push(...b)
96
+ return new Uint8Array(bytes)
97
+ } else {
98
+ if (isStream) return rsToAsyncIterator(body)
99
+ else return await streamToString(body, schema)
100
+ }
101
+ } else if (contentType === JSON_HEADER) {
102
+ if (!schema) {
103
+ return await streamToString(body, T.Object(T.Any()))
104
+ } else {
105
+ const str = await streamToString(body)
106
+ let json
107
+ try {
108
+ json = JSON.parse(str)
109
+ } catch (err: any) {
110
+ throw new RequestError({
111
+ status: 400,
112
+ error: { body: err?.message ?? 'Parsing error' }
113
+ })
114
+ }
115
+ return validate(json, schema, true)
116
+ }
117
+ } else if (contentType.match(TXT_HEADER_RX)) {
118
+ if (!schema) {
119
+ return await streamToString(body)
120
+ } else {
121
+ return await streamToString(body, schema)
122
+ }
123
+ } else if (contentType.match(FORM_HEADER_RX)) {
124
+ if (!schema) {
125
+ return await streamToUrlForm(body)
126
+ } else {
127
+ if (kind !== 'UrlForm')
128
+ throw new RequestError({ status: 400, error: { body: `Expected ${kind}, received UrlForm` } })
129
+ if (isStream) return $streamToUrlForm(body, schema as TStream<TUrlForm>)
130
+ else return await streamToUrlForm(body, schema as TUrlForm)
131
+ }
132
+ } else if (contentType.match(MP_HEADER_RX)) {
133
+ const boundary = contentType.match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
134
+ if (!schema) {
135
+ return await streamToMultipartForm(body, boundary)
136
+ } else {
137
+ if (kind !== 'MultipartForm')
138
+ throw new RequestError({ status: 400, error: { body: `Expected ${kind}, received MultipartForm` } })
139
+ if (isStream) return $streamToMultipartForm(body, boundary, schema as TStream<TMultipartForm>)
140
+ else {
141
+ return streamToMultipartForm(body, boundary, schema as TMultipartForm)
142
+ }
143
+ }
144
+ } else {
145
+ return rsToAsyncIterator(body)
146
+ }
147
+ }
148
+ } catch (error) {
149
+ if (error instanceof RequestError) throw error
150
+ else throw new RequestError({ status: 400, error: { body: error } })
151
+ }
152
+ }
153
+ async function* $streamToString(body: ReadableStream) {
154
+ for await (const chunk of body) yield textDecoder.decode(chunk)
155
+ }
156
+ const streamToString = async (body: ReadableStream, schema?: TBody): Promise<any> => {
157
+ let res = ''
158
+ for await (const chunk of $streamToString(body)) res += chunk
159
+ if (schema) return validate(res, schema, true)
160
+ return res
161
+ }
162
+ async function* $streamToUrlForm(
163
+ body: ReadableStream<Uint8Array>,
164
+ schema?: TStream<TUrlForm>
165
+ ): AsyncGenerator<[string, any]> {
166
+ let rest: Uint8Array = new Uint8Array()
167
+ let bK: Uint8Array = new Uint8Array()
168
+ let bV: Uint8Array = new Uint8Array()
169
+ let start = 0
170
+ const required = Object.fromEntries(
171
+ Object.entries(schema?.properties || {}).filter(([_, v]: [string, any]) => !(Optional in v))
172
+ )
173
+ for await (const chunk of body) {
174
+ start = 0
175
+ for (let i = 0; i < chunk.length; i++) {
176
+ if (chunk[i] === 0x26) {
177
+ bV = new Uint8Array(rest.length + i - start)
178
+ bV.set(rest)
179
+ bV.set(chunk.slice(start, i), rest.length)
180
+ let [key, val]: [string, any] = [
181
+ decodeURIComponent(textDecoder.decode(bK)),
182
+ decodeURIComponent(textDecoder.decode(bV))
183
+ ]
184
+ try {
185
+ let s =
186
+ schema?.properties?.[key]?.[Kind] === 'Array' ? schema?.properties?.[key].items : schema?.properties?.[key]
187
+ val = s ? paramParser(val, s) : val
188
+ } catch (error) {
189
+ throw new RequestError({ status: 400, error: { body: { [key]: error } } })
190
+ }
191
+ delete required[key]
192
+ yield [key, val]
193
+ bK = new Uint8Array()
194
+ bV = new Uint8Array()
195
+ start = i + 1
196
+ rest = new Uint8Array()
197
+ } else if (chunk[i] === 0x3d) {
198
+ bK = new Uint8Array(rest.length + i - start)
199
+ bK.set(rest)
200
+ bK.set(chunk.slice(start, i), rest.length)
201
+ start = i + 1
202
+ rest = new Uint8Array()
203
+ }
204
+ if (i === chunk.length - 1) {
205
+ const newRest = new Uint8Array(rest.length + i + 1 - start)
206
+ newRest.set(rest)
207
+ newRest.set(chunk.slice(start, i + 1), rest.length)
208
+ rest = newRest
209
+ }
210
+ }
211
+ }
212
+ let [key, val]: [string, any] = [
213
+ decodeURIComponent(textDecoder.decode(bK)),
214
+ decodeURIComponent(textDecoder.decode(rest))
215
+ ]
216
+ try {
217
+ let s = schema?.properties?.[key]?.[Kind] === 'Array' ? schema?.properties?.[key].items : schema?.properties?.[key]
218
+ val = s ? paramParser(val, s) : val
219
+ } catch (error) {
220
+ throw new RequestError({ status: 400, error: { body: { [key]: error } } })
221
+ }
222
+ delete required[key]
223
+ yield [key, val]
224
+ for (const [k, s] of Object.entries(required)) {
225
+ //@ts-ignore
226
+ if (s[Kind] === 'Array') {
227
+ yield [k, []]
228
+ delete required[k]
229
+ }
230
+ }
231
+ const reqKeys = Object.keys(required)
232
+ if (reqKeys.length > 0)
233
+ throw new RequestError({
234
+ status: 400,
235
+ error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
236
+ })
237
+ }
238
+ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: TUrlForm) => {
239
+ let entries = []
240
+ const required = Object.fromEntries(
241
+ Object.entries(schema?.properties || {}).filter(([_, v]: [string, any]) => !(Optional in v))
242
+ )
243
+ let errors: Record<string, any> = {}
244
+ for await (const chunk of $streamToUrlForm(body)) entries.push(chunk)
245
+ const object: Record<string, any> = {}
246
+ for (let e of entries) {
247
+ if (e[0] in object) {
248
+ if (Array.isArray(object[e[0]])) object[e[0]].push(e[1])
249
+ else object[e[0]] = [object[e[0]], e[1]]
250
+ } else object[e[0]] = schema?.properties?.[e[0]]?.[Kind] === 'Array' ? [e[1]] : e[1]
251
+ }
252
+ if (schema?.properties)
253
+ for (let [k, v] of Object.entries(object)) {
254
+ delete required[k]
255
+ try {
256
+ object[k] = schema?.properties && k in schema?.properties ? paramParser(v, schema?.properties[k]) : v
257
+ } catch (error) {
258
+ errors[k] = k in errors ? [...errors[k], error] : error
259
+ }
260
+ }
261
+ if (Object.keys(errors).length) throw new RequestError({ status: 400, error: { body: errors } })
262
+ for (const [k, s] of Object.entries(required)) {
263
+ //@ts-ignore
264
+ if (s[Kind] === 'Array') {
265
+ object[k] = []
266
+ delete required[k]
267
+ }
268
+ }
269
+ const reqKeys = Object.keys(required)
270
+ if (reqKeys.length > 0)
271
+ throw new RequestError({
272
+ status: 400,
273
+ error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
274
+ })
275
+ return object
276
+ }
277
+ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundary: string, schema?: TMultipartForm) {
278
+ const bound = textEncoder.encode(boundary)
279
+ let rest = new Uint8Array()
280
+ let bK: Uint8Array = new Uint8Array()
281
+ let bV: Uint8Array = new Uint8Array()
282
+ let start = 0
283
+ const required = Object.fromEntries(
284
+ Object.entries(schema?.properties || {}).filter(([_, v]: [string, any]) => !(Optional in v))
285
+ )
286
+ for await (const chunk of data) {
287
+ start = 0
288
+ for (let i = 0; i < chunk.length; i++) {
289
+ let matchBound = true
290
+ for (let b = 0; b < bound.length; b++) {
291
+ if (chunk[i + b] === bound[b]) continue
292
+ else {
293
+ matchBound = false
294
+ break
295
+ }
296
+ }
297
+ if (matchBound) {
298
+ bV = new Uint8Array(rest.length + i - start)
299
+ bV.set(rest)
300
+ bV.set(chunk.slice(start, i), rest.length)
301
+ bV = bV.slice(1, bV.length - 4)
302
+ const headers = parseMultipartHeader(textDecoder.decode(bK))
303
+ if (headers) {
304
+ try {
305
+ delete required[headers.name]
306
+ yield {
307
+ headers,
308
+ content: parseMultipartContent(bV, headers, schema)
309
+ }
310
+ } catch (err) {
311
+ if (err instanceof RequestError) throw err
312
+ throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
313
+ }
314
+ }
315
+ bK = new Uint8Array()
316
+ bV = new Uint8Array()
317
+ start = i + bound.length
318
+ rest = new Uint8Array()
319
+ i = start
320
+ } else if (chunk[i] === 0x0d && chunk[i + 1] === 0x0a && chunk[i + 2] === 0x0d) {
321
+ bK = new Uint8Array(rest.length + i - start)
322
+ bK.set(rest)
323
+ bK.set(chunk.slice(start, i), rest.length)
324
+ start = i + 3
325
+ rest = new Uint8Array()
326
+ i = start
327
+ }
328
+ if (i === chunk.length - 1) {
329
+ const newRest = new Uint8Array(rest.length + i - start + 1)
330
+ newRest.set(rest)
331
+ newRest.set(chunk.slice(start, i + 1), rest.length)
332
+ rest = newRest
333
+ }
334
+ }
335
+ }
336
+ const headers = parseMultipartHeader(textDecoder.decode(bK))
337
+ if (headers) {
338
+ try {
339
+ delete required[headers.name]
340
+ yield {
341
+ headers,
342
+ content: parseMultipartContent(bV, headers, schema)
343
+ }
344
+ } catch (err) {
345
+ throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
346
+ }
347
+ }
348
+ for (const [k, s] of Object.entries(required)) {
349
+ //@ts-ignore
350
+ if (s[Kind] === 'Array') {
351
+ yield { headers: { name: k }, content: [] }
352
+ delete required[k]
353
+ }
354
+ }
355
+ const reqKeys = Object.keys(required)
356
+ if (reqKeys.length > 0)
357
+ throw new RequestError({
358
+ status: 400,
359
+ error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
360
+ })
361
+ }
362
+ const parseMultipartHeader = (header: string): { name: string; [key: string]: string } | null => {
363
+ if (!header) return null
364
+ let disposition = 'form-data'
365
+ const multipartHeader = [
366
+ ...header.matchAll(/\s*([\w-]+)\s*:\s*([^;]*);?/g),
367
+ ...header.matchAll(/;?\s*(\w+)\s*=\s*\"([^"]*)\";?/g)
368
+ ].reduce((acc: Record<string, string>, v: string[]) => {
369
+ const key = v[1].toLowerCase().replace(/^content-/, '')
370
+ if (key === 'disposition') {
371
+ disposition = v[2]
372
+ return acc
373
+ }
374
+ acc[key] = v[2]
375
+ return acc
376
+ }, {})
377
+ if (disposition !== 'form-data') null
378
+ //@ts-ignore
379
+ return multipartHeader
380
+ }
381
+ const parseMultipartContent = (
382
+ content: Uint8Array,
383
+ headers: { name: string; type?: string },
384
+ schema?: TMultipartForm
385
+ ) => {
386
+ const type = headers?.type ?? 'text/plain'
387
+ let result: any = content
388
+ if (type === 'text/plain') {
389
+ const str = textDecoder.decode(content).trim()
390
+ let s = schema?.properties?.[headers.name]
391
+ return s ? paramParser(str, s?.[Kind] === 'Array' ? s?.items : s) : str
392
+ } else if (type === 'application/json') {
393
+ if (!schema?.properties || !(headers.name in schema?.properties)) {
394
+ try {
395
+ result = JSON.parse(textDecoder.decode(content).trim())
396
+ } catch (err: any) {
397
+ throw new RequestError({ status: 400, error: { body: { [headers.name]: err?.message || 'Parsing error' } } })
398
+ }
399
+ } else if (schema?.properties) {
400
+ if (schema?.properties[headers.name][Kind] === 'Object') {
401
+ try {
402
+ result = JSON.parse(textDecoder.decode(content).trim())
403
+ } catch (err: any) {
404
+ throw new RequestError({ status: 400, error: { body: { [headers.name]: err?.message || 'Parsing error' } } })
405
+ }
406
+ try {
407
+ validate(result, schema?.properties[headers.name])
408
+ } catch (err) {
409
+ throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
410
+ }
411
+ } else if (schema?.properties[headers.name][Kind] === 'ByteArray') {
412
+ return content
413
+ } else if (schema?.properties[headers.name][Kind] === 'String') {
414
+ result = textDecoder.decode(content).trim()
415
+ } else {
416
+ throw new RequestError({
417
+ status: 400,
418
+ error: { body: { [headers.name]: `Expect ${schema?.properties[headers.name][Kind]} found json` } }
419
+ })
420
+ }
421
+ }
422
+ } else if (schema?.properties?.[headers.name]) {
423
+ try {
424
+ let s = schema?.properties[headers.name]
425
+ validate(result, s?.[Kind] === 'Array' ? s?.items : s)
426
+ } catch (err) {
427
+ throw new RequestError({ status: 400, error: { body: { [headers.name]: err } } })
428
+ }
429
+ }
430
+ return result
431
+ }
432
+ const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary: string, schema?: TMultipartForm) => {
433
+ const res: Record<string, MultipartFormData> = {}
434
+ const errors: Record<string, any> = {}
435
+ const required = Object.fromEntries(
436
+ Object.entries(schema?.properties || {}).filter(([_, v]: [string, any]) => !(Optional in v))
437
+ )
438
+ for await (const chunk of $streamToMultipartForm(data, boundary)) {
439
+ if (chunk.headers.name in res) {
440
+ if (!Array.isArray(res[chunk.headers.name].content))
441
+ res[chunk.headers.name].content = [res[chunk.headers.name].content]
442
+ res[chunk.headers.name].content.push(chunk.content)
443
+ } else {
444
+ if (schema?.properties?.[chunk.headers.name]?.[Kind] === 'Array')
445
+ res[chunk.headers.name] = { ...chunk, content: [chunk.content] }
446
+ else res[chunk.headers.name] = chunk
447
+ }
448
+ delete required[chunk.headers.name]
449
+
450
+ if (schema?.properties && chunk?.headers?.name in schema.properties) {
451
+ try {
452
+ if (
453
+ Array.isArray(res[chunk.headers.name].content) &&
454
+ schema?.properties?.[chunk.headers.name]?.[Kind] !== 'Array'
455
+ )
456
+ throw `Multiple values found`
457
+ res[chunk.headers.name].content = validate(
458
+ res[chunk.headers.name].content,
459
+ schema?.properties[chunk.headers.name],
460
+ true
461
+ )
462
+ if (schema.properties[chunk.headers.name][Kind] === 'Array')
463
+ for (let [k, v] of Object.entries(res[chunk.headers.name].content)) {
464
+ try {
465
+ //@ts-ignore
466
+ res[chunk.headers.name].content[k] = paramParser(v, schema.properties[chunk.headers.name].items)
467
+ } catch (error) {
468
+ errors[chunk.headers.name] = chunk.headers.name in errors ? [...errors[chunk.headers.name], error] : error
469
+ }
470
+ }
471
+ } catch (err) {
472
+ errors[chunk.headers.name] = err
473
+ }
474
+ }
475
+ }
476
+ if (Object.keys(errors).length)
477
+ throw new RequestError({
478
+ status: 400,
479
+ error: { body: errors }
480
+ })
481
+ for (const [k, s] of Object.entries(required)) {
482
+ //@ts-ignore
483
+ if (s[Kind] === 'Array') {
484
+ res[k] = { headers: { name: k }, content: [] }
485
+ delete required[k]
486
+ }
487
+ }
488
+ const reqKeys = Object.keys(required)
489
+ if (reqKeys.length > 0)
490
+ throw new RequestError({
491
+ status: 400,
492
+ error: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
493
+ })
494
+ return res
495
+ }
496
+ const paramParser = (value: string | string[] | null, type: TMultipartFormParam): MaybeArray<Static<TUrlFormParam>> => {
497
+ if (value === undefined) {
498
+ if (type[Optional]) return type?.default
499
+ else throw `Required`
500
+ } else if (value === null) return null
501
+ else if (Array.isArray(value)) {
502
+ if (type[Kind] !== 'Array') throw `Multiple values found`
503
+ validate(value, type)
504
+ let pv = []
505
+ let errors: Record<number, any> = {}
506
+ for (let [idx, v] of value.entries()) {
507
+ try {
508
+ pv.push(paramParser(v, type.items as TMultipartFormParam) as Static<TUrlFormParam>)
509
+ } catch (error) {
510
+ errors[idx] = error
511
+ }
512
+ }
513
+ if (Object.keys(errors).length) throw errors
514
+ return pv
515
+ } else {
516
+ if (type[Kind] === 'Boolean') {
517
+ if (typeof value === 'boolean') return value
518
+ if (value === 'true') return true
519
+ if (value === 'false') return false
520
+ else throw `${value} is not a valid boolean. Should be 'true' or 'false'`
521
+ } else if (type[Kind] === 'Integer') {
522
+ if (value === null || value === undefined) throw `${value} is not a valid integer`
523
+ const parsedValue = parseInt(value, 10)
524
+ if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `${value} is not a valid integer`
525
+ validate(parsedValue, type)
526
+ return parsedValue
527
+ } else if (type[Kind] === 'Number') {
528
+ if (value === null || value === undefined) throw `${value} is not a valid number`
529
+ const parsedValue = Number(value)
530
+ if (isNaN(parsedValue) || String(parsedValue) !== String(value)) throw `${value} is not a valid number`
531
+ validate(parsedValue, type)
532
+ return parsedValue
533
+ } else if (type[Kind] === 'String') {
534
+ validate(value, type)
535
+ return value
536
+ } else if (type[Kind] === 'Literal') {
537
+ if (value !== type.const) throw `${value} is not a valid value`
538
+ return value
539
+ } else if (type[Kind] === 'Array') {
540
+ return [paramParser(value, type.items as TMultipartFormParam) as Static<TUrlFormParam>]
541
+ } else if (type[Kind] === 'ByteArray') {
542
+ return Uint8Array.from(value, c => c.charCodeAt(0))
543
+ } else if (type[Kind] === 'Union') {
544
+ const union = Object.values(type.anyOf)
545
+ for (const elt of union) {
546
+ try {
547
+ return paramParser(value, elt as TMultipartFormParam)
548
+ } catch (err) {
549
+ continue
550
+ }
551
+ }
552
+ throw `${value} could not be parsed to any of ${union.map(u => u?.const ?? u[Kind]).join(', ')}`
553
+ } else if (type[Kind] === 'Any') {
554
+ return value
555
+ }
556
+ throw `Unknown parsing type ${type[Kind]}`
557
+ }
558
+ }
559
+
560
+ export const requestPathParser = (input: string, path: string) => {
561
+ let pPath = path.replace(/^\/$(.*)\/?$/, '$1').split('/')
562
+ let pInput = input.replace(/^\/$(.*)\/?$/, '$1').split('/')
563
+ let params: Record<string, any> = {}
564
+ pPath.shift()
565
+ pInput.shift()
566
+ for (const [i, p] of pPath.entries()) {
567
+ const match = p.match(/^:(.*)/)
568
+ if (match) params[match[1]] = pInput[i]
569
+ }
570
+ return params
571
+ }
572
+
573
+ export const parseEntry = <T extends TProperties>(
574
+ params: { [key: string]: string | string[] },
575
+ schema: T,
576
+ options?: { name?: string; i?: boolean }
577
+ ): Static<TObject<T>> => {
578
+ const parsedParams: Partial<Static<TObject<T>>> = {}
579
+ const errors: { [key: string]: string | string[] } = {}
580
+
581
+ if (options?.i === true) {
582
+ params = Object.keys(params).reduce((acc, key) => {
583
+ acc[key.toLowerCase()] = params[key]
584
+ return acc
585
+ }, {} as { [key: string]: string | string[] })
586
+ }
587
+
588
+ Object.entries(schema).forEach(([key, s]) => {
589
+ const k = options?.i === true ? key.toLowerCase() : key
590
+ let v = params[k]
591
+ if (s[Kind] === 'Array' && options?.name === 'query' && typeof v === 'string') v = v.split(',')
592
+ try {
593
+ let p = paramParser(v, s as TMultipartFormParam)
594
+ //@ts-ignore
595
+ if (p !== undefined) parsedParams[k] = p
596
+ } catch (errMsg) {
597
+ //@ts-ignore
598
+ errors[k] = errMsg
599
+ }
600
+ })
601
+
602
+ if (Object.keys(errors).length) {
603
+ throw new RequestError({ status: 400, error: options?.name ? { [options.name]: errors } : errors })
604
+ }
605
+
606
+ return parsedParams as Static<TObject<T>>
607
+ }
608
+
609
+ const isIterator = (obj: any) => typeof obj?.next === 'function'
610
+
611
+ export const responseParser = (response: any, ctx: Context) => {
612
+ const details = {
613
+ status: ctx.set.status || 200,
614
+ headers: new Headers(ctx.set.headers)
615
+ }
616
+ if (response instanceof Response) return response
617
+ else if (typeof response === 'string') {
618
+ if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'text/plain')
619
+ //@ts-ignore
620
+ return new Response(response, details)
621
+ }
622
+ if (response instanceof ReadableStream) {
623
+ response = rsToAsyncIterator(response)
624
+ }
625
+ if (isIterator(response)) {
626
+ const rs = new ReadableStream({
627
+ type: 'direct',
628
+ async pull(controller) {
629
+ let id = ctx.request.headers.get('last-event-id') ?? crypto.randomUUID()
630
+ for await (const r of response) {
631
+ let data = `id:${id}\ndata:${r}\n\n`
632
+ try {
633
+ await controller.write(data)
634
+ // await controller.flush()
635
+ } catch (err) {
636
+ console.error(err)
637
+ }
638
+ id = crypto.randomUUID()
639
+ }
640
+ controller.close()
641
+ }
642
+ })
643
+ details.headers.set('Content-Type', 'text/event-stream')
644
+ //@ts-ignore
645
+ return new Response(rs, details)
646
+ } else {
647
+ try {
648
+ if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'application/json')
649
+ //@ts-ignore
650
+ return new Response(JSON.stringify(response), details)
651
+ } catch (error) {
652
+ console.error(error)
653
+ throw new RequestError({ status: 500, error: 'Internal Server Error' })
654
+ }
655
+ }
656
+ }