@kubb/adapter-oas 5.0.0-beta.63 → 5.0.0-beta.65
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 +86 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +87 -195
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
- package/src/adapter.bench.ts +0 -60
- package/src/adapter.ts +0 -207
- package/src/bundler.ts +0 -71
- package/src/constants.ts +0 -135
- package/src/dedupe.ts +0 -237
- package/src/dialect.ts +0 -42
- package/src/discriminator.ts +0 -132
- package/src/factory.ts +0 -180
- package/src/guards.ts +0 -68
- package/src/index.ts +0 -15
- package/src/mime.ts +0 -21
- package/src/operation.ts +0 -194
- package/src/parser.ts +0 -1129
- package/src/refs.ts +0 -86
- package/src/resolvers.ts +0 -590
- package/src/schemaDiagnostics.ts +0 -76
- package/src/stream.ts +0 -221
- package/src/types.ts +0 -235
- /package/dist/{chunk-C0LytTxp.js → rolldown-runtime-C0LytTxp.js} +0 -0
package/src/refs.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { type Diagnostic, Diagnostics } from '@kubb/core'
|
|
2
|
-
import { isReference } from './guards.ts'
|
|
3
|
-
import type { Document } from './types.ts'
|
|
4
|
-
|
|
5
|
-
const _refCache = new WeakMap<Document, Map<string, unknown>>()
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Resolves a local JSON pointer reference from a document.
|
|
9
|
-
*
|
|
10
|
-
* Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
|
|
11
|
-
* resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
|
|
12
|
-
* build there is no sink to collect it, so it throws instead.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```ts
|
|
16
|
-
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
export function resolveRef<T = unknown>(document: Document, $ref: string): T | null {
|
|
20
|
-
const origRef = $ref
|
|
21
|
-
$ref = $ref.trim()
|
|
22
|
-
if ($ref === '') {
|
|
23
|
-
return null
|
|
24
|
-
}
|
|
25
|
-
if (!$ref.startsWith('#')) return null
|
|
26
|
-
$ref = globalThis.decodeURIComponent($ref.substring(1))
|
|
27
|
-
|
|
28
|
-
let docCache = _refCache.get(document)
|
|
29
|
-
if (!docCache) {
|
|
30
|
-
docCache = new Map()
|
|
31
|
-
_refCache.set(document, docCache)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (docCache.has($ref)) {
|
|
35
|
-
return docCache.get($ref) as T
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const current = $ref
|
|
39
|
-
.split('/')
|
|
40
|
-
.filter(Boolean)
|
|
41
|
-
.reduce((obj: unknown, key: string) => (obj as Record<string, unknown>)?.[key], document as unknown)
|
|
42
|
-
|
|
43
|
-
if (!current) {
|
|
44
|
-
const diagnostic: Diagnostic = {
|
|
45
|
-
code: Diagnostics.code.refNotFound,
|
|
46
|
-
severity: 'error',
|
|
47
|
-
message: `Could not find a definition for ${origRef}.`,
|
|
48
|
-
help: 'Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.',
|
|
49
|
-
location: { kind: 'schema', pointer: origRef, ref: origRef },
|
|
50
|
-
}
|
|
51
|
-
// Report the unresolved ref into the active build and resolve to null, like any
|
|
52
|
-
// other unresolvable ref. The build collects it and keeps going. Outside a build there is no
|
|
53
|
-
// sink, so throw rather than silently returning null.
|
|
54
|
-
if (!Diagnostics.report(diagnostic)) {
|
|
55
|
-
throw new Diagnostics.Error(diagnostic)
|
|
56
|
-
}
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
docCache.set($ref, current)
|
|
61
|
-
return current as T
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Resolves a `$ref` object while preserving the original `$ref` field on the result.
|
|
66
|
-
*
|
|
67
|
-
* Useful for parser flows that need both dereferenced fields and pointer
|
|
68
|
-
* identity (for naming/import purposes). Non-reference values are returned as-is.
|
|
69
|
-
*
|
|
70
|
-
* @example
|
|
71
|
-
* ```ts
|
|
72
|
-
* dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
|
|
73
|
-
* // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
export function dereferenceWithRef<T = unknown>(document: Document, schema?: T): T {
|
|
77
|
-
if (isReference(schema)) {
|
|
78
|
-
return {
|
|
79
|
-
...schema,
|
|
80
|
-
...resolveRef(document, schema.$ref),
|
|
81
|
-
$ref: schema.$ref,
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return schema as T
|
|
86
|
-
}
|
package/src/resolvers.ts
DELETED
|
@@ -1,590 +0,0 @@
|
|
|
1
|
-
import { pascalCase } from '@internals/utils'
|
|
2
|
-
import { Diagnostics } from '@kubb/core'
|
|
3
|
-
import type { ast } from '@kubb/core'
|
|
4
|
-
import { formatMap, SCHEMA_REF_PREFIX, specialCasedFormats, structuralKeys } from './constants.ts'
|
|
5
|
-
import { isReference } from './guards.ts'
|
|
6
|
-
import { isJsonMimeType } from './mime.ts'
|
|
7
|
-
import { getRequestContent, getResponseByStatusCode } from './operation.ts'
|
|
8
|
-
import { dereferenceWithRef, resolveRef } from './refs.ts'
|
|
9
|
-
import type { ContentType, Document, MediaTypeObject, Operation, ParameterObject, ResponseObject, SchemaObject, ServerObject } from './types.ts'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
13
|
-
* Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
|
|
14
|
-
* Throws if an override value is not in the variable's `enum` list.
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```ts
|
|
18
|
-
* resolveServerUrl(
|
|
19
|
-
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
20
|
-
* { env: 'prod' },
|
|
21
|
-
* )
|
|
22
|
-
* // 'https://prod.api.example.com'
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
export function resolveServerUrl(server: ServerObject, overrides?: Record<string, string>): string {
|
|
26
|
-
if (!server.variables) {
|
|
27
|
-
return server.url
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let url = server.url
|
|
31
|
-
for (const [key, variable] of Object.entries(server.variables)) {
|
|
32
|
-
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : undefined)
|
|
33
|
-
if (value === undefined) {
|
|
34
|
-
continue
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) {
|
|
38
|
-
throw new Diagnostics.Error({
|
|
39
|
-
code: Diagnostics.code.invalidServerVariable,
|
|
40
|
-
severity: 'error',
|
|
41
|
-
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(', ')}.`,
|
|
42
|
-
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
43
|
-
location: { kind: 'document', pointer: '#/servers' },
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
url = url.replaceAll(`{${key}}`, value)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return url
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
|
|
55
|
-
* Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
|
|
56
|
-
*/
|
|
57
|
-
export function getSchemaType(format: string): ast.SchemaType | null {
|
|
58
|
-
return formatMap[format as keyof typeof formatMap] ?? null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
|
|
63
|
-
* `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
|
|
64
|
-
* the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
|
|
65
|
-
* diagnostic in step with the parser as `formatMap` grows.
|
|
66
|
-
*/
|
|
67
|
-
export function isHandledFormat(format: string): boolean {
|
|
68
|
-
return getSchemaType(format) !== null || specialCasedFormats.has(format)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
73
|
-
* Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
|
|
74
|
-
*/
|
|
75
|
-
export function getPrimitiveType(type: string | undefined): ast.PrimitiveSchemaType {
|
|
76
|
-
if (type === 'number' || type === 'integer' || type === 'bigint') return type
|
|
77
|
-
if (type === 'boolean') return 'boolean'
|
|
78
|
-
|
|
79
|
-
return 'string'
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export type OperationsOptions = {
|
|
83
|
-
contentType?: ContentType
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Returns all parameters for an operation, merging path-level and operation-level entries.
|
|
88
|
-
* Operation-level parameters override path-level ones with the same `in:name` key.
|
|
89
|
-
* Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
|
|
90
|
-
*
|
|
91
|
-
* @example
|
|
92
|
-
* ```ts
|
|
93
|
-
* getParameters(document, operation)
|
|
94
|
-
* // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
|
|
95
|
-
* ```
|
|
96
|
-
*/
|
|
97
|
-
export function getParameters(document: Document, operation: Operation): Array<ParameterObject> {
|
|
98
|
-
const resolveParams = (params: Array<unknown>): Array<ParameterObject> =>
|
|
99
|
-
params.map((p) => dereferenceWithRef(document, p)).filter((p): p is ParameterObject => !!p && typeof p === 'object' && 'in' in p && 'name' in p)
|
|
100
|
-
|
|
101
|
-
const operationParams = resolveParams(operation.schema?.parameters || [])
|
|
102
|
-
const pathItem = document.paths?.[operation.path]
|
|
103
|
-
const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : [])
|
|
104
|
-
|
|
105
|
-
const paramMap = new Map<string, ParameterObject>()
|
|
106
|
-
for (const p of pathLevelParams) {
|
|
107
|
-
if (p.name && p.in) {
|
|
108
|
-
paramMap.set(`${p.in}:${p.name}`, p)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
for (const p of operationParams) {
|
|
112
|
-
if (p.name && p.in) {
|
|
113
|
-
paramMap.set(`${p.in}:${p.name}`, p)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return Array.from(paramMap.values())
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getResponseBody(responseBody: boolean | ResponseObject, contentType?: string): MediaTypeObject | false | [string, MediaTypeObject, ...Array<string>] {
|
|
121
|
-
if (!responseBody) return false
|
|
122
|
-
if (isReference(responseBody)) return false
|
|
123
|
-
|
|
124
|
-
const body = responseBody as ResponseObject
|
|
125
|
-
if (!body.content) return false
|
|
126
|
-
|
|
127
|
-
if (contentType) {
|
|
128
|
-
if (!(contentType in body.content)) return false
|
|
129
|
-
return body.content[contentType]!
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
let availableContentType: string | undefined
|
|
133
|
-
const contentTypes = Object.keys(body.content)
|
|
134
|
-
for (const mt of contentTypes) {
|
|
135
|
-
if (isJsonMimeType(mt)) {
|
|
136
|
-
availableContentType = mt
|
|
137
|
-
break
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (!availableContentType) {
|
|
142
|
-
availableContentType = contentTypes[0]
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (availableContentType) {
|
|
146
|
-
return [availableContentType, body.content[availableContentType]!, ...(body.description ? [body.description] : [])]
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return false
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Returns the response schema for a given operation and HTTP status code.
|
|
154
|
-
*
|
|
155
|
-
* Returns an empty object `{}` when no response body schema is available.
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* ```ts
|
|
159
|
-
* getResponseSchema(document, operation, 200) // SchemaObject
|
|
160
|
-
* getResponseSchema(document, operation, '4XX') // {}
|
|
161
|
-
* ```
|
|
162
|
-
*/
|
|
163
|
-
export function getResponseSchema(document: Document, operation: Operation, statusCode: string | number, options: OperationsOptions = {}): SchemaObject {
|
|
164
|
-
if (operation.schema.responses) {
|
|
165
|
-
const responses = operation.schema.responses
|
|
166
|
-
for (const key in responses) {
|
|
167
|
-
const schema = responses[key]
|
|
168
|
-
if (schema && isReference(schema)) {
|
|
169
|
-
responses[key] = resolveRef<any>(document, schema.$ref)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const responseBody = getResponseBody(getResponseByStatusCode({ document, operation, 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 `null` 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 = getRequestContent({ document, operation, mediaType: 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 its component section source and original name. `getSchemas` uses this
|
|
224
|
-
* to resolve name collisions across sources.
|
|
225
|
-
*/
|
|
226
|
-
type SchemaWithMetadata = {
|
|
227
|
-
schema: SchemaObject
|
|
228
|
-
source: SchemaSourceMode
|
|
229
|
-
originalName: string
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export type GetSchemasOptions = {
|
|
233
|
-
contentType?: ContentType
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export type GetSchemasResult = {
|
|
237
|
-
schemas: Record<string, SchemaObject>
|
|
238
|
-
/**
|
|
239
|
-
* Maps each original component pointer (`#/components/<source>/<name>`) to the
|
|
240
|
-
* collision-resolved unique name used as the key in `schemas`.
|
|
241
|
-
*/
|
|
242
|
-
nameMapping: Map<string, string>
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Flattens a keyword-only `allOf` into its parent schema.
|
|
247
|
-
*
|
|
248
|
-
* Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
|
|
249
|
-
* (see `structuralKeys`). Outer schema values take precedence over fragment values.
|
|
250
|
-
* Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
|
|
251
|
-
*
|
|
252
|
-
* @example
|
|
253
|
-
* ```ts
|
|
254
|
-
* flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
|
|
255
|
-
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
256
|
-
* ```
|
|
257
|
-
*
|
|
258
|
-
* @example
|
|
259
|
-
* ```ts
|
|
260
|
-
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
261
|
-
* // returned unchanged, contains a $ref
|
|
262
|
-
* ```
|
|
263
|
-
*/
|
|
264
|
-
/**
|
|
265
|
-
* Returns `true` when `fragment` carries any JSON Schema keyword that makes it
|
|
266
|
-
* structurally significant on its own (see `structuralKeys`).
|
|
267
|
-
*
|
|
268
|
-
* A fragment with a structural keyword can't be safely merged into a parent schema.
|
|
269
|
-
*/
|
|
270
|
-
function hasStructuralKeywords(fragment: SchemaObject): boolean {
|
|
271
|
-
for (const key in fragment) {
|
|
272
|
-
if (structuralKeys.has(key as 'properties')) return true
|
|
273
|
-
}
|
|
274
|
-
return false
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function flattenSchema(schema: SchemaObject | null): SchemaObject | null {
|
|
278
|
-
if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null
|
|
279
|
-
|
|
280
|
-
const allOfFragments = schema.allOf as Array<SchemaObject>
|
|
281
|
-
if (allOfFragments.some((item) => isReference(item))) return schema
|
|
282
|
-
if (allOfFragments.some(hasStructuralKeywords)) return schema
|
|
283
|
-
|
|
284
|
-
const merged: SchemaObject = { ...schema }
|
|
285
|
-
delete merged.allOf
|
|
286
|
-
|
|
287
|
-
for (const fragment of allOfFragments) {
|
|
288
|
-
for (const [key, value] of Object.entries(fragment)) {
|
|
289
|
-
if (merged[key as keyof typeof merged] === undefined) {
|
|
290
|
-
merged[key as keyof typeof merged] = value
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return merged
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Extracts the inline schema from a media-type `content` map.
|
|
300
|
-
*
|
|
301
|
-
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
302
|
-
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
303
|
-
*
|
|
304
|
-
* @example
|
|
305
|
-
* ```ts
|
|
306
|
-
* extractSchemaFromContent(operation.content, 'application/json')
|
|
307
|
-
* // SchemaObject | null
|
|
308
|
-
* ```
|
|
309
|
-
*/
|
|
310
|
-
export function extractSchemaFromContent(content: Record<string, unknown> | undefined, preferredContentType?: ContentType): SchemaObject | null {
|
|
311
|
-
if (!content) return null
|
|
312
|
-
|
|
313
|
-
const firstContentType = Object.keys(content)[0] ?? 'application/json'
|
|
314
|
-
const targetContentType = preferredContentType ?? firstContentType
|
|
315
|
-
const contentSchema = content[targetContentType] as { schema?: SchemaObject } | undefined
|
|
316
|
-
const schema = contentSchema?.schema
|
|
317
|
-
|
|
318
|
-
if (schema && '$ref' in schema) return null
|
|
319
|
-
return schema ?? null
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
324
|
-
*/
|
|
325
|
-
function* collectRefs(schema: unknown): Generator<string, void, undefined> {
|
|
326
|
-
if (Array.isArray(schema)) {
|
|
327
|
-
for (const item of schema) yield* collectRefs(item)
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (schema && typeof schema === 'object') {
|
|
332
|
-
for (const key in schema) {
|
|
333
|
-
const value = (schema as Record<string, unknown>)[key]
|
|
334
|
-
if (!(key === '$ref' && typeof value === 'string')) {
|
|
335
|
-
yield* collectRefs(value)
|
|
336
|
-
continue
|
|
337
|
-
}
|
|
338
|
-
if (value.startsWith(SCHEMA_REF_PREFIX)) {
|
|
339
|
-
const name = value.slice(SCHEMA_REF_PREFIX.length)
|
|
340
|
-
if (name) yield name
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
348
|
-
*
|
|
349
|
-
* Referenced schemas appear before the schemas that depend on them, so code generators
|
|
350
|
-
* can emit types in the correct order. Cycles are silently skipped.
|
|
351
|
-
*
|
|
352
|
-
* @example
|
|
353
|
-
* ```ts
|
|
354
|
-
* const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
|
|
355
|
-
* // Pet appears before Order when Order.$ref points at Pet
|
|
356
|
-
* ```
|
|
357
|
-
*/
|
|
358
|
-
export function sortSchemas(schemas: Record<string, SchemaObject>): Record<string, SchemaObject> {
|
|
359
|
-
const deps = new Map<string, Array<string>>()
|
|
360
|
-
|
|
361
|
-
for (const [name, schema] of Object.entries(schemas)) {
|
|
362
|
-
deps.set(name, [...new Set(collectRefs(schema))])
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const sorted: Array<string> = []
|
|
366
|
-
const visited = new Set<string>()
|
|
367
|
-
|
|
368
|
-
function visit(name: string, stack: Set<string>) {
|
|
369
|
-
if (visited.has(name) || stack.has(name)) return
|
|
370
|
-
stack.add(name)
|
|
371
|
-
for (const child of deps.get(name) ?? []) {
|
|
372
|
-
if (deps.has(child)) visit(child, stack)
|
|
373
|
-
}
|
|
374
|
-
stack.delete(name)
|
|
375
|
-
visited.add(name)
|
|
376
|
-
sorted.push(name)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
for (const name of Object.keys(schemas)) {
|
|
380
|
-
visit(name, new Set())
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const result: Record<string, SchemaObject> = {}
|
|
384
|
-
for (const name of sorted) result[name] = schemas[name]!
|
|
385
|
-
return result
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const semanticSuffixes: Record<SchemaSourceMode, string> = {
|
|
389
|
-
schemas: 'Schema',
|
|
390
|
-
responses: 'Response',
|
|
391
|
-
requestBodies: 'Request',
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function getSemanticSuffix(source: SchemaSourceMode): string {
|
|
395
|
-
return semanticSuffixes[source]
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function resolveSchemaRef(document: Document, schema: SchemaObject): SchemaObject {
|
|
399
|
-
if (!isReference(schema)) return schema
|
|
400
|
-
const resolved = resolveRef<SchemaObject>(document, schema.$ref)
|
|
401
|
-
return resolved && !isReference(resolved) ? resolved : schema
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Collects component schemas from one or more sources and resolves name collisions.
|
|
406
|
-
*
|
|
407
|
-
* Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
|
|
408
|
-
* topologically sorted by `$ref` dependency so generators emit types in the correct order.
|
|
409
|
-
*
|
|
410
|
-
* When two or more schemas normalize to the same PascalCase name:
|
|
411
|
-
* - Same source → numeric suffix (`2`, `3`, …).
|
|
412
|
-
* - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
|
|
413
|
-
*
|
|
414
|
-
* @example
|
|
415
|
-
* ```ts
|
|
416
|
-
* const { schemas, nameMapping } = getSchemas(document, { contentType: 'application/json' })
|
|
417
|
-
* ```
|
|
418
|
-
*/
|
|
419
|
-
export function getSchemas(document: Document, { contentType }: GetSchemasOptions): GetSchemasResult {
|
|
420
|
-
const components = document.components
|
|
421
|
-
|
|
422
|
-
const candidates: Array<SchemaWithMetadata> = [
|
|
423
|
-
...Object.entries((components?.schemas as Record<string, SchemaObject>) ?? {}).map(([name, schema]) => ({
|
|
424
|
-
schema: resolveSchemaRef(document, schema),
|
|
425
|
-
source: 'schemas' as const,
|
|
426
|
-
originalName: name,
|
|
427
|
-
})),
|
|
428
|
-
...(['responses', 'requestBodies'] as const).flatMap((source) =>
|
|
429
|
-
Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
430
|
-
const schema = extractSchemaFromContent((item as { content?: Record<string, unknown> }).content, contentType)
|
|
431
|
-
return schema
|
|
432
|
-
? [
|
|
433
|
-
{
|
|
434
|
-
schema: resolveSchemaRef(document, schema),
|
|
435
|
-
source,
|
|
436
|
-
originalName: name,
|
|
437
|
-
},
|
|
438
|
-
]
|
|
439
|
-
: []
|
|
440
|
-
}),
|
|
441
|
-
),
|
|
442
|
-
]
|
|
443
|
-
|
|
444
|
-
const normalizedNames = new Map<string, Array<SchemaWithMetadata>>()
|
|
445
|
-
for (const item of candidates) {
|
|
446
|
-
const key = pascalCase(item.originalName)
|
|
447
|
-
const bucket = normalizedNames.get(key) ?? []
|
|
448
|
-
bucket.push(item)
|
|
449
|
-
normalizedNames.set(key, bucket)
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const schemas: Record<string, SchemaObject> = {}
|
|
453
|
-
const nameMapping = new Map<string, string>()
|
|
454
|
-
|
|
455
|
-
for (const [, items] of normalizedNames) {
|
|
456
|
-
const isSingle = items.length === 1
|
|
457
|
-
let hasMultipleSources = false
|
|
458
|
-
if (!isSingle) {
|
|
459
|
-
const firstSource = items[0]!.source
|
|
460
|
-
for (let i = 1; i < items.length; i++) {
|
|
461
|
-
if (items[i]!.source !== firstSource) {
|
|
462
|
-
hasMultipleSources = true
|
|
463
|
-
break
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
items.forEach((item, index) => {
|
|
469
|
-
const suffix = isSingle ? '' : hasMultipleSources ? getSemanticSuffix(item.source) : index === 0 ? '' : String(index + 1)
|
|
470
|
-
const uniqueName = item.originalName + suffix
|
|
471
|
-
schemas[uniqueName] = item.schema
|
|
472
|
-
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName)
|
|
473
|
-
})
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return { schemas: sortSchemas(schemas), nameMapping }
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
481
|
-
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
482
|
-
*/
|
|
483
|
-
export function getDateType(
|
|
484
|
-
options: ast.ParserOptions,
|
|
485
|
-
format: 'date-time' | 'date' | 'time',
|
|
486
|
-
): { type: 'datetime'; offset?: boolean; local?: boolean } | { type: 'date' | 'time'; representation: 'date' | 'string' } | null {
|
|
487
|
-
if (!options.dateType) {
|
|
488
|
-
return null
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (format === 'date-time') {
|
|
492
|
-
if (options.dateType === 'date') {
|
|
493
|
-
return { type: 'date', representation: 'date' }
|
|
494
|
-
}
|
|
495
|
-
if (options.dateType === 'stringOffset') {
|
|
496
|
-
return { type: 'datetime', offset: true }
|
|
497
|
-
}
|
|
498
|
-
if (options.dateType === 'stringLocal') {
|
|
499
|
-
return { type: 'datetime', local: true }
|
|
500
|
-
}
|
|
501
|
-
return { type: 'datetime', offset: false }
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (format === 'date') {
|
|
505
|
-
return {
|
|
506
|
-
type: 'date',
|
|
507
|
-
representation: options.dateType === 'date' ? 'date' : 'string',
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// time
|
|
512
|
-
return {
|
|
513
|
-
type: 'time',
|
|
514
|
-
representation: options.dateType === 'date' ? 'date' : 'string',
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Collects the shared metadata fields passed to every `createSchema` call.
|
|
520
|
-
*/
|
|
521
|
-
export function buildSchemaNode(schema: SchemaObject, name: string | null | undefined, nullable: true | undefined, defaultValue: unknown) {
|
|
522
|
-
return {
|
|
523
|
-
name,
|
|
524
|
-
nullable,
|
|
525
|
-
title: schema.title,
|
|
526
|
-
description: schema.description,
|
|
527
|
-
deprecated: schema.deprecated,
|
|
528
|
-
readOnly: schema.readOnly,
|
|
529
|
-
writeOnly: schema.writeOnly,
|
|
530
|
-
default: defaultValue,
|
|
531
|
-
example: schema.example,
|
|
532
|
-
format: schema.format,
|
|
533
|
-
} as const
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Returns all request body content type keys for an operation.
|
|
538
|
-
*
|
|
539
|
-
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
540
|
-
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
541
|
-
* available content types even for referenced bodies.
|
|
542
|
-
*
|
|
543
|
-
* @example
|
|
544
|
-
* ```ts
|
|
545
|
-
* getRequestBodyContentTypes(document, operation)
|
|
546
|
-
* // ['application/json', 'multipart/form-data']
|
|
547
|
-
* ```
|
|
548
|
-
*/
|
|
549
|
-
export function getRequestBodyContentTypes(document: Document, operation: Operation): Array<string> {
|
|
550
|
-
if (operation.schema.requestBody) {
|
|
551
|
-
operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody)
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const body = operation.schema.requestBody as { content?: Record<string, unknown> } | undefined
|
|
555
|
-
if (!body) return []
|
|
556
|
-
|
|
557
|
-
// dereferenceWithRef keeps $ref but spreads all resolved fields (including `content`).
|
|
558
|
-
// Do not bail out on isReference, the content is already present on the merged object.
|
|
559
|
-
return body.content ? Object.keys(body.content) : []
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Returns all response content type keys for an operation at a given status code.
|
|
564
|
-
*
|
|
565
|
-
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
566
|
-
* so the returned list reflects the available content types even for referenced responses.
|
|
567
|
-
*
|
|
568
|
-
* @example
|
|
569
|
-
* ```ts
|
|
570
|
-
* getResponseBodyContentTypes(document, operation, 200)
|
|
571
|
-
* // ['application/json', 'application/xml']
|
|
572
|
-
* ```
|
|
573
|
-
*/
|
|
574
|
-
export function getResponseBodyContentTypes(document: Document, operation: Operation, statusCode: string | number): Array<string> {
|
|
575
|
-
if (operation.schema.responses) {
|
|
576
|
-
const responses = operation.schema.responses
|
|
577
|
-
for (const key in responses) {
|
|
578
|
-
const schema = responses[key]
|
|
579
|
-
if (schema && isReference(schema)) {
|
|
580
|
-
responses[key] = resolveRef<any>(document, schema.$ref)
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const responseObj = getResponseByStatusCode({ document, operation, statusCode })
|
|
586
|
-
if (!responseObj || typeof responseObj !== 'object' || isReference(responseObj)) return []
|
|
587
|
-
|
|
588
|
-
const body = responseObj as { content?: Record<string, unknown> }
|
|
589
|
-
return body.content ? Object.keys(body.content) : []
|
|
590
|
-
}
|