galbe 0.10.1 → 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/docs/schemas.md CHANGED
@@ -1,18 +1,18 @@
1
- # Shemas
1
+ # Schemas
2
2
 
3
- Galbe provides a custom Schema Type processor that offers type safety, data parsing, and validation. The primary purpose of this feature is to simplify request input validation and error handling automatically. Additionally, it enhances the developer's experience by inferring static TypeScript types from schema definitions.
3
+ Galbe provides a custom Schema Type processor that offers type safety, data parsing, and validation. The use of Schemas highly simplifies request input validation and automatic error handling. Additionally, it enhances the developer experience by inferring static TypeScript types from schema definitions.
4
4
 
5
5
  ## Schema Types
6
6
 
7
- To start using Schema definitions, import `$T` from the `galbe` library:
7
+ To use Schema definitions, import `$T` from the `galbe` library:
8
8
 
9
9
  ```js
10
10
  import { $T } from 'galbe'
11
11
  ```
12
12
 
13
- Here the list of available Schema types in Galbe:
13
+ Here is the list of available Schema types in Galbe:
14
14
 
15
- #### Boolean
15
+ #### boolean
16
16
 
17
17
  Schema Type matching `boolean` values.
18
18
 
@@ -20,15 +20,15 @@ Schema Type matching `boolean` values.
20
20
  const boolSchema = $T.boolean()
21
21
  ```
22
22
 
23
- #### String
23
+ #### string
24
24
 
25
- Schema Type matching `string` vlues.
25
+ Schema Type matching `string` values.
26
26
 
27
27
  ```ts
28
28
  const strSchema = $T.string(options)
29
29
  ```
30
30
 
31
- #### Number
31
+ #### number
32
32
 
33
33
  Schema Type matching `number` values.
34
34
 
@@ -36,7 +36,7 @@ Schema Type matching `number` values.
36
36
  const numSchema = $T.number(options)
37
37
  ```
38
38
 
39
- #### Integer
39
+ #### integer
40
40
 
41
41
  Schema Type matching integer `number` values.
42
42
 
@@ -44,7 +44,7 @@ Schema Type matching integer `number` values.
44
44
  const intSchema = $T.integer(options)
45
45
  ```
46
46
 
47
- #### Null
47
+ #### null
48
48
 
49
49
  Schema Type matching `null` values.
50
50
 
@@ -52,7 +52,7 @@ Schema Type matching `null` values.
52
52
  const nullSchema = $T.null(options)
53
53
  ```
54
54
 
55
- #### Any
55
+ #### any
56
56
 
57
57
  Schema Type matching `any` of the previous Schema Types.
58
58
 
@@ -60,7 +60,7 @@ Schema Type matching `any` of the previous Schema Types.
60
60
  const anySchema = $T.any()
61
61
  ```
62
62
 
63
- #### Array
63
+ #### array
64
64
 
65
65
  Schema Type matching `array` values.
66
66
 
@@ -68,41 +68,41 @@ Schema Type matching `array` values.
68
68
  const arraySchema = $T.array($T.any(), options)
69
69
  ```
70
70
 
71
- #### Optional
71
+ #### optional
72
72
 
73
- Makes any type optional. This allows for `undefined` values.
73
+ Makes any type optional, allowing `undefined` values.
74
74
 
75
75
  ```ts
76
76
  const optionalSchema = $T.optional($T.string())
77
77
  ```
78
78
 
79
- #### Nullable
79
+ #### nullable
80
80
 
81
- Makes any type nullable. This allows for `null` values.
81
+ Makes any type nullable, allowing `null` values.
82
82
 
83
83
  ```ts
84
84
  const nullableSchema = $T.nullable($T.string())
85
85
  ```
86
86
 
87
- #### Nullish
87
+ #### nullish
88
88
 
89
- Makes any type nullish. This allows for `undefined` and `null` values.
89
+ Makes any type nullish, allowing both `undefined` and `null` values.
90
90
 
91
91
  ```ts
92
92
  const nullishSchema = $T.nullish($T.string())
93
93
  ```
94
94
 
95
- #### Union
95
+ #### union
96
96
 
97
- Creates an union of Schema Types.
97
+ Creates a union of Schema Types.
98
98
 
99
99
  ```ts
100
100
  const unionSchema = $T.union([$T.string(), $T.number()])
101
101
  ```
102
102
 
103
- ## Request Schema definition
103
+ ## Request Schema Definition
104
104
 
105
- The Request Schema definition allows you to define a schema for your request on your [Route Definition](routes.md#route-defintion). It must be defined right after the path of your route.
105
+ The Request Schema definition allows you to define a schema for your request in your [Route Definition](routes.md#route-definition). It must be defined right after the route path.
106
106
 
107
107
  ```js
108
108
  const schema = {}
@@ -117,9 +117,9 @@ The Request Schema has four optional properties:
117
117
  headers: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
118
118
  ```
119
119
 
120
- This is a key-value object where each key represents a request header name, and the value is the associated Schema.
120
+ Defines request headers with their respective Schema types.
121
121
 
122
- **Example**:
122
+ **Example:**
123
123
 
124
124
  ```ts
125
125
  const schema = {
@@ -135,9 +135,9 @@ const schema = {
135
135
  params: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
136
136
  ```
137
137
 
138
- This is a key-value object where each key represents a request path parameter name, and the value is the associated Schema.
138
+ Defines route parameters with their respective Schema types.
139
139
 
140
- **Example**:
140
+ **Example:**
141
141
 
142
142
  ```ts
143
143
  const schema = {
@@ -149,9 +149,7 @@ const schema = {
149
149
  ```
150
150
 
151
151
  > [!WARNING]
152
- > Every key should match an existing [route path](routes.md#route-defintion) parameter. Otherwise Typescript will show an error.
153
- >
154
- > By default, if no schema is defined for a given parameter. Galbe will assume it is of type `string`.
152
+ > Every key should match an existing [route path](routes.md#route-definition) parameter. Otherwise, TypeScript will show an error. If no schema is defined for a given parameter, Galbe assumes it is of type `string`.
155
153
 
156
154
  ### query
157
155
 
@@ -159,9 +157,9 @@ const schema = {
159
157
  query: { [key: string]: STString | STBoolean | STNumber | STInteger | STLiteral }
160
158
  ```
161
159
 
162
- This is a key-value object where each key represents a request query parameter name, and the value is the associated Schema.
160
+ Defines query parameters with their respective Schema types.
163
161
 
164
- **Example**:
162
+ **Example:**
165
163
 
166
164
  ```ts
167
165
  const schema = {
@@ -176,56 +174,96 @@ const schema = {
176
174
 
177
175
  <!-- prettier-ignore -->
178
176
  ```ts
179
- body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STMulripartForm | 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
+ }
180
185
  ```
181
186
 
182
- #### Object
187
+ Defines the request body Schema type based on content type.
188
+
189
+ #### Byte Array
183
190
 
184
- To define an `application/json` request body, use `STObject` Schema Type. Example:
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
- #### Multipart
199
+ #### Text
194
200
 
195
- To define a `multipart/form-data` request body, use `TMultipartForm` Schema Type. Example:
201
+ Defines an `text/*` request body.
196
202
 
197
203
  ```ts
198
- const multipartBody = $T.multipartForm({
199
- name: $T.string(),
200
- age: $T.integer({ minimum: 0 })
201
- })
204
+ const body = {
205
+ text: $T.string()
206
+ }
202
207
  ```
203
208
 
204
- #### Url Form
209
+ #### JSON
205
210
 
206
- To define an `application/x-www-form-urlencoded` request body, use `TUrlForm` Schema Type. Example:
211
+ Defines an `application/json` request body.
207
212
 
208
213
  ```ts
209
- const urlBody = $T.urlForm({
210
- name: $T.string(),
211
- age: $T.integer({ minimum: 0 })
212
- })
214
+ const body = {
215
+ json: $T.object({
216
+ name: $T.string(),
217
+ age: $T.integer({ min: 0 })
218
+ })
219
+ }
220
+ ```
221
+
222
+ #### URL Form
223
+
224
+ Defines an `application/x-www-form-urlencoded` request body.
225
+
226
+ ```ts
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
- #### Stream
248
+ #### stream
216
249
 
217
- Some body request types can be streamed by using `STStream` Schema Type wrapper. The streamable Schema Types are `STByteArray`, `STString`, `STUrlForm` and `STMultipartForm`. This can be usefull to imporve performances in case you have heavy body payloads and you want to perform early validations on the body.
250
+ Certain request body types can be streamed using `STStream` wrapper, improving performance by validating data incrementally.
251
+ This can be usefull to imporve performances in case you have heavy body payloads by leveraging early validation and fail fast behaviors.
218
252
 
219
- Let's look at a concrete example where this could be useful. Imagine you want a `multipart/form-data` body request that has two properties: `username` and `heavyImageFile`. In a normal case, you would define something like this:
253
+ **Example**
254
+
255
+ Let's consider a `multipart/form-data` body request that has two properties: `username` and `heavyImageFile`:
220
256
 
221
257
  ```ts
222
258
  galbe.post(
223
259
  'user/create',
224
260
  {
225
- body: $T.multipartForm({
226
- username: $T.string(),
227
- heavyImageFile: $T.byteArray()
228
- })
261
+ body: {
262
+ multipart: $T.multipartForm({
263
+ username: $T.string(),
264
+ heavyImageFile: $T.byteArray()
265
+ })
266
+ }
229
267
  },
230
268
  ctx => {
231
269
  // At that point, the full body request has been processed
@@ -236,20 +274,22 @@ galbe.post(
236
274
  })
237
275
  ```
238
276
 
239
- This means that in the case where the username wouldn't pass the validation, the full request body, including the `heavyImageFile`, would have been processed for nothing, as it is not used. This would induce unnecessary time and resource consumption.
277
+ In that case, even if the `username` doesn't pass the validation, the full request body, including the `heavyImageFile` is processed before sending the response. This induces unnecessary time and resource consumption because the `heavyImageFile` is processed despite never been used.
240
278
 
241
- The `STStream` Schema Type wrapper was created to remediate to remediate this issue. In practice it allows you to perform validations on the fly. Now in your handler, instead of receiving an object as `ctx.body`, you will receive an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator).
279
+ A better approach would consist in leveraging `STStream` wrapper to implement early validation and fail fast behavior. By defining the request body as a stream. Instead of receiving a plain js object as `ctx.body`, you will receive an [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator).
242
280
 
243
281
  ```ts
244
282
  galbe.post(
245
283
  'user/create',
246
284
  {
247
- body: $T.stream($T.multipartForm({
248
- username: $T.string(),
249
- heavyImageFile: $T.byteArray()
250
- }))
285
+ body: {
286
+ multipart: $T.stream($T.multipartForm({
287
+ username: $T.string(),
288
+ heavyImageFile: $T.byteArray()
289
+ }))
290
+ }
251
291
  },
252
- ctx => {
292
+ async ctx => {
253
293
  // At that point, the body has not been processed yet.
254
294
  for await (const [key, value] of ctx.body) {
255
295
  if (key === "username" && !isValid(value)) {
@@ -266,16 +306,22 @@ galbe.post(
266
306
 
267
307
  <!-- prettier-ignore -->
268
308
  ```ts
269
- response: Record<number, STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STStream>
309
+ response: Record<number | 'default', STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STStream>
270
310
  ```
271
311
 
272
- Same as for request body validation but to validate handler responses. Every schema type must be associated to a specific response status.
312
+ Defines response validation by associating schema types with specific HTTP status codes. The special key `default` can also be used to define the Response Schema for the remaining status codes.
273
313
 
274
- #### Example
314
+ **Example:**
275
315
 
276
316
  ```ts
277
317
  const response = {
278
- 200: $T.object({ data: $T.array($T.number()) })
279
- 404: $T.literal("Not found")
318
+ 200: $T.object({ data: $T.array($T.number()) }),
319
+ 404: $T.literal("Not found"),
320
+ default: $T.string(),
280
321
  }
281
322
  ```
323
+
324
+ This ensures all responses adhere to the defined schema.
325
+
326
+ > [!NOTE]
327
+ > The response validation is enabled by default, meaning that every enpoint response that has a schema defined will be validated at runtime. To disable runtime validation, you can set the `responseValidator?.enabled` option to `false` in the [configuration](getting-started.md#configuration).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.10.1",
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
  }