galbe 0.13.0 → 0.14.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/README.md +18 -1
- package/bin/commands/build.ts +10 -4
- package/bin/commands/generate/cli/index.ts +156 -0
- package/bin/commands/generate/cli/targets/cac.ts +535 -0
- package/bin/commands/generate/client.ts +32 -109
- package/bin/commands/generate/code/openapi.parser.ts +428 -133
- package/bin/commands/generate/code/route-merge.ts +248 -0
- package/bin/commands/generate/code.ts +110 -23
- package/bin/commands/generate/index.ts +2 -0
- package/package.json +4 -1
- package/src/cookies.ts +87 -0
- package/src/extras/spec/openapi.serializer.ts +236 -101
- package/src/extras.ts +1 -0
- package/src/index.ts +4 -7
- package/src/parser.ts +104 -63
- package/src/router.ts +34 -20
- package/src/routes.ts +14 -10
- package/src/schema.ts +82 -95
- package/src/server.ts +35 -21
- package/src/types.ts +180 -61
- package/src/util.ts +10 -18
- package/src/validator.ts +62 -32
- package/bin/res/cli.template.js +0 -122
|
@@ -2,7 +2,6 @@ import { semver } from 'bun'
|
|
|
2
2
|
import { transformSync } from '@swc/core'
|
|
3
3
|
import { resolve, relative, dirname } from 'path'
|
|
4
4
|
import { OpenAPIV3 } from 'openapi-types'
|
|
5
|
-
import { inferBodyType } from '../../../../src/util'
|
|
6
5
|
|
|
7
6
|
type SchemaEntry = {
|
|
8
7
|
key: string
|
|
@@ -10,6 +9,13 @@ type SchemaEntry = {
|
|
|
10
9
|
schema: string
|
|
11
10
|
dependsOn: Set<string>
|
|
12
11
|
usedBy: Set<string>
|
|
12
|
+
/** Response-only: the original component-level description, kept distinct
|
|
13
|
+
* from the inner schema's own description. */
|
|
14
|
+
responseDescription?: string
|
|
15
|
+
/** Response-only: content-level single example. */
|
|
16
|
+
responseExample?: any
|
|
17
|
+
/** Response-only: content-level multi-key examples. */
|
|
18
|
+
responseExamples?: Record<string, any>
|
|
13
19
|
}
|
|
14
20
|
type EndpointEntry = {
|
|
15
21
|
version?: string
|
|
@@ -46,14 +52,15 @@ const refToPath = (ref: string, basePath?: string) => {
|
|
|
46
52
|
const orderDeps = (deps: Record<string, SchemaEntry>) => {
|
|
47
53
|
let stack = Object.keys(deps)
|
|
48
54
|
let l = new Set<string>()
|
|
49
|
-
const ascend = (d: { dependsOn: Set<string> }) => {
|
|
50
|
-
for (let p of d.dependsOn
|
|
55
|
+
const ascend = (d: { dependsOn: Set<string> }, visiting: Set<string>) => {
|
|
56
|
+
for (let p of d.dependsOn) {
|
|
57
|
+
if (visiting.has(p) || l.has(p)) continue
|
|
51
58
|
if (p in deps) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
visiting.add(p)
|
|
60
|
+
ascend(deps[p], visiting)
|
|
61
|
+
l.add(p)
|
|
62
|
+
let idx = stack.indexOf(p)
|
|
63
|
+
if (idx >= 0) stack.splice(idx, 1)
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
}
|
|
@@ -61,7 +68,7 @@ const orderDeps = (deps: Record<string, SchemaEntry>) => {
|
|
|
61
68
|
let k = stack.pop()
|
|
62
69
|
if (!k) continue
|
|
63
70
|
let d = deps[k]
|
|
64
|
-
ascend(d)
|
|
71
|
+
ascend(d, new Set([k]))
|
|
65
72
|
if (!l.has(k)) {
|
|
66
73
|
l.add(k)
|
|
67
74
|
}
|
|
@@ -70,9 +77,12 @@ const orderDeps = (deps: Record<string, SchemaEntry>) => {
|
|
|
70
77
|
}
|
|
71
78
|
const serialize = (obj: any) => {
|
|
72
79
|
return JSON.stringify(obj, (k, value) => {
|
|
73
|
-
if (k === 'pattern' && value) return
|
|
80
|
+
if (k === 'pattern' && value) return `__PATTERN__${value}__ENDPATTERN__`
|
|
74
81
|
return value
|
|
75
|
-
}).replace(/"
|
|
82
|
+
}).replace(/"__PATTERN__([\s\S]*?)__ENDPATTERN__"/g, (_, body) => {
|
|
83
|
+
const decoded = JSON.parse(`"${body}"`) as string
|
|
84
|
+
return `/${decoded.replace(/\//g, '\\/')}/`
|
|
85
|
+
})
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
const writeCodeFile = async (path: string, content: string, target: 'js' | 'ts') => {
|
|
@@ -110,17 +120,22 @@ const parseOapiSchema = (
|
|
|
110
120
|
minLength?: number
|
|
111
121
|
maxLength?: number
|
|
112
122
|
pattern?: string
|
|
123
|
+
format?: string
|
|
113
124
|
minItems?: number
|
|
114
125
|
maxItems?: number
|
|
115
126
|
unique?: boolean
|
|
127
|
+
default?: any
|
|
128
|
+
examples?: any
|
|
116
129
|
} = {
|
|
117
130
|
...details,
|
|
118
131
|
title: os.title,
|
|
119
132
|
description: details.description || os.description,
|
|
133
|
+
default: os.default,
|
|
134
|
+
...(os.example !== undefined ? { examples: os.example } : {}),
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
let resp = ''
|
|
123
|
-
let hasOptions = Object.values(options).some(v =>
|
|
138
|
+
let hasOptions = Object.values(options).some(v => v !== undefined)
|
|
124
139
|
let optArg = hasOptions ? serialize(options) : ''
|
|
125
140
|
let anyOf = os.oneOf || os.anyOf
|
|
126
141
|
let allOf = os.allOf
|
|
@@ -139,9 +154,8 @@ const parseOapiSchema = (
|
|
|
139
154
|
} else if (anyOf?.length) {
|
|
140
155
|
if (anyOf.length === 1) resp = parseOapiSchema(anyOf[0] as OpenAPIV3.SchemaObject, details, extra)
|
|
141
156
|
else {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)})`
|
|
157
|
+
const builder = os.oneOf ? '$T.oneOf' : '$T.anyOf'
|
|
158
|
+
resp = `${builder}([${anyOf.map(s => parseOapiSchema(s as OpenAPIV3.SchemaObject)).join(',')}]${optArg ? `, ${optArg}` : ''})`
|
|
145
159
|
}
|
|
146
160
|
} else if (allOf?.length) {
|
|
147
161
|
if (allOf.length === 1) resp = parseOapiSchema(allOf[0] as OpenAPIV3.SchemaObject, details, extra)
|
|
@@ -151,7 +165,7 @@ const parseOapiSchema = (
|
|
|
151
165
|
)})`
|
|
152
166
|
}
|
|
153
167
|
} else if (!os?.type) {
|
|
154
|
-
return `$T.any(${
|
|
168
|
+
return `$T.any(${hasOptions ? optArg : ''})`
|
|
155
169
|
} else if (os.type === 'boolean') resp = `$T.boolean(${hasOptions ? serialize(options) : ''})`
|
|
156
170
|
else if (os.type === 'number') {
|
|
157
171
|
let { max, min, exclusiveMax, exclusiveMin } = {
|
|
@@ -161,7 +175,7 @@ const parseOapiSchema = (
|
|
|
161
175
|
exclusiveMin: os.minimum !== undefined && os.exclusiveMinimum ? os.minimum : undefined,
|
|
162
176
|
}
|
|
163
177
|
options = { ...options, min, max, exclusiveMax, exclusiveMin }
|
|
164
|
-
hasOptions = Object.values(options).some(v =>
|
|
178
|
+
hasOptions = Object.values(options).some(v => v !== undefined)
|
|
165
179
|
resp = `$T.number(${hasOptions ? serialize(options) : ''})`
|
|
166
180
|
} else if (os.type === 'integer') {
|
|
167
181
|
let max = os.maximum !== undefined && !os.exclusiveMaximum ? os.maximum : undefined
|
|
@@ -169,35 +183,43 @@ const parseOapiSchema = (
|
|
|
169
183
|
let exclusiveMax = os.maximum !== undefined && os.exclusiveMaximum ? os.maximum : undefined
|
|
170
184
|
let exclusiveMin = os.minimum !== undefined && os.exclusiveMinimum ? os.minimum : undefined
|
|
171
185
|
options = { ...options, min, max, exclusiveMax, exclusiveMin }
|
|
172
|
-
hasOptions = Object.values(options).some(v =>
|
|
186
|
+
hasOptions = Object.values(options).some(v => v !== undefined)
|
|
173
187
|
resp = `$T.integer(${hasOptions ? serialize(options) : ''})`
|
|
174
188
|
} else if (os.type === 'string') {
|
|
175
189
|
if (os.format === 'binary') resp = `$T.byteArray(${hasOptions ? serialize(options) : ''})`
|
|
176
190
|
else if (os.enum?.length === 1) {
|
|
177
191
|
resp = `$T.literal("${os.enum[0]}")`
|
|
178
192
|
} else if (os.enum?.length) {
|
|
179
|
-
|
|
193
|
+
const literals = os.enum.map(v => `$T.literal("${v}")`).join(', ')
|
|
194
|
+
resp = `$T.union([${literals}]${optArg ? `, ${optArg}` : ''})`
|
|
180
195
|
} else {
|
|
181
196
|
let minLength = os.minLength
|
|
182
197
|
let maxLength = os.maxLength
|
|
183
198
|
let pattern = os.pattern
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
let format = os.format
|
|
200
|
+
options = { ...options, minLength, maxLength, pattern, format }
|
|
201
|
+
hasOptions = Object.values(options).some(v => v !== undefined)
|
|
186
202
|
resp = `$T.string(${hasOptions ? serialize(options) : ''})`
|
|
187
203
|
}
|
|
188
204
|
} else if (os.type === 'array') {
|
|
189
|
-
|
|
190
|
-
|
|
205
|
+
// Galbe's ArrayOptions uses minLength/maxLength/unique (mirroring the
|
|
206
|
+
// builder API), not OpenAPI's minItems/maxItems/uniqueItems names.
|
|
207
|
+
let minLength = os.minItems
|
|
208
|
+
let maxLength = os.maxItems
|
|
191
209
|
let unique = os.uniqueItems
|
|
192
|
-
options = { ...options,
|
|
193
|
-
|
|
210
|
+
options = { ...options, minLength, maxLength, unique }
|
|
211
|
+
hasOptions = Object.values(options).some(v => v !== undefined)
|
|
212
|
+
optArg = hasOptions ? serialize(options) : ''
|
|
213
|
+
resp = `$T.array(${parseOapiSchema(os?.items)}${optArg ? `, ${optArg}` : ''})`
|
|
194
214
|
} else if (os.type === 'object') {
|
|
215
|
+
let required = new Set(os.required || [])
|
|
195
216
|
let props = Object.entries(os?.properties || {})
|
|
196
217
|
.map(([k, v]) => {
|
|
197
218
|
v = v as OpenAPIV3.SchemaObject
|
|
219
|
+
const isRequired = required.has(k)
|
|
198
220
|
const w = (s: string) => {
|
|
199
|
-
if (!
|
|
200
|
-
else if (!
|
|
221
|
+
if (!isRequired && v.nullable) return `$T.nullish(${s})`
|
|
222
|
+
else if (!isRequired) return `$T.optional(${s})`
|
|
201
223
|
else if (v.nullable) return `$T.nullable(${s})`
|
|
202
224
|
return s
|
|
203
225
|
}
|
|
@@ -222,21 +244,48 @@ const buildSchemaIndex = (def: OpenAPIV3.Document) => {
|
|
|
222
244
|
const initSchema = (k: string, s: any, kind: 'schemas' | 'requestBodies' | 'responses') => {
|
|
223
245
|
let schema = ''
|
|
224
246
|
let dependsOn = new Set<string>()
|
|
247
|
+
let responseExample: any = undefined
|
|
248
|
+
let responseExamples: Record<string, any> | undefined = undefined
|
|
225
249
|
if (kind === 'schemas') schema = parseOapiSchema(s, { id: k })
|
|
226
|
-
else if (kind === 'requestBodies'
|
|
250
|
+
else if (kind === 'requestBodies') {
|
|
227
251
|
let schemas = [] as string[]
|
|
228
252
|
if (!!s.content) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Object.entries((s as OpenAPIV3.RequestBodyObject)?.content || { null: {} }).map(([media, v]) => {
|
|
232
|
-
return parseOapiSchema(v.schema, { id: k }, { media })
|
|
233
|
-
})
|
|
234
|
-
),
|
|
235
|
-
]
|
|
253
|
+
const contentMap = (s as OpenAPIV3.RequestBodyObject)?.content || { null: {} }
|
|
254
|
+
schemas = [...new Set(Object.entries(contentMap).map(([media, v]) => parseOapiSchema(v.schema, { id: k }, { media })))]
|
|
236
255
|
} else {
|
|
237
256
|
schemas = [parseOapiSchema(undefined, { id: k, ...s })]
|
|
238
257
|
}
|
|
239
258
|
schema = schemas.length <= 0 ? '' : schemas.length === 1 ? schemas[0] : `$T.union([${schemas.join(',')}])`
|
|
259
|
+
} else if (kind === 'responses') {
|
|
260
|
+
if (!s.content) {
|
|
261
|
+
schema = parseOapiSchema(undefined, { id: k, ...s })
|
|
262
|
+
} else {
|
|
263
|
+
const contentMap = s.content as Record<string, { schema?: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; example?: any; examples?: any }>
|
|
264
|
+
const entries = Object.entries(contentMap)
|
|
265
|
+
for (const [, v] of entries) {
|
|
266
|
+
if (v.example !== undefined && responseExample === undefined) responseExample = v.example
|
|
267
|
+
if (v.examples && Object.keys(v.examples).length) responseExamples = { ...(responseExamples || {}), ...v.examples }
|
|
268
|
+
}
|
|
269
|
+
const keyGroups: Record<string, string[]> = {}
|
|
270
|
+
for (const [media, v] of entries) {
|
|
271
|
+
const galbeKey = media
|
|
272
|
+
const schemaStr = parseOapiSchema(v.schema, {}, { media })
|
|
273
|
+
if (!keyGroups[galbeKey]) keyGroups[galbeKey] = []
|
|
274
|
+
keyGroups[galbeKey].push(schemaStr)
|
|
275
|
+
}
|
|
276
|
+
const uniqueKeys = Object.keys(keyGroups)
|
|
277
|
+
if (uniqueKeys.length <= 1) {
|
|
278
|
+
const [key] = uniqueKeys
|
|
279
|
+
const uniqueSchemas = [...new Set(keyGroups[key] || [])]
|
|
280
|
+
schema = uniqueSchemas.length === 0 ? '' : uniqueSchemas.length === 1 ? uniqueSchemas[0] : `$T.union([${uniqueSchemas.join(',')}])`
|
|
281
|
+
} else {
|
|
282
|
+
const parts = Object.entries(keyGroups).map(([k, schemas]) => {
|
|
283
|
+
const unique = [...new Set(schemas)]
|
|
284
|
+
return `"${k}": ${unique.length === 1 ? unique[0] : `$T.union([${unique.join(',')}])`}`
|
|
285
|
+
})
|
|
286
|
+
schema = `{${parts.join(',')}}`
|
|
287
|
+
}
|
|
288
|
+
}
|
|
240
289
|
}
|
|
241
290
|
schema = unref(schema, m => {
|
|
242
291
|
let l = m.split('/')
|
|
@@ -249,6 +298,11 @@ const buildSchemaIndex = (def: OpenAPIV3.Document) => {
|
|
|
249
298
|
schema,
|
|
250
299
|
dependsOn,
|
|
251
300
|
usedBy: new Set(),
|
|
301
|
+
...(kind === 'responses' && typeof s?.description === 'string' && s.description
|
|
302
|
+
? { responseDescription: s.description }
|
|
303
|
+
: {}),
|
|
304
|
+
...(kind === 'responses' && responseExample !== undefined ? { responseExample } : {}),
|
|
305
|
+
...(kind === 'responses' && responseExamples ? { responseExamples } : {}),
|
|
252
306
|
}
|
|
253
307
|
}
|
|
254
308
|
for (let [k, v] of Object.entries(def.components?.schemas || {})) initSchema(k, v, 'schemas')
|
|
@@ -256,35 +310,77 @@ const buildSchemaIndex = (def: OpenAPIV3.Document) => {
|
|
|
256
310
|
for (let [k, v] of Object.entries(def.components?.responses || {})) initSchema(k, v, 'responses')
|
|
257
311
|
|
|
258
312
|
Object.entries(index).forEach(([k, v]) => {
|
|
259
|
-
for (let d of v.dependsOn) index[d]
|
|
313
|
+
for (let d of v.dependsOn) index[d]?.usedBy.add(k)
|
|
260
314
|
})
|
|
261
315
|
|
|
262
316
|
return index
|
|
263
317
|
}
|
|
264
318
|
|
|
265
|
-
const
|
|
319
|
+
const resolveParamRef = (
|
|
320
|
+
ref: string,
|
|
321
|
+
components: OpenAPIV3.ComponentsObject | undefined
|
|
322
|
+
): OpenAPIV3.ParameterObject | undefined => {
|
|
323
|
+
let match = ref.match(/^#\/components\/parameters\/(.+)$/)
|
|
324
|
+
if (!match) return undefined
|
|
325
|
+
let target = components?.parameters?.[match[1]]
|
|
326
|
+
if (!target) return undefined
|
|
327
|
+
if ('$ref' in target) return resolveParamRef(target.$ref, components)
|
|
328
|
+
return target
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const parseEndpointDef = (
|
|
332
|
+
method: string,
|
|
333
|
+
path: string,
|
|
334
|
+
def?: OpenAPIV3.OperationObject,
|
|
335
|
+
components?: OpenAPIV3.ComponentsObject
|
|
336
|
+
) => {
|
|
266
337
|
if (!def) return {}
|
|
267
|
-
let imports = {}
|
|
338
|
+
let imports: Record<string, string> = {}
|
|
268
339
|
let p = path.replaceAll(/\{([^\}]*)\}/g, ':$1')
|
|
269
340
|
// let description = def.summary || def.description
|
|
270
|
-
let
|
|
271
|
-
|
|
341
|
+
let schemaName = def.operationId
|
|
342
|
+
? def.operationId.replace(/^\w/, c => c.toUpperCase())
|
|
343
|
+
: `${method}${path
|
|
344
|
+
.replaceAll(/\{([^\}]+)\}/g, (_, p) => `By${p.replace(/^\w/, (c: string) => c.toUpperCase())}`)
|
|
345
|
+
.replaceAll(/[^$\w\d_]+([$\w\d_])/g, (_, $1) => $1.toUpperCase())
|
|
346
|
+
}`.replace(/^\w/, c => c.toUpperCase())
|
|
272
347
|
|
|
273
348
|
let meta = '/**\n'
|
|
274
349
|
if (def.summary) meta += ` * ${def.summary}\n *\n`
|
|
275
350
|
if (def.description) meta += ` * ${def.description.replace(/\n/g, '\n * ')}\n`
|
|
276
351
|
if (def.operationId) meta += ` * @operationId ${def.operationId}\n`
|
|
277
|
-
if (def.externalDocs) meta += ` * @externalDocs ${def.externalDocs}\n`
|
|
352
|
+
if (def.externalDocs?.url) meta += ` * @externalDocs ${def.externalDocs.url}\n`
|
|
278
353
|
if (def.tags) meta += ` * @tags ${def.tags.join(' ')}\n`
|
|
279
|
-
if (def.
|
|
354
|
+
if (Array.isArray(def.security)) {
|
|
355
|
+
if (def.security.length === 0) {
|
|
356
|
+
meta += ` * @security none\n`
|
|
357
|
+
} else {
|
|
358
|
+
for (const s of def.security) {
|
|
359
|
+
const keys = Object.keys(s)
|
|
360
|
+
if (keys.length === 0) {
|
|
361
|
+
meta += ` * @security none\n`
|
|
362
|
+
} else {
|
|
363
|
+
for (const k of keys) {
|
|
364
|
+
const scopes = (s as any)[k] as string[]
|
|
365
|
+
meta += ` * @security ${k}${scopes && scopes.length ? ` ${scopes.join(' ')}` : ''}\n`
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (def.deprecated) meta += ' * @deprecated\n'
|
|
280
372
|
meta += ' */'
|
|
281
373
|
let endpoint = `${method}("${p}", ${schemaName}, ctx => {\n throw new NotImplementedError()\n})`
|
|
282
374
|
|
|
283
|
-
let sp = { path: {}, query: {}, header: {}, body: {}, formData: {} } // TODO handle body and formData cases
|
|
375
|
+
let sp: Record<string, Record<string, string>> = { path: {}, query: {}, header: {}, body: {}, formData: {} } // TODO handle body and formData cases
|
|
376
|
+
|
|
284
377
|
for (let _p of def?.parameters || []) {
|
|
285
|
-
|
|
286
|
-
if (_p
|
|
287
|
-
|
|
378
|
+
let p: OpenAPIV3.ParameterObject | undefined
|
|
379
|
+
if ('$ref' in _p) {
|
|
380
|
+
p = resolveParamRef(_p.$ref, components)
|
|
381
|
+
if (!p) continue
|
|
382
|
+
} else p = _p as OpenAPIV3.ParameterObject
|
|
383
|
+
if (!sp[p.in]) continue
|
|
288
384
|
let o = (s: string) => {
|
|
289
385
|
const [_, so] = [...(s.match(/^\$T.optional\((.*)\)$/) || [])]
|
|
290
386
|
s = so ?? s
|
|
@@ -335,7 +431,7 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
|
|
|
335
431
|
])
|
|
336
432
|
),
|
|
337
433
|
]
|
|
338
|
-
body = bs.length ? ` body: {${bs.map(([k, v]) => `"${
|
|
434
|
+
body = bs.length ? ` body: {${bs.map(([k, v]) => `"${k}":${o(v)}`).join(',')}}` : ''
|
|
339
435
|
}
|
|
340
436
|
}
|
|
341
437
|
|
|
@@ -343,7 +439,6 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
|
|
|
343
439
|
let r = def?.responses
|
|
344
440
|
let rs = Object.fromEntries(
|
|
345
441
|
Object.entries(r || {}).map(([status, sv]) => {
|
|
346
|
-
let entries: string[] = []
|
|
347
442
|
let s: string = Number.isInteger(Number(status)) ? status : 'default'
|
|
348
443
|
|
|
349
444
|
//@ts-ignore
|
|
@@ -354,27 +449,91 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
|
|
|
354
449
|
return l[l.length - 1]
|
|
355
450
|
})
|
|
356
451
|
: null
|
|
357
|
-
if (rootRef)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
452
|
+
if (rootRef) return [s, rootRef]
|
|
453
|
+
|
|
454
|
+
const respObj = sv as OpenAPIV3.ResponseObject
|
|
455
|
+
const content = respObj?.content || {}
|
|
456
|
+
|
|
457
|
+
// Collect response-level headers
|
|
458
|
+
const headerEntries: string[] = []
|
|
459
|
+
for (const [hName, hVal] of Object.entries(respObj?.headers || {})) {
|
|
460
|
+
if ('$ref' in (hVal as any)) continue
|
|
461
|
+
const h = hVal as OpenAPIV3.HeaderObject
|
|
462
|
+
const headerSchema = unref(
|
|
463
|
+
parseOapiSchema(h.schema || ({ type: 'string' } as any), { description: h.description }),
|
|
464
|
+
m => {
|
|
364
465
|
let l = m.split('/')
|
|
365
466
|
imports[l[l.length - 1]] = m
|
|
366
467
|
return l[l.length - 1]
|
|
367
|
-
}
|
|
468
|
+
}
|
|
368
469
|
)
|
|
470
|
+
headerEntries.push(`${JSON.stringify(hName)}:${h.required ? headerSchema : `$T.optional(${headerSchema})`}`)
|
|
471
|
+
}
|
|
472
|
+
const description = typeof respObj?.description === 'string' && respObj.description ? respObj.description : undefined
|
|
473
|
+
|
|
474
|
+
if (Object.keys(content).length === 0) {
|
|
475
|
+
let nullSchema = description ? `$T.null({description:${JSON.stringify(description)}})` : `$T.null()`
|
|
476
|
+
if (headerEntries.length) nullSchema = `({...${nullSchema}, responseHeaders:{${headerEntries.join(',')}}})`
|
|
477
|
+
return [s, nullSchema]
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Group schemas by galbe body key, collect examples
|
|
481
|
+
const keyGroups: Record<string, string[]> = {}
|
|
482
|
+
const exampleParts: string[] = []
|
|
483
|
+
let singleExample: any = undefined
|
|
484
|
+
for (const [mediaType, tv] of Object.entries(content)) {
|
|
485
|
+
const galbeKey = mediaType
|
|
486
|
+
if ((tv as any).example !== undefined && singleExample === undefined) singleExample = (tv as any).example
|
|
487
|
+
if (tv.examples && Object.keys(tv.examples).length) {
|
|
488
|
+
for (const [k, ex] of Object.entries(tv.examples)) exampleParts.push(`${JSON.stringify(k)}:${JSON.stringify(ex)}`)
|
|
489
|
+
}
|
|
490
|
+
const schemaStr = unref(parseOapiSchema(tv.schema), m => {
|
|
491
|
+
let l = m.split('/')
|
|
492
|
+
imports[l[l.length - 1]] = m
|
|
493
|
+
return l[l.length - 1]
|
|
494
|
+
})
|
|
495
|
+
if (!keyGroups[galbeKey]) keyGroups[galbeKey] = []
|
|
496
|
+
keyGroups[galbeKey].push(schemaStr)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const uniqueKeys = Object.keys(keyGroups)
|
|
500
|
+
|
|
501
|
+
if (uniqueKeys.length > 1) {
|
|
502
|
+
// Multiple body keys → STResponseContent object
|
|
503
|
+
const parts: string[] = []
|
|
504
|
+
for (const [key, schemas] of Object.entries(keyGroups)) {
|
|
505
|
+
const unique = [...new Set(schemas)]
|
|
506
|
+
parts.push(`"${key}": ${unique.length === 1 ? unique[0] : `$T.union([${unique.join(',')}])`}`)
|
|
507
|
+
}
|
|
508
|
+
if (description) parts.push(`description: ${JSON.stringify(description)}`)
|
|
509
|
+
if (headerEntries.length) parts.push(`responseHeaders: {${headerEntries.join(',')}}`)
|
|
510
|
+
if (exampleParts.length) parts.push(`examples: {${exampleParts.join(',')}}`)
|
|
511
|
+
if (singleExample !== undefined) parts.push(`example: ${JSON.stringify(singleExample)}`)
|
|
512
|
+
return [s, `{${parts.join(',')}}`]
|
|
513
|
+
} else {
|
|
514
|
+
// Single body key → STResponseContent object (preserves exact media type key)
|
|
515
|
+
const [key] = uniqueKeys
|
|
516
|
+
const unique = [...new Set(keyGroups[key] || [])]
|
|
517
|
+
let schemaStr = unique.length === 0 ? `$T.null()` : unique.length === 1 ? unique[0] : `$T.union([${unique.join(',')}])`
|
|
518
|
+
// Embed per-media-type example/examples inside the schema spread so the
|
|
519
|
+
// content-map serializer can read them from the per-key body schema.
|
|
520
|
+
const perKeyExtras: string[] = []
|
|
521
|
+
if (exampleParts.length) perKeyExtras.push(`examples:{${exampleParts.join(',')}}`)
|
|
522
|
+
if (singleExample !== undefined) perKeyExtras.push(`example:${JSON.stringify(singleExample)}`)
|
|
523
|
+
if (perKeyExtras.length) schemaStr = `({...${schemaStr},${perKeyExtras.join(',')}})`
|
|
524
|
+
const parts: string[] = []
|
|
525
|
+
if (key) parts.push(`"${key}":${schemaStr}`)
|
|
526
|
+
if (description) parts.push(`description:${JSON.stringify(description)}`)
|
|
527
|
+
if (headerEntries.length) parts.push(`responseHeaders:{${headerEntries.join(',')}}`)
|
|
528
|
+
return [s, `{${parts.join(',')}}`]
|
|
369
529
|
}
|
|
370
|
-
return [s, [...new Set(entries)]]
|
|
371
530
|
})
|
|
372
531
|
)
|
|
373
532
|
|
|
374
533
|
if (Object.keys(rs).length) {
|
|
375
534
|
resp = ` response: {${Object.entries(rs)
|
|
376
|
-
.filter(([_, v]) => v
|
|
377
|
-
.map(([s, v]) => `${s}: ${v
|
|
535
|
+
.filter(([_, v]) => v)
|
|
536
|
+
.map(([s, v]) => `${s}: ${v}`)
|
|
378
537
|
.join(',')}}`
|
|
379
538
|
} else resp = ''
|
|
380
539
|
|
|
@@ -397,18 +556,34 @@ const parseEndpoints = (def: OpenAPIV3.Document) => {
|
|
|
397
556
|
let endpoints: Record<string, EndpointEntry> = {}
|
|
398
557
|
for (let [fullPath, pathVal] of Object.entries(def.paths || {})) {
|
|
399
558
|
if (!pathVal) continue
|
|
400
|
-
let match = fullPath.match(/^\/?(v\d+[^\/]*\/)?(?:\/?(public|private))?\/?([^\/]+)\/?(.*)$/)
|
|
559
|
+
let match = fullPath.match(/^\/?(?:(v\d+)[^\/]*\/)?(?:\/?(public|private))?\/?([^\/]+)\/?(.*)$/)
|
|
401
560
|
if (!match) continue
|
|
402
561
|
let [_, version, visibility, scope, path] = [...match]
|
|
403
562
|
path = `/${path}`
|
|
404
563
|
let methods = ['get', 'put', 'patch', 'post', 'delete', 'options', 'head'] as const
|
|
564
|
+
let pathParams = pathVal.parameters || []
|
|
405
565
|
for (let m of methods) {
|
|
406
566
|
let endpointDef = pathVal?.[m]
|
|
407
567
|
if (!endpointDef) continue
|
|
408
568
|
let ref = `#/paths${version ? `/${version}` : ''}${visibility ? `/${visibility}` : ''}${
|
|
409
569
|
scope ? `/${scope}` : ''
|
|
410
570
|
}/${m}${path}`
|
|
411
|
-
let
|
|
571
|
+
let opParams = endpointDef.parameters || []
|
|
572
|
+
let opKeys = new Set(
|
|
573
|
+
opParams
|
|
574
|
+
.map(p => {
|
|
575
|
+
let resolved = '$ref' in p ? resolveParamRef(p.$ref, def.components) : (p as OpenAPIV3.ParameterObject)
|
|
576
|
+
return resolved ? `${resolved.in}:${resolved.name}` : null
|
|
577
|
+
})
|
|
578
|
+
.filter((k): k is string => k !== null)
|
|
579
|
+
)
|
|
580
|
+
let inheritedParams = pathParams.filter(p => {
|
|
581
|
+
let resolved = '$ref' in p ? resolveParamRef(p.$ref, def.components) : (p as OpenAPIV3.ParameterObject)
|
|
582
|
+
if (!resolved) return false
|
|
583
|
+
return !opKeys.has(`${resolved.in}:${resolved.name}`)
|
|
584
|
+
})
|
|
585
|
+
let mergedDef = { ...endpointDef, parameters: [...inheritedParams, ...opParams] }
|
|
586
|
+
let { schema, endpoint } = parseEndpointDef(m, fullPath, mergedDef, def.components)
|
|
412
587
|
endpoints[ref] = {
|
|
413
588
|
version,
|
|
414
589
|
visibility: visibility as 'public' | 'private',
|
|
@@ -423,56 +598,120 @@ const parseEndpoints = (def: OpenAPIV3.Document) => {
|
|
|
423
598
|
return endpoints
|
|
424
599
|
}
|
|
425
600
|
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (!depMatch) continue
|
|
455
|
-
let [_, depOrig, depName] = [...depMatch]
|
|
456
|
-
if (depOrig !== type) {
|
|
457
|
-
if (!(depOrig in imports)) imports[depOrig] = []
|
|
458
|
-
imports[depOrig].push(depName)
|
|
459
|
-
}
|
|
601
|
+
const COMPONENT_TYPE_MAP = {
|
|
602
|
+
schemas: 'commons',
|
|
603
|
+
requestBodies: 'requests',
|
|
604
|
+
responses: 'responses',
|
|
605
|
+
} as const
|
|
606
|
+
|
|
607
|
+
const renderComponentSchemaFile = (
|
|
608
|
+
schemas: Record<string, SchemaEntry>,
|
|
609
|
+
type: 'schemas' | 'requestBodies' | 'responses'
|
|
610
|
+
): string => {
|
|
611
|
+
if (Object.keys(schemas).length === 0) return ''
|
|
612
|
+
let imports: Record<string, string[]> = {}
|
|
613
|
+
let decl: string[] = []
|
|
614
|
+
Object.entries(schemas).forEach(([k, s]) => {
|
|
615
|
+
if (s.key === s.schema && s.dependsOn.size === 1) {
|
|
616
|
+
let depMatch = [...s.dependsOn][0].match(/^#\/components\/([^\/]+)\/([^\/]+)/)
|
|
617
|
+
if (!depMatch) return
|
|
618
|
+
let [_, depOrig, depName] = [...depMatch]
|
|
619
|
+
decl.push(`export { ${depName} } from './${COMPONENT_TYPE_MAP[depOrig as keyof typeof COMPONENT_TYPE_MAP]}.schema'\n`)
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
for (let dep of [k, ...s.dependsOn]) {
|
|
623
|
+
let depMatch = dep.match(/^#\/components\/([^\/]+)\/([^\/]+)/)
|
|
624
|
+
if (!depMatch) continue
|
|
625
|
+
let [_, depOrig, depName] = [...depMatch]
|
|
626
|
+
if (depOrig !== type) {
|
|
627
|
+
if (!(depOrig in imports)) imports[depOrig] = []
|
|
628
|
+
imports[depOrig].push(depName)
|
|
460
629
|
}
|
|
630
|
+
}
|
|
631
|
+
// For requestBodies/responses that are just a single ref to another schema,
|
|
632
|
+
// preserve identity by tagging a _responseId / _requestBodyId rather than
|
|
633
|
+
// aliasing it (which would lose the original component name in the spec).
|
|
634
|
+
const isSingleAlias =
|
|
635
|
+
(type === 'responses' || type === 'requestBodies') &&
|
|
636
|
+
s.dependsOn.size === 1 &&
|
|
637
|
+
s.schema.trim() === [...s.dependsOn][0].split('/').pop()
|
|
638
|
+
const responseExtras: string[] = []
|
|
639
|
+
if (type === 'responses') {
|
|
640
|
+
if (s.responseDescription) responseExtras.push(`description: ${JSON.stringify(s.responseDescription)}`)
|
|
641
|
+
if (s.responseExample !== undefined) responseExtras.push(`example: ${JSON.stringify(s.responseExample)}`)
|
|
642
|
+
if (s.responseExamples) responseExtras.push(`examples: ${JSON.stringify(s.responseExamples)}`)
|
|
643
|
+
}
|
|
644
|
+
if (isSingleAlias) {
|
|
645
|
+
const tagKey = type === 'responses' ? '_responseId' : '_requestBodyId'
|
|
646
|
+
const extras = [`${tagKey}: "${s.key}"`, ...responseExtras]
|
|
647
|
+
decl.push(
|
|
648
|
+
`export const ${s.key} = { ...${s.schema}, ${extras.join(', ')} } as typeof ${s.schema}\nexport type ${s.key} = Static<typeof ${s.key}>\n`
|
|
649
|
+
)
|
|
650
|
+
} else if (type === 'responses' && responseExtras.length) {
|
|
651
|
+
decl.push(
|
|
652
|
+
`export const ${s.key} = {...${s.schema}, ${responseExtras.join(', ')}}\nexport type ${s.key} = Static<typeof ${s.key}>\n`
|
|
653
|
+
)
|
|
654
|
+
} else {
|
|
461
655
|
decl.push(`export const ${s.key} = ${s.schema}\nexport type ${s.key} = Static<typeof ${s.key}>\n`)
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
656
|
+
}
|
|
657
|
+
})
|
|
658
|
+
if (decl.length === 0) return ''
|
|
659
|
+
return `import type { Static } from 'galbe/schema'\nimport { $T } from 'galbe'\n${Object.entries(imports)
|
|
660
|
+
.map(
|
|
661
|
+
([k, v]) =>
|
|
662
|
+
`import { ${[...new Set(v)].join(', ')} } from './${COMPONENT_TYPE_MAP[k as keyof typeof COMPONENT_TYPE_MAP]}.schema'\n`
|
|
663
|
+
)
|
|
664
|
+
.join('\n')}\n${decl.join('\n')}\n`
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export type RoutePlanEntry = {
|
|
668
|
+
/** HTTP method, lowercase (matches the property accessed on `g`). */
|
|
669
|
+
method: string
|
|
670
|
+
/** Path as emitted in the call expression: full prefix included, OpenAPI braces converted to `:param`. Stable identity for diff/rename. */
|
|
671
|
+
path: string
|
|
672
|
+
schemaName: string
|
|
673
|
+
/** JSDoc block string (e.g. '/**\n * summary\n *\/'). */
|
|
674
|
+
meta: string
|
|
675
|
+
/** Rendered call expression body, e.g. 'get("/path", FooSchema, ctx => { ... })'. */
|
|
676
|
+
call: string
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export type ScopePlan = {
|
|
680
|
+
/** e.g. '/main', '/v1/public/admin'. */
|
|
681
|
+
scopeKey: string
|
|
682
|
+
/** Output path without extension, e.g. 'routes/main.route'. */
|
|
683
|
+
routeFile: string
|
|
684
|
+
/** Output path without extension, e.g. 'schemas/main.schema'. */
|
|
685
|
+
schemaFile: string
|
|
686
|
+
/** Imports to inject in the scope schema file: relative path -> imported names. */
|
|
687
|
+
schemaImports: Record<string, string[]>
|
|
688
|
+
/** 'export const X = {...}' declarations for the scope schema file. */
|
|
689
|
+
schemaDecls: string[]
|
|
690
|
+
/** Schema names to import in the scope route file from its sibling schema file. */
|
|
691
|
+
routeSchemaImports: string[]
|
|
692
|
+
routes: RoutePlanEntry[]
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export type GenerationPlan = {
|
|
696
|
+
/** Component schema files (commons/requests/responses), ready to write. Path is relative to outDir without extension. */
|
|
697
|
+
componentFiles: { path: string; content: string }[]
|
|
698
|
+
scopes: ScopePlan[]
|
|
699
|
+
target: 'js' | 'ts'
|
|
700
|
+
}
|
|
468
701
|
|
|
702
|
+
export const buildPlan = (
|
|
703
|
+
endpoints: Record<string, EndpointEntry>,
|
|
704
|
+
schemaIndex: Record<string, SchemaEntry>,
|
|
705
|
+
target: 'js' | 'ts'
|
|
706
|
+
): GenerationPlan => {
|
|
707
|
+
const componentFiles: { path: string; content: string }[] = []
|
|
469
708
|
const sMaps = [
|
|
470
709
|
{ g: 'commons', o: 'schemas' },
|
|
471
710
|
{ g: 'requests', o: 'requestBodies' },
|
|
472
711
|
{ g: 'responses', o: 'responses' },
|
|
473
712
|
] as const
|
|
474
713
|
for (let { g, o } of sMaps) {
|
|
475
|
-
let
|
|
714
|
+
let content = renderComponentSchemaFile(
|
|
476
715
|
orderDeps(
|
|
477
716
|
Object.fromEntries(
|
|
478
717
|
Object.entries(schemaIndex).filter(([k, _]) => {
|
|
@@ -482,69 +721,117 @@ const writeFiles = async (
|
|
|
482
721
|
),
|
|
483
722
|
o
|
|
484
723
|
)
|
|
485
|
-
if (
|
|
724
|
+
if (content) componentFiles.push({ path: `schemas/${g}.schema`, content })
|
|
486
725
|
}
|
|
487
726
|
|
|
488
|
-
let scopedDefs = Object.entries(endpoints).reduce((p, [_, v]) => {
|
|
727
|
+
let scopedDefs = Object.entries(endpoints).reduce<Record<string, EndpointEntry[]>>((p, [_, v]) => {
|
|
489
728
|
let scopeKey = `${v.version ? `/${v.version}` : ''}${v.visibility ? `/${v.visibility}` : ''}${
|
|
490
729
|
v.scope ? `/${v.scope}` : '/main'
|
|
491
730
|
}`
|
|
492
731
|
if (!(scopeKey in p)) p[scopeKey] = []
|
|
493
732
|
p[scopeKey].push(v)
|
|
494
733
|
return p
|
|
495
|
-
}, {})
|
|
734
|
+
}, {})
|
|
496
735
|
|
|
736
|
+
const scopes: ScopePlan[] = []
|
|
497
737
|
for (let [scopeKey, def] of Object.entries(scopedDefs)) {
|
|
498
|
-
let
|
|
499
|
-
let
|
|
738
|
+
let routeFile = `routes${scopeKey}.route`
|
|
739
|
+
let schemaFile = `schemas${scopeKey}.schema`
|
|
500
740
|
|
|
501
741
|
let sImports: Record<string, Set<string>> = {}
|
|
502
742
|
let sDecl: string[] = []
|
|
503
|
-
|
|
504
743
|
let rImports: Set<string> = new Set()
|
|
505
|
-
let
|
|
744
|
+
let routes: RoutePlanEntry[] = []
|
|
506
745
|
|
|
507
746
|
for (let d of def) {
|
|
508
|
-
// schema
|
|
509
747
|
Object.entries(d.schema?.imports || {}).forEach(([iK, dep]) => {
|
|
510
|
-
let k = refToPath(dep, dirname(
|
|
748
|
+
let k = refToPath(dep, dirname(schemaFile))
|
|
511
749
|
if (!k) return
|
|
512
750
|
if (!(k in sImports)) sImports[k] = new Set()
|
|
513
751
|
sImports[k].add(iK)
|
|
514
752
|
})
|
|
515
753
|
sDecl.push(`export const ${d.schema?.name} = ${d.schema?.def}`)
|
|
516
754
|
|
|
517
|
-
// route
|
|
518
755
|
let ep = d.endpoint
|
|
519
756
|
if (!ep) continue
|
|
520
757
|
if (d.schema?.name) rImports.add(d.schema?.name)
|
|
521
|
-
|
|
758
|
+
// Source of truth for the path is the rendered call expression itself —
|
|
759
|
+
// reconstructing from version/visibility/scope/path drifts on edge cases
|
|
760
|
+
// (e.g. an empty trailing segment yields `/users/` instead of `/users`).
|
|
761
|
+
const callPathMatch = (ep.def ?? '').match(/^\w+\("([^"]*)"/)
|
|
762
|
+
const emittedPath = callPathMatch?.[1] ?? ''
|
|
763
|
+
routes.push({
|
|
764
|
+
method: d.method ?? '',
|
|
765
|
+
path: emittedPath,
|
|
766
|
+
schemaName: d.schema?.name ?? '',
|
|
767
|
+
meta: ep.meta ?? '',
|
|
768
|
+
call: ep.def ?? '',
|
|
769
|
+
})
|
|
522
770
|
}
|
|
523
771
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
`import { ${[...rImports].join(', ')} } from '../schemas${scopeKey}.schema'\n\n` +
|
|
534
|
-
`export default (g: Galbe) => {\n` +
|
|
535
|
-
rDecl.map(d => d.replaceAll('\n', '\n ')).join('\n\n') +
|
|
536
|
-
`\n}\n`
|
|
537
|
-
|
|
538
|
-
if (sDecl?.length) await writeCodeFile(resolve(path, schemaPath), schemaFile, target)
|
|
539
|
-
if (rDecl?.length) await writeCodeFile(resolve(path, routePath), routeFile, target)
|
|
772
|
+
scopes.push({
|
|
773
|
+
scopeKey,
|
|
774
|
+
routeFile,
|
|
775
|
+
schemaFile,
|
|
776
|
+
schemaImports: Object.fromEntries(Object.entries(sImports).map(([k, v]) => [k, [...v]])),
|
|
777
|
+
schemaDecls: sDecl,
|
|
778
|
+
routeSchemaImports: [...rImports],
|
|
779
|
+
routes,
|
|
780
|
+
})
|
|
540
781
|
}
|
|
782
|
+
|
|
783
|
+
return { componentFiles, scopes, target }
|
|
541
784
|
}
|
|
542
785
|
|
|
543
|
-
export
|
|
786
|
+
export type ApplyPlanOptions = {
|
|
787
|
+
/** Override the route file content for given scope keys. When set, the value is written verbatim
|
|
788
|
+
* instead of fresh-rendering from the plan — used by the merger to preserve user code. */
|
|
789
|
+
routeContents?: Map<string, string>
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export const applyPlan = async (plan: GenerationPlan, outDir: string, opts: ApplyPlanOptions = {}): Promise<void> => {
|
|
793
|
+
const { target } = plan
|
|
794
|
+
|
|
795
|
+
for (const f of plan.componentFiles) {
|
|
796
|
+
await writeCodeFile(resolve(outDir, f.path), f.content, target)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
for (const scope of plan.scopes) {
|
|
800
|
+
if (scope.schemaDecls.length) {
|
|
801
|
+
const schemaContent =
|
|
802
|
+
`import { $T } from 'galbe'\n\n` +
|
|
803
|
+
`${Object.entries(scope.schemaImports)
|
|
804
|
+
.map(([k, v]) => `import { ${v.join(', ')} } from '${k}'`)
|
|
805
|
+
.join('\n')}\n\n` +
|
|
806
|
+
`${scope.schemaDecls.join('\n\n')}\n`
|
|
807
|
+
await writeCodeFile(resolve(outDir, scope.schemaFile), schemaContent, target)
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (scope.routes.length) {
|
|
811
|
+
const override = opts.routeContents?.get(scope.scopeKey)
|
|
812
|
+
let routeContent: string
|
|
813
|
+
if (override !== undefined) {
|
|
814
|
+
routeContent = override
|
|
815
|
+
} else {
|
|
816
|
+
const deepness = scope.scopeKey.split('/').length - 1
|
|
817
|
+
const importPath = `${Array(deepness).fill('../').join('')}schemas${scope.scopeKey}.schema`
|
|
818
|
+
const rDecl = scope.routes.map(r => ` ${r.meta}\ng.${r.call}`)
|
|
819
|
+
routeContent =
|
|
820
|
+
`import { NotImplementedError, type Galbe } from 'galbe'\n` +
|
|
821
|
+
`import { ${scope.routeSchemaImports.join(', ')} } from '${importPath}'\n\n` +
|
|
822
|
+
`export default (g: Galbe) => {\n` +
|
|
823
|
+
rDecl.map(d => d.replaceAll('\n', '\n ')).join('\n\n') +
|
|
824
|
+
`\n}\n`
|
|
825
|
+
}
|
|
826
|
+
await writeCodeFile(resolve(outDir, scope.routeFile), routeContent, target)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export const planFromOapi = async (
|
|
544
832
|
input: string,
|
|
545
|
-
out: string,
|
|
546
833
|
{ version, ext, target }: { version: string; ext: 'json' | 'yaml'; target: 'js' | 'ts' }
|
|
547
|
-
) => {
|
|
834
|
+
): Promise<GenerationPlan> => {
|
|
548
835
|
let def: OpenAPIV3.Document =
|
|
549
836
|
ext === 'json' ? await Bun.file(input).json() : Bun.YAML.parse(await Bun.file(input).text())
|
|
550
837
|
|
|
@@ -554,5 +841,13 @@ export const generateFromOapi = async (
|
|
|
554
841
|
let schemaIndex = buildSchemaIndex(def)
|
|
555
842
|
let endpointDefs = parseEndpoints(def)
|
|
556
843
|
|
|
557
|
-
|
|
844
|
+
return buildPlan(endpointDefs, schemaIndex, target)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export const generateFromOapi = async (
|
|
848
|
+
input: string,
|
|
849
|
+
out: string,
|
|
850
|
+
opts: { version: string; ext: 'json' | 'yaml'; target: 'js' | 'ts' }
|
|
851
|
+
) => {
|
|
852
|
+
await applyPlan(await planFromOapi(input, opts), out)
|
|
558
853
|
}
|