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.
@@ -1,4 +1,3 @@
1
- export type GalbeClientMode = 'response' | 'direct'
2
1
  export type GalbeClientConfig = {
3
2
  server?: { url?: string }
4
3
  }
@@ -7,7 +6,7 @@ export const Kind = Symbol.for('json.string')
7
6
  type Json<T> = { T: T }
8
7
 
9
8
  interface GR<S extends number | 'default' = 'default', B = any, OKS extends number = OKStatusCode> {
10
- status: Exclude<S, "default">
9
+ status: Exclude<S, 'default'>
11
10
  ok: S extends OKS ? true : false
12
11
  redirected: boolean
13
12
  statusText: string
@@ -18,16 +17,16 @@ interface GR<S extends number | 'default' = 'default', B = any, OKS extends numb
18
17
  stream?: ST
19
18
  ) => B extends Uint8Array
20
19
  ? ST extends true
21
- ? Promise<AsyncGenerator<Uint8Array, void, unknown>>
22
- : B extends Json<infer T>
23
- ? Promise<T>
24
- : Promise<B>
20
+ ? Promise<AsyncGenerator<Uint8Array, void, unknown>>
21
+ : B extends Json<infer T>
22
+ ? Promise<T>
23
+ : Promise<B>
25
24
  : B extends string
26
25
  ? ST extends true
27
- ? Promise<AsyncGenerator<string, void, unknown>>
28
- : B extends Json<infer T>
29
- ? Promise<T>
30
- : Promise<B>
26
+ ? Promise<AsyncGenerator<string, void, unknown>>
27
+ : B extends Json<infer T>
28
+ ? Promise<T>
29
+ : Promise<B>
31
30
  : B extends Json<infer T>
32
31
  ? Promise<T>
33
32
  : Promise<B>
@@ -42,16 +41,32 @@ type PGR<
42
41
  O extends number = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226
43
42
  > = Promise<GR<S, B, O>>
44
43
 
45
- type RequestOptions<H = any, Q = any, B = any> = {
44
+ type ContentType = 'byteArray' | 'text' | 'json' | 'urlForm' | 'multipart' | 'default'
45
+ type RequestOptions<
46
+ H = any,
47
+ Q = any,
48
+ B extends Partial<Record<ContentType, any>> = Partial<Record<ContentType, any>>,
49
+ C extends keyof B = keyof B
50
+ > = {
51
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'
46
52
  headers?: H
47
53
  query?: Q
48
- body?: B
49
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'
54
+ contentType?: C
55
+ body?: B[C]
50
56
  }
51
57
 
52
58
  const decoder = new TextDecoder()
53
59
  const DEFAULT_HEADERS = {
54
- 'user-agent': 'Galbe//*%(()=>version)()%*/'
60
+ 'user-agent': 'Galbe//*%(()=>version)()%*/',
61
+ }
62
+
63
+ const formdata = (data: Record<string, string | string[] | Blob>): FormData => {
64
+ const form = new FormData()
65
+ for (const [k, v] of Object.entries(data)) {
66
+ if (Array.isArray(v)) for (const v2 of v) form.append(String(k), String(v2))
67
+ else form.append(String(k), String(v))
68
+ }
69
+ return form
55
70
  }
56
71
 
57
72
  // Typescript types
@@ -62,27 +77,23 @@ Object.entries(types).map(([tk, t])=>{
62
77
  %*/
63
78
 
64
79
  export default class GalbeClient {
65
- config?: GalbeClientConfig
66
80
  /*%
67
81
  Object.entries(routes).map(([method, list])=>{
68
82
  return`${method} = {\n${list.map( r => {
69
83
  let p = Object.entries(r.params)
70
84
  let schemas = Object.keys(r.schemas).length ?
71
- `<${r.schemas.headers??'any'},${r.schemas.query??'any'},${r.schemas.body??'any'}>`:
85
+ `<${r.schemas.headers??'any'},${r.schemas.query??'any'},${r.schemas.body??'any'},CT>`:
72
86
  ''
73
87
  let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
74
88
  let responses = Object.keys(r.schemas?.response||{}).length ?
75
89
  `${Object.entries(r.schemas.response).filter(([s,_])=>s!=='"default"').map(([k,v])=>`PGR<${k},${v}${oks?.length?`,${oks.join('|')}`:''}>`).join('|')}|PGR<Exclude<HttpStatusCode,${Object.keys(r.schemas.response).filter(s=>s!=='"default"').join('|')}>,${'"default"' in r.schemas.response ? r.schemas.response['"default"'] : 'any'}${oks?.length?`,${oks.join('|')}`:''}>`:
76
90
  `PGR<HttpStatusCode,any${oks?.length?`,${oks.join('|')}`:',any'}>`
77
- return ` '${r.path}':(${p.length?p.map(([k,v])=>`${k}:${v.type}`).join(',')+', ':''}options:RequestOptions${schemas}={})=>this.fetch(\`${r.pathT}\`,{...options,method:'${r.method.toUpperCase()}'}) as ${responses}`
91
+ return ` '${r.path}':<CT extends ${r.contentTypes}>(${p.length?p.map(([k,v])=>`${k}:${v.type}`).join(',')+', ':''}options:RequestOptions${schemas}={})=>this.fetch(\`${r.pathT}\`,{...options,method:'${r.method.toUpperCase()}'}) as ${responses}`
78
92
  }).join(',\n')}\n}`
79
93
  }).join('\n')
80
94
  %*/
81
95
 
82
- constructor(config?: GalbeClientConfig) {
83
- //@ts-ignore
84
- this.config = { mode: 'response', ...config }
85
- }
96
+ constructor(private readonly config?: GalbeClientConfig) {}
86
97
 
87
98
  async fetch(path: string, options: RequestOptions) {
88
99
  let url = `${this?.config?.server?.url ?? ''}${path}`
@@ -90,8 +101,23 @@ export default class GalbeClient {
90
101
  url = `${url}?${params.toString()}`
91
102
  let res = await fetch(url, {
92
103
  method: options?.method || 'GET',
93
- headers: { ...DEFAULT_HEADERS, ...(options?.headers || {}) },
94
- ...(options?.body ? { body: JSON.stringify(options.body) } : {})
104
+ headers: {
105
+ ...DEFAULT_HEADERS,
106
+ ...(options?.contentType && ['byteArray', 'text', 'json', 'urlForm'].includes(options.contentType)
107
+ ? {
108
+ 'content-type': {
109
+ byteArray: 'application/octet-stream',
110
+ text: 'text/plain',
111
+ json: 'application/json',
112
+ urlForm: 'application/x-www-form-urlencoded',
113
+ }[options.contentType],
114
+ }
115
+ : {}),
116
+ ...(options?.headers || {}),
117
+ },
118
+ ...(options?.body
119
+ ? { body: options?.contentType === 'multipart' ? formdata(options?.body) : JSON.stringify(options.body) }
120
+ : {}),
95
121
  })
96
122
  return {
97
123
  headers: res.headers,
@@ -147,7 +173,7 @@ export default class GalbeClient {
147
173
  }
148
174
  }
149
175
  return res.body
150
- }
176
+ },
151
177
  }
152
178
  }
153
179
 
@@ -157,7 +183,7 @@ export default class GalbeClient {
157
183
  return list.filter(r=>r.alias).map(r => {
158
184
  let p = Object.entries(r.params)
159
185
  let schemas = Object.keys(r.schemas).length ?
160
- `<${r.schemas.headers??'any'},${r.schemas.query??'any'},${r.schemas.body??'any'}>`:
186
+ `<${r.schemas.headers??'any'},${r.schemas.query??'any'},${r.schemas.body??'any'},CT>`:
161
187
  ''
162
188
  let oks = Object.keys(r.schemas?.response||{}).filter(s=>s>=200&&s<300)
163
189
  let responses = Object.keys(r.schemas?.response||{}).length ?
@@ -167,7 +193,7 @@ export default class GalbeClient {
167
193
  let description = r.description ? ` * ${r.description.replace(/\n/g,'\n * ')}` : ''
168
194
  let params = Object.entries(r.schema.params || {}).map( ([k,v])=>`\n * @param ${k} - ${v.description?.replace(/\n/g,'\n ')}` ).join('')
169
195
  let query = Object.entries(r.schema.query || {}).map( ([k,v])=>`\n * @param options.query.${k} - ${v.description?.replace(/\n/g,'\n ')}` ).join('')
170
- return `/**\n${summary}${description}\n *${params}${query}\n *\/\n ${r.alias}(${p.length ? p.map(([k,v])=>`${k}: ${v.type}`).join(', ')+', ':''}options: RequestOptions${schemas} = {}){return this.fetch(\`${r.pathT}\`, {...options, method: '${r.method.toUpperCase()}'}) as ${responses}}\n`
196
+ return `/**\n${summary}${description}\n *${params}${query}\n *\/\n ${r.alias}<CT extends ${r.contentTypes}>(${p.length ? p.map(([k,v])=>`${k}: ${v.type}`).join(', ')+', ':''}options: RequestOptions${schemas} = {}){return this.fetch(\`${r.pathT}\`, {...options, method: '${r.method.toUpperCase()}'}) as ${responses}}\n`
171
197
  }).join(' ')
172
198
  })
173
199
  %*/
package/bin/util.ts CHANGED
@@ -15,14 +15,14 @@ export const fmtVal = (v: any) => {
15
15
  if (typeof v === 'number') return `\x1b[36m${v}\x1b[0m`
16
16
  return v
17
17
  }
18
- export const fmtList = (l: any) => `[${l.map((v:any) => fmtVal(v)).join(', ')}]`
18
+ export const fmtList = (l: any) => `[${l.map((v: any) => fmtVal(v)).join(', ')}]`
19
19
  export const fmtInterval = (a: any, b: any) => `[${fmtVal(a)}-${fmtVal(b)}]`
20
20
 
21
21
  export const silentExec = async (fn: () => any) => {
22
22
  let consoleMock = Object.fromEntries(
23
23
  Object.entries(console)
24
24
  .filter(([_, v]) => typeof v === 'function')
25
- .map(([k, _]) => [k, () => { }])
25
+ .map(([k, _]) => [k, () => {}])
26
26
  )
27
27
  const _console = console
28
28
  const _processStdoutWrite = process.stdout.write
@@ -30,9 +30,9 @@ export const silentExec = async (fn: () => any) => {
30
30
  //@ts-ignore
31
31
  global.console = consoleMock
32
32
  //@ts-ignore
33
- process.stdout.write = function () { }
33
+ process.stdout.write = function () {}
34
34
  //@ts-ignore
35
- process.stderr.write = function () { }
35
+ process.stderr.write = function () {}
36
36
  const r = await fn()
37
37
  global.console = _console
38
38
  process.stdout.write = _processStdoutWrite
@@ -50,7 +50,7 @@ export const watchDir = async (
50
50
  let watcher = watch(path, {
51
51
  persistent: false,
52
52
  ignored: options?.ignore,
53
- ignoreInitial: true
53
+ ignoreInitial: true,
54
54
  })
55
55
  watcher.on('all', async (eventType, filename) => {
56
56
  if (filename.match(WATCH_IGNORE)) return
@@ -90,11 +90,31 @@ export const instanciateRoutes = async (g: Galbe) => {
90
90
  console.log(`\x1b\[0;31m Error:\x1b[0m`)
91
91
  console.log(errors?.[fp])
92
92
  }
93
- console.log("")
93
+ console.log('')
94
94
  }
95
95
  console.log('\x1b[1;30m\x1b[32mdone\x1b[0m\n')
96
96
  }
97
97
 
98
+ export function abbreviateVar(input: string): string {
99
+ if (!input) return ''
100
+ const normalized = input
101
+ .replace(/[_\-\s]+/g, ' ')
102
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
103
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
104
+
105
+ const tokens = normalized
106
+ .trim()
107
+ .split(/[^\p{L}\p{N}]+/u)
108
+ .filter(Boolean)
109
+
110
+ if (tokens.length === 0) return ''
111
+
112
+ return tokens
113
+ .map(t => t[0])
114
+ .join('')
115
+ .toLowerCase()
116
+ }
117
+
98
118
  export const killPort = async (port: number) => {
99
119
  let getProcCmd: string[], killCmd: (port: string) => string[]
100
120
 
@@ -172,5 +192,5 @@ export const HttpStatus = {
172
192
  504: 'Gateway Timeout',
173
193
  505: 'HTTP Version Not Supported',
174
194
  507: 'Insufficient Storage',
175
- 511: 'Network Authentication Required'
195
+ 511: 'Network Authentication Required',
176
196
  }
package/docs/context.md CHANGED
@@ -49,7 +49,7 @@ galbe.get('/test', ctx => console.log(ctx.query))
49
49
 
50
50
  The body payload of the incoming request. The body type is determined based on the following rules:
51
51
 
52
- If no [Schema](schemas.md) is defined, Galbe will parse the body type according to the `Content-Type` header:
52
+ If no [Schema](schemas.md) is defined, Galbe will parse the body type according to the `content-type` header:
53
53
 
54
54
  - `text/.*`: string
55
55
  - `application/json`: object
package/docs/schemas.md CHANGED
@@ -174,42 +174,75 @@ const schema = {
174
174
 
175
175
  <!-- prettier-ignore -->
176
176
  ```ts
177
- body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STMultipartForm | STUrlForm | STStream
177
+ body: {
178
+ byteArray?: STByteArray | STStream
179
+ text?: STString | STLiteral | STBoolean | STNumber | STInteger | STUnion | STStream
180
+ json?: STJson | STObject | STBoolean | STInteger | STNumber | STString | STArray | STUnion
181
+ urlForm?: STObject | STStream | STUnion
182
+ multipart?: STMultipartForm | STStream | STUnion
183
+ default?: STString | STByteArray | STStream | STAny
184
+ }
178
185
  ```
179
186
 
180
187
  Defines the request body Schema type based on content type.
181
188
 
182
- #### object
189
+ #### Byte Array
183
190
 
184
- Defines an `application/json` request body.
191
+ Defines an `application/octet-stream` request body.
185
192
 
186
193
  ```ts
187
- const jsonBody = $T.object({
188
- name: $T.string(),
189
- age: $T.integer({ min: 0 })
190
- })
194
+ const body = {
195
+ byteArray: $T.byteArray()
196
+ }
191
197
  ```
192
198
 
193
- #### multipartForm
199
+ #### Text
194
200
 
195
- Defines a `multipart/form-data` request body.
201
+ Defines an `text/*` request body.
196
202
 
197
203
  ```ts
198
- const multipartBody = $T.multipartForm({
199
- name: $T.string(),
200
- age: $T.integer({ min: 0 })
201
- })
204
+ const body = {
205
+ text: $T.string()
206
+ }
207
+ ```
208
+
209
+ #### JSON
210
+
211
+ Defines an `application/json` request body.
212
+
213
+ ```ts
214
+ const body = {
215
+ json: $T.object({
216
+ name: $T.string(),
217
+ age: $T.integer({ min: 0 })
218
+ })
219
+ }
202
220
  ```
203
221
 
204
- #### urlForm
222
+ #### URL Form
205
223
 
206
224
  Defines an `application/x-www-form-urlencoded` request body.
207
225
 
208
226
  ```ts
209
- const urlBody = $T.urlForm({
210
- name: $T.string(),
211
- age: $T.integer({ min: 0 })
212
- })
227
+ const body = {
228
+ urlForm: {$T.object({
229
+ name: $T.string(),
230
+ age: $T.integer({ min: 0 })
231
+ })
232
+ }
233
+ ```
234
+
235
+ #### Multipart Form
236
+
237
+ Defines a `multipart/form-data` request body.
238
+
239
+ ```ts
240
+ const body = {
241
+ multipart: $T.multipartForm({
242
+ name: $T.string(),
243
+ age: $T.integer({ min: 0 })
244
+ })
245
+ }
213
246
  ```
214
247
 
215
248
  #### stream
@@ -225,10 +258,12 @@ Let's consider a `multipart/form-data` body request that has two properties: `us
225
258
  galbe.post(
226
259
  'user/create',
227
260
  {
228
- body: $T.multipartForm({
229
- username: $T.string(),
230
- heavyImageFile: $T.byteArray()
231
- })
261
+ body: {
262
+ multipart: $T.multipartForm({
263
+ username: $T.string(),
264
+ heavyImageFile: $T.byteArray()
265
+ })
266
+ }
232
267
  },
233
268
  ctx => {
234
269
  // At that point, the full body request has been processed
@@ -247,10 +282,12 @@ A better approach would consist in leveraging `STStream` wrapper to implement ea
247
282
  galbe.post(
248
283
  'user/create',
249
284
  {
250
- body: $T.stream($T.multipartForm({
251
- username: $T.string(),
252
- heavyImageFile: $T.byteArray()
253
- }))
285
+ body: {
286
+ multipart: $T.stream($T.multipartForm({
287
+ username: $T.string(),
288
+ heavyImageFile: $T.byteArray()
289
+ }))
290
+ }
254
291
  },
255
292
  async ctx => {
256
293
  // At that point, the body has not been processed yet.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
5
5
  "author": "Pierre Caillaud M (https://github.com/pierre-cm)",
6
6
  "type": "module",
@@ -1,7 +1,16 @@
1
- import type { STArray, STJson, STLiteral, STObject, STProps, STSchema, STUnion } from '../../../src/schema'
1
+ import type {
2
+ STArray,
3
+ STIntersection,
4
+ STJson,
5
+ STLiteral,
6
+ STObject,
7
+ STProps,
8
+ STSchema,
9
+ STUnion,
10
+ } from '../../../src/schema'
2
11
 
3
12
  import { Galbe } from '../../../src'
4
- import { walkRoutes, HttpStatus } from '../../../src/util'
13
+ import { walkRoutes, HttpStatus, inferContentType } from '../../../src/util'
5
14
  import { Kind, Optional } from '../../../src/schema'
6
15
 
7
16
  import { OpenAPIV3 } from 'openapi-types'
@@ -12,10 +21,10 @@ const schemaToMedia = ({ type, format, isJson }: SchemaType) =>
12
21
  isJson || (type && ['object', 'number', 'boolean', 'array'].includes(type))
13
22
  ? 'application/json'
14
23
  : format === 'byte'
15
- ? 'application/octet-stream'
16
- : type === 'string'
17
- ? 'text/plain'
18
- : '*/*'
24
+ ? 'application/octet-stream'
25
+ : type === 'string'
26
+ ? 'text/plain'
27
+ : '*/*'
19
28
 
20
29
  export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<OpenAPIV3.Document> => {
21
30
  let paths: any = {}
@@ -24,7 +33,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
24
33
  schemas: {},
25
34
  parameters: {},
26
35
  requestBodies: {},
27
- responses: {}
36
+ responses: {},
28
37
  }
29
38
 
30
39
  const schemaToOpenapi = (
@@ -54,7 +63,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
54
63
 
55
64
  if (kind === 'null') {
56
65
  s = {
57
- anyOf: ['null']
66
+ anyOf: ['null'],
58
67
  }
59
68
  } else if (kind === 'boolean') s = { type: 'boolean' }
60
69
  else if (kind === 'byteArray') s = { type: 'string', format: 'byte' }
@@ -64,7 +73,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
64
73
  ...(exclusiveMinimum ? { exclusiveMinimum } : {}),
65
74
  ...(exclusiveMaximum ? { exclusiveMaximum } : {}),
66
75
  ...(minimum ? { minimum } : {}),
67
- ...(maximum ? { maximum } : {})
76
+ ...(maximum ? { maximum } : {}),
68
77
  }
69
78
  else if (kind === 'integer')
70
79
  s = {
@@ -72,14 +81,14 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
72
81
  ...(exclusiveMinimum ? { exclusiveMinimum } : {}),
73
82
  ...(exclusiveMaximum ? { exclusiveMaximum } : {}),
74
83
  ...(minimum ? { minimum } : {}),
75
- ...(maximum ? { maximum } : {})
84
+ ...(maximum ? { maximum } : {}),
76
85
  }
77
86
  else if (kind === 'string')
78
87
  s = {
79
88
  type: 'string',
80
89
  ...(pattern ? { pattern } : {}),
81
90
  ...(minLength ? { minLength } : {}),
82
- ...(maxLength ? { maxLength } : {})
91
+ ...(maxLength ? { maxLength } : {}),
83
92
  }
84
93
  else if (kind === 'any') s = { type: 'string' }
85
94
  else if (kind === 'literal') {
@@ -91,7 +100,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
91
100
  items: schemaToOpenapi((schema as STArray).items).schema,
92
101
  ...(minItems ? { minItems } : {}),
93
102
  ...(maxItems ? { maxItems } : {}),
94
- ...(uniqueItems ? { uniqueItems } : {})
103
+ ...(uniqueItems ? { uniqueItems } : {}),
95
104
  }
96
105
  } else if (kind === 'object') {
97
106
  let props = (schema as STObject).props || {}
@@ -101,7 +110,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
101
110
  s = {
102
111
  type: 'object',
103
112
  properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
104
- ...(required.length ? { required } : {})
113
+ ...(required.length ? { required } : {}),
105
114
  }
106
115
  } else if (kind === 'json') {
107
116
  let props = ((schema as STJson).props || {}) as STProps
@@ -115,13 +124,13 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
115
124
  type: type,
116
125
  ...(type === 'object'
117
126
  ? {
118
- properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
119
- ...(required.length ? { required } : {})
120
- }
121
- : {})
127
+ properties: Object.fromEntries(Object.entries(props).map(([k, v]) => [k, schemaToOpenapi(v).schema])),
128
+ ...(required.length ? { required } : {}),
129
+ }
130
+ : {}),
122
131
  }
123
132
  } else if (kind === 'union') {
124
- let anyOf = (schema as STUnion).anyOf
133
+ let anyOf: STSchema[] = (schema as STUnion).anyOf
125
134
  let nullable = anyOf.some(s => s[Kind] === 'null')
126
135
  anyOf = anyOf.filter(s => s[Kind] !== 'null')
127
136
 
@@ -131,12 +140,23 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
131
140
  s = schemaToOpenapi(anyOf[0]).schema
132
141
  } else if (anyOf.length > 1) {
133
142
  s = {
134
- anyOf: anyOf.map(s => schemaToOpenapi(s).schema)
143
+ anyOf: anyOf.map(e => schemaToOpenapi(e).schema),
135
144
  }
136
145
  }
137
146
 
138
147
  //@ts-ignore
139
148
  if (nullable) s.nullable = nullable
149
+ } else if (kind === 'intersection') {
150
+ let allOf: STSchema[] = (schema as STIntersection).allOf
151
+ if (allOf.length === 0) {
152
+ s = {}
153
+ } else if (allOf.length === 1) {
154
+ s = schemaToOpenapi(allOf[0]).schema
155
+ } else if (allOf.length > 1) {
156
+ s = {
157
+ allOf: allOf.map(s => schemaToOpenapi(s).schema),
158
+ }
159
+ }
140
160
  }
141
161
 
142
162
  s = { title: schema.title, description: schema.description, ...s }
@@ -168,7 +188,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
168
188
  description: param?.description,
169
189
  required: kind === 'path' ? true : !param[Optional] || undefined,
170
190
  deprecated: param.deprecated,
171
- schema
191
+ schema,
172
192
  }
173
193
  if (components.parameters && param.id) components.parameters[param.id] = p
174
194
  return p
@@ -178,18 +198,17 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
178
198
  (routes, c) => ({ ...routes, ...c.routes }),
179
199
  {} as Record<string, Record<string, Record<string, any>>>
180
200
  )
181
- let metaStatic = Object.fromEntries(Object.entries(metaRoutes || {}).filter((([_, d]) => d?.static)))
201
+ let metaStatic = Object.fromEntries(Object.entries(metaRoutes || {}).filter(([_, d]) => d?.static))
182
202
 
183
203
  walkRoutes(g.router.routes, r => {
184
204
  let meta = metaRoutes?.[r.path]?.[r.method]
185
- if (r.static?.root)
186
- meta = metaStatic[r.static?.root]?.static
205
+ if (r.static?.root) meta = metaStatic[r.static?.root]?.static
187
206
  if (meta?.hide) return
188
207
  let path = r.path.replaceAll(/:([^\/]+)/g, '{$1}')
189
208
  if (!(path in paths)) paths[path] = {}
190
209
  let tags = [
191
210
  ...(meta?.tags?.split(' ')?.map((t: string) => t.trim()) || []),
192
- ...(typeof meta?.tag === 'string' ? [meta?.tag] : meta?.tag || [])
211
+ ...(typeof meta?.tag === 'string' ? [meta?.tag] : meta?.tag || []),
193
212
  ]
194
213
  let security: Record<string, any> = []
195
214
 
@@ -201,52 +220,62 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
201
220
  : []
202
221
  let headerParam = r.schema?.headers
203
222
  ? Object.entries(r.schema?.headers as Record<string, STSchema>)
204
- .map(([k, v]) => {
205
- let p = parseParam(k, v, 'header')
206
- if (k.match(/authorization/i)) {
207
- // TODO: handle other auth methods
208
- if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
209
- security.push({ bearerAuth: [] })
210
- components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
211
- return null
223
+ .map(([k, v]) => {
224
+ let p = parseParam(k, v, 'header')
225
+ if (k.match(/authorization/i)) {
226
+ // TODO: handle other auth methods
227
+ if (v.pattern && v?.pattern?.toString() === '/^Bearer /') {
228
+ security.push({ bearerAuth: [] })
229
+ components.securitySchemes = { bearerAuth: { type: 'http', scheme: 'bearer' } }
230
+ return null
231
+ }
212
232
  }
213
- }
214
- return p
215
- })
216
- .filter(p => p)
233
+ return p
234
+ })
235
+ .filter(p => p)
217
236
  : []
218
237
  // TODO cookieParam
219
238
  let parameters = [...pathParam, ...queryParam, ...headerParam]
220
239
 
221
240
  let requestBody
222
241
  if (r.schema.body) {
223
- let { schema, isJson } = schemaToOpenapi(r.schema.body)
224
- let { type, format } = resolveRef(schema)
225
- let media = schemaToMedia({ type, format, isJson } as SchemaType)
242
+ let description: string | undefined
243
+ let conflictDescription = false
244
+ let required = false
245
+ let content = Object.fromEntries(
246
+ Object.entries(r.schema.body).map(([bodyType, schema]) => {
247
+ const s = schema.description
248
+ const isDefined = typeof s === 'string' && s !== ''
249
+ if (s?.[Optional] === false) required = true
250
+ if (isDefined) {
251
+ if (description === undefined) {
252
+ description = s
253
+ } else if (description !== s) {
254
+ conflictDescription = true
255
+ }
256
+ }
257
+ description = conflictDescription ? undefined : description ?? undefined
258
+ return [inferContentType(bodyType), { schema: schemaToOpenapi(schema).schema }]
259
+ })
260
+ )
226
261
  requestBody = {
227
- description: r.schema.body.description,
228
- required: !r.schema.body[Optional],
229
- content: {
230
- [media]: { schema }
231
- }
232
- }
233
- if (r.schema.body.id && components.requestBodies) {
234
- components.requestBodies[r.schema.body.id] = requestBody
235
- requestBody = { $ref: `#/components/requestBodies/${r.schema.body.id}` }
262
+ description,
263
+ required,
264
+ content,
236
265
  }
237
266
  }
238
267
  let responses
239
268
  if (r.schema.response && Object.keys(r.schema.response).length) {
240
269
  responses = Object.fromEntries(
241
270
  Object.entries(r.schema.response).map(([status, v]) => {
242
- if(!v) return []
271
+ if (!v) return []
243
272
  let s = status as keyof typeof HttpStatus | 'default'
244
273
  let { schema, isJson } = schemaToOpenapi(v)
245
274
  let { type, format } = resolveRef(schema)
246
275
  let media = schemaToMedia({ type, format, isJson } as SchemaType)
247
276
  let response: OpenAPIV3.ResponseObject = {
248
277
  description: v.description || HttpStatus[s as keyof typeof HttpStatus] || 'Response',
249
- content: { [media]: { schema: schema } }
278
+ content: { [media]: { schema: schema } },
250
279
  }
251
280
  if (components.responses && r.schema.response?.[s]?.id) {
252
281
  components.responses[r.schema.response?.[s]?.id as string] = response
@@ -258,12 +287,10 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
258
287
  )
259
288
  } else {
260
289
  responses = {
261
- default: { description: HttpStatus[200] }
290
+ default: { description: HttpStatus[200] },
262
291
  }
263
292
  }
264
293
  let summary = meta?.head.match(/^([^\n]+)/)?.[1]
265
- console.log('#', r.method, r.path)
266
- console.log(r.schema.body)
267
294
  paths[path][r.method] = {
268
295
  tags: tags.length ? tags : undefined,
269
296
  summary: summary,
@@ -272,7 +299,7 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
272
299
  requestBody,
273
300
  responses,
274
301
  ...(security.length ? { security } : {}),
275
- deprecated: meta?.deprecated ? true : undefined
302
+ deprecated: meta?.deprecated ? true : undefined,
276
303
  }
277
304
  })
278
305
 
@@ -285,9 +312,9 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
285
312
  openapi: version,
286
313
  info: {
287
314
  title: 'Galbe app',
288
- version: '0.1.0'
315
+ version: '0.1.0',
289
316
  },
290
317
  paths,
291
- components: Object.keys(components)?.length ? components : undefined
318
+ components: Object.keys(components)?.length ? components : undefined,
292
319
  }
293
320
  }