galbe 0.3.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.
@@ -0,0 +1,264 @@
1
+ import type { STArray, STJson, STLiteral, STObject, STProps, STSchema, STUnion } from '../../../src/schema'
2
+
3
+ import { Galbe } from '../../../src'
4
+ import { walkRoutes, HttpStatus } from '../../../src/util'
5
+ import { Kind, Optional } from '../../../src/schema'
6
+
7
+ import { OpenAPIV3 } from 'openapi-types'
8
+
9
+ type SchemaType = { type: string; format: string; isJson: boolean }
10
+
11
+ const schemaToMedia = ({ type, format, isJson }: SchemaType) =>
12
+ isJson || (type && ['object', 'number', 'boolean', 'array'].includes(type))
13
+ ? 'application/json'
14
+ : format === 'byte'
15
+ ? 'application/octet-stream'
16
+ : type === 'string'
17
+ ? 'text/plain'
18
+ : '*/*'
19
+
20
+ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<OpenAPIV3.Document> => {
21
+ let paths: any = {}
22
+ let components: OpenAPIV3.ComponentsObject = {
23
+ securitySchemes: {},
24
+ schemas: {},
25
+ parameters: {},
26
+ requestBodies: {},
27
+ responses: {}
28
+ }
29
+
30
+ const schemaToOpenapi = (
31
+ schema: STSchema
32
+ ): { schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; isJson?: boolean } => {
33
+ let s = {}
34
+ let kind = schema[Kind]
35
+ let isJson = false
36
+
37
+ let pattern = schema?.pattern?.toString()
38
+ if (pattern) pattern = pattern.substring(1, pattern.length - 1)
39
+
40
+ let minLength = schema?.minLength
41
+ let maxLength = schema?.maxLength
42
+ let minimum = schema?.min
43
+ let maximum = schema?.max
44
+ let exclusiveMinimum = schema?.exclusiveMin
45
+ let exclusiveMaximum = schema?.exclusiveMax
46
+ let minItems = schema?.minItems
47
+ let maxItems = schema?.maxItems
48
+ let uniqueItems = schema?.unique
49
+
50
+ if (components.schemas && (schema.id as string) in components.schemas) {
51
+ //@ts-ignore
52
+ return { schema: { $ref: `#/components/schemas/${schema.id}` } }
53
+ }
54
+ // TODO add constraints min, max etc.
55
+ if (kind === 'boolean') s = { type: 'boolean' }
56
+ else if (kind === 'byteArray') s = { type: 'string', format: 'byte' }
57
+ else if (kind === 'number')
58
+ s = {
59
+ type: 'number',
60
+ ...(exclusiveMinimum ? { exclusiveMinimum } : {}),
61
+ ...(exclusiveMaximum ? { exclusiveMaximum } : {}),
62
+ ...(minimum ? { minimum } : {}),
63
+ ...(maximum ? { maximum } : {})
64
+ }
65
+ else if (kind === 'integer')
66
+ s = {
67
+ type: 'integer',
68
+ ...(exclusiveMinimum ? { exclusiveMinimum } : {}),
69
+ ...(exclusiveMaximum ? { exclusiveMaximum } : {}),
70
+ ...(minimum ? { minimum } : {}),
71
+ ...(maximum ? { maximum } : {})
72
+ }
73
+ else if (kind === 'string')
74
+ s = {
75
+ type: 'string',
76
+ ...(pattern ? { pattern } : {}),
77
+ ...(minLength ? { minLength } : {}),
78
+ ...(maxLength ? { maxLength } : {})
79
+ }
80
+ else if (kind === 'any') s = { type: 'string' }
81
+ else if (kind === 'literal') {
82
+ let value = (schema as STLiteral).value
83
+ s = { type: 'string', enum: [value] }
84
+ } else if (kind === 'array') {
85
+ s = {
86
+ type: 'array',
87
+ items: schemaToOpenapi((schema as STArray).items).schema,
88
+ ...(minItems ? { minItems } : {}),
89
+ ...(maxItems ? { maxItems } : {}),
90
+ ...(uniqueItems ? { uniqueItems } : {})
91
+ }
92
+ } else if (kind === 'object') {
93
+ let props = (schema as STObject).props || {}
94
+ let required = Object.entries(props)
95
+ .filter(([_, v]) => !v?.[Optional])
96
+ .map(([k, _]) => k)
97
+ s = {
98
+ type: 'object',
99
+ properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
100
+ ...(required.length ? { required } : {})
101
+ }
102
+ } else if (kind === 'json') {
103
+ let props = ((schema as STJson).props || {}) as STProps
104
+ let type = (schema as STJson).type
105
+ if (type === 'unknown') type = 'object'
106
+ let required = Object.entries(props)
107
+ .filter(([_, v]) => !v?.[Optional])
108
+ .map(([k, _]) => k)
109
+ isJson = true
110
+ s = {
111
+ type: type,
112
+ ...(type === 'object'
113
+ ? {
114
+ properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
115
+ ...(required.length ? { required } : {})
116
+ }
117
+ : {})
118
+ }
119
+ } else if (kind === 'union') {
120
+ let anyOf = (schema as STUnion).anyOf
121
+ s = {
122
+ anyOf: anyOf.map(s => schemaToOpenapi(s).schema)
123
+ }
124
+ }
125
+ s = { title: schema.title, description: schema.description, ...s }
126
+ if (components.schemas && schema.id) {
127
+ components.schemas[schema.id] = s
128
+ return { schema: { $ref: `#/components/schemas/${schema.id}` } }
129
+ }
130
+ return { schema: s, isJson }
131
+ }
132
+
133
+ const resolveRef = (schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject): OpenAPIV3.SchemaObject => {
134
+ if (!(schema as OpenAPIV3.ReferenceObject)?.$ref) return schema as OpenAPIV3.SchemaObject
135
+ let ref = (schema as OpenAPIV3.ReferenceObject)?.$ref
136
+ let match = ref.match('^#/components/(schemas|requestBodies|responses)/(.*)$')
137
+ if (!match) throw new Error(`Invalid schema ref ${ref}`)
138
+ let [_, kind, refPath] = match as [string, keyof typeof components, string]
139
+ //@ts-ignore
140
+ return refPath.split('/').reduce((c, k) => {
141
+ if (c && k in c) return c[k]
142
+ else return undefined
143
+ }, components[kind])
144
+ }
145
+
146
+ const parseParam = (key: string, param: STSchema, kind: 'query' | 'header' | 'path' | 'cookie') => {
147
+ let { schema } = schemaToOpenapi({ ...param, [Optional]: false })
148
+ let p: OpenAPIV3.ParameterObject = {
149
+ name: key,
150
+ in: kind,
151
+ description: param?.description,
152
+ required: kind === 'path' ? true : !param[Optional] || undefined,
153
+ deprecated: param.deprecated,
154
+ schema
155
+ }
156
+ if (components.parameters && param.id) components.parameters[param.id] = p
157
+ return p
158
+ }
159
+
160
+ const metaRoutes = g.meta?.reduce(
161
+ (routes, c) => ({ ...routes, ...c.routes }),
162
+ {} as Record<string, Record<string, Record<string, any>>>
163
+ )
164
+ walkRoutes(g.router.routes, r => {
165
+ let meta = metaRoutes?.[r.path]?.[r.method]
166
+ let path = r.path.replaceAll(/:([^\/]+)/g, '{$1}')
167
+ if (!(path in paths)) paths[path] = {}
168
+ let tags = [...(meta?.tags?.split(' ')?.map((t: string) => t.trim()) || []), ...(meta?.tag || [])]
169
+ let security: Record<string, any> = []
170
+
171
+ let pathParam = r.schema?.params
172
+ ? Object.entries(r.schema?.params as Record<string, STSchema>).map(([k, v]) => parseParam(k, v, 'path'))
173
+ : []
174
+ let queryParam = r.schema?.query
175
+ ? Object.entries(r.schema?.query as Record<string, STSchema>).map(([k, v]) => parseParam(k, v, 'query'))
176
+ : []
177
+ let headerParam = r.schema?.headers
178
+ ? Object.entries(r.schema?.headers as Record<string, STSchema>)
179
+ .map(([k, v]) => {
180
+ let p = parseParam(k, v, 'header')
181
+ if (k.match(/authorization/i)) {
182
+ // TODO: handle other auth methods
183
+ if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
184
+ security.push({ bearerAuth: [] })
185
+ components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
186
+ return null
187
+ }
188
+ }
189
+ return p
190
+ })
191
+ .filter(p => p)
192
+ : []
193
+ // TODO cookieParam
194
+ let parameters = [...pathParam, ...queryParam, ...headerParam]
195
+
196
+ let requestBody
197
+ if (r.schema.body) {
198
+ let { schema, isJson } = schemaToOpenapi(r.schema.body)
199
+ let { type, format } = resolveRef(schema)
200
+ let media = schemaToMedia({ type, format, isJson } as SchemaType)
201
+ requestBody = {
202
+ description: r.schema.body.description,
203
+ required: !r.schema.body[Optional],
204
+ content: {
205
+ [media]: { schema }
206
+ }
207
+ }
208
+ if (r.schema.body.id && components.requestBodies) {
209
+ components.requestBodies[r.schema.body.id] = requestBody
210
+ requestBody = { $ref: `#/components/requestBodies/${r.schema.body.id}` }
211
+ }
212
+ }
213
+ let responses
214
+ if (r.schema.response && Object.keys(r.schema.response).length) {
215
+ responses = Object.fromEntries(
216
+ Object.entries(r.schema.response).map(([status, v]) => {
217
+ let s = Number(status) as keyof typeof HttpStatus
218
+ let { schema, isJson } = schemaToOpenapi(v)
219
+ let { type, format } = resolveRef(schema)
220
+ let media = schemaToMedia({ type, format, isJson } as SchemaType)
221
+ let response: OpenAPIV3.ResponseObject = {
222
+ description: v.description || HttpStatus[Number(s) as keyof typeof HttpStatus] || 'Response',
223
+ content: { [media]: { schema: schema } }
224
+ }
225
+ if (components.responses && r.schema.response?.[s].id) {
226
+ components.responses[r.schema.response?.[s].id as string] = response
227
+ //@ts-ignore
228
+ response = { $ref: `#/components/responses/${r.schema.response?.[s].id}` }
229
+ }
230
+ return [s, response]
231
+ })
232
+ )
233
+ } else {
234
+ responses = {
235
+ default: { description: HttpStatus[200] }
236
+ }
237
+ }
238
+ paths[path][r.method] = {
239
+ tags: tags.length ? tags : undefined,
240
+ summary: meta?.head,
241
+ operationId: meta?.operationId,
242
+ parameters: parameters.length ? parameters : undefined,
243
+ requestBody,
244
+ responses,
245
+ ...(security.length ? { security } : {}),
246
+ deprecated: meta?.deprecated ? true : undefined
247
+ }
248
+ })
249
+
250
+ //@ts-ignore
251
+ components = Object.entries(components).reduce((p, [k, v]) => {
252
+ if (Object.keys(v).length) p[k] = v
253
+ return p
254
+ }, {} as Record<string, OpenAPIV3.ComponentsObject>)
255
+ return {
256
+ openapi: version,
257
+ info: {
258
+ title: 'Galbe app',
259
+ version: '0.1.0'
260
+ },
261
+ paths,
262
+ components: Object.keys(components)?.length ? components : undefined
263
+ }
264
+ }
package/src/extras.ts ADDED
@@ -0,0 +1 @@
1
+ export { OpenAPISerializer } from './extras/spec/openapi.serializer'
package/src/index.ts CHANGED
@@ -19,11 +19,10 @@ import type {
19
19
 
20
20
  import server from './server'
21
21
  import { GalbeRouter } from './router'
22
- import { defineRoutes } from './routes'
23
- import { extractMetaRoute, logRoute } from './util'
24
22
  import { SchemaType, type STObject, type Static } from './schema'
25
23
 
26
24
  const overloadDiscriminer = <
25
+ M extends Method,
27
26
  Path extends string,
28
27
  H extends STHeaders,
29
28
  P extends Partial<STParams<Path>>,
@@ -32,14 +31,16 @@ const overloadDiscriminer = <
32
31
  R extends STResponse
33
32
  >(
34
33
  galbe: Galbe,
35
- method: Method,
34
+ method: M,
36
35
  path: Path,
37
36
  arg2:
38
- | RequestSchema<Path, H, P, Q, B, R>
39
- | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
40
- | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
41
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
42
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
37
+ | RequestSchema<M, Path, H, P, Q, B, R>
38
+ | Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
39
+ | Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
40
+ arg3?:
41
+ | Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[]
42
+ | Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>,
43
+ arg4?: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
43
44
  ) => {
44
45
  const defaultSchema = {}
45
46
  if (typeof arg2 === 'function') {
@@ -55,6 +56,7 @@ const overloadDiscriminer = <
55
56
  throw new Error('Undefined route signature')
56
57
  }
57
58
  const galbeMethod = <
59
+ M extends Method,
58
60
  Path extends string,
59
61
  H extends STHeaders,
60
62
  P extends Partial<STParams<Path>>,
@@ -63,19 +65,22 @@ const galbeMethod = <
63
65
  R extends STResponse
64
66
  >(
65
67
  _galbe: Galbe,
66
- method: Method,
68
+ method: M,
67
69
  path: Path,
68
- schema: RequestSchema<Path, H, P, Q, B, R> | undefined,
69
- hooks: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | undefined,
70
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
70
+ schema: RequestSchema<M, Path, H, P, Q, B, R> | undefined,
71
+ hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[] | undefined,
72
+ handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
71
73
  ) => {
72
74
  schema = schema ?? {}
73
75
  hooks = hooks || []
74
- const context: Context<Path, typeof schema> = {
76
+ const context: Context<M, Path, typeof schema> = {
75
77
  headers: {} as Static<STObject<Exclude<(typeof schema)['headers'], undefined>>>,
76
78
  params: {} as any,
77
79
  query: {} as Static<STObject<Exclude<(typeof schema)['query'], undefined>>>,
78
- body: {} as Static<Exclude<(typeof schema)['body'], undefined>>,
80
+ //@ts-ignore
81
+ body: ['get', 'options', 'head'].includes(method)
82
+ ? null
83
+ : ({} as Static<Exclude<(typeof schema)['body'], undefined>>),
79
84
  request: {} as Request,
80
85
  state: {},
81
86
  set: {} as {
@@ -100,7 +105,6 @@ export const $T = new SchemaType()
100
105
 
101
106
  export { RequestError } from './types'
102
107
 
103
- const indexRoutes: { method: string; path: string }[] = []
104
108
  /**
105
109
  * #### Galbe Server
106
110
  * Instanciate a Galbe web server
@@ -120,7 +124,6 @@ export class Galbe {
120
124
  router: GalbeRouter
121
125
  errorHandler?: ErrorHandler
122
126
  listening: boolean = false
123
- #prepare: boolean = false
124
127
  server?: Server
125
128
  plugins: GalbePlugin[] = []
126
129
  constructor(config?: GalbeConfig) {
@@ -135,32 +138,27 @@ export class Galbe {
135
138
  }
136
139
  private add(route: any) {
137
140
  this.router.add(route)
138
- if (Bun.env.BUN_ENV === 'development') {
139
- if (!this.#prepare) indexRoutes.push({ method: route.method, path: route.path })
140
- else logRoute(route, extractMetaRoute(route, this.meta))
141
- }
141
+ return route
142
142
  }
143
143
  async use(plugin: GalbePlugin) {
144
144
  this.plugins.push(plugin)
145
145
  }
146
+ async init() {
147
+ for (const p of this.plugins) {
148
+ if (p.init) await p.init(this.config?.plugin?.[p.name] || {}, this)
149
+ }
150
+ }
146
151
  async listen(port?: number) {
147
152
  port = port || this.config?.port || 3000
148
153
  this.config.port = port
149
154
  if (this.listening) this.stop()
155
+ await this.init()
156
+ this.server = await server(this, port)
150
157
  if (Bun.env.BUN_ENV === 'development') {
151
- this.#prepare = true
152
- console.log('🏗️ \x1b[1;30mConstructing routes\x1b[0m')
153
- for (const r of indexRoutes) logRoute(r, extractMetaRoute(r, this.meta))
154
- await defineRoutes(this.config || {}, this)
155
- console.log('\n✅ \x1b[1;30mdone\x1b[0m')
156
- this.server = await server(this, port)
157
158
  const url = `http://localhost:${port}${this.config?.basePath || ''}`
158
- console.log(`\n\x1b[1;30m🚀 API running at\x1b[0m \x1b[4;34m${url}\x1b[0m`)
159
- } else {
160
- this.server = await server(this, port)
159
+ console.log(`\x1b[1;30m🚀 Server running at\x1b[0m \x1b[4;34m${url}\x1b[0m\n`)
161
160
  }
162
161
  this.listening = true
163
- this.#prepare = false
164
162
  return this.server
165
163
  }
166
164
  stop() {
@@ -169,102 +167,134 @@ export class Galbe {
169
167
  onError(handler: ErrorHandler) {
170
168
  this.errorHandler = handler
171
169
  }
172
- get: Endpoint = <
170
+ get: Endpoint<'get'> = <
173
171
  Path extends string,
174
- H extends STHeaders,
175
172
  P extends Partial<STParams<Path>>,
173
+ H extends STHeaders,
176
174
  Q extends STQuery,
177
- B extends STBody,
175
+ B extends undefined,
178
176
  R extends STResponse
179
177
  >(
180
178
  path: Path,
181
179
  arg2:
182
- | RequestSchema<Path, H, P, Q, B, R>
183
- | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
184
- | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
185
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
186
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
180
+ | RequestSchema<'get', Path, H, P, Q, B, R>
181
+ | Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>[]
182
+ | Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>,
183
+ arg3?:
184
+ | Hook<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>[]
185
+ | Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>,
186
+ arg4?: Handler<'get', Path, RequestSchema<'get', Path, H, P, Q, B, R>>
187
+ //@ts-ignore
187
188
  ) => this.add(overloadDiscriminer(this, 'get', path, arg2, arg3, arg4))
188
- post: Endpoint = <
189
+ post: Endpoint<'post'> = <
189
190
  Path extends string,
190
- H extends STHeaders,
191
191
  P extends Partial<STParams<Path>>,
192
+ H extends STHeaders,
192
193
  Q extends STQuery,
193
194
  B extends STBody,
194
195
  R extends STResponse
195
196
  >(
196
197
  path: Path,
197
198
  arg2:
198
- | RequestSchema<Path, H, P, Q, B, R>
199
- | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
200
- | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
201
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
202
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
199
+ | RequestSchema<'post', Path, H, P, Q, B, R>
200
+ | Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>[]
201
+ | Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>,
202
+ arg3?:
203
+ | Hook<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>[]
204
+ | Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>,
205
+ arg4?: Handler<'post', Path, RequestSchema<'post', Path, H, P, Q, B, R>>
206
+ //@ts-ignore
203
207
  ) => this.add(overloadDiscriminer(this, 'post', path, arg2, arg3, arg4))
204
- put: Endpoint = <
208
+ put: Endpoint<'put'> = <
205
209
  Path extends string,
206
- H extends STHeaders,
207
210
  P extends Partial<STParams<Path>>,
211
+ H extends STHeaders,
208
212
  Q extends STQuery,
209
213
  B extends STBody,
210
214
  R extends STResponse
211
215
  >(
212
216
  path: Path,
213
217
  arg2:
214
- | RequestSchema<Path, H, P, Q, B, R>
215
- | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
216
- | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
217
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
218
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
218
+ | RequestSchema<'put', Path, H, P, Q, B, R>
219
+ | Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>[]
220
+ | Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>,
221
+ arg3?:
222
+ | Hook<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>[]
223
+ | Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>,
224
+ arg4?: Handler<'put', Path, RequestSchema<'put', Path, H, P, Q, B, R>>
219
225
  ) => this.add(overloadDiscriminer(this, 'put', path, arg2, arg3, arg4))
220
- patch: Endpoint = <
226
+ patch: Endpoint<'patch'> = <
221
227
  Path extends string,
222
- H extends STHeaders,
223
228
  P extends Partial<STParams<Path>>,
229
+ H extends STHeaders,
224
230
  Q extends STQuery,
225
231
  B extends STBody,
226
232
  R extends STResponse
227
233
  >(
228
234
  path: Path,
229
235
  arg2:
230
- | RequestSchema<Path, H, P, Q, B, R>
231
- | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
232
- | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
233
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
234
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
236
+ | RequestSchema<'patch', Path, H, P, Q, B, R>
237
+ | Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>[]
238
+ | Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>,
239
+ arg3?:
240
+ | Hook<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>[]
241
+ | Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>,
242
+ arg4?: Handler<'patch', Path, RequestSchema<'patch', Path, H, P, Q, B, R>>
235
243
  ) => this.add(overloadDiscriminer(this, 'patch', path, arg2, arg3, arg4))
236
- delete: Endpoint = <
244
+ delete: Endpoint<'delete'> = <
237
245
  Path extends string,
238
- H extends STHeaders,
239
246
  P extends Partial<STParams<Path>>,
247
+ H extends STHeaders,
240
248
  Q extends STQuery,
241
249
  B extends STBody,
242
250
  R extends STResponse
243
251
  >(
244
252
  path: Path,
245
253
  arg2:
246
- | RequestSchema<Path, H, P, Q, B, R>
247
- | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
248
- | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
249
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
250
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
254
+ | RequestSchema<'delete', Path, H, P, Q, B, R>
255
+ | Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>[]
256
+ | Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>,
257
+ arg3?:
258
+ | Hook<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>[]
259
+ | Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>,
260
+ arg4?: Handler<'delete', Path, RequestSchema<'delete', Path, H, P, Q, B, R>>
251
261
  ) => this.add(overloadDiscriminer(this, 'delete', path, arg2, arg3, arg4))
252
- options: Endpoint = <
262
+ options: Endpoint<'options'> = <
253
263
  Path extends string,
254
- H extends STHeaders,
255
264
  P extends Partial<STParams<Path>>,
265
+ H extends STHeaders,
256
266
  Q extends STQuery,
257
267
  B extends STBody,
258
268
  R extends STResponse
259
269
  >(
260
270
  path: Path,
261
271
  arg2:
262
- | RequestSchema<Path, H, P, Q, B, R>
263
- | Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[]
264
- | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
265
- arg3?: Hook<Path, RequestSchema<Path, H, P, Q, B, R>>[] | Handler<Path, RequestSchema<Path, H, P, Q, B, R>>,
266
- arg4?: Handler<Path, RequestSchema<Path, H, P, Q, B, R>>
272
+ | RequestSchema<'options', Path, H, P, Q, B, R>
273
+ | Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>[]
274
+ | Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>,
275
+ arg3?:
276
+ | Hook<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>[]
277
+ | Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>,
278
+ arg4?: Handler<'options', Path, RequestSchema<'options', Path, H, P, Q, B, R>>
267
279
  ) => this.add(overloadDiscriminer(this, 'options', path, arg2, arg3, arg4))
280
+ head: Endpoint<'head'> = <
281
+ Path extends string,
282
+ P extends Partial<STParams<Path>>,
283
+ H extends STHeaders,
284
+ Q extends STQuery,
285
+ B extends STBody,
286
+ R extends STResponse
287
+ >(
288
+ path: Path,
289
+ arg2:
290
+ | RequestSchema<'head', Path, H, P, Q, B, R>
291
+ | Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>[]
292
+ | Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
293
+ arg3?:
294
+ | Hook<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>[]
295
+ | Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>,
296
+ arg4?: Handler<'head', Path, RequestSchema<'head', Path, H, P, Q, B, R>>
297
+ ) => this.add(overloadDiscriminer(this, 'head', path, arg2, arg3, arg4))
268
298
  }
269
299
 
270
300
  export * from './types'
package/src/parser.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MaybeArray, STBody, Context } from './index'
1
+ import type { MaybeArray, STBody, Context, STResponse } from './index'
2
2
  import type {
3
3
  STStream,
4
4
  STUrlForm,
@@ -283,6 +283,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
283
283
  }
284
284
  async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundary: string, schema?: STMultipartForm) {
285
285
  const bound = textEncoder.encode(boundary)
286
+ const delimiter = textEncoder.encode('\r\n\r\n')
286
287
  let rest = new Uint8Array()
287
288
  let bK: Uint8Array = new Uint8Array()
288
289
  let bV: Uint8Array = new Uint8Array()
@@ -294,6 +295,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
294
295
  start = 0
295
296
  for (let i = 0; i < chunk.length; i++) {
296
297
  let matchBound = true
298
+ let matchDelimiter = true
297
299
  for (let b = 0; b < bound.length; b++) {
298
300
  if (chunk[i + b] === bound[b]) continue
299
301
  else {
@@ -301,6 +303,15 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
301
303
  break
302
304
  }
303
305
  }
306
+ if (!matchBound) {
307
+ for (let b = 0; b < delimiter.length; b++) {
308
+ if (chunk[i + b] === delimiter[b]) continue
309
+ else {
310
+ matchDelimiter = false
311
+ break
312
+ }
313
+ }
314
+ }
304
315
  if (matchBound) {
305
316
  bV = new Uint8Array(rest.length + i - start)
306
317
  bV.set(rest)
@@ -324,7 +335,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
324
335
  start = i + bound.length
325
336
  rest = new Uint8Array()
326
337
  i = start
327
- } else if (chunk[i] === 0x0d && chunk[i + 1] === 0x0a && chunk[i + 2] === 0x0d) {
338
+ } else if (matchDelimiter) {
328
339
  bK = new Uint8Array(rest.length + i - start)
329
340
  bK.set(rest)
330
341
  bK.set(chunk.slice(start, i), rest.length)
@@ -630,14 +641,24 @@ export const parseEntry = <T extends STProps>(
630
641
  return parsedParams as Static<STObject<T>>
631
642
  }
632
643
 
633
- export const responseParser = (response: any, ctx: Context) => {
644
+ export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
634
645
  const details = {
635
646
  status: ctx.set.status || 200,
636
647
  headers: new Headers(ctx.set.headers)
637
648
  }
638
649
  if (response instanceof Response) return response
639
650
  else if (typeof response === 'string') {
640
- if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'text/plain')
651
+ if (!details?.headers?.has('content-type')) {
652
+ if (schema?.[details.status][Kind] === 'json') {
653
+ details?.headers?.set('content-type', 'application/json')
654
+ response = `"${response}"`
655
+ } else details?.headers?.set('content-type', 'text/plain')
656
+ }
657
+ return new Response(response, details)
658
+ } else if (response instanceof Uint8Array) {
659
+ if (!details?.headers?.has('content-type')) {
660
+ details?.headers?.set('content-type', 'application/octet-stream')
661
+ }
641
662
  return new Response(response, details)
642
663
  }
643
664
  if (response instanceof ReadableStream) {
@@ -666,7 +687,8 @@ export const responseParser = (response: any, ctx: Context) => {
666
687
  } else {
667
688
  try {
668
689
  if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'application/json')
669
- return new Response(JSON.stringify(response), details)
690
+ if (details.headers.get('content-type') === 'application/json') response = JSON.stringify(response)
691
+ return new Response(response, details)
670
692
  } catch (error) {
671
693
  console.error(error)
672
694
  throw new InternalError()