@kubb/adapter-oas 5.0.0-alpha.10 → 5.0.0-alpha.12
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 +215 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +34 -26
- package/dist/index.js +215 -150
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/adapter.ts +9 -6
- package/src/constants.ts +11 -5
- package/src/parser.ts +123 -78
- package/src/types.ts +35 -19
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.12",
|
|
4
4
|
"description": "OpenAPI / Swagger adapter for Kubb — converts OAS input into a @kubb/ast RootNode.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openapi",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@kubb/fabric-core": "0.14.0",
|
|
40
|
-
"@redocly/openapi-core": "^2.24.
|
|
40
|
+
"@redocly/openapi-core": "^2.24.1",
|
|
41
41
|
"@stoplight/yaml": "^4.3.0",
|
|
42
42
|
"fflate": "^0.8.2",
|
|
43
43
|
"jsonpointer": "^5.0.1",
|
|
@@ -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.12",
|
|
50
|
+
"@kubb/core": "5.0.0-alpha.12"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/swagger2openapi": "^7.0.4",
|
package/src/adapter.ts
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path'
|
|
|
2
2
|
import { createRoot } from '@kubb/ast'
|
|
3
3
|
import type { AdapterSource } from '@kubb/core'
|
|
4
4
|
import { createAdapter } from '@kubb/core'
|
|
5
|
+
import { DEFAULT_PARSER_OPTIONS } from './constants.ts'
|
|
5
6
|
import { resolveServerUrl } from './oas/resolveServerUrl.ts'
|
|
6
7
|
import { parseFromConfig } from './oas/utils.ts'
|
|
7
8
|
import { createOasParser } from './parser.ts'
|
|
@@ -36,11 +37,12 @@ export const adapterOas = createAdapter<OasAdapter>((options) => {
|
|
|
36
37
|
serverIndex,
|
|
37
38
|
serverVariables,
|
|
38
39
|
discriminator = 'strict',
|
|
39
|
-
collisionDetection =
|
|
40
|
-
dateType =
|
|
41
|
-
integerType =
|
|
42
|
-
unknownType =
|
|
43
|
-
|
|
40
|
+
collisionDetection = true,
|
|
41
|
+
dateType = DEFAULT_PARSER_OPTIONS.dateType,
|
|
42
|
+
integerType = DEFAULT_PARSER_OPTIONS.integerType,
|
|
43
|
+
unknownType = DEFAULT_PARSER_OPTIONS.unknownType,
|
|
44
|
+
enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix,
|
|
45
|
+
emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType,
|
|
44
46
|
} = options
|
|
45
47
|
|
|
46
48
|
// Mutable Map shared between `options` and each `parse()` call.
|
|
@@ -61,6 +63,7 @@ export const adapterOas = createAdapter<OasAdapter>((options) => {
|
|
|
61
63
|
integerType,
|
|
62
64
|
unknownType,
|
|
63
65
|
emptySchemaType,
|
|
66
|
+
enumSuffix,
|
|
64
67
|
nameMapping,
|
|
65
68
|
},
|
|
66
69
|
getImports(node, resolve) {
|
|
@@ -91,7 +94,7 @@ export const adapterOas = createAdapter<OasAdapter>((options) => {
|
|
|
91
94
|
nameMapping.set(key, value)
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
const root = parser.parse({ dateType, integerType, unknownType, emptySchemaType })
|
|
97
|
+
const root = parser.parse({ dateType, integerType, unknownType, emptySchemaType, enumSuffix })
|
|
95
98
|
|
|
96
99
|
return createRoot({
|
|
97
100
|
...root,
|
package/src/constants.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { SchemaType } from '@kubb/ast/types'
|
|
2
2
|
import type { HttpMethods as OASHttpMethods } from 'oas/types'
|
|
3
|
+
import type { ParserOptions } from './types.ts'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Default values for all `Options` fields.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_PARSER_OPTIONS = {
|
|
9
|
+
dateType: 'string',
|
|
10
|
+
integerType: 'number',
|
|
11
|
+
unknownType: 'any',
|
|
12
|
+
emptySchemaType: 'any',
|
|
13
|
+
enumSuffix: 'enum',
|
|
14
|
+
} as const satisfies ParserOptions
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
17
|
* OpenAPI version string written into merged document stubs.
|
|
@@ -18,8 +28,6 @@ export const MERGE_DEFAULT_TITLE = 'Merged API' as const
|
|
|
18
28
|
*/
|
|
19
29
|
export const MERGE_DEFAULT_VERSION = '1.0.0' as const
|
|
20
30
|
|
|
21
|
-
// ─── Schema analysis ───────────────────────────────────────────────────────────
|
|
22
|
-
|
|
23
31
|
/**
|
|
24
32
|
* JSON Schema keywords that indicate structural composition.
|
|
25
33
|
* A schema fragment containing any of these keys must not be inlined into its
|
|
@@ -93,8 +101,6 @@ export const enumExtensionKeys = ['x-enumNames', 'x-enum-varnames'] as const
|
|
|
93
101
|
*/
|
|
94
102
|
export const SCALAR_PRIMITIVE_TYPES = new Set(['string', 'number', 'integer', 'bigint', 'boolean'] as const)
|
|
95
103
|
|
|
96
|
-
// ─── HTTP ──────────────────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
104
|
/**
|
|
99
105
|
* Canonical HTTP method names for the Kubb OAS layer.
|
|
100
106
|
* Keys are uppercase (used in generated code); values are the lowercase strings
|
package/src/parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pascalCase, URLPath } from '@internals/utils'
|
|
1
|
+
import { getUniqueName, pascalCase, URLPath } from '@internals/utils'
|
|
2
2
|
import { createOperation, createParameter, createProperty, createResponse, createRoot, createSchema, narrowSchema, schemaTypes, transform } from '@kubb/ast'
|
|
3
3
|
import type {
|
|
4
4
|
ArraySchemaNode,
|
|
@@ -27,10 +27,11 @@ import type {
|
|
|
27
27
|
TimeSchemaNode,
|
|
28
28
|
UnionSchemaNode,
|
|
29
29
|
} from '@kubb/ast/types'
|
|
30
|
-
import { enumExtensionKeys, formatMap, knownMediaTypes } from './constants.ts'
|
|
30
|
+
import { DEFAULT_PARSER_OPTIONS, enumExtensionKeys, formatMap, knownMediaTypes } from './constants.ts'
|
|
31
31
|
import type { Oas } from './oas/Oas.ts'
|
|
32
32
|
import type { contentType, Operation, ReferenceObject, SchemaObject } from './oas/types.ts'
|
|
33
33
|
import { flattenSchema, isDiscriminator, isNullable, isReference } from './oas/utils.ts'
|
|
34
|
+
import type { ParserOptions } from './types.ts'
|
|
34
35
|
import { applyDiscriminatorEnum, extractRefName, mergeAdjacentAnonymousObjects, simplifyUnionMembers } from './utils.ts'
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -54,14 +55,16 @@ type DateTimeNodeByDateType = {
|
|
|
54
55
|
/**
|
|
55
56
|
* Resolves the AST node produced by `format: 'date-time'` based on the `dateType` option.
|
|
56
57
|
*/
|
|
57
|
-
type ResolveDateTimeNode<TDateType extends
|
|
58
|
+
type ResolveDateTimeNode<TDateType extends ParserOptions['dateType']> = DateTimeNodeByDateType[TDateType extends keyof DateTimeNodeByDateType
|
|
59
|
+
? TDateType
|
|
60
|
+
: 'string']
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
63
|
* Single source of truth: ordered list of `[shape, SchemaNode]` pairs.
|
|
61
64
|
* `InferSchemaNode` walks this tuple in order and returns the node type of the first matching entry.
|
|
62
65
|
* Parameterized over `TDateType` so `format: 'date-time'` resolves to the correct node based on the option.
|
|
63
66
|
*/
|
|
64
|
-
type SchemaNodeMap<TDateType extends
|
|
67
|
+
type SchemaNodeMap<TDateType extends ParserOptions['dateType'] = 'string'> = [
|
|
65
68
|
[{ $ref: string }, RefSchemaNode],
|
|
66
69
|
// allOf with sibling `properties` always produces an intersection (shared props are appended as a member).
|
|
67
70
|
[{ allOf: ReadonlyArray<unknown>; properties: object }, IntersectionSchemaNode],
|
|
@@ -105,7 +108,7 @@ type SchemaNodeMap<TDateType extends Options['dateType'] = Options['dateType']>
|
|
|
105
108
|
|
|
106
109
|
export type InferSchemaNode<
|
|
107
110
|
TSchema extends SchemaObject,
|
|
108
|
-
TDateType extends
|
|
111
|
+
TDateType extends ParserOptions['dateType'] = 'string',
|
|
109
112
|
TEntries extends ReadonlyArray<[object, SchemaNode]> = SchemaNodeMap<TDateType>,
|
|
110
113
|
> = TEntries extends [infer TEntry extends [object, SchemaNode], ...infer TRest extends ReadonlyArray<[object, SchemaNode]>]
|
|
111
114
|
? TSchema extends TEntry[0]
|
|
@@ -113,32 +116,6 @@ export type InferSchemaNode<
|
|
|
113
116
|
: InferSchemaNode<TSchema, TDateType, TRest>
|
|
114
117
|
: SchemaNode
|
|
115
118
|
|
|
116
|
-
/**
|
|
117
|
-
* Controls how various OAS constructs are mapped to Kubb AST nodes.
|
|
118
|
-
*/
|
|
119
|
-
export type Options = {
|
|
120
|
-
/**
|
|
121
|
-
* How `format: 'date-time'` schemas are represented. `false` falls through to a plain string.
|
|
122
|
-
*/
|
|
123
|
-
dateType: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
|
|
124
|
-
/**
|
|
125
|
-
* Whether `type: 'integer'` and `format: 'int64'` produce `number` or `bigint` nodes.
|
|
126
|
-
*/
|
|
127
|
-
integerType?: 'number' | 'bigint'
|
|
128
|
-
/**
|
|
129
|
-
* AST type used when no schema type can be inferred.
|
|
130
|
-
*/
|
|
131
|
-
unknownType: 'any' | 'unknown' | 'void'
|
|
132
|
-
/**
|
|
133
|
-
* AST type used for completely empty schemas (`{}`).
|
|
134
|
-
*/
|
|
135
|
-
emptySchemaType: 'any' | 'unknown' | 'void'
|
|
136
|
-
/**
|
|
137
|
-
* Suffix appended to derived enum names when building property schema names.
|
|
138
|
-
*/
|
|
139
|
-
enumSuffix: string
|
|
140
|
-
}
|
|
141
|
-
|
|
142
119
|
/**
|
|
143
120
|
* Construction-time options for `createOasParser`.
|
|
144
121
|
*/
|
|
@@ -147,17 +124,6 @@ export type OasParserOptions = {
|
|
|
147
124
|
collisionDetection?: boolean
|
|
148
125
|
}
|
|
149
126
|
|
|
150
|
-
/**
|
|
151
|
-
* Default values for all `Options` fields.
|
|
152
|
-
*/
|
|
153
|
-
const DEFAULT_OPTIONS = {
|
|
154
|
-
dateType: 'string',
|
|
155
|
-
integerType: 'number',
|
|
156
|
-
unknownType: 'any',
|
|
157
|
-
emptySchemaType: 'any',
|
|
158
|
-
enumSuffix: 'enum',
|
|
159
|
-
} as const satisfies Options
|
|
160
|
-
|
|
161
127
|
/**
|
|
162
128
|
* Looks up the Kubb `SchemaType` for a given OAS `format` string.
|
|
163
129
|
* Returns `undefined` for formats not in `formatMap` (e.g. `int64`, `date-time`),
|
|
@@ -199,8 +165,8 @@ type SchemaContext = {
|
|
|
199
165
|
* Normalized single type string (first element when OAS 3.1 multi-type array).
|
|
200
166
|
*/
|
|
201
167
|
type: string | undefined
|
|
202
|
-
options: Partial<
|
|
203
|
-
mergedOptions:
|
|
168
|
+
options: Partial<ParserOptions> | undefined
|
|
169
|
+
mergedOptions: ParserOptions
|
|
204
170
|
}
|
|
205
171
|
|
|
206
172
|
/**
|
|
@@ -211,11 +177,11 @@ export type OasParser = {
|
|
|
211
177
|
* Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
|
|
212
178
|
* a `RootNode` — the top-level node of the `@kubb/ast` tree.
|
|
213
179
|
*/
|
|
214
|
-
parse: <TOptions extends Partial<
|
|
215
|
-
convertSchema: <TFormat extends string, TSchema extends SchemaObject & { format?: TFormat }, TOptions extends Partial<
|
|
180
|
+
parse: <TOptions extends Partial<ParserOptions> = object>(options?: TOptions) => RootNode
|
|
181
|
+
convertSchema: <TFormat extends string, TSchema extends SchemaObject & { format?: TFormat }, TOptions extends Partial<ParserOptions> = object>(
|
|
216
182
|
params: { schema: TSchema; name?: string },
|
|
217
183
|
options?: TOptions,
|
|
218
|
-
) => InferSchemaNode<TSchema, TOptions extends { dateType:
|
|
184
|
+
) => InferSchemaNode<TSchema, TOptions extends { dateType: ParserOptions['dateType'] } ? TOptions['dateType'] : 'string'>
|
|
219
185
|
/**
|
|
220
186
|
* Walks `node` and replaces each `ref` value with the name returned by
|
|
221
187
|
* `resolveName`. The callback receives the full `$ref` path (e.g. `#/components/schemas/Order`)
|
|
@@ -260,6 +226,14 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
260
226
|
// e.g., { '#/components/schemas/Order': 'OrderSchema', '#/components/responses/Product': 'ProductResponse' }
|
|
261
227
|
const { schemas: schemaObjects, nameMapping } = oas.getSchemas({ contentType, collisionDetection })
|
|
262
228
|
|
|
229
|
+
// Legacy enum name deduplication: tracks used enum names and appends numeric suffixes
|
|
230
|
+
// (e.g. ParamsStatusEnum, ParamsStatusEnum2) when collisionDetection is disabled.
|
|
231
|
+
const usedEnumNames: Record<string, number> = {}
|
|
232
|
+
|
|
233
|
+
// Only apply legacy naming when collisionDetection is explicitly false.
|
|
234
|
+
// When undefined (e.g. direct parser usage without adapter), use the default (new) behavior.
|
|
235
|
+
const isLegacyNaming = collisionDetection === false
|
|
236
|
+
|
|
263
237
|
/**
|
|
264
238
|
* Maps an `'any' | 'unknown' | 'void'` option string to the corresponding `SchemaType` constant.
|
|
265
239
|
* Used for both `unknownType` (unannotated schemas) and `emptySchemaType` (empty `{}` schemas).
|
|
@@ -275,7 +249,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
275
249
|
* Returns `undefined` when `dateType` is `false`, meaning the format should fall through to `string`.
|
|
276
250
|
*/
|
|
277
251
|
function getDateType(
|
|
278
|
-
options:
|
|
252
|
+
options: ParserOptions,
|
|
279
253
|
format: 'date-time' | 'date' | 'time',
|
|
280
254
|
): { type: 'datetime'; offset?: boolean; local?: boolean } | { type: 'date' | 'time'; representation: 'date' | 'string' } | undefined {
|
|
281
255
|
if (!options.dateType) {
|
|
@@ -307,7 +281,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
307
281
|
* Shared metadata fields included in every `createSchema` call.
|
|
308
282
|
* Centralizes the common properties so sub-handlers don't repeat them.
|
|
309
283
|
*/
|
|
310
|
-
function
|
|
284
|
+
function renderSchemaBase(schema: SchemaObject, name: string | undefined, nullable: true | undefined, defaultValue: unknown) {
|
|
311
285
|
return {
|
|
312
286
|
name,
|
|
313
287
|
nullable,
|
|
@@ -444,7 +418,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
444
418
|
return createSchema({
|
|
445
419
|
type: 'intersection',
|
|
446
420
|
members: [...allOfMembers.slice(0, syntheticStart), ...mergeAdjacentAnonymousObjects(allOfMembers.slice(syntheticStart))],
|
|
447
|
-
...
|
|
421
|
+
...renderSchemaBase(schema, name, nullable, defaultValue),
|
|
448
422
|
})
|
|
449
423
|
}
|
|
450
424
|
|
|
@@ -459,7 +433,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
459
433
|
function convertUnion({ schema, name, nullable, defaultValue, options }: SchemaContext): SchemaNode {
|
|
460
434
|
const unionMembers = [...(schema.oneOf ?? []), ...(schema.anyOf ?? [])]
|
|
461
435
|
const unionBase = {
|
|
462
|
-
...
|
|
436
|
+
...renderSchemaBase(schema, name, nullable, defaultValue),
|
|
463
437
|
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : undefined,
|
|
464
438
|
}
|
|
465
439
|
|
|
@@ -472,6 +446,10 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
472
446
|
? (Object.fromEntries(Object.entries(schemaWithoutUnion).filter(([key]) => key !== 'discriminator')) as SchemaObject)
|
|
473
447
|
: schemaWithoutUnion
|
|
474
448
|
|
|
449
|
+
// Convert shared properties once to avoid duplicate enum naming
|
|
450
|
+
// (e.g. StatusEnum appearing twice and getting a numeric suffix).
|
|
451
|
+
const sharedPropertiesNode = convertSchema({ schema: memberBaseSchema, name: isLegacyNaming ? undefined : name }, options)
|
|
452
|
+
|
|
475
453
|
return createSchema({
|
|
476
454
|
type: 'union',
|
|
477
455
|
...unionBase,
|
|
@@ -479,7 +457,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
479
457
|
const ref = isReference(s) ? s.$ref : undefined
|
|
480
458
|
const discriminatorValue = discriminator?.mapping && ref ? Object.entries(discriminator.mapping).find(([, v]) => v === ref)?.[0] : undefined
|
|
481
459
|
|
|
482
|
-
let propertiesNode =
|
|
460
|
+
let propertiesNode = sharedPropertiesNode
|
|
483
461
|
|
|
484
462
|
if (discriminatorValue && discriminator) {
|
|
485
463
|
propertiesNode = applyDiscriminatorEnum({ node: propertiesNode, propertyName: discriminator.propertyName, values: [discriminatorValue] })
|
|
@@ -525,7 +503,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
525
503
|
type: 'enum',
|
|
526
504
|
primitive: constPrimitive,
|
|
527
505
|
enumValues: [constValue as string | number | boolean],
|
|
528
|
-
...
|
|
506
|
+
...renderSchemaBase(schema, name, nullable, defaultValue),
|
|
529
507
|
})
|
|
530
508
|
}
|
|
531
509
|
|
|
@@ -535,7 +513,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
535
513
|
* (i.e. `format: 'date-time'` with `dateType: false`).
|
|
536
514
|
*/
|
|
537
515
|
function convertFormat({ schema, name, nullable, defaultValue, mergedOptions }: SchemaContext): SchemaNode | undefined {
|
|
538
|
-
const base =
|
|
516
|
+
const base = renderSchemaBase(schema, name, nullable, defaultValue)
|
|
539
517
|
|
|
540
518
|
// int64 is option-dependent so it can't live in the static formatMap.
|
|
541
519
|
if (schema.format === 'int64') {
|
|
@@ -684,17 +662,64 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
684
662
|
* - not required + not nullable → `optional: true`
|
|
685
663
|
* - not required + nullable → `nullish: true`
|
|
686
664
|
*/
|
|
665
|
+
/**
|
|
666
|
+
* Builds the propagation name for a child property during recursive schema conversion.
|
|
667
|
+
*
|
|
668
|
+
* - **Legacy naming** (`isLegacyNaming`): only the immediate property key is used
|
|
669
|
+
* (e.g. `Params` for property `params`), keeping nested names short.
|
|
670
|
+
* - **Default naming**: the parent name is prepended so the full path is encoded
|
|
671
|
+
* (e.g. `OrderParams` when parent is `Order`).
|
|
672
|
+
*/
|
|
673
|
+
function resolveChildName(parentName: string | undefined, propName: string): string | undefined {
|
|
674
|
+
if (isLegacyNaming) {
|
|
675
|
+
return pascalCase(propName)
|
|
676
|
+
}
|
|
677
|
+
return parentName ? pascalCase([parentName, propName].join(' ')) : undefined
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Derives the final name for an enum property schema node.
|
|
682
|
+
*
|
|
683
|
+
* The raw name always includes the enum suffix (e.g. `StatusEnum`).
|
|
684
|
+
* In legacy mode an additional deduplication step appends a numeric suffix
|
|
685
|
+
* when the same name has already been used (e.g. `ParamsStatusEnum2`).
|
|
686
|
+
*/
|
|
687
|
+
function resolveEnumPropName(parentName: string | undefined, propName: string, enumSuffix: string): string {
|
|
688
|
+
const raw = pascalCase([parentName, propName, enumSuffix].filter(Boolean).join(' '))
|
|
689
|
+
return isLegacyNaming ? getUniqueName(raw, usedEnumNames) : raw
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Given a freshly-converted property schema, returns the node with a correct
|
|
694
|
+
* `name` attached — or stripped — depending on whether the node is a named
|
|
695
|
+
* enum, a boolean const-enum (always inlined), or a regular schema.
|
|
696
|
+
*/
|
|
697
|
+
function applyEnumName(propNode: SchemaNode, parentName: string | undefined, propName: string, enumSuffix: string): SchemaNode {
|
|
698
|
+
const enumNode = narrowSchema(propNode, 'enum')
|
|
699
|
+
|
|
700
|
+
// Boolean-primitive enum nodes (e.g. `const: false`) are always inlined as
|
|
701
|
+
// literal types and must not receive a named identifier.
|
|
702
|
+
if (enumNode?.primitive === 'boolean') {
|
|
703
|
+
return { ...propNode, name: undefined }
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (enumNode) {
|
|
707
|
+
return { ...propNode, name: resolveEnumPropName(parentName, propName, enumSuffix) }
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return propNode
|
|
711
|
+
}
|
|
712
|
+
|
|
687
713
|
function convertObject({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
|
|
688
714
|
const properties: Array<PropertyNode> = schema.properties
|
|
689
715
|
? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
690
716
|
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required
|
|
691
717
|
const resolvedPropSchema = propSchema as SchemaObject
|
|
692
718
|
const propNullable = isNullable(resolvedPropSchema)
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
const
|
|
696
|
-
const
|
|
697
|
-
const schemaNode = isEnumNode && derivedPropName !== basePropName ? { ...propNode, name: derivedPropName } : propNode
|
|
719
|
+
|
|
720
|
+
const childName = resolveChildName(name, propName)
|
|
721
|
+
const propNode = convertSchema({ schema: resolvedPropSchema, name: childName }, options)
|
|
722
|
+
const schemaNode = applyEnumName(propNode, name, propName, mergedOptions.enumSuffix)
|
|
698
723
|
|
|
699
724
|
return createProperty({
|
|
700
725
|
name: propName,
|
|
@@ -740,7 +765,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
740
765
|
properties,
|
|
741
766
|
additionalProperties: additionalPropertiesNode,
|
|
742
767
|
patternProperties,
|
|
743
|
-
...
|
|
768
|
+
...renderSchemaBase(schema, name, nullable, defaultValue),
|
|
744
769
|
})
|
|
745
770
|
|
|
746
771
|
// When a discriminator is present, replace the discriminator property's schema
|
|
@@ -748,7 +773,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
748
773
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
749
774
|
const discPropName = schema.discriminator.propertyName
|
|
750
775
|
const values = Object.keys(schema.discriminator.mapping)
|
|
751
|
-
const enumName = name ?
|
|
776
|
+
const enumName = name ? resolveEnumPropName(name, discPropName, mergedOptions.enumSuffix) : undefined
|
|
752
777
|
return applyDiscriminatorEnum({ node: objectNode, propertyName: discPropName, values, enumName })
|
|
753
778
|
}
|
|
754
779
|
|
|
@@ -772,7 +797,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
772
797
|
rest,
|
|
773
798
|
min: schema.minItems,
|
|
774
799
|
max: schema.maxItems,
|
|
775
|
-
...
|
|
800
|
+
...renderSchemaBase(schema, name, nullable, defaultValue),
|
|
776
801
|
})
|
|
777
802
|
}
|
|
778
803
|
|
|
@@ -784,9 +809,9 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
784
809
|
*/
|
|
785
810
|
function convertArray({ schema, name, nullable, defaultValue, options, mergedOptions }: SchemaContext): SchemaNode {
|
|
786
811
|
const rawItems = schema.items as SchemaObject | undefined
|
|
787
|
-
// When the
|
|
788
|
-
//
|
|
789
|
-
const itemName = rawItems?.enum?.length && name ?
|
|
812
|
+
// When the items schema contains an inline enum, derive a named identifier
|
|
813
|
+
// so generators can emit a standalone enum declaration.
|
|
814
|
+
const itemName = rawItems?.enum?.length && name ? resolveEnumPropName(undefined, name, mergedOptions.enumSuffix) : undefined
|
|
790
815
|
const items = rawItems ? [convertSchema({ schema: rawItems, name: itemName }, options)] : []
|
|
791
816
|
|
|
792
817
|
return createSchema({
|
|
@@ -796,7 +821,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
796
821
|
min: schema.minItems,
|
|
797
822
|
max: schema.maxItems,
|
|
798
823
|
unique: schema.uniqueItems ?? undefined,
|
|
799
|
-
...
|
|
824
|
+
...renderSchemaBase(schema, name, nullable, defaultValue),
|
|
800
825
|
})
|
|
801
826
|
}
|
|
802
827
|
|
|
@@ -810,7 +835,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
810
835
|
min: schema.minLength,
|
|
811
836
|
max: schema.maxLength,
|
|
812
837
|
pattern: schema.pattern,
|
|
813
|
-
...
|
|
838
|
+
...renderSchemaBase(schema, name, nullable, defaultValue),
|
|
814
839
|
})
|
|
815
840
|
}
|
|
816
841
|
|
|
@@ -825,7 +850,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
825
850
|
max: schema.maximum,
|
|
826
851
|
exclusiveMinimum: typeof schema.exclusiveMinimum === 'number' ? schema.exclusiveMinimum : undefined,
|
|
827
852
|
exclusiveMaximum: typeof schema.exclusiveMaximum === 'number' ? schema.exclusiveMaximum : undefined,
|
|
828
|
-
...
|
|
853
|
+
...renderSchemaBase(schema, name, nullable, defaultValue),
|
|
829
854
|
})
|
|
830
855
|
}
|
|
831
856
|
|
|
@@ -836,7 +861,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
836
861
|
return createSchema({
|
|
837
862
|
type: 'boolean',
|
|
838
863
|
primitive: 'boolean',
|
|
839
|
-
...
|
|
864
|
+
...renderSchemaBase(schema, name, nullable, defaultValue),
|
|
840
865
|
})
|
|
841
866
|
}
|
|
842
867
|
|
|
@@ -871,8 +896,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
871
896
|
* 10. Object / array / tuple / scalar by `type`
|
|
872
897
|
* 11. Empty schema fallback (`emptySchemaType` option)
|
|
873
898
|
*/
|
|
874
|
-
function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<
|
|
875
|
-
const mergedOptions:
|
|
899
|
+
function convertSchema({ schema, name }: { schema: SchemaObject; name?: string }, options?: Partial<ParserOptions>): SchemaNode {
|
|
900
|
+
const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...options }
|
|
876
901
|
// Flatten keyword-only allOf fragments (no $ref, no structural keys) into the parent
|
|
877
902
|
// schema before parsing, so simple annotation patterns don't produce needless intersections.
|
|
878
903
|
const flattenedSchema = flattenSchema(schema)
|
|
@@ -911,7 +936,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
911
936
|
|
|
912
937
|
// OAS 3.1: `contentMediaType: 'application/octet-stream'` on a string schema signals binary data.
|
|
913
938
|
if (schema.type === 'string' && schema.contentMediaType === 'application/octet-stream') {
|
|
914
|
-
return createSchema({ type: 'blob', primitive: 'string', ...
|
|
939
|
+
return createSchema({ type: 'blob', primitive: 'string', ...renderSchemaBase(schema, name, nullable, defaultValue) })
|
|
915
940
|
}
|
|
916
941
|
|
|
917
942
|
// OAS 3.1: `type` may be an array — e.g. `["string", "integer", "null"]`.
|
|
@@ -925,7 +950,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
925
950
|
return createSchema({
|
|
926
951
|
type: 'union',
|
|
927
952
|
members: nonNullTypes.map((t) => convertSchema({ schema: { ...schema, type: t } as SchemaObject, name }, options)),
|
|
928
|
-
...
|
|
953
|
+
...renderSchemaBase(schema, name, arrayNullable, defaultValue),
|
|
929
954
|
})
|
|
930
955
|
}
|
|
931
956
|
}
|
|
@@ -960,7 +985,7 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
960
985
|
* Converts a single dereferenced OAS parameter object into a `ParameterNode`.
|
|
961
986
|
* When the parameter has no `schema` or its schema is a `$ref`, falls back to `unknownType`.
|
|
962
987
|
*/
|
|
963
|
-
function parseParameter(options:
|
|
988
|
+
function parseParameter(options: ParserOptions, param: Record<string, unknown>): ParameterNode {
|
|
964
989
|
const required = (param['required'] as boolean | undefined) ?? false
|
|
965
990
|
|
|
966
991
|
const schema: SchemaNode =
|
|
@@ -984,11 +1009,24 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
984
1009
|
* Converts an OAS `Operation` into an `OperationNode`, resolving parameters,
|
|
985
1010
|
* request body, and all response codes into their AST node equivalents.
|
|
986
1011
|
*/
|
|
987
|
-
function parseOperation(options:
|
|
1012
|
+
function parseOperation(options: ParserOptions, oas: Oas, operation: Operation): OperationNode {
|
|
988
1013
|
const parameters: Array<ParameterNode> = oas.getParameters(operation).map((param) => parseParameter(options, param as unknown as Record<string, unknown>))
|
|
989
1014
|
|
|
990
1015
|
const requestBodySchema = oas.getRequestSchema(operation)
|
|
991
|
-
const
|
|
1016
|
+
const requestBodySchemaNode = requestBodySchema ? convertSchema({ schema: requestBodySchema }, options) : undefined
|
|
1017
|
+
|
|
1018
|
+
const requestBodyKeysToOmit = requestBodySchema?.properties
|
|
1019
|
+
? Object.entries(requestBodySchema.properties)
|
|
1020
|
+
.filter(([, prop]) => !isReference(prop) && (prop as { readOnly?: boolean }).readOnly)
|
|
1021
|
+
.map(([key]) => key)
|
|
1022
|
+
: undefined
|
|
1023
|
+
|
|
1024
|
+
const requestBody = requestBodySchemaNode
|
|
1025
|
+
? {
|
|
1026
|
+
schema: requestBodySchemaNode,
|
|
1027
|
+
keysToOmit: requestBodyKeysToOmit?.length ? requestBodyKeysToOmit : undefined,
|
|
1028
|
+
}
|
|
1029
|
+
: undefined
|
|
992
1030
|
|
|
993
1031
|
const responses: Array<ResponseNode> = operation.getResponseStatusCodes().map((statusCode) => {
|
|
994
1032
|
const responseObj = operation.getResponseByStatusCode(statusCode)
|
|
@@ -1008,11 +1046,18 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
1008
1046
|
|
|
1009
1047
|
const mediaType = rawContent ? toMediaType(Object.keys(rawContent)[0] ?? '') : toMediaType(operation.contentType ?? '')
|
|
1010
1048
|
|
|
1049
|
+
const keysToOmit = responseSchema?.properties
|
|
1050
|
+
? Object.entries(responseSchema.properties)
|
|
1051
|
+
.filter(([, prop]) => !isReference(prop) && (prop as { writeOnly?: boolean }).writeOnly)
|
|
1052
|
+
.map(([key]) => key)
|
|
1053
|
+
: undefined
|
|
1054
|
+
|
|
1011
1055
|
return createResponse({
|
|
1012
1056
|
statusCode: statusCode as StatusCode,
|
|
1013
1057
|
description,
|
|
1014
1058
|
schema,
|
|
1015
1059
|
mediaType,
|
|
1060
|
+
keysToOmit: keysToOmit?.length ? keysToOmit : undefined,
|
|
1016
1061
|
})
|
|
1017
1062
|
})
|
|
1018
1063
|
|
|
@@ -1034,8 +1079,8 @@ export function createOasParser(oas: Oas, { contentType, collisionDetection }: O
|
|
|
1034
1079
|
* Converts an OpenAPI/Swagger spec (wrapped in a Kubb `Oas` instance) into
|
|
1035
1080
|
* a `RootNode` — the top-level node of the `@kubb/ast` tree.
|
|
1036
1081
|
*/
|
|
1037
|
-
function parse<TOptions extends Partial<
|
|
1038
|
-
const mergedOptions:
|
|
1082
|
+
function parse<TOptions extends Partial<ParserOptions> = object>(options?: TOptions): RootNode {
|
|
1083
|
+
const mergedOptions: ParserOptions = { ...DEFAULT_PARSER_OPTIONS, ...options }
|
|
1039
1084
|
|
|
1040
1085
|
const schemas: Array<SchemaNode> = Object.entries(schemaObjects).map(([name, schemaObject]) =>
|
|
1041
1086
|
convertSchema({ schema: schemaObject as SchemaObject, name }, mergedOptions),
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,32 @@ import type { AdapterFactoryOptions } from '@kubb/core'
|
|
|
2
2
|
import type { Oas as OasClass } from './oas/Oas.ts'
|
|
3
3
|
import type { contentType } from './oas/types.ts'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Controls how various OAS constructs are mapped to Kubb AST nodes.
|
|
7
|
+
*/
|
|
8
|
+
export type ParserOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* How `format: 'date-time'` schemas are represented. `false` falls through to a plain string.
|
|
11
|
+
*/
|
|
12
|
+
dateType: false | 'string' | 'stringOffset' | 'stringLocal' | 'date'
|
|
13
|
+
/**
|
|
14
|
+
* Whether `type: 'integer'` and `format: 'int64'` produce `number` or `bigint` nodes.
|
|
15
|
+
*/
|
|
16
|
+
integerType?: 'number' | 'bigint'
|
|
17
|
+
/**
|
|
18
|
+
* AST type used when no schema type can be inferred.
|
|
19
|
+
*/
|
|
20
|
+
unknownType: 'any' | 'unknown' | 'void'
|
|
21
|
+
/**
|
|
22
|
+
* AST type used for completely empty schemas (`{}`).
|
|
23
|
+
*/
|
|
24
|
+
emptySchemaType: 'any' | 'unknown' | 'void'
|
|
25
|
+
/**
|
|
26
|
+
* Suffix appended to derived enum names when building property schema names.
|
|
27
|
+
*/
|
|
28
|
+
enumSuffix: 'enum' | (string & {})
|
|
29
|
+
}
|
|
30
|
+
|
|
5
31
|
export type OasAdapterOptions = {
|
|
6
32
|
/**
|
|
7
33
|
* Validate the OpenAPI spec before parsing.
|
|
@@ -41,8 +67,13 @@ export type OasAdapterOptions = {
|
|
|
41
67
|
*/
|
|
42
68
|
discriminator?: 'strict' | 'inherit'
|
|
43
69
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
70
|
+
* Enable collision detection for inline enum and schema type names.
|
|
71
|
+
* When `true` (default), full-path names are used and name collisions
|
|
72
|
+
* across schema components are automatically resolved (e.g. `OrderParamsStatusEnum`).
|
|
73
|
+
* When `false`, enum names use only the immediate property context
|
|
74
|
+
* (e.g. `ParamsStatusEnum`) matching the v4 naming conventions, with numeric
|
|
75
|
+
* suffixes for deduplication (e.g. `ParamsStatusEnum2`).
|
|
76
|
+
* @default true
|
|
46
77
|
*/
|
|
47
78
|
collisionDetection?: boolean
|
|
48
79
|
/**
|
|
@@ -52,23 +83,7 @@ export type OasAdapterOptions = {
|
|
|
52
83
|
* - `false` falls through to a plain `string` node.
|
|
53
84
|
* @default 'string'
|
|
54
85
|
*/
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Whether `type: 'integer'` / `format: 'int64'` produces `number` or `bigint` nodes.
|
|
58
|
-
* @default 'number'
|
|
59
|
-
*/
|
|
60
|
-
integerType?: 'number' | 'bigint'
|
|
61
|
-
/**
|
|
62
|
-
* AST type used when no schema type can be inferred.
|
|
63
|
-
* @default 'any'
|
|
64
|
-
*/
|
|
65
|
-
unknownType?: 'any' | 'unknown' | 'void'
|
|
66
|
-
/**
|
|
67
|
-
* AST type used for completely empty schemas (`{}`).
|
|
68
|
-
* @default `unknownType`
|
|
69
|
-
*/
|
|
70
|
-
emptySchemaType?: 'any' | 'unknown' | 'void'
|
|
71
|
-
}
|
|
86
|
+
} & Partial<ParserOptions>
|
|
72
87
|
|
|
73
88
|
export type OasAdapterResolvedOptions = {
|
|
74
89
|
validate: boolean
|
|
@@ -82,6 +97,7 @@ export type OasAdapterResolvedOptions = {
|
|
|
82
97
|
integerType: NonNullable<OasAdapterOptions['integerType']>
|
|
83
98
|
unknownType: NonNullable<OasAdapterOptions['unknownType']>
|
|
84
99
|
emptySchemaType: NonNullable<OasAdapterOptions['emptySchemaType']>
|
|
100
|
+
enumSuffix: OasAdapterOptions['enumSuffix']
|
|
85
101
|
/**
|
|
86
102
|
* Map from original `$ref` paths to their collision-resolved schema names.
|
|
87
103
|
* Populated by the adapter after each `parse()` call.
|