galbe 0.11.0 → 0.12.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.
- package/.prettierrc +1 -1
- package/bin/commands/generate/client.ts +58 -30
- package/bin/commands/generate/code/openapi.parser.ts +72 -45
- package/bin/res/cli.template.js +20 -18
- package/bin/res/client.template.ts +52 -26
- package/bin/util.ts +27 -7
- package/docs/context.md +1 -1
- package/docs/schemas.md +63 -26
- package/package.json +1 -1
- package/src/extras/spec/openapi.serializer.ts +83 -56
- package/src/index.ts +16 -11
- package/src/parser.ts +198 -146
- package/src/schema.ts +156 -66
- package/src/server.ts +18 -16
- package/src/types.ts +54 -42
- package/src/util.ts +37 -9
- package/src/validator.ts +23 -19
- package/test/parser.test.ts +369 -281
- package/test/requests.test.ts +242 -176
- package/test/resources/static/chameleon.png +0 -0
- package/test/resources/static/index.html +13 -0
- package/test/resources/static/sub/index.html +13 -0
- package/test/resources/static/sub/other.html +13 -0
- package/test/responses.test.ts +49 -49
- package/test/test.utils.ts +4 -2
- package/test/types.test.ts +909 -0
|
@@ -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,
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
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}'
|
|
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: {
|
|
94
|
-
|
|
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 `
|
|
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:
|
|
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
|
-
####
|
|
189
|
+
#### Byte Array
|
|
183
190
|
|
|
184
|
-
Defines an `application/
|
|
191
|
+
Defines an `application/octet-stream` request body.
|
|
185
192
|
|
|
186
193
|
```ts
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
194
|
+
const body = {
|
|
195
|
+
byteArray: $T.byteArray()
|
|
196
|
+
}
|
|
191
197
|
```
|
|
192
198
|
|
|
193
|
-
####
|
|
199
|
+
#### Text
|
|
194
200
|
|
|
195
|
-
Defines
|
|
201
|
+
Defines an `text/*` request body.
|
|
196
202
|
|
|
197
203
|
```ts
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
####
|
|
222
|
+
#### URL Form
|
|
205
223
|
|
|
206
224
|
Defines an `application/x-www-form-urlencoded` request body.
|
|
207
225
|
|
|
208
226
|
```ts
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
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:
|
|
229
|
-
|
|
230
|
-
|
|
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:
|
|
251
|
-
|
|
252
|
-
|
|
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,7 +1,16 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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(
|
|
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((
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
224
|
-
let
|
|
225
|
-
let
|
|
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
|
|
228
|
-
required
|
|
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
|
}
|