@kubb/adapter-oas 5.0.0-alpha.2 → 5.0.0-alpha.3
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 +235 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +27 -0
- package/dist/index.js +235 -101
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/adapter.ts +16 -0
- package/src/oas/types.ts +19 -0
- package/src/parser.ts +104 -152
- package/src/types.ts +6 -0
- package/src/utils.ts +161 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/adapter-oas",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.3",
|
|
4
4
|
"description": "OpenAPI / Swagger adapter for Kubb — converts OAS input into a @kubb/ast RootNode.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openapi",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"openapi-types": "^12.1.3",
|
|
47
47
|
"remeda": "^2.33.6",
|
|
48
48
|
"swagger2openapi": "^7.0.8",
|
|
49
|
-
"@kubb/ast": "5.0.0-alpha.
|
|
50
|
-
"@kubb/core": "5.0.0-alpha.
|
|
49
|
+
"@kubb/ast": "5.0.0-alpha.3",
|
|
50
|
+
"@kubb/core": "5.0.0-alpha.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/swagger2openapi": "^7.0.4",
|
package/src/adapter.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { resolveServerUrl } from './oas/resolveServerUrl.ts'
|
|
|
6
6
|
import { parseFromConfig } from './oas/utils.ts'
|
|
7
7
|
import { createOasParser } from './parser.ts'
|
|
8
8
|
import type { OasAdapter } from './types.ts'
|
|
9
|
+
import { getImports } from './utils.ts'
|
|
9
10
|
|
|
10
11
|
export const adapterOasName = 'oas' satisfies OasAdapter['name']
|
|
11
12
|
|
|
@@ -42,6 +43,10 @@ export const adapterOas = defineAdapter<OasAdapter>((options) => {
|
|
|
42
43
|
emptySchemaType = unknownType,
|
|
43
44
|
} = options
|
|
44
45
|
|
|
46
|
+
// Mutable Map shared between `options` and each `parse()` call.
|
|
47
|
+
// Populated (and replaced) on every parse so consumers always see the latest state.
|
|
48
|
+
const nameMapping = new Map<string, string>()
|
|
49
|
+
|
|
45
50
|
return {
|
|
46
51
|
name: adapterOasName,
|
|
47
52
|
options: {
|
|
@@ -56,6 +61,10 @@ export const adapterOas = defineAdapter<OasAdapter>((options) => {
|
|
|
56
61
|
integerType,
|
|
57
62
|
unknownType,
|
|
58
63
|
emptySchemaType,
|
|
64
|
+
nameMapping,
|
|
65
|
+
},
|
|
66
|
+
getImports(node, resolve) {
|
|
67
|
+
return getImports({ node, nameMapping, resolve })
|
|
59
68
|
},
|
|
60
69
|
async parse(source) {
|
|
61
70
|
const fakeConfig = sourceToFakeConfig(source)
|
|
@@ -75,6 +84,13 @@ export const adapterOas = defineAdapter<OasAdapter>((options) => {
|
|
|
75
84
|
const baseURL = server?.url ? resolveServerUrl(server, serverVariables) : undefined
|
|
76
85
|
|
|
77
86
|
const parser = createOasParser(oas, { contentType, collisionDetection })
|
|
87
|
+
|
|
88
|
+
// Sync the adapter's shared nameMapping with the one computed by the parser.
|
|
89
|
+
nameMapping.clear()
|
|
90
|
+
for (const [key, value] of parser.nameMapping) {
|
|
91
|
+
nameMapping.set(key, value)
|
|
92
|
+
}
|
|
93
|
+
|
|
78
94
|
const root = parser.parse({ dateType, integerType, unknownType, emptySchemaType })
|
|
79
95
|
|
|
80
96
|
return createRoot({
|
package/src/oas/types.ts
CHANGED
|
@@ -30,6 +30,25 @@ export type SchemaObject = OASSchemaObject & {
|
|
|
30
30
|
*/
|
|
31
31
|
contentMediaType?: string
|
|
32
32
|
$ref?: string
|
|
33
|
+
/**
|
|
34
|
+
* OAS 3.1 / JSON Schema: positional items in a tuple schema.
|
|
35
|
+
* Replaces the OAS 3.0 multi-item `items` array syntax.
|
|
36
|
+
*/
|
|
37
|
+
prefixItems?: Array<SchemaObject | ReferenceObject>
|
|
38
|
+
/**
|
|
39
|
+
* JSON Schema: maps regex patterns to sub-schemas for additional property validation.
|
|
40
|
+
*/
|
|
41
|
+
patternProperties?: Record<string, SchemaObject | boolean>
|
|
42
|
+
/**
|
|
43
|
+
* OAS 3.0 / JSON Schema: single-schema form.
|
|
44
|
+
* The OAS base type already includes this, but we re-declare it here to ensure
|
|
45
|
+
* the single-schema overload takes precedence over the multi-schema tuple form.
|
|
46
|
+
*/
|
|
47
|
+
items?: SchemaObject | ReferenceObject
|
|
48
|
+
/**
|
|
49
|
+
* Enum values for this schema (narrowed from `unknown[]` in the base type).
|
|
50
|
+
*/
|
|
51
|
+
enum?: Array<string | number | boolean | null>
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
/**
|
package/src/parser.ts
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
|
-
import { pascalCase } from '@internals/utils'
|
|
2
|
-
import {
|
|
3
|
-
collect,
|
|
4
|
-
createOperation,
|
|
5
|
-
createParameter,
|
|
6
|
-
createProperty,
|
|
7
|
-
createResponse,
|
|
8
|
-
createRoot,
|
|
9
|
-
createSchema,
|
|
10
|
-
narrowSchema,
|
|
11
|
-
schemaTypes,
|
|
12
|
-
transform,
|
|
13
|
-
} from '@kubb/ast'
|
|
1
|
+
import { pascalCase, URLPath } from '@internals/utils'
|
|
2
|
+
import { createOperation, createParameter, createProperty, createResponse, createRoot, createSchema, narrowSchema, schemaTypes, transform } from '@kubb/ast'
|
|
14
3
|
import type {
|
|
15
4
|
ArraySchemaNode,
|
|
16
5
|
DateSchemaNode,
|
|
@@ -38,11 +27,11 @@ import type {
|
|
|
38
27
|
TimeSchemaNode,
|
|
39
28
|
UnionSchemaNode,
|
|
40
29
|
} from '@kubb/ast/types'
|
|
41
|
-
import type { KubbFile } from '@kubb/fabric-core/types'
|
|
42
30
|
import { enumExtensionKeys, formatMap, knownMediaTypes } from './constants.ts'
|
|
43
31
|
import type { Oas } from './oas/Oas.ts'
|
|
44
|
-
import type { contentType, Operation, SchemaObject } from './oas/types.ts'
|
|
32
|
+
import type { contentType, Operation, ReferenceObject, SchemaObject } from './oas/types.ts'
|
|
45
33
|
import { flattenSchema, isDiscriminator, isNullable, isReference } from './oas/utils.ts'
|
|
34
|
+
import { applyDiscriminatorEnum, extractRefName, mergeAdjacentAnonymousObjects, simplifyUnionMembers } from './utils.ts'
|
|
46
35
|
|
|
47
36
|
/**
|
|
48
37
|
* Distributive `Omit` — correctly distributes over union types so that
|
|
@@ -178,15 +167,6 @@ function formatToSchemaType(format: string): SchemaType | undefined {
|
|
|
178
167
|
return formatMap[format as keyof typeof formatMap]
|
|
179
168
|
}
|
|
180
169
|
|
|
181
|
-
/**
|
|
182
|
-
* Extracts the final path segment of a JSON Pointer `$ref` string.
|
|
183
|
-
* For `#/components/schemas/Order` this returns `'Order'`.
|
|
184
|
-
* Falls back to the full ref string when no slash is present.
|
|
185
|
-
*/
|
|
186
|
-
function extractRefName($ref: string): string {
|
|
187
|
-
return $ref.split('/').at(-1) ?? $ref
|
|
188
|
-
}
|
|
189
|
-
|
|
190
170
|
/**
|
|
191
171
|
* Maps an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
192
172
|
* Numeric types (`number`, `integer`, `bigint`) are returned unchanged;
|
|
@@ -245,45 +225,15 @@ export type OasParser = {
|
|
|
245
225
|
* the transformed name to use (e.g. with a plugin `transformers.name` applied).
|
|
246
226
|
*/
|
|
247
227
|
resolveRefs: (node: SchemaNode, resolveName: (ref: string) => string | undefined, resolveEnumName?: (name: string) => string | undefined) => SchemaNode
|
|
228
|
+
|
|
248
229
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* to a known component in the OAS spec (`oas.get($ref)` is truthy).
|
|
252
|
-
*
|
|
253
|
-
* The `resolve` callback is called with the schema name (last segment of the
|
|
254
|
-
* `$ref`, collision-corrected via the OAS name mapping) and must return the
|
|
255
|
-
* `{ name, path }` pair for the generated import, or `undefined` to skip it.
|
|
230
|
+
* Map from original `$ref` paths to their collision-resolved schema names.
|
|
231
|
+
* e.g. `'#/components/schemas/Order'` → `'OrderSchema'`
|
|
256
232
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* const imports = parser.getImports(schemaNode, (schemaName) => ({
|
|
260
|
-
* name: schemaManager.getName(schemaName, { type: 'type' }),
|
|
261
|
-
* path: schemaManager.getFile(schemaName).path,
|
|
262
|
-
* }))
|
|
263
|
-
* ```
|
|
233
|
+
* Pass this to the standalone `getImports()` to resolve imports without holding
|
|
234
|
+
* a reference to the full parser or OAS instance.
|
|
264
235
|
*/
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* When a discriminator is present, replaces the discriminator property's schema with
|
|
270
|
-
* an enum of the mapping keys so downstream code emits a precise literal-union type.
|
|
271
|
-
* Returns the original schema unchanged when there is no discriminator or no mapping.
|
|
272
|
-
*/
|
|
273
|
-
function applyDiscriminatorEnum(schema: SchemaObject): SchemaObject {
|
|
274
|
-
if (!isDiscriminator(schema)) return schema
|
|
275
|
-
const propName = schema.discriminator.propertyName
|
|
276
|
-
if (!schema.properties?.[propName]) return schema
|
|
277
|
-
return {
|
|
278
|
-
...schema,
|
|
279
|
-
properties: {
|
|
280
|
-
...schema.properties,
|
|
281
|
-
[propName]: {
|
|
282
|
-
...(schema.properties[propName] as SchemaObject),
|
|
283
|
-
enum: schema.discriminator.mapping ? Object.keys(schema.discriminator.mapping) : undefined,
|
|
284
|
-
},
|
|
285
|
-
},
|
|
286
|
-
} as SchemaObject
|
|
236
|
+
nameMapping: Map<string, string>
|
|
287
237
|
}
|
|
288
238
|
|
|
289
239
|
/**
|
|
@@ -383,18 +333,17 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
383
333
|
* reflected in generated JSDoc and type modifiers.
|
|
384
334
|
*/
|
|
385
335
|
function convertRef({ schema, nullable, defaultValue }: SchemaContext): SchemaNode {
|
|
386
|
-
const schemaObject = schema as unknown as SchemaObject & { $ref: string }
|
|
387
336
|
return createSchema({
|
|
388
337
|
type: 'ref',
|
|
389
|
-
name: extractRefName(
|
|
390
|
-
ref:
|
|
338
|
+
name: extractRefName(schema.$ref!),
|
|
339
|
+
ref: schema.$ref,
|
|
391
340
|
nullable,
|
|
392
|
-
description:
|
|
393
|
-
deprecated:
|
|
394
|
-
readOnly:
|
|
395
|
-
writeOnly:
|
|
396
|
-
pattern:
|
|
397
|
-
example:
|
|
341
|
+
description: schema.description,
|
|
342
|
+
deprecated: schema.deprecated,
|
|
343
|
+
readOnly: schema.readOnly,
|
|
344
|
+
writeOnly: schema.writeOnly,
|
|
345
|
+
pattern: schema.type === 'string' ? schema.pattern : undefined,
|
|
346
|
+
example: schema.example,
|
|
398
347
|
default: defaultValue,
|
|
399
348
|
})
|
|
400
349
|
}
|
|
@@ -421,8 +370,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
421
370
|
!(Array.isArray(schema.required) && schema.required.length) &&
|
|
422
371
|
schema.additionalProperties === undefined
|
|
423
372
|
) {
|
|
424
|
-
const [memberSchema] = schema.allOf as SchemaObject
|
|
425
|
-
const memberNode = convertSchema({ schema: memberSchema! }, options)
|
|
373
|
+
const [memberSchema] = schema.allOf as Array<SchemaObject | ReferenceObject>
|
|
374
|
+
const memberNode = convertSchema({ schema: memberSchema! as SchemaObject }, options)
|
|
426
375
|
const { kind: _kind, ...memberNodeProps } = memberNode
|
|
427
376
|
const mergedNullable = nullable || memberNode.nullable || undefined
|
|
428
377
|
const mergedDefault = schema.default === null && mergedNullable ? undefined : (schema.default ?? memberNode.default)
|
|
@@ -444,22 +393,24 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
444
393
|
|
|
445
394
|
// When a child schema extends a discriminator parent via allOf and the parent's oneOf/anyOf
|
|
446
395
|
// references that child back, skip that allOf item to prevent a circular type reference.
|
|
447
|
-
const allOfMembers: SchemaNode
|
|
396
|
+
const allOfMembers: Array<SchemaNode> = (schema.allOf as Array<SchemaObject | ReferenceObject>)
|
|
448
397
|
.filter((item) => {
|
|
449
398
|
if (!isReference(item) || !name) return true
|
|
450
|
-
const deref = oas.get<SchemaObject>(
|
|
399
|
+
const deref = oas.get<SchemaObject>(item.$ref)
|
|
451
400
|
if (!deref || !isDiscriminator(deref)) return true
|
|
452
|
-
const parentUnion =
|
|
401
|
+
const parentUnion = deref.oneOf ?? deref.anyOf
|
|
453
402
|
if (!parentUnion) return true
|
|
454
403
|
const childRef = `#/components/schemas/${name}`
|
|
455
|
-
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) &&
|
|
456
|
-
const inMapping = Object.values(
|
|
457
|
-
(v) => v === childRef,
|
|
458
|
-
)
|
|
404
|
+
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef)
|
|
405
|
+
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef)
|
|
459
406
|
return !inOneOf && !inMapping
|
|
460
407
|
})
|
|
461
408
|
.map((s) => convertSchema({ schema: s as SchemaObject }, options))
|
|
462
409
|
|
|
410
|
+
// Track where allOf-derived members end so only the synthetic members added below
|
|
411
|
+
// (injected required-key objects + outer-properties object) are candidates for merging.
|
|
412
|
+
const syntheticStart = allOfMembers.length
|
|
413
|
+
|
|
463
414
|
// When `required` lists keys not present in the outer `properties`, resolve them from
|
|
464
415
|
// the allOf member schemas and inject them as extra intersection members.
|
|
465
416
|
if (Array.isArray(schema.required) && schema.required.length) {
|
|
@@ -467,10 +418,10 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
467
418
|
const missingRequired = schema.required.filter((key) => !outerKeys.has(key))
|
|
468
419
|
|
|
469
420
|
if (missingRequired.length) {
|
|
470
|
-
const resolvedMembers = (schema.allOf as SchemaObject
|
|
471
|
-
if (!isReference(item)) return [item]
|
|
421
|
+
const resolvedMembers = (schema.allOf as Array<SchemaObject | ReferenceObject>).flatMap((item) => {
|
|
422
|
+
if (!isReference(item)) return [item as SchemaObject]
|
|
472
423
|
const deref = oas.get<SchemaObject>(item.$ref)
|
|
473
|
-
return deref && !isReference(deref) ? [deref
|
|
424
|
+
return deref && !isReference(deref) ? [deref] : []
|
|
474
425
|
})
|
|
475
426
|
|
|
476
427
|
for (const key of missingRequired) {
|
|
@@ -485,13 +436,14 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
485
436
|
}
|
|
486
437
|
|
|
487
438
|
if (schema.properties) {
|
|
488
|
-
const { allOf: _allOf, ...schemaWithoutAllOf } = schema
|
|
489
|
-
allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf
|
|
439
|
+
const { allOf: _allOf, ...schemaWithoutAllOf } = schema
|
|
440
|
+
allOfMembers.push(convertSchema({ schema: schemaWithoutAllOf }, options))
|
|
490
441
|
}
|
|
491
442
|
|
|
443
|
+
// Merge consecutive anonymous object members within the synthetic portion — see `mergeAdjacentAnonymousObjects`.
|
|
492
444
|
return createSchema({
|
|
493
445
|
type: 'intersection',
|
|
494
|
-
members: allOfMembers,
|
|
446
|
+
members: [...allOfMembers.slice(0, syntheticStart), ...mergeAdjacentAnonymousObjects(allOfMembers.slice(syntheticStart))],
|
|
495
447
|
...buildSchemaBase(schema, name, nullable, defaultValue),
|
|
496
448
|
})
|
|
497
449
|
}
|
|
@@ -512,25 +464,39 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
512
464
|
}
|
|
513
465
|
|
|
514
466
|
if (schema.properties) {
|
|
515
|
-
const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
|
|
516
|
-
const
|
|
467
|
+
const { oneOf: _oneOf, anyOf: _anyOf, ...schemaWithoutUnion } = schema
|
|
468
|
+
const discriminator = isDiscriminator(schema) ? schema.discriminator : undefined
|
|
469
|
+
|
|
470
|
+
// Strip discriminator so convertObject won't re-apply the full mapping enum.
|
|
471
|
+
const memberBaseSchema: SchemaObject = discriminator
|
|
472
|
+
? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
|
|
473
|
+
: schemaWithoutUnion
|
|
517
474
|
|
|
518
475
|
return createSchema({
|
|
519
476
|
type: 'union',
|
|
520
477
|
...unionBase,
|
|
521
|
-
members: unionMembers.map((s) =>
|
|
522
|
-
|
|
478
|
+
members: unionMembers.map((s) => {
|
|
479
|
+
const ref = isReference(s) ? s.$ref : undefined
|
|
480
|
+
const discriminatorValue = discriminator?.mapping && ref ? Object.entries(discriminator.mapping).find(([, v]) => v === ref)?.[0] : undefined
|
|
481
|
+
|
|
482
|
+
let propertiesNode = convertSchema({ schema: memberBaseSchema, name }, options)
|
|
483
|
+
|
|
484
|
+
if (discriminatorValue && discriminator) {
|
|
485
|
+
propertiesNode = applyDiscriminatorEnum({ node: propertiesNode, propertyName: discriminator.propertyName, values: [discriminatorValue] })
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return createSchema({
|
|
523
489
|
type: 'intersection',
|
|
524
490
|
members: [convertSchema({ schema: s as SchemaObject }, options), propertiesNode],
|
|
525
|
-
})
|
|
526
|
-
),
|
|
491
|
+
})
|
|
492
|
+
}),
|
|
527
493
|
})
|
|
528
494
|
}
|
|
529
495
|
|
|
530
496
|
return createSchema({
|
|
531
497
|
type: 'union',
|
|
532
498
|
...unionBase,
|
|
533
|
-
members: unionMembers.map((s) => convertSchema({ schema: s as SchemaObject }, options)),
|
|
499
|
+
members: simplifyUnionMembers(unionMembers.map((s) => convertSchema({ schema: s as SchemaObject }, options))),
|
|
534
500
|
})
|
|
535
501
|
}
|
|
536
502
|
|
|
@@ -619,10 +585,9 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
619
585
|
function convertEnum({ schema, name, nullable, type, options }: SchemaContext): SchemaNode {
|
|
620
586
|
// Malformed schema: `{ type: 'array', enum: [...] }` — normalize by moving the enum into items.
|
|
621
587
|
if (type === 'array') {
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
const
|
|
625
|
-
const { enum: _enum, ...schemaWithoutEnum } = schema as SchemaObject & { enum?: unknown[] }
|
|
588
|
+
const isItemsObject = typeof schema.items === 'object' && !Array.isArray(schema.items)
|
|
589
|
+
const normalizedItems: SchemaObject = { ...(isItemsObject ? (schema.items as SchemaObject) : {}), enum: schema.enum }
|
|
590
|
+
const { enum: _enum, ...schemaWithoutEnum } = schema
|
|
626
591
|
return convertSchema({ schema: { ...schemaWithoutEnum, items: normalizedItems } as SchemaObject, name }, options)
|
|
627
592
|
}
|
|
628
593
|
|
|
@@ -716,21 +681,21 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
716
681
|
* - not required + nullable → `nullish: true`
|
|
717
682
|
*/
|
|
718
683
|
function convertObject({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const properties: Array<PropertyNode> = resolvedSchema.properties
|
|
724
|
-
? Object.entries(resolvedSchema.properties).map(([propName, propSchema]) => {
|
|
725
|
-
const required = Array.isArray(resolvedSchema.required) ? resolvedSchema.required.includes(propName) : !!resolvedSchema.required
|
|
684
|
+
const properties: Array<PropertyNode> = schema.properties
|
|
685
|
+
? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
686
|
+
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
|
|
726
687
|
const resolvedPropSchema = propSchema as SchemaObject
|
|
727
688
|
const propNullable = isNullable(resolvedPropSchema)
|
|
728
|
-
const
|
|
689
|
+
const basePropName = name ? pascalCase([name, propName].join(' ')) : undefined
|
|
690
|
+
const propNode = convertSchema({ schema: resolvedPropSchema, name: basePropName }, options)
|
|
691
|
+
const isEnumNode = !!narrowSchema(propNode, 'enum')
|
|
692
|
+
const derivedPropName = isEnumNode && name ? pascalCase([name, propName, mergedOptions.enumSuffix].filter(Boolean).join(' ')) : basePropName
|
|
693
|
+
const schemaNode = isEnumNode && derivedPropName !== basePropName ? { ...propNode, name: derivedPropName } : propNode
|
|
729
694
|
|
|
730
695
|
return createProperty({
|
|
731
696
|
name: propName,
|
|
732
697
|
schema: {
|
|
733
|
-
...
|
|
698
|
+
...schemaNode,
|
|
734
699
|
nullable: propNullable || undefined,
|
|
735
700
|
optional: !required && !propNullable ? true : undefined,
|
|
736
701
|
nullish: !required && propNullable ? true : undefined,
|
|
@@ -740,7 +705,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
740
705
|
})
|
|
741
706
|
: []
|
|
742
707
|
|
|
743
|
-
const additionalProperties =
|
|
708
|
+
const additionalProperties = schema.additionalProperties
|
|
744
709
|
let additionalPropertiesNode: SchemaNode | true | undefined
|
|
745
710
|
if (additionalProperties === true) {
|
|
746
711
|
additionalPropertiesNode = true
|
|
@@ -752,21 +717,20 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
752
717
|
additionalPropertiesNode = createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
|
|
753
718
|
}
|
|
754
719
|
|
|
755
|
-
const rawPatternProperties =
|
|
756
|
-
'patternProperties' in resolvedSchema ? (resolvedSchema as unknown as { patternProperties?: Record<string, SchemaObject> }).patternProperties : undefined
|
|
720
|
+
const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
|
|
757
721
|
|
|
758
722
|
const patternProperties = rawPatternProperties
|
|
759
723
|
? Object.fromEntries(
|
|
760
724
|
Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [
|
|
761
725
|
pattern,
|
|
762
|
-
|
|
726
|
+
patternSchema === true || (typeof patternSchema === 'object' && Object.keys(patternSchema).length === 0)
|
|
763
727
|
? createSchema({ type: resolveTypeOption(mergedOptions.unknownType) })
|
|
764
728
|
: convertSchema({ schema: patternSchema as SchemaObject }, options),
|
|
765
729
|
]),
|
|
766
730
|
)
|
|
767
731
|
: undefined
|
|
768
732
|
|
|
769
|
-
|
|
733
|
+
const objectNode: SchemaNode = createSchema({
|
|
770
734
|
type: 'object',
|
|
771
735
|
primitive: 'object',
|
|
772
736
|
properties,
|
|
@@ -774,6 +738,17 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
774
738
|
patternProperties,
|
|
775
739
|
...buildSchemaBase(schema, name, nullable, defaultValue),
|
|
776
740
|
})
|
|
741
|
+
|
|
742
|
+
// When a discriminator is present, replace the discriminator property's schema
|
|
743
|
+
// with an enum of the mapping keys for a precise literal-union type.
|
|
744
|
+
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
745
|
+
const discPropName = schema.discriminator.propertyName
|
|
746
|
+
const values = Object.keys(schema.discriminator.mapping)
|
|
747
|
+
const enumName = name ? pascalCase([name, discPropName, mergedOptions.enumSuffix].filter(Boolean).join(' ')) : undefined
|
|
748
|
+
return applyDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return objectNode
|
|
777
752
|
}
|
|
778
753
|
|
|
779
754
|
/**
|
|
@@ -783,9 +758,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
783
758
|
* after the prefix items is mapped to the rest parameter of the tuple.
|
|
784
759
|
*/
|
|
785
760
|
function convertTuple({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
|
|
786
|
-
const
|
|
787
|
-
const
|
|
788
|
-
const rest = rawSchema.items ? convertSchema({ schema: rawSchema.items }, options) : undefined
|
|
761
|
+
const tupleItems = (schema.prefixItems ?? []).map((item) => convertSchema({ schema: item as SchemaObject }, options))
|
|
762
|
+
const rest = schema.items ? convertSchema({ schema: schema.items as SchemaObject }, options) : undefined
|
|
789
763
|
|
|
790
764
|
return createSchema({
|
|
791
765
|
type: 'tuple',
|
|
@@ -805,12 +779,11 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
805
779
|
* `enumSuffix` is forwarded so generators can emit a named enum declaration.
|
|
806
780
|
*/
|
|
807
781
|
function convertArray({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
|
|
808
|
-
const
|
|
782
|
+
const rawItems = schema.items as SchemaObject | undefined
|
|
809
783
|
// When the array items schema contains an inline enum, derive a name from the parent
|
|
810
784
|
// array's name + enumSuffix so generators can emit a named enum declaration.
|
|
811
|
-
const rawItems = rawSchema.items as SchemaObject | undefined
|
|
812
785
|
const itemName = rawItems?.enum?.length && name ? pascalCase([name, mergedOptions.enumSuffix].join(' ')) : undefined
|
|
813
|
-
const items =
|
|
786
|
+
const items = rawItems ? [convertSchema({ schema: rawItems, name: itemName }, options)] : []
|
|
814
787
|
|
|
815
788
|
return createSchema({
|
|
816
789
|
type: 'array',
|
|
@@ -898,8 +871,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
898
871
|
const mergedOptions: Options = { ...DEFAULT_OPTIONS, ...options }
|
|
899
872
|
// Flatten keyword-only allOf fragments (no $ref, no structural keys) into the parent
|
|
900
873
|
// schema before parsing, so simple annotation patterns don't produce needless intersections.
|
|
901
|
-
const flattenedSchema = flattenSchema(schema
|
|
902
|
-
if (flattenedSchema && flattenedSchema !==
|
|
874
|
+
const flattenedSchema = flattenSchema(schema)
|
|
875
|
+
if (flattenedSchema && flattenedSchema !== schema) {
|
|
903
876
|
return convertSchema({ schema: flattenedSchema, name }, options)
|
|
904
877
|
}
|
|
905
878
|
|
|
@@ -933,7 +906,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
933
906
|
}
|
|
934
907
|
|
|
935
908
|
// OAS 3.1: `contentMediaType: 'application/octet-stream'` on a string schema signals binary data.
|
|
936
|
-
if (schema.type === 'string' &&
|
|
909
|
+
if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
|
|
937
910
|
return createSchema({ type: 'blob', primitive: 'string', ...buildSchemaBase(schema, name, nullable, defaultValue) })
|
|
938
911
|
}
|
|
939
912
|
|
|
@@ -947,7 +920,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
947
920
|
if (nonNullTypes.length > 1) {
|
|
948
921
|
return createSchema({
|
|
949
922
|
type: 'union',
|
|
950
|
-
members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject }, options)),
|
|
923
|
+
members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject, name }, options)),
|
|
951
924
|
...buildSchemaBase(schema, name, arrayNullable, defaultValue),
|
|
952
925
|
})
|
|
953
926
|
}
|
|
@@ -984,16 +957,21 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
984
957
|
* When the parameter has no `schema` or its schema is a `$ref`, falls back to `unknownType`.
|
|
985
958
|
*/
|
|
986
959
|
function parseParameter(options: Options, param: Record<string, unknown>): ParameterNode {
|
|
987
|
-
const
|
|
988
|
-
|
|
960
|
+
const required = (param['required'] as boolean | undefined) ?? false
|
|
961
|
+
|
|
962
|
+
const schema: SchemaNode =
|
|
963
|
+
param['schema'] && !isReference(param['schema'])
|
|
989
964
|
? convertSchema({ schema: param['schema'] as SchemaObject }, options)
|
|
990
965
|
: createSchema({ type: resolveTypeOption(options.unknownType) })
|
|
991
966
|
|
|
992
967
|
return createParameter({
|
|
993
968
|
name: param['name'] as string,
|
|
994
969
|
in: param['in'] as ParameterLocation,
|
|
995
|
-
schema
|
|
996
|
-
|
|
970
|
+
schema: {
|
|
971
|
+
...schema,
|
|
972
|
+
optional: (!required || !!schema.optional) ? true : undefined,
|
|
973
|
+
},
|
|
974
|
+
required,
|
|
997
975
|
})
|
|
998
976
|
}
|
|
999
977
|
|
|
@@ -1037,7 +1015,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
1037
1015
|
return createOperation({
|
|
1038
1016
|
operationId: operation.getOperationId(),
|
|
1039
1017
|
method: operation.method.toUpperCase() as HttpMethod,
|
|
1040
|
-
path: operation.path,
|
|
1018
|
+
path: new URLPath(operation.path).URL,
|
|
1041
1019
|
tags: operation.getTags().map((tag) => tag.name),
|
|
1042
1020
|
summary: operation.getSummary() || undefined,
|
|
1043
1021
|
description: operation.getDescription() || undefined,
|
|
@@ -1101,36 +1079,10 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
1101
1079
|
}) as SchemaNode
|
|
1102
1080
|
}
|
|
1103
1081
|
|
|
1104
|
-
/**
|
|
1105
|
-
* Collects all `KubbFile.Import` descriptors needed by a `SchemaNode` tree.
|
|
1106
|
-
*
|
|
1107
|
-
* Walks the tree looking for `ref` nodes, verifies each `$ref` is resolvable in the spec,
|
|
1108
|
-
* applies collision-resolved names from `nameMapping`, and calls `resolve` to obtain the
|
|
1109
|
-
* import path and name. Returns an empty array for refs that cannot be resolved.
|
|
1110
|
-
*/
|
|
1111
|
-
function getImports(node: SchemaNode, resolve: (schemaName: string) => { name: string; path: string } | undefined): Array<KubbFile.Import> {
|
|
1112
|
-
return collect<KubbFile.Import>(node, {
|
|
1113
|
-
schema(schemaNode): KubbFile.Import | undefined {
|
|
1114
|
-
if (schemaNode.type !== 'ref' || !schemaNode.ref) return
|
|
1115
|
-
// Use the OAS instance to verify this $ref is importable (exists in the spec).
|
|
1116
|
-
if (!oas.get(schemaNode.ref)) return
|
|
1117
|
-
|
|
1118
|
-
const rawName = extractRefName(schemaNode.ref)
|
|
1119
|
-
|
|
1120
|
-
// Apply collision-resolved name if available.
|
|
1121
|
-
const schemaName = nameMapping.get(rawName) ?? rawName
|
|
1122
|
-
const result = resolve(schemaName)
|
|
1123
|
-
if (!result) return
|
|
1124
|
-
|
|
1125
|
-
return { name: [result.name], path: result.path }
|
|
1126
|
-
},
|
|
1127
|
-
})
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
1082
|
return {
|
|
1131
|
-
parse
|
|
1083
|
+
parse,
|
|
1132
1084
|
convertSchema,
|
|
1133
1085
|
resolveRefs,
|
|
1134
|
-
|
|
1086
|
+
nameMapping,
|
|
1135
1087
|
} as OasParser
|
|
1136
1088
|
}
|
package/src/types.ts
CHANGED
|
@@ -82,6 +82,12 @@ export type OasAdapterResolvedOptions = {
|
|
|
82
82
|
integerType: NonNullable<OasAdapterOptions['integerType']>
|
|
83
83
|
unknownType: NonNullable<OasAdapterOptions['unknownType']>
|
|
84
84
|
emptySchemaType: NonNullable<OasAdapterOptions['emptySchemaType']>
|
|
85
|
+
/**
|
|
86
|
+
* Map from original `$ref` paths to their collision-resolved schema names.
|
|
87
|
+
* Populated by the adapter after each `parse()` call.
|
|
88
|
+
* e.g. `'#/components/schemas/Order'` → `'OrderSchema'`
|
|
89
|
+
*/
|
|
90
|
+
nameMapping: Map<string, string>
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
export type OasAdapter = AdapterFactoryOptions<'oas', OasAdapterOptions, OasAdapterResolvedOptions>
|