galbe 0.11.0 → 0.12.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/index.ts CHANGED
@@ -17,7 +17,8 @@ import type {
17
17
  STQuery,
18
18
  StaticEndpoint,
19
19
  Route,
20
- StaticEndpointOptions
20
+ StaticEndpointOptions,
21
+ STBodyValue,
21
22
  } from './types'
22
23
 
23
24
  import { readdirSync, statSync } from 'fs'
@@ -78,14 +79,12 @@ const galbeMethod = <
78
79
  ): Route<M, Path, P, H, Q, B, R> => {
79
80
  schema = schema ?? {}
80
81
  hooks = hooks || []
82
+ //@ts-ignore
81
83
  const context: Context<M, Path, typeof schema> = {
82
84
  headers: {} as Static<STObject<Exclude<(typeof schema)['headers'], undefined>>>,
83
85
  params: {} as any,
84
86
  query: {} as Static<STObject<Exclude<(typeof schema)['query'], undefined>>>,
85
- //@ts-ignore
86
- body: ['get', 'options', 'head'].includes(method)
87
- ? null
88
- : ({} as Static<Exclude<(typeof schema)['body'], undefined>>),
87
+ body: ['get', 'options', 'head'].includes(method) ? null : ({} as unknown as STBodyValue),
89
88
  request: {} as Request,
90
89
  state: {},
91
90
  set: {} as {
@@ -94,7 +93,7 @@ const galbeMethod = <
94
93
  [header: string]: string | string[]
95
94
  }
96
95
  status?: number
97
- }
96
+ },
98
97
  }
99
98
  return {
100
99
  method,
@@ -102,7 +101,7 @@ const galbeMethod = <
102
101
  schema,
103
102
  context,
104
103
  hooks,
105
- handler
104
+ handler,
106
105
  }
107
106
  }
108
107
 
@@ -140,7 +139,7 @@ export class Galbe {
140
139
  this.config = config ?? {}
141
140
  this.router = new GalbeRouter({
142
141
  prefix: this.config?.basePath || '',
143
- cacheEnabled: this.config?.router?.cacheEnabled
142
+ cacheEnabled: this.config?.router?.cacheEnabled,
144
143
  })
145
144
  }
146
145
  private add(route: any) {
@@ -189,7 +188,7 @@ export class Galbe {
189
188
  P extends Partial<STParams<Path>>,
190
189
  H extends STHeaders,
191
190
  Q extends STQuery,
192
- B extends undefined,
191
+ B extends STBody,
193
192
  R extends STResponse
194
193
  >(
195
194
  path: Path,
@@ -317,7 +316,7 @@ export class Galbe {
317
316
 
318
317
  const walkStatic = (path: string, target: string) => {
319
318
  path = path?.[0] === '/' ? path : `/${path}`
320
- path = path.endsWith('/') ? path.slice(0, -1) : path;
319
+ path = path.endsWith('/') ? path.slice(0, -1) : path
321
320
 
322
321
  let t = target
323
322
  if (Bun.env.BUN_ENV === 'production') {
@@ -330,7 +329,13 @@ export class Galbe {
330
329
  if (resolve) ut = resolve(path, ut)
331
330
  if (ut) {
332
331
  let handler = () => new Response(Bun.file(ut))
332
+ const isIndex = /index\.html$/.test(ut)
333
333
  this.add({ ...galbeMethod(this, 'get', path, {}, undefined, handler), static: { path: ut, root: rootPath } })
334
+ if (isIndex)
335
+ this.add({
336
+ ...galbeMethod(this, 'get', `${path}/index.html`, {}, undefined, handler),
337
+ static: { path: ut, root: rootPath },
338
+ })
334
339
  }
335
340
  } else {
336
341
  let root = readdirSync(t)
@@ -340,7 +345,7 @@ export class Galbe {
340
345
  }
341
346
  }
342
347
 
343
- return { ...galbeMethod(this, 'get', path, {}, undefined, () => { }), static: { path: t, root: rootPath } }
348
+ return { ...galbeMethod(this, 'get', path, {}, undefined, () => {}), static: { path: t, root: rootPath } }
344
349
  }
345
350
 
346
351
  return walkStatic(path, target)
package/src/parser.ts CHANGED
@@ -1,16 +1,17 @@
1
- import type { MaybeArray, STBody, Context, STResponse } from './index'
1
+ import type { MaybeArray, STBody, Context, STResponse, STBodyValue, STBodyType } from './index'
2
2
  import type {
3
3
  STStream,
4
- STUrlForm,
5
4
  STMultipartForm,
6
5
  Static,
7
6
  STProps,
8
7
  STObject,
9
8
  MultipartFormData,
10
- STUrlFormValues,
11
9
  STMultipartFormValues,
12
10
  STLiteral,
13
- STSchema
11
+ STSchema,
12
+ STNull,
13
+ STPropsValue,
14
+ STUnion,
14
15
  } from './schema'
15
16
 
16
17
  import { readableStreamToArrayBuffer } from 'bun'
@@ -30,130 +31,151 @@ async function* rsToAsyncIterator(readableStream: ReadableStream) {
30
31
  }
31
32
  }
32
33
 
33
- const BA_HEADER = 'application/octet-stream'
34
- const JSON_HEADER = 'application/json'
35
- const TXT_HEADER_RX = /^text\//
36
- const FORM_HEADER_RX = /^application\/x-www-form-urlencoded/
37
- const MP_HEADER_RX = /^multipart\/form-data/
38
-
39
34
  export const requestBodyParser = async (
40
35
  body: ReadableStream | null,
41
36
  headers: Record<string, string>,
42
- schema?: STBody
37
+ schemas?: STBody | STNull,
38
+ contentType?: STBodyType
43
39
  ) => {
44
- const contentType = headers?.['content-type']
45
- const kind = schema?.[Kind]
46
- const isStream = schema && Stream in schema
40
+ let schema: STBodyValue =
41
+ (schemas as STNull)?.[Kind] === 'null' ? schemas : contentType ? schemas?.[contentType] : undefined
42
+ let kind = schema?.[Kind]
43
+ let isStream = schema && Stream in schema
47
44
  try {
48
- if (body === null) {
49
- if (schema) {
50
- if (kind === 'byteArray') {
51
- if (Stream in schema) {
52
- return new ReadableStream({
53
- start(controller) {
54
- controller.enqueue(new Uint8Array())
55
- controller.close()
56
- }
57
- })
58
- } else return new Uint8Array()
59
- } else if (schema?.[Optional] || kind === 'null') {
60
- return null
61
- } else {
62
- throw new RequestError({ status: 400, payload: { body: `Not a valid ${kind}` } })
45
+ if (kind === 'null') {
46
+ if (body === null) return null
47
+ throw new RequestError({ status: 400, payload: { body: `Expected null body` } })
48
+ }
49
+ if (!schemas || !Object.keys(schemas).length) {
50
+ // No schema defined, we base parsing on contentType only
51
+ if (contentType === 'byteArray') {
52
+ if (body === null) return new Uint8Array()
53
+ return new Uint8Array(await readableStreamToArrayBuffer(body))
54
+ } else if (contentType === 'json') {
55
+ if (body === null) return null
56
+ try {
57
+ return JSON.parse(await streamToString(body))
58
+ } catch (err: any) {
59
+ throw new RequestError({
60
+ status: 400,
61
+ payload: { body: err?.message ?? 'Parsing error' },
62
+ })
63
63
  }
64
- } else {
65
- if (contentType === BA_HEADER) {
66
- return new Uint8Array()
67
- } else if (contentType === JSON_HEADER) {
68
- throw new RequestError({ status: 400, payload: { body: 'Not a valid json' } })
69
- } else if (contentType?.match(TXT_HEADER_RX)) {
70
- throw new RequestError({ status: 400, payload: { body: 'Not a valid text' } })
71
- } else if (contentType?.match(FORM_HEADER_RX)) {
72
- throw new RequestError({ status: 400, payload: { body: 'Not a valid form' } })
73
- } else if (contentType?.match(MP_HEADER_RX)) {
74
- throw new RequestError({ status: 400, payload: { body: 'Not a valid multipart form' } })
75
- } else return null
76
- }
64
+ } else if (contentType === 'text') {
65
+ if (body === null) return ''
66
+ return streamToString(body)
67
+ } else if (contentType === 'urlForm') {
68
+ if (body === null) return {}
69
+ return await streamToUrlForm(body)
70
+ } else if (contentType === 'multipart') {
71
+ if (body === null) return {}
72
+ const boundary = headers?.['content-type'].match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
73
+ return await streamToMultipartForm(body, boundary)
74
+ } else return body === null ? null : rsToAsyncIterator(body)
77
75
  } else {
78
- if (kind === 'null') throw new RequestError({ status: 400, payload: { body: `Expected null body` } })
79
- else if (kind === 'byteArray') {
76
+ // Schemas found
77
+ if (contentType === 'default' && (schema || Object.values(schemas).every(s => s?.[Optional]))) {
78
+ if (kind === 'byteArray') contentType = 'byteArray'
79
+ else if (kind === 'string') contentType = 'text'
80
+ else return body === null ? null : rsToAsyncIterator(body)
81
+ }
82
+ if (contentType === 'byteArray') {
83
+ if (kind !== 'byteArray') throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
84
+ if (body === null) {
85
+ return isStream
86
+ ? new ReadableStream({
87
+ start(controller) {
88
+ controller.enqueue(new Uint8Array())
89
+ controller.close()
90
+ },
91
+ })
92
+ : new Uint8Array()
93
+ }
80
94
  if (isStream) return rsToAsyncIterator(body)
81
- const bytes = await readableStreamToArrayBuffer(body)
82
- return new Uint8Array(bytes)
83
- } else if (kind === 'string' && isStream) {
84
- return $streamToString(body)
85
- } else if (
86
- (!contentType?.match(FORM_HEADER_RX) && kind === 'urlForm') ||
87
- (!contentType?.match(MP_HEADER_RX) && kind === 'multipartForm')
88
- ) {
89
- let received = contentType?.match(FORM_HEADER_RX)
90
- ? 'urlForm'
91
- : contentType?.match(MP_HEADER_RX)
92
- ? 'multipartForm'
93
- : contentType
94
- throw new RequestError({ status: 400, payload: { body: `Expected ${kind}, received ${received}` } })
95
- } else if (!contentType || contentType === BA_HEADER) {
96
- if (!schema) {
97
- if (isStream) return rsToAsyncIterator(body)
98
- const bytes = []
99
- for await (const b of body) bytes.push(...b)
100
- return new Uint8Array(bytes)
101
- } else {
102
- if (isStream) return rsToAsyncIterator(body)
103
- else return await streamToString(body, schema)
95
+ return new Uint8Array(await readableStreamToArrayBuffer(body))
96
+ } else if (contentType === 'text') {
97
+ if (!['string', 'boolean', 'number', 'integer', 'union', 'literal'].includes(kind))
98
+ throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
99
+ if (body === null)
100
+ return isStream
101
+ ? new ReadableStream({
102
+ start(controller) {
103
+ controller.enqueue('')
104
+ controller.close()
105
+ },
106
+ })
107
+ : validate('', schema, { parse: true })
108
+ if (isStream) return $streamToString(body)
109
+ if (kind === 'union') {
110
+ let str = await streamToString(body)
111
+ return unionize(str, schema)
104
112
  }
105
- } else if (contentType === JSON_HEADER) {
106
- if (!schema) {
107
- try {
108
- return JSON.parse(await streamToString(body))
109
- } catch (err: any) {
110
- throw new RequestError({
111
- status: 400,
112
- payload: { body: err?.message ?? 'Parsing error' }
113
- })
114
- }
115
- } else {
116
- const str = await streamToString(body)
117
- let json
118
- try {
119
- json = JSON.parse(str)
120
- } catch (err: any) {
121
- throw new RequestError({
122
- status: 400,
123
- payload: { body: err?.message ?? 'Parsing error' }
124
- })
125
- }
126
- return validate(json, schema, true)
113
+ return await streamToString(body, schema as STBodyValue)
114
+ } else if (contentType === 'json') {
115
+ if (!['object', 'json', 'boolean', 'number', 'integer', 'string', 'array', 'union'].includes(kind))
116
+ throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
117
+ if (kind === 'union') {
118
+ let str = body === null ? 'null' : await streamToString(body)
119
+ let json = JSON.parse(str)
120
+ return unionize(json, schema)
127
121
  }
128
- } else if (contentType.match(TXT_HEADER_RX)) {
129
- if (!schema) {
130
- return await streamToString(body)
131
- } else {
132
- return await streamToString(body, schema)
122
+ const str = body === null ? 'null' : await streamToString(body)
123
+ let json
124
+ try {
125
+ json = JSON.parse(str)
126
+ } catch (err: any) {
127
+ throw new RequestError({
128
+ status: 400,
129
+ payload: { body: err?.message ?? 'Parsing error' },
130
+ })
133
131
  }
134
- } else if (contentType.match(FORM_HEADER_RX)) {
135
- if (!schema) {
136
- return await streamToUrlForm(body)
137
- } else {
138
- if (kind !== 'urlForm')
139
- throw new RequestError({ status: 400, payload: { body: `Expected ${kind}, received urlForm` } })
140
- if (isStream) return $streamToUrlForm(body, schema as STStream<STUrlForm>)
141
- else return await streamToUrlForm(body, schema as STUrlForm)
132
+ return validate(json, schema, { parse: true })
133
+ } else if (contentType === 'urlForm') {
134
+ if (!['object', 'union'].includes(kind))
135
+ throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
136
+ if (body === null)
137
+ return isStream
138
+ ? new ReadableStream({
139
+ start(controller) {
140
+ controller.enqueue([])
141
+ controller.close()
142
+ },
143
+ })
144
+ : await streamToUrlForm(
145
+ new ReadableStream({
146
+ start(controller) {
147
+ controller.enqueue(new Uint8Array())
148
+ controller.close()
149
+ },
150
+ }),
151
+ schema
152
+ )
153
+ if (kind === 'union') {
154
+ const b = await streamToUrlForm(body)
155
+ return unionize(b, schema)
142
156
  }
143
- } else if (contentType.match(MP_HEADER_RX)) {
144
- const boundary = contentType.match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
145
- if (!schema) {
146
- return await streamToMultipartForm(body, boundary)
147
- } else {
148
- if (kind !== 'multipartForm')
149
- throw new RequestError({ status: 400, payload: { body: `Expected ${kind}, received MultipartForm` } })
150
- if (isStream) return $streamToMultipartForm(body, boundary, schema as STStream<STMultipartForm>)
151
- else {
152
- return streamToMultipartForm(body, boundary, schema as STMultipartForm)
153
- }
157
+ if (isStream) return $streamToUrlForm(body, schema as STStream<STObject>)
158
+ else return await streamToUrlForm(body, schema as STObject)
159
+ } else if (contentType === 'multipart') {
160
+ if (kind !== 'multipartForm') throw new RequestError({ status: 400, payload: { body: `Not a valid body` } })
161
+ if (body === null)
162
+ return isStream
163
+ ? new ReadableStream({
164
+ start(controller) {
165
+ controller.enqueue({ headers: {} })
166
+ controller.close()
167
+ },
168
+ })
169
+ : {}
170
+ const boundary = headers?.['content-type'].match(/boundary\="?([^"]*)"?;?.*$/)?.[1] || ''
171
+ if (kind === 'union') {
172
+ let mp = await streamToMultipartForm(body, boundary)
173
+ return unionize(mp, schema)
154
174
  }
155
- } else {
156
- return rsToAsyncIterator(body)
175
+ if (isStream) return $streamToMultipartForm(body, boundary, schema as STStream<STMultipartForm>)
176
+ return streamToMultipartForm(body, boundary, schema as STMultipartForm)
177
+ } else if (contentType === 'default') {
178
+ throw new RequestError({ status: 400, payload: { body: `Not a valid content-type` } })
157
179
  }
158
180
  }
159
181
  } catch (error) {
@@ -164,15 +186,15 @@ export const requestBodyParser = async (
164
186
  async function* $streamToString(body: ReadableStream) {
165
187
  for await (const chunk of body) yield textDecoder.decode(chunk)
166
188
  }
167
- const streamToString = async (body: ReadableStream, schema?: STBody): Promise<any> => {
189
+ const streamToString = async (body: ReadableStream, schema?: STBodyValue): Promise<any> => {
168
190
  let res = ''
169
191
  for await (const chunk of $streamToString(body)) res += chunk
170
- if (schema) return validate(res, schema, true)
192
+ if (schema) return validate(res, schema, { parse: true })
171
193
  return res
172
194
  }
173
195
  async function* $streamToUrlForm(
174
196
  body: ReadableStream<Uint8Array>,
175
- schema?: STStream<STUrlForm>
197
+ schema?: STStream<STObject>
176
198
  ): AsyncGenerator<[string, any]> {
177
199
  let rest: Uint8Array = new Uint8Array()
178
200
  let bK: Uint8Array = new Uint8Array()
@@ -190,7 +212,7 @@ async function* $streamToUrlForm(
190
212
  bV.set(chunk.slice(start, i), rest.length)
191
213
  let [key, val]: [string, any] = [
192
214
  decodeURIComponent(textDecoder.decode(bK)),
193
- decodeURIComponent(textDecoder.decode(bV))
215
+ decodeURIComponent(textDecoder.decode(bV)),
194
216
  ]
195
217
  try {
196
218
  let s = schema?.props?.[key]?.[Kind] === 'array' ? schema?.props?.[key].items : schema?.props?.[key]
@@ -221,7 +243,7 @@ async function* $streamToUrlForm(
221
243
  }
222
244
  let [key, val]: [string, any] = [
223
245
  decodeURIComponent(textDecoder.decode(bK)),
224
- decodeURIComponent(textDecoder.decode(rest))
246
+ decodeURIComponent(textDecoder.decode(rest)),
225
247
  ]
226
248
  try {
227
249
  let s = schema?.props?.[key]?.[Kind] === 'array' ? schema?.props?.[key].items : schema?.props?.[key]
@@ -241,10 +263,10 @@ async function* $streamToUrlForm(
241
263
  if (reqKeys.length > 0)
242
264
  throw new RequestError({
243
265
  status: 400,
244
- payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
266
+ payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` },
245
267
  })
246
268
  }
247
- const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlForm) => {
269
+ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STObject) => {
248
270
  let entries = []
249
271
  const required = Object.fromEntries(
250
272
  Object.entries(schema?.props || {}).filter(([_, v]: [string, any]) => !v?.[Optional])
@@ -252,7 +274,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
252
274
  let errors: Record<string, any> = {}
253
275
  for await (const chunk of $streamToUrlForm(body)) entries.push(chunk)
254
276
  const object: Record<string, any> = {}
255
- for (let e of entries) {
277
+ for (let e of entries.filter(([k]) => k)) {
256
278
  if (e[0] in object) {
257
279
  if (Array.isArray(object[e[0]])) object[e[0]].push(e[1])
258
280
  else object[e[0]] = [object[e[0]], e[1]]
@@ -278,7 +300,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
278
300
  if (reqKeys.length > 0)
279
301
  throw new RequestError({
280
302
  status: 400,
281
- payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
303
+ payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` },
282
304
  })
283
305
  return object
284
306
  }
@@ -324,7 +346,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
324
346
  delete required[headers.name]
325
347
  yield {
326
348
  headers,
327
- content: parseMultipartContent(bV, headers, schema)
349
+ content: parseMultipartContent(bV, headers, schema),
328
350
  }
329
351
  } catch (err) {
330
352
  if (err instanceof RequestError) throw err
@@ -358,7 +380,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
358
380
  delete required[headers.name]
359
381
  yield {
360
382
  headers,
361
- content: parseMultipartContent(bV, headers, schema)
383
+ content: parseMultipartContent(bV, headers, schema),
362
384
  }
363
385
  } catch (err) {
364
386
  throw new RequestError({ status: 400, payload: { body: { [headers.name]: err } } })
@@ -374,15 +396,15 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
374
396
  if (reqKeys.length > 0)
375
397
  throw new RequestError({
376
398
  status: 400,
377
- payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
399
+ payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` },
378
400
  })
379
401
  }
380
- const parseMultipartHeader = (header: string): { name: string;[key: string]: string } | null => {
402
+ const parseMultipartHeader = (header: string): { name: string; [key: string]: string } | null => {
381
403
  if (!header) return null
382
404
  let disposition = 'form-data'
383
405
  const multipartHeader = [
384
406
  ...header.matchAll(/\s*([\w-]+)\s*:\s*([^;]*);?/g),
385
- ...header.matchAll(/;?\s*(\w+)\s*=\s*\"([^"]*)\";?/g)
407
+ ...header.matchAll(/;?\s*(\w+)\s*=\s*\"([^"]*)\";?/g),
386
408
  ].reduce((acc: Record<string, string>, v: string[]) => {
387
409
  const key = v[1].toLowerCase().replace(/^content-/, '')
388
410
  if (key === 'disposition') {
@@ -421,7 +443,7 @@ const parseMultipartContent = (
421
443
  } catch (err: any) {
422
444
  throw new RequestError({
423
445
  status: 400,
424
- payload: { body: { [headers.name]: err?.message || 'Parsing error' } }
446
+ payload: { body: { [headers.name]: err?.message || 'Parsing error' } },
425
447
  })
426
448
  }
427
449
  try {
@@ -436,7 +458,7 @@ const parseMultipartContent = (
436
458
  } else {
437
459
  throw new RequestError({
438
460
  status: 400,
439
- payload: { body: { [headers.name]: `Expected ${schema?.props[headers.name][Kind]} found json` } }
461
+ payload: { body: { [headers.name]: `Expected ${schema?.props[headers.name][Kind]} found json` } },
440
462
  })
441
463
  }
442
464
  }
@@ -472,11 +494,9 @@ const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary:
472
494
  try {
473
495
  if (Array.isArray(res[chunk.headers.name].content) && schema?.props?.[chunk.headers.name]?.[Kind] !== 'array')
474
496
  throw `Multiple values found`
475
- res[chunk.headers.name].content = validate(
476
- res[chunk.headers.name].content,
477
- schema?.props[chunk.headers.name],
478
- true
479
- )
497
+ res[chunk.headers.name].content = validate(res[chunk.headers.name].content, schema?.props[chunk.headers.name], {
498
+ parse: true,
499
+ })
480
500
  if (schema.props[chunk.headers.name][Kind] === 'array')
481
501
  for (let [k, v] of Object.entries(res[chunk.headers.name].content)) {
482
502
  try {
@@ -494,7 +514,7 @@ const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary:
494
514
  if (Object.keys(errors).length)
495
515
  throw new RequestError({
496
516
  status: 400,
497
- payload: { body: errors }
517
+ payload: { body: errors },
498
518
  })
499
519
  for (const [k, s] of Object.entries(required)) {
500
520
  if (s[Kind] === 'array') {
@@ -506,14 +526,14 @@ const streamToMultipartForm = async (data: ReadableStream<Uint8Array>, boundary:
506
526
  if (reqKeys.length > 0)
507
527
  throw new RequestError({
508
528
  status: 400,
509
- payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` }
529
+ payload: { body: `Missing field${reqKeys.length > 1 ? 's' : ''}: ${reqKeys.join(', ')}` },
510
530
  })
511
531
  return res
512
532
  }
513
533
  const paramParser = (
514
534
  value: string | string[] | null,
515
535
  type: STMultipartFormValues
516
- ): MaybeArray<Static<STUrlFormValues>> => {
536
+ ): MaybeArray<Static<STPropsValue>> => {
517
537
  if (value === undefined) {
518
538
  if (type?.[Optional]) return type?.default
519
539
  else throw `Required`
@@ -525,7 +545,7 @@ const paramParser = (
525
545
  let errors: Record<number, any> = {}
526
546
  for (let [idx, v] of value.entries()) {
527
547
  try {
528
- pv.push(paramParser(v, type.items as STMultipartFormValues) as Static<STUrlFormValues>)
548
+ pv.push(paramParser(v, type.items as STMultipartFormValues) as Static<STPropsValue>)
529
549
  } catch (error) {
530
550
  errors[idx] = error
531
551
  }
@@ -554,10 +574,21 @@ const paramParser = (
554
574
  validate(value, type)
555
575
  return value
556
576
  } else if (type[Kind] === 'literal') {
557
- if (value !== type.value) throw `Not a valid value`
558
- return value
577
+ let val: any = value
578
+ if (typeof type.value === 'boolean') val = value === 'true' ? true : value === 'false' ? false : value
579
+ if (typeof type.value === 'number') val = Number(value)
580
+ if (val !== type.value) throw `Not a valid value`
581
+ return val
582
+ } else if (type[Kind] === 'object') {
583
+ let json
584
+ try {
585
+ json = JSON.parse(value)
586
+ } catch (e) {
587
+ throw `Not a valid object`
588
+ }
589
+ return validate(json, type)
559
590
  } else if (type[Kind] === 'array') {
560
- return [paramParser(value, type.items as STMultipartFormValues) as Static<STUrlFormValues>]
591
+ return [paramParser(value, type.items as STMultipartFormValues) as Static<STPropsValue>]
561
592
  } else if (type[Kind] === 'byteArray') {
562
593
  return Uint8Array.from(value, c => c.charCodeAt(0))
563
594
  } else if (type[Kind] === 'union') {
@@ -645,7 +676,7 @@ export const parseEntry = <T extends STProps>(
645
676
  export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
646
677
  const details = {
647
678
  status: ctx.set.status || 200,
648
- headers: new Headers()
679
+ headers: new Headers(),
649
680
  }
650
681
  for (const [key, value] of Object.entries(ctx.set.headers)) {
651
682
  if (Array.isArray(value)) {
@@ -686,9 +717,9 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
686
717
  id = crypto.randomUUID()
687
718
  }
688
719
  controller.close()
689
- }
720
+ },
690
721
  })
691
- details.headers.set('Content-Type', 'text/event-stream')
722
+ details.headers.set('content-type', 'text/event-stream')
692
723
  return new Response(rs, details)
693
724
  } else {
694
725
  try {
@@ -701,3 +732,24 @@ export const responseParser = (response: any, ctx: Context, schema?: STResponse)
701
732
  }
702
733
  }
703
734
  }
735
+
736
+ const unionize = (b: any, schema: STUnion) => {
737
+ let res
738
+ let error
739
+ const discirminants = schema.anyOf.reduce(
740
+ (acc, obj) =>
741
+ acc.filter(k => obj?.props && k in obj.props && obj.props[k]?.[Kind] === 'literal' && !obj.props[k]?.Optional),
742
+ Object.keys(schema.anyOf[0]?.props || {})
743
+ )
744
+ for (let s of schema.anyOf) {
745
+ try {
746
+ res = validate(b, s, { parse: true })
747
+ if (res !== undefined) break
748
+ } catch (err: any) {
749
+ if (discirminants.every(d => !err?.[d]?.startsWith('Not a valid value'))) error = err
750
+ }
751
+ }
752
+ if (res !== undefined) return res
753
+ else if (error) throw new RequestError({ status: 400, payload: { body: error } })
754
+ else throw new RequestError({ status: 400, payload: { body: `No matching body schema found` } })
755
+ }