@kubb/adapter-oas 5.0.0-alpha.2 → 5.0.0-alpha.20
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/dist/index.cjs +989 -1070
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +83 -128
- package/dist/index.js +988 -1069
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/adapter.ts +79 -63
- package/src/constants.ts +63 -63
- package/src/discriminator.ts +99 -0
- package/src/factory.ts +151 -0
- package/src/guards.ts +68 -0
- package/src/index.ts +1 -0
- package/src/parser.ts +352 -667
- package/src/refs.ts +57 -0
- package/src/resolvers.ts +490 -0
- package/src/types.ts +178 -56
- package/src/oas/Oas.ts +0 -616
- package/src/oas/resolveServerUrl.ts +0 -47
- package/src/oas/types.ts +0 -52
- package/src/oas/utils.ts +0 -402
package/src/refs.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import jsonpointer from 'jsonpointer'
|
|
2
|
+
import { isReference } from './guards.ts'
|
|
3
|
+
import type { Document } from './types.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a local JSON pointer reference from a document.
|
|
7
|
+
*
|
|
8
|
+
* Accepts `#/...` refs. Returns `null` for empty or non-local refs.
|
|
9
|
+
* Throws when the pointer cannot be resolved.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet') // SchemaObject | null
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function resolveRef<T = unknown>(document: Document, $ref: string): T | null {
|
|
17
|
+
const origRef = $ref
|
|
18
|
+
$ref = $ref.trim()
|
|
19
|
+
if ($ref === '') {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
if ($ref.startsWith('#')) {
|
|
23
|
+
$ref = globalThis.decodeURIComponent($ref.substring(1))
|
|
24
|
+
} else {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
const current = jsonpointer.get(document, $ref)
|
|
28
|
+
|
|
29
|
+
if (!current) {
|
|
30
|
+
throw new Error(`Could not find a definition for ${origRef}.`)
|
|
31
|
+
}
|
|
32
|
+
return current as T
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves a `$ref` object while preserving the original `$ref` field on the result.
|
|
37
|
+
*
|
|
38
|
+
* Useful for parser flows that need both dereferenced fields and pointer
|
|
39
|
+
* identity (for naming/import purposes). Non-reference values are returned as-is.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
|
|
44
|
+
* // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function dereferenceWithRef<T = unknown>(document: Document, schema?: T): T {
|
|
48
|
+
if (isReference(schema)) {
|
|
49
|
+
return {
|
|
50
|
+
...schema,
|
|
51
|
+
...resolveRef(document, schema.$ref),
|
|
52
|
+
$ref: schema.$ref,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return schema as T
|
|
57
|
+
}
|
package/src/resolvers.ts
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { pascalCase } from '@internals/utils'
|
|
2
|
+
import { mediaTypes } from '@kubb/ast'
|
|
3
|
+
import type { MediaType, ParserOptions, PrimitiveSchemaType, SchemaType } from '@kubb/ast/types'
|
|
4
|
+
import type { ParameterObject, ServerObject } from 'oas/types'
|
|
5
|
+
import { isRef } from 'oas/types'
|
|
6
|
+
import { matchesMimeType } from 'oas/utils'
|
|
7
|
+
import { formatMap, structuralKeys } from './constants.ts'
|
|
8
|
+
import { isReference } from './guards.ts'
|
|
9
|
+
import { dereferenceWithRef, resolveRef } from './refs.ts'
|
|
10
|
+
import type { contentType, Document, MediaTypeObject, Operation, ResponseObject, SchemaObject } from './types.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolves `{variable}` placeholders in an OpenAPI server URL.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order per variable: `overrides[key]` → `variable.default` → left unreplaced.
|
|
16
|
+
* Throws when an override value is not in the variable's allowed `enum` list.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* resolveServerUrl(
|
|
21
|
+
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
22
|
+
* { env: 'prod' },
|
|
23
|
+
* )
|
|
24
|
+
* // 'https://prod.api.example.com'
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function resolveServerUrl(server: ServerObject, overrides?: Record<string, string>): string {
|
|
28
|
+
if (!server.variables) {
|
|
29
|
+
return server.url
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let url = server.url
|
|
33
|
+
for (const [key, variable] of Object.entries(server.variables)) {
|
|
34
|
+
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : undefined)
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) {
|
|
40
|
+
throw new Error(`Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(', ')}.`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
url = url.replaceAll(`{${key}}`, value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return url
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Looks up the Kubb `SchemaType` for a given OAS `format` string.
|
|
51
|
+
* Returns `undefined` for formats not in `formatMap` (e.g. `int64`, `date-time`),
|
|
52
|
+
* which are handled separately because their output depends on parser options.
|
|
53
|
+
*/
|
|
54
|
+
export function getSchemaType(format: string): SchemaType | null {
|
|
55
|
+
return formatMap[format as keyof typeof formatMap] ?? null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Maps an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
60
|
+
* Numeric types (`number`, `integer`, `bigint`) are returned unchanged;
|
|
61
|
+
* `boolean` maps to `'boolean'`; everything else defaults to `'string'`.
|
|
62
|
+
*/
|
|
63
|
+
export function getPrimitiveType(type: string | undefined): PrimitiveSchemaType {
|
|
64
|
+
if (type === 'number' || type === 'integer' || type === 'bigint') return type
|
|
65
|
+
if (type === 'boolean') return 'boolean'
|
|
66
|
+
|
|
67
|
+
return 'string'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Narrows a raw content-type string to the `MediaType` union recognized by Kubb.
|
|
72
|
+
* Returns `undefined` for content types not present in `KNOWN_MEDIA_TYPES`.
|
|
73
|
+
*/
|
|
74
|
+
export function getMediaType(contentType: string): MediaType | null {
|
|
75
|
+
return Object.values(mediaTypes).includes(contentType as MediaType) ? (contentType as MediaType) : null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type OperationsOptions = {
|
|
79
|
+
contentType?: contentType
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns all resolved parameters for an operation, merging path-level and operation-level entries.
|
|
84
|
+
*
|
|
85
|
+
* Operation-level parameters take precedence over path-level ones with the same `in:name` key.
|
|
86
|
+
* `$ref` parameters are resolved via `dereferenceWithRef` to restore backward compatibility
|
|
87
|
+
* with `oas` v31+ which otherwise filters them out.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* getParameters(document, operation)
|
|
92
|
+
* // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function getParameters(document: Document, operation: Operation): Array<ParameterObject> {
|
|
96
|
+
const resolveParams = (params: unknown[]): Array<ParameterObject> =>
|
|
97
|
+
params.map((p) => dereferenceWithRef(document, p)).filter((p): p is ParameterObject => !!p && typeof p === 'object' && 'in' in p && 'name' in p)
|
|
98
|
+
|
|
99
|
+
const operationParams = resolveParams(operation.schema?.parameters || [])
|
|
100
|
+
const pathItem = document.paths?.[operation.path]
|
|
101
|
+
const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : [])
|
|
102
|
+
|
|
103
|
+
const paramMap = new Map<string, ParameterObject>()
|
|
104
|
+
for (const p of pathLevelParams) {
|
|
105
|
+
if (p.name && p.in) {
|
|
106
|
+
paramMap.set(`${p.in}:${p.name}`, p)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const p of operationParams) {
|
|
110
|
+
if (p.name && p.in) {
|
|
111
|
+
paramMap.set(`${p.in}:${p.name}`, p)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return Array.from(paramMap.values())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getResponseBody(responseBody: boolean | ResponseObject, contentType?: string): MediaTypeObject | false | [string, MediaTypeObject, ...string[]] {
|
|
119
|
+
if (!responseBody) return false
|
|
120
|
+
if (isReference(responseBody)) return false
|
|
121
|
+
|
|
122
|
+
const body = responseBody as ResponseObject
|
|
123
|
+
if (!body.content) return false
|
|
124
|
+
|
|
125
|
+
if (contentType) {
|
|
126
|
+
if (!(contentType in body.content)) return false
|
|
127
|
+
return body.content[contentType]!
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let availableContentType: string | undefined
|
|
131
|
+
const contentTypes = Object.keys(body.content)
|
|
132
|
+
contentTypes.forEach((mt: string) => {
|
|
133
|
+
if (!availableContentType && matchesMimeType.json(mt)) {
|
|
134
|
+
availableContentType = mt
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if (!availableContentType) {
|
|
139
|
+
contentTypes.forEach((mt: string) => {
|
|
140
|
+
if (!availableContentType) availableContentType = mt
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (availableContentType) {
|
|
145
|
+
return [availableContentType, body.content[availableContentType]!, ...(body.description ? [body.description] : [])]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Returns the response schema for a given operation and HTTP status code.
|
|
153
|
+
*
|
|
154
|
+
* Returns an empty object `{}` when no response body schema is available.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* getResponseSchema(document, operation, 200) // SchemaObject
|
|
159
|
+
* getResponseSchema(document, operation, '4XX') // {}
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export function getResponseSchema(document: Document, operation: Operation, statusCode: string | number, options: OperationsOptions = {}): SchemaObject {
|
|
163
|
+
if (operation.schema.responses) {
|
|
164
|
+
Object.keys(operation.schema.responses).forEach((key) => {
|
|
165
|
+
const schema = operation.schema.responses![key]
|
|
166
|
+
const $ref = isReference(schema) ? schema.$ref : undefined
|
|
167
|
+
|
|
168
|
+
if (schema && $ref) {
|
|
169
|
+
operation.schema.responses![key] = resolveRef<any>(document, $ref)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType)
|
|
175
|
+
|
|
176
|
+
if (responseBody === false) {
|
|
177
|
+
return {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema
|
|
181
|
+
|
|
182
|
+
if (!schema) {
|
|
183
|
+
return {}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return dereferenceWithRef(document, schema)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Returns the request body schema for an operation, or `undefined` when absent.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* getRequestSchema(document, operation) // SchemaObject | null
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function getRequestSchema(document: Document, operation: Operation, options: OperationsOptions = {}): SchemaObject | null {
|
|
198
|
+
if (operation.schema.requestBody) {
|
|
199
|
+
operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const requestBody = operation.getRequestBody(options.contentType)
|
|
203
|
+
|
|
204
|
+
if (requestBody === false) {
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema
|
|
209
|
+
|
|
210
|
+
if (!schema) {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return dereferenceWithRef(document, schema)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The three component sections Kubb reads schemas from.
|
|
219
|
+
*/
|
|
220
|
+
type SchemaSourceMode = 'schemas' | 'responses' | 'requestBodies'
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* A schema annotated with the component section it came from and its original name.
|
|
224
|
+
*
|
|
225
|
+
* Used during cross-source name-collision resolution in `resolveNameCollisions`.
|
|
226
|
+
*/
|
|
227
|
+
export type SchemaWithMetadata = {
|
|
228
|
+
schema: SchemaObject
|
|
229
|
+
source: SchemaSourceMode
|
|
230
|
+
originalName: string
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export type GetSchemasOptions = {
|
|
234
|
+
contentType?: contentType
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export type GetSchemasResult = {
|
|
238
|
+
schemas: Record<string, SchemaObject>
|
|
239
|
+
nameMapping: Map<string, string>
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Flattens a keyword-only `allOf` into its parent schema.
|
|
244
|
+
*
|
|
245
|
+
* Only flattens when every member is a plain fragment — no `$ref` and no structural keywords
|
|
246
|
+
* (see `structuralKeys`). Outer schema values take precedence over fragment values.
|
|
247
|
+
* Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
|
|
252
|
+
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
253
|
+
*
|
|
254
|
+
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
255
|
+
* // returned unchanged — contains a $ref
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
export function flattenSchema(schema: SchemaObject | null): SchemaObject | null {
|
|
259
|
+
if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null
|
|
260
|
+
if (schema.allOf.some((item) => isRef(item))) return schema
|
|
261
|
+
|
|
262
|
+
const isPlainFragment = (item: SchemaObject) => !Object.keys(item).some((key) => structuralKeys.has(key as 'properties'))
|
|
263
|
+
if (!schema.allOf.every((item) => isPlainFragment(item as SchemaObject))) {
|
|
264
|
+
return schema
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const merged: SchemaObject = { ...schema }
|
|
268
|
+
delete merged.allOf
|
|
269
|
+
|
|
270
|
+
for (const fragment of schema.allOf as SchemaObject[]) {
|
|
271
|
+
for (const [key, value] of Object.entries(fragment)) {
|
|
272
|
+
if (merged[key as keyof typeof merged] === undefined) {
|
|
273
|
+
merged[key as keyof typeof merged] = value
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return merged
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Extracts the inline schema from a media-type `content` map.
|
|
283
|
+
*
|
|
284
|
+
* Prefers `preferredContentType` when given; otherwise uses the first key in the map.
|
|
285
|
+
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* extractSchemaFromContent(operation.content, 'application/json')
|
|
290
|
+
* // SchemaObject | null
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export function extractSchemaFromContent(content: Record<string, unknown> | undefined, preferredContentType?: contentType): SchemaObject | null {
|
|
294
|
+
if (!content) return null
|
|
295
|
+
|
|
296
|
+
const firstContentType = Object.keys(content)[0] ?? 'application/json'
|
|
297
|
+
const targetContentType = preferredContentType ?? firstContentType
|
|
298
|
+
const contentSchema = content[targetContentType] as { schema?: SchemaObject } | undefined
|
|
299
|
+
const schema = contentSchema?.schema
|
|
300
|
+
|
|
301
|
+
if (schema && '$ref' in schema) return null
|
|
302
|
+
return schema ?? null
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
307
|
+
*/
|
|
308
|
+
function collectRefs(schema: unknown, refs = new Set<string>()): Set<string> {
|
|
309
|
+
if (Array.isArray(schema)) {
|
|
310
|
+
for (const item of schema) collectRefs(item, refs)
|
|
311
|
+
return refs
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (schema && typeof schema === 'object') {
|
|
315
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
316
|
+
if (key === '$ref' && typeof value === 'string') {
|
|
317
|
+
const match = value.match(/^#\/components\/schemas\/(.+)$/)
|
|
318
|
+
if (match) refs.add(match[1]!)
|
|
319
|
+
} else {
|
|
320
|
+
collectRefs(value, refs)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return refs
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
330
|
+
*
|
|
331
|
+
* Referenced schemas appear before the schemas that depend on them, so code generators
|
|
332
|
+
* can emit types in the correct order. Cycles are silently skipped.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```ts
|
|
336
|
+
* const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
|
|
337
|
+
* // Pet appears before Order when Order.$ref points at Pet
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
export function sortSchemas(schemas: Record<string, SchemaObject>): Record<string, SchemaObject> {
|
|
341
|
+
const deps = new Map<string, string[]>()
|
|
342
|
+
|
|
343
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
344
|
+
deps.set(name, Array.from(collectRefs(schema)))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const sorted: string[] = []
|
|
348
|
+
const visited = new Set<string>()
|
|
349
|
+
|
|
350
|
+
function visit(name: string, stack: Set<string>) {
|
|
351
|
+
if (visited.has(name) || stack.has(name)) return
|
|
352
|
+
stack.add(name)
|
|
353
|
+
for (const child of deps.get(name) ?? []) {
|
|
354
|
+
if (deps.has(child)) visit(child, stack)
|
|
355
|
+
}
|
|
356
|
+
stack.delete(name)
|
|
357
|
+
visited.add(name)
|
|
358
|
+
sorted.push(name)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const name of Object.keys(schemas)) {
|
|
362
|
+
visit(name, new Set())
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const result: Record<string, SchemaObject> = {}
|
|
366
|
+
for (const name of sorted) result[name] = schemas[name]!
|
|
367
|
+
return result
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const semanticSuffixes: Record<SchemaSourceMode, string> = {
|
|
371
|
+
schemas: 'Schema',
|
|
372
|
+
responses: 'Response',
|
|
373
|
+
requestBodies: 'Request',
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getSemanticSuffix(source: SchemaSourceMode): string {
|
|
377
|
+
return semanticSuffixes[source]
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function resolveSchemaRef(document: Document, schema: SchemaObject): SchemaObject {
|
|
381
|
+
if (!isReference(schema)) return schema
|
|
382
|
+
const resolved = resolveRef<SchemaObject>(document, schema.$ref)
|
|
383
|
+
return resolved && !isReference(resolved) ? resolved : schema
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Collects component schemas from one or more sources and resolves name collisions.
|
|
388
|
+
*
|
|
389
|
+
* Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
|
|
390
|
+
* topologically sorted by `$ref` dependency so generators emit types in the correct order.
|
|
391
|
+
*
|
|
392
|
+
* When two or more schemas normalize to the same PascalCase name:
|
|
393
|
+
* - Same source → numeric suffix (`2`, `3`, …).
|
|
394
|
+
* - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* ```ts
|
|
398
|
+
* const { schemas, nameMapping } = getSchemas(document, { contentType: 'application/json' })
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
export function getSchemas(document: Document, { contentType }: GetSchemasOptions): GetSchemasResult {
|
|
402
|
+
const components = document.components
|
|
403
|
+
|
|
404
|
+
const candidates: SchemaWithMetadata[] = [
|
|
405
|
+
...Object.entries((components?.schemas as Record<string, SchemaObject>) ?? {}).map(([name, schema]) => ({
|
|
406
|
+
schema: resolveSchemaRef(document, schema),
|
|
407
|
+
source: 'schemas' as const,
|
|
408
|
+
originalName: name,
|
|
409
|
+
})),
|
|
410
|
+
...(['responses', 'requestBodies'] as const).flatMap((source) =>
|
|
411
|
+
Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
412
|
+
const schema = extractSchemaFromContent((item as { content?: Record<string, unknown> }).content, contentType)
|
|
413
|
+
return schema ? [{ schema: resolveSchemaRef(document, schema), source, originalName: name }] : []
|
|
414
|
+
}),
|
|
415
|
+
),
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
const normalizedNames = new Map<string, SchemaWithMetadata[]>()
|
|
419
|
+
for (const item of candidates) {
|
|
420
|
+
const key = pascalCase(item.originalName)
|
|
421
|
+
const bucket = normalizedNames.get(key) ?? []
|
|
422
|
+
bucket.push(item)
|
|
423
|
+
normalizedNames.set(key, bucket)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const schemas: Record<string, SchemaObject> = {}
|
|
427
|
+
const nameMapping = new Map<string, string>()
|
|
428
|
+
const multipleSources = (items: SchemaWithMetadata[]) => new Set(items.map((i) => i.source)).size > 1
|
|
429
|
+
|
|
430
|
+
for (const [, items] of normalizedNames) {
|
|
431
|
+
items.forEach((item, index) => {
|
|
432
|
+
const suffix = items.length === 1 ? '' : multipleSources(items) ? getSemanticSuffix(item.source) : index === 0 ? '' : String(index + 1)
|
|
433
|
+
const uniqueName = item.originalName + suffix
|
|
434
|
+
schemas[uniqueName] = item.schema
|
|
435
|
+
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName)
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return { schemas: sortSchemas(schemas), nameMapping }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
444
|
+
* Returns `null` when `dateType: false`, signalling the format should fall through to `string`.
|
|
445
|
+
*/
|
|
446
|
+
export function getDateType(
|
|
447
|
+
options: ParserOptions,
|
|
448
|
+
format: 'date-time' | 'date' | 'time',
|
|
449
|
+
): { type: 'datetime'; offset?: boolean; local?: boolean } | { type: 'date' | 'time'; representation: 'date' | 'string' } | null {
|
|
450
|
+
if (!options.dateType) {
|
|
451
|
+
return null
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (format === 'date-time') {
|
|
455
|
+
if (options.dateType === 'date') {
|
|
456
|
+
return { type: 'date', representation: 'date' }
|
|
457
|
+
}
|
|
458
|
+
if (options.dateType === 'stringOffset') {
|
|
459
|
+
return { type: 'datetime', offset: true }
|
|
460
|
+
}
|
|
461
|
+
if (options.dateType === 'stringLocal') {
|
|
462
|
+
return { type: 'datetime', local: true }
|
|
463
|
+
}
|
|
464
|
+
return { type: 'datetime', offset: false }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (format === 'date') {
|
|
468
|
+
return { type: 'date', representation: options.dateType === 'date' ? 'date' : 'string' }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// time
|
|
472
|
+
return { type: 'time', representation: options.dateType === 'date' ? 'date' : 'string' }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Collects the shared metadata fields passed to every `createSchema` call.
|
|
477
|
+
*/
|
|
478
|
+
export function buildSchemaNode(schema: SchemaObject, name: string | null | undefined, nullable: true | undefined, defaultValue: unknown) {
|
|
479
|
+
return {
|
|
480
|
+
name,
|
|
481
|
+
nullable,
|
|
482
|
+
title: schema.title,
|
|
483
|
+
description: schema.description,
|
|
484
|
+
deprecated: schema.deprecated,
|
|
485
|
+
readOnly: schema.readOnly,
|
|
486
|
+
writeOnly: schema.writeOnly,
|
|
487
|
+
default: defaultValue,
|
|
488
|
+
example: schema.example,
|
|
489
|
+
} as const
|
|
490
|
+
}
|