@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/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
- }