@kubb/adapter-oas 5.0.0-alpha.16 → 5.0.0-alpha.18

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