@kubb/adapter-oas 5.0.0-alpha.9 → 5.0.0-beta.2
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 +1299 -1242
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +522 -144
- package/dist/index.js +1290 -1236
- package/dist/index.js.map +1 -1
- package/package.json +32 -35
- package/src/adapter.ts +82 -78
- package/src/constants.ts +77 -69
- package/src/discriminator.ts +108 -0
- package/src/factory.ts +165 -0
- package/src/guards.ts +68 -0
- package/src/index.ts +17 -0
- package/src/parser.ts +565 -655
- package/src/refs.ts +59 -0
- package/src/resolvers.ts +544 -0
- package/src/types.ts +172 -59
- package/src/oas/Oas.ts +0 -623
- package/src/oas/resolveServerUrl.ts +0 -47
- package/src/oas/types.ts +0 -71
- package/src/oas/utils.ts +0 -402
- package/src/utils.ts +0 -168
package/src/oas/Oas.ts
DELETED
|
@@ -1,623 +0,0 @@
|
|
|
1
|
-
import jsonpointer from 'jsonpointer'
|
|
2
|
-
import BaseOas from 'oas'
|
|
3
|
-
import type { ParameterObject } from 'oas/types'
|
|
4
|
-
import { matchesMimeType } from 'oas/utils'
|
|
5
|
-
import type { contentType, DiscriminatorObject, Document, MediaTypeObject, Operation, ReferenceObject, ResponseObject, SchemaObject } from './types.ts'
|
|
6
|
-
import {
|
|
7
|
-
extractSchemaFromContent,
|
|
8
|
-
flattenSchema,
|
|
9
|
-
isDiscriminator,
|
|
10
|
-
isReference,
|
|
11
|
-
legacyResolve,
|
|
12
|
-
resolveCollisions,
|
|
13
|
-
type SchemaWithMetadata,
|
|
14
|
-
sortSchemas,
|
|
15
|
-
validate,
|
|
16
|
-
} from './utils.ts'
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Prefix used to create synthetic `$ref` values for anonymous (inline) discriminator schemas.
|
|
20
|
-
* The suffix is the schema index within the discriminator's `oneOf`/`anyOf` array.
|
|
21
|
-
* @example `#kubb-inline-0`
|
|
22
|
-
*/
|
|
23
|
-
const KUBB_INLINE_REF_PREFIX = '#kubb-inline-'
|
|
24
|
-
|
|
25
|
-
type OasOptions = {
|
|
26
|
-
contentType?: contentType
|
|
27
|
-
discriminator?: 'strict' | 'inherit'
|
|
28
|
-
/**
|
|
29
|
-
* Resolve name collisions when schemas from different components share the same name (case-insensitive).
|
|
30
|
-
* @default false
|
|
31
|
-
*/
|
|
32
|
-
collisionDetection?: boolean
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class Oas extends BaseOas {
|
|
36
|
-
#options: OasOptions = {
|
|
37
|
-
discriminator: 'strict',
|
|
38
|
-
}
|
|
39
|
-
document: Document
|
|
40
|
-
|
|
41
|
-
constructor(document: Document) {
|
|
42
|
-
super(document, undefined)
|
|
43
|
-
|
|
44
|
-
this.document = document
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
setOptions(options: OasOptions) {
|
|
48
|
-
this.#options = {
|
|
49
|
-
...this.#options,
|
|
50
|
-
...options,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (this.#options.discriminator === 'inherit') {
|
|
54
|
-
this.#applyDiscriminatorInheritance()
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
get options(): OasOptions {
|
|
59
|
-
return this.#options
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
get<T = unknown>($ref: string): T | null {
|
|
63
|
-
const origRef = $ref
|
|
64
|
-
$ref = $ref.trim()
|
|
65
|
-
if ($ref === '') {
|
|
66
|
-
return null
|
|
67
|
-
}
|
|
68
|
-
if ($ref.startsWith('#')) {
|
|
69
|
-
$ref = globalThis.decodeURIComponent($ref.substring(1))
|
|
70
|
-
} else {
|
|
71
|
-
return null
|
|
72
|
-
}
|
|
73
|
-
const current = jsonpointer.get(this.api, $ref)
|
|
74
|
-
|
|
75
|
-
if (!current) {
|
|
76
|
-
throw new Error(`Could not find a definition for ${origRef}.`)
|
|
77
|
-
}
|
|
78
|
-
return current as T
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
getKey($ref: string) {
|
|
82
|
-
const key = $ref.split('/').pop()
|
|
83
|
-
return key === '' ? undefined : key
|
|
84
|
-
}
|
|
85
|
-
set($ref: string, value: unknown) {
|
|
86
|
-
$ref = $ref.trim()
|
|
87
|
-
if ($ref === '') {
|
|
88
|
-
return false
|
|
89
|
-
}
|
|
90
|
-
if ($ref.startsWith('#')) {
|
|
91
|
-
$ref = globalThis.decodeURIComponent($ref.substring(1))
|
|
92
|
-
|
|
93
|
-
jsonpointer.set(this.api, $ref, value)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
#setDiscriminator(schema: SchemaObject & { discriminator: DiscriminatorObject }): void {
|
|
98
|
-
const { mapping = {}, propertyName } = schema.discriminator
|
|
99
|
-
|
|
100
|
-
if (this.#options.discriminator === 'inherit') {
|
|
101
|
-
Object.entries(mapping).forEach(([mappingKey, mappingValue]) => {
|
|
102
|
-
if (mappingValue) {
|
|
103
|
-
const childSchema = this.get<any>(mappingValue)
|
|
104
|
-
if (!childSchema) {
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!childSchema.properties) {
|
|
109
|
-
childSchema.properties = {}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const property = childSchema.properties[propertyName] as SchemaObject
|
|
113
|
-
|
|
114
|
-
if (childSchema.properties) {
|
|
115
|
-
childSchema.properties[propertyName] = {
|
|
116
|
-
...((childSchema.properties ? childSchema.properties[propertyName] : {}) as SchemaObject),
|
|
117
|
-
enum: [...(property?.enum?.filter((value) => value !== mappingKey) ?? []), mappingKey],
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
childSchema.required =
|
|
121
|
-
typeof childSchema.required === 'boolean' ? childSchema.required : [...new Set([...(childSchema.required ?? []), propertyName])]
|
|
122
|
-
|
|
123
|
-
this.set(mappingValue, childSchema)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
getDiscriminator(schema: SchemaObject | null): DiscriminatorObject | null {
|
|
131
|
-
if (!isDiscriminator(schema) || !schema) {
|
|
132
|
-
return null
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const { mapping = {}, propertyName } = schema.discriminator
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Helper to extract discriminator value from a schema.
|
|
139
|
-
* Checks in order:
|
|
140
|
-
* 1. Extension property matching propertyName (e.g., x-linode-ref-name)
|
|
141
|
-
* 2. Property with const value
|
|
142
|
-
* 3. Property with single enum value
|
|
143
|
-
* 4. Title as fallback
|
|
144
|
-
*/
|
|
145
|
-
const getDiscriminatorValue = (schema: SchemaObject | null): string | null => {
|
|
146
|
-
if (!schema) {
|
|
147
|
-
return null
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Check extension properties first (e.g., x-linode-ref-name)
|
|
151
|
-
// Only check if propertyName starts with 'x-' to avoid conflicts with standard properties
|
|
152
|
-
if (propertyName.startsWith('x-')) {
|
|
153
|
-
const extensionValue = (schema as Record<string, unknown>)[propertyName]
|
|
154
|
-
if (extensionValue && typeof extensionValue === 'string') {
|
|
155
|
-
return extensionValue
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Check if property has const value
|
|
160
|
-
const propertySchema = schema.properties?.[propertyName] as SchemaObject
|
|
161
|
-
if (propertySchema && 'const' in propertySchema && propertySchema.const !== undefined) {
|
|
162
|
-
return String(propertySchema.const)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Check if property has single enum value
|
|
166
|
-
if (propertySchema && propertySchema.enum?.length === 1) {
|
|
167
|
-
return String(propertySchema.enum[0])
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Fallback to title if available
|
|
171
|
-
return schema.title || null
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Process oneOf/anyOf items to build mapping.
|
|
176
|
-
* Handles both $ref and inline schemas.
|
|
177
|
-
*/
|
|
178
|
-
const processSchemas = (schemas: Array<SchemaObject>, existingMapping: Record<string, string>) => {
|
|
179
|
-
schemas.forEach((schemaItem, index) => {
|
|
180
|
-
if (isReference(schemaItem)) {
|
|
181
|
-
// Handle $ref case
|
|
182
|
-
const key = this.getKey(schemaItem.$ref)
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
const refSchema = this.get<SchemaObject>(schemaItem.$ref)
|
|
186
|
-
const discriminatorValue = getDiscriminatorValue(refSchema)
|
|
187
|
-
const canAdd = key && !Object.values(existingMapping).includes(schemaItem.$ref)
|
|
188
|
-
|
|
189
|
-
if (canAdd && discriminatorValue) {
|
|
190
|
-
existingMapping[discriminatorValue] = schemaItem.$ref
|
|
191
|
-
} else if (canAdd) {
|
|
192
|
-
existingMapping[key] = schemaItem.$ref
|
|
193
|
-
}
|
|
194
|
-
} catch (_error) {
|
|
195
|
-
// If we can't resolve the reference, skip it and use the key as fallback
|
|
196
|
-
if (key && !Object.values(existingMapping).includes(schemaItem.$ref)) {
|
|
197
|
-
existingMapping[key] = schemaItem.$ref
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
// Handle inline schema case
|
|
202
|
-
const inlineSchema = schemaItem as SchemaObject
|
|
203
|
-
const discriminatorValue = getDiscriminatorValue(inlineSchema)
|
|
204
|
-
|
|
205
|
-
if (discriminatorValue) {
|
|
206
|
-
// Create a synthetic ref for inline schemas using index
|
|
207
|
-
// The value points to the inline schema itself via a special marker
|
|
208
|
-
existingMapping[discriminatorValue] = `${KUBB_INLINE_REF_PREFIX}${index}`
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Process oneOf schemas
|
|
215
|
-
if (schema.oneOf) {
|
|
216
|
-
processSchemas(schema.oneOf as Array<SchemaObject>, mapping)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Process anyOf schemas
|
|
220
|
-
if (schema.anyOf) {
|
|
221
|
-
processSchemas(schema.anyOf as Array<SchemaObject>, mapping)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
...schema.discriminator,
|
|
226
|
-
mapping,
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// TODO add better typing
|
|
231
|
-
dereferenceWithRef<T = unknown>(schema?: T): T {
|
|
232
|
-
if (isReference(schema)) {
|
|
233
|
-
return {
|
|
234
|
-
...schema,
|
|
235
|
-
...this.get(schema.$ref),
|
|
236
|
-
$ref: schema.$ref,
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return schema as T
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
#applyDiscriminatorInheritance() {
|
|
244
|
-
const components = this.api.components
|
|
245
|
-
if (!components?.schemas) {
|
|
246
|
-
return
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const visited = new WeakSet<object>()
|
|
250
|
-
const enqueue = (value: unknown) => {
|
|
251
|
-
if (!value) {
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (Array.isArray(value)) {
|
|
256
|
-
for (const item of value) {
|
|
257
|
-
enqueue(item)
|
|
258
|
-
}
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (typeof value === 'object') {
|
|
263
|
-
visit(value as SchemaObject)
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const visit = (schema?: SchemaObject | ReferenceObject | null) => {
|
|
268
|
-
if (!schema || typeof schema !== 'object') {
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (isReference(schema)) {
|
|
273
|
-
visit(this.get(schema.$ref) as SchemaObject)
|
|
274
|
-
return
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const schemaObject = schema as SchemaObject
|
|
278
|
-
|
|
279
|
-
if (visited.has(schemaObject as object)) {
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
visited.add(schemaObject as object)
|
|
284
|
-
|
|
285
|
-
if (isDiscriminator(schemaObject)) {
|
|
286
|
-
this.#setDiscriminator(schemaObject)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if ('allOf' in schemaObject) {
|
|
290
|
-
enqueue(schemaObject.allOf)
|
|
291
|
-
}
|
|
292
|
-
if ('oneOf' in schemaObject) {
|
|
293
|
-
enqueue(schemaObject.oneOf)
|
|
294
|
-
}
|
|
295
|
-
if ('anyOf' in schemaObject) {
|
|
296
|
-
enqueue(schemaObject.anyOf)
|
|
297
|
-
}
|
|
298
|
-
if ('not' in schemaObject) {
|
|
299
|
-
enqueue(schemaObject.not)
|
|
300
|
-
}
|
|
301
|
-
if ('items' in schemaObject) {
|
|
302
|
-
enqueue(schemaObject.items)
|
|
303
|
-
}
|
|
304
|
-
if ('prefixItems' in schemaObject) {
|
|
305
|
-
enqueue(schemaObject.prefixItems)
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (schemaObject.properties) {
|
|
309
|
-
enqueue(Object.values(schemaObject.properties))
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (schemaObject.additionalProperties && typeof schemaObject.additionalProperties === 'object') {
|
|
313
|
-
enqueue(schemaObject.additionalProperties)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
for (const schema of Object.values(components.schemas)) {
|
|
318
|
-
visit(schema as SchemaObject)
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Oas does not have a getResponseBody(contentType)
|
|
324
|
-
*/
|
|
325
|
-
#getResponseBodyFactory(responseBody: boolean | ResponseObject): (contentType?: string) => MediaTypeObject | false | [string, MediaTypeObject, ...string[]] {
|
|
326
|
-
function hasResponseBody(res = responseBody): res is ResponseObject {
|
|
327
|
-
return !!res
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return (contentType) => {
|
|
331
|
-
if (!hasResponseBody(responseBody)) {
|
|
332
|
-
return false
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (isReference(responseBody)) {
|
|
336
|
-
// If the request body is still a `$ref` pointer we should return false because this library
|
|
337
|
-
// assumes that you've run dereferencing beforehand.
|
|
338
|
-
return false
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (!responseBody.content) {
|
|
342
|
-
return false
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (contentType) {
|
|
346
|
-
if (!(contentType in responseBody.content)) {
|
|
347
|
-
return false
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return responseBody.content[contentType]!
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Since no media type was supplied we need to find either the first JSON-like media type that
|
|
354
|
-
// we've got, or the first available of anything else if no JSON-like media types are present.
|
|
355
|
-
let availableContentType: string | undefined
|
|
356
|
-
const contentTypes = Object.keys(responseBody.content)
|
|
357
|
-
contentTypes.forEach((mt: string) => {
|
|
358
|
-
if (!availableContentType && matchesMimeType.json(mt)) {
|
|
359
|
-
availableContentType = mt
|
|
360
|
-
}
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
if (!availableContentType) {
|
|
364
|
-
contentTypes.forEach((mt: string) => {
|
|
365
|
-
if (!availableContentType) {
|
|
366
|
-
availableContentType = mt
|
|
367
|
-
}
|
|
368
|
-
})
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (availableContentType) {
|
|
372
|
-
return [availableContentType, responseBody.content[availableContentType]!, ...(responseBody.description ? [responseBody.description] : [])]
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return false
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
getResponseSchema(operation: Operation, statusCode: string | number): SchemaObject {
|
|
380
|
-
if (operation.schema.responses) {
|
|
381
|
-
Object.keys(operation.schema.responses).forEach((key) => {
|
|
382
|
-
const schema = operation.schema.responses![key]
|
|
383
|
-
const $ref = isReference(schema) ? schema.$ref : undefined
|
|
384
|
-
|
|
385
|
-
if (schema && $ref) {
|
|
386
|
-
operation.schema.responses![key] = this.get<any>($ref)
|
|
387
|
-
}
|
|
388
|
-
})
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const getResponseBody = this.#getResponseBodyFactory(operation.getResponseByStatusCode(statusCode))
|
|
392
|
-
|
|
393
|
-
const { contentType } = this.#options
|
|
394
|
-
const responseBody = getResponseBody(contentType)
|
|
395
|
-
|
|
396
|
-
if (responseBody === false) {
|
|
397
|
-
// return empty object because response will always be defined(request does not need a body)
|
|
398
|
-
return {}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const schema = Array.isArray(responseBody) ? responseBody[1].schema : responseBody.schema
|
|
402
|
-
|
|
403
|
-
if (!schema) {
|
|
404
|
-
// return empty object because response will always be defined(request does not need a body)
|
|
405
|
-
|
|
406
|
-
return {}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return this.dereferenceWithRef(schema)
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
getRequestSchema(operation: Operation): SchemaObject | undefined {
|
|
413
|
-
const { contentType } = this.#options
|
|
414
|
-
|
|
415
|
-
if (operation.schema.requestBody) {
|
|
416
|
-
operation.schema.requestBody = this.dereferenceWithRef(operation.schema.requestBody)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const requestBody = operation.getRequestBody(contentType)
|
|
420
|
-
|
|
421
|
-
if (requestBody === false) {
|
|
422
|
-
return undefined
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema
|
|
426
|
-
|
|
427
|
-
if (!schema) {
|
|
428
|
-
return undefined
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
return this.dereferenceWithRef(schema)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Returns all resolved parameters for an operation, merging path-level and operation-level
|
|
436
|
-
* parameters and deduplicating by `in:name` (operation-level takes precedence).
|
|
437
|
-
*
|
|
438
|
-
* oas v31+ filters out `$ref` parameters in `getParameters()`, so this method accesses the
|
|
439
|
-
* raw `operation.schema.parameters` and path-item parameters directly and resolves `$ref`
|
|
440
|
-
* pointers via `dereferenceWithRef` to preserve backward compatibility.
|
|
441
|
-
*/
|
|
442
|
-
getParameters(operation: Operation): Array<ParameterObject> {
|
|
443
|
-
const resolveParams = (params: unknown[]): Array<ParameterObject> =>
|
|
444
|
-
params.map((p) => this.dereferenceWithRef(p)).filter((p): p is ParameterObject => !!p && typeof p === 'object' && 'in' in p && 'name' in p)
|
|
445
|
-
|
|
446
|
-
const operationParams = resolveParams(operation.schema?.parameters || [])
|
|
447
|
-
const pathItem = this.api?.paths?.[operation.path]
|
|
448
|
-
const pathLevelParams = resolveParams(pathItem && !isReference(pathItem) && pathItem.parameters ? pathItem.parameters : [])
|
|
449
|
-
|
|
450
|
-
// Deduplicate: operation-level parameters override path-level ones with the same name+in
|
|
451
|
-
const paramMap = new Map<string, ParameterObject>()
|
|
452
|
-
for (const p of pathLevelParams) {
|
|
453
|
-
if (p.name && p.in) {
|
|
454
|
-
paramMap.set(`${p.in}:${p.name}`, p)
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
for (const p of operationParams) {
|
|
458
|
-
if (p.name && p.in) {
|
|
459
|
-
paramMap.set(`${p.in}:${p.name}`, p)
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return Array.from(paramMap.values())
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
getParametersSchema(operation: Operation, inKey: 'path' | 'query' | 'header'): SchemaObject | null {
|
|
467
|
-
const { contentType = operation.getContentType() } = this.#options
|
|
468
|
-
|
|
469
|
-
const params = this.getParameters(operation).filter((v) => v.in === inKey)
|
|
470
|
-
|
|
471
|
-
if (!params.length) {
|
|
472
|
-
return null
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return params.reduce(
|
|
476
|
-
(schema, pathParameters) => {
|
|
477
|
-
const property = (pathParameters.content?.[contentType]?.schema ?? (pathParameters.schema as SchemaObject)) as SchemaObject | null
|
|
478
|
-
const required =
|
|
479
|
-
typeof schema.required === 'boolean'
|
|
480
|
-
? schema.required
|
|
481
|
-
: [...(schema.required || []), pathParameters.required ? pathParameters.name : undefined].filter(Boolean)
|
|
482
|
-
|
|
483
|
-
// Handle explode=true with style=form for object with additionalProperties
|
|
484
|
-
// According to OpenAPI spec, when explode is true, object properties are flattened
|
|
485
|
-
const getDefaultStyle = (location: string): string => {
|
|
486
|
-
if (location === 'query') return 'form'
|
|
487
|
-
if (location === 'path') return 'simple'
|
|
488
|
-
return 'simple'
|
|
489
|
-
}
|
|
490
|
-
const style = pathParameters.style || getDefaultStyle(inKey)
|
|
491
|
-
const explode = pathParameters.explode !== undefined ? pathParameters.explode : style === 'form'
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
inKey === 'query' &&
|
|
495
|
-
style === 'form' &&
|
|
496
|
-
explode === true &&
|
|
497
|
-
property?.type === 'object' &&
|
|
498
|
-
property?.additionalProperties &&
|
|
499
|
-
!property?.properties
|
|
500
|
-
) {
|
|
501
|
-
// When explode is true for an object with only additionalProperties,
|
|
502
|
-
// flatten it to the root level by merging additionalProperties with existing schema.
|
|
503
|
-
// This preserves other query parameters while allowing dynamic key-value pairs.
|
|
504
|
-
return {
|
|
505
|
-
...schema,
|
|
506
|
-
description: pathParameters.description || schema.description,
|
|
507
|
-
deprecated: schema.deprecated,
|
|
508
|
-
example: property.example || schema.example,
|
|
509
|
-
additionalProperties: property.additionalProperties,
|
|
510
|
-
} as SchemaObject
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return {
|
|
514
|
-
...schema,
|
|
515
|
-
description: schema.description,
|
|
516
|
-
deprecated: schema.deprecated,
|
|
517
|
-
example: schema.example,
|
|
518
|
-
required,
|
|
519
|
-
properties: {
|
|
520
|
-
...schema.properties,
|
|
521
|
-
[pathParameters.name]: {
|
|
522
|
-
description: pathParameters.description,
|
|
523
|
-
...property,
|
|
524
|
-
},
|
|
525
|
-
},
|
|
526
|
-
} as SchemaObject
|
|
527
|
-
},
|
|
528
|
-
{ type: 'object', required: [], properties: {} } as SchemaObject,
|
|
529
|
-
)
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
async validate() {
|
|
533
|
-
return validate(this.api)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
flattenSchema(schema: SchemaObject | null): SchemaObject | null {
|
|
537
|
-
return flattenSchema(schema)
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Get schemas from OpenAPI components (schemas, responses, requestBodies).
|
|
542
|
-
* Returns schemas in dependency order along with name mapping for collision resolution.
|
|
543
|
-
*/
|
|
544
|
-
getSchemas(options: { contentType?: contentType; includes?: Array<'schemas' | 'responses' | 'requestBodies'>; collisionDetection?: boolean } = {}): {
|
|
545
|
-
schemas: Record<string, SchemaObject>
|
|
546
|
-
nameMapping: Map<string, string>
|
|
547
|
-
} {
|
|
548
|
-
const contentType = options.contentType ?? this.#options.contentType
|
|
549
|
-
const includes = options.includes ?? ['schemas', 'requestBodies', 'responses']
|
|
550
|
-
const shouldResolveCollisions = options.collisionDetection ?? this.#options.collisionDetection ?? false
|
|
551
|
-
|
|
552
|
-
const components = this.getDefinition().components
|
|
553
|
-
const schemasWithMeta: SchemaWithMetadata[] = []
|
|
554
|
-
|
|
555
|
-
// Collect schemas from components
|
|
556
|
-
if (includes.includes('schemas')) {
|
|
557
|
-
const componentSchemas = (components?.schemas as Record<string, SchemaObject>) || {}
|
|
558
|
-
for (const [name, schemaObject] of Object.entries(componentSchemas)) {
|
|
559
|
-
// Resolve schema if it's a $ref (can happen when the bundler deduplicates schemas
|
|
560
|
-
// referenced from multiple external files). Without this, a $ref schema would be
|
|
561
|
-
// parsed as a reference to itself, generating `z.lazy(() => schemaName)`.
|
|
562
|
-
let schema = schemaObject
|
|
563
|
-
if (isReference(schemaObject)) {
|
|
564
|
-
const resolved = this.get<SchemaObject>(schemaObject.$ref)
|
|
565
|
-
if (resolved && !isReference(resolved)) {
|
|
566
|
-
schema = resolved
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
schemasWithMeta.push({ schema, source: 'schemas', originalName: name })
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (includes.includes('responses')) {
|
|
574
|
-
const responses = components?.responses || {}
|
|
575
|
-
for (const [name, response] of Object.entries(responses)) {
|
|
576
|
-
const responseObject = response as ResponseObject
|
|
577
|
-
const schema = extractSchemaFromContent(responseObject.content, contentType)
|
|
578
|
-
if (schema) {
|
|
579
|
-
// Resolve schema if it's a $ref (can happen when the bundler deduplicates schemas
|
|
580
|
-
// referenced from multiple external files). Without this, a $ref schema would be
|
|
581
|
-
// parsed as a reference to itself, generating `z.lazy(() => schemaName)`.
|
|
582
|
-
let resolvedSchema = schema
|
|
583
|
-
if (isReference(schema)) {
|
|
584
|
-
const resolved = this.get<SchemaObject>(schema.$ref)
|
|
585
|
-
if (resolved && !isReference(resolved)) {
|
|
586
|
-
resolvedSchema = resolved
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
schemasWithMeta.push({ schema: resolvedSchema, source: 'responses', originalName: name })
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if (includes.includes('requestBodies')) {
|
|
595
|
-
const requestBodies = components?.requestBodies || {}
|
|
596
|
-
for (const [name, request] of Object.entries(requestBodies)) {
|
|
597
|
-
const requestObject = request as { content?: Record<string, unknown> }
|
|
598
|
-
const schema = extractSchemaFromContent(requestObject.content, contentType)
|
|
599
|
-
if (schema) {
|
|
600
|
-
// Resolve schema if it's a $ref (can happen when the bundler deduplicates schemas
|
|
601
|
-
// referenced from multiple external files). Without this, a $ref schema would be
|
|
602
|
-
// parsed as a reference to itself, generating `z.lazy(() => schemaName)`.
|
|
603
|
-
let resolvedSchema = schema
|
|
604
|
-
if (isReference(schema)) {
|
|
605
|
-
const resolved = this.get<SchemaObject>(schema.$ref)
|
|
606
|
-
if (resolved && !isReference(resolved)) {
|
|
607
|
-
resolvedSchema = resolved
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
schemasWithMeta.push({ schema: resolvedSchema, source: 'requestBodies', originalName: name })
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Apply collision resolution only if enabled
|
|
616
|
-
const { schemas, nameMapping } = shouldResolveCollisions ? resolveCollisions(schemasWithMeta) : legacyResolve(schemasWithMeta)
|
|
617
|
-
|
|
618
|
-
return {
|
|
619
|
-
schemas: sortSchemas(schemas),
|
|
620
|
-
nameMapping,
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Models one variable definition in an OpenAPI server object.
|
|
3
|
-
*/
|
|
4
|
-
type ServerVariable = {
|
|
5
|
-
default?: string | number
|
|
6
|
-
enum?: (string | number)[]
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Minimal shape of an OpenAPI server object.
|
|
11
|
-
*/
|
|
12
|
-
type ServerObject = {
|
|
13
|
-
url: string
|
|
14
|
-
variables?: Record<string, ServerVariable>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Resolves an OpenAPI server URL by substituting `{variable}` placeholders.
|
|
19
|
-
*
|
|
20
|
-
* Resolution priority per variable:
|
|
21
|
-
* 1. `overrides[key]` — caller-supplied value.
|
|
22
|
-
* 2. `variable.default` — spec-defined default, coerced to `string`.
|
|
23
|
-
* 3. Variable is left unreplaced when neither is available.
|
|
24
|
-
*
|
|
25
|
-
* Throws when an `overrides` value is not present in the variable's `enum` list.
|
|
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
|
-
}
|