@kubb/adapter-oas 5.0.0-alpha.9 → 5.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1299 -1242
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +522 -144
- package/dist/index.js +1290 -1236
- package/dist/index.js.map +1 -1
- package/package.json +32 -35
- package/src/adapter.ts +82 -78
- package/src/constants.ts +77 -69
- package/src/discriminator.ts +108 -0
- package/src/factory.ts +165 -0
- package/src/guards.ts +68 -0
- package/src/index.ts +17 -0
- package/src/parser.ts +565 -655
- package/src/refs.ts +59 -0
- package/src/resolvers.ts +544 -0
- package/src/types.ts +172 -59
- package/src/oas/Oas.ts +0 -623
- package/src/oas/resolveServerUrl.ts +0 -47
- package/src/oas/types.ts +0 -71
- package/src/oas/utils.ts +0 -402
- package/src/utils.ts +0 -168
package/src/oas/types.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// external packages
|
|
2
|
-
import type { Operation as OASOperation } from 'oas/operation'
|
|
3
|
-
import type {
|
|
4
|
-
DiscriminatorObject as OASDiscriminatorObject,
|
|
5
|
-
OASDocument,
|
|
6
|
-
HttpMethods as OASHttpMethods,
|
|
7
|
-
MediaTypeObject as OASMediaTypeObject,
|
|
8
|
-
ResponseObject as OASResponseObject,
|
|
9
|
-
SchemaObject as OASSchemaObject,
|
|
10
|
-
} from 'oas/types'
|
|
11
|
-
import type { OpenAPIV3 } from 'openapi-types'
|
|
12
|
-
|
|
13
|
-
export type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
|
|
14
|
-
|
|
15
|
-
export type contentType = 'application/json' | (string & {})
|
|
16
|
-
|
|
17
|
-
export type SchemaObject = OASSchemaObject & {
|
|
18
|
-
/**
|
|
19
|
-
* OAS 3.1 extension: allows marking a schema as nullable even when `type` does not include `'null'`.
|
|
20
|
-
*/
|
|
21
|
-
'x-nullable'?: boolean
|
|
22
|
-
/**
|
|
23
|
-
* OAS 3.1: constrains the schema to a single fixed value.
|
|
24
|
-
* Semantically equivalent to a one-item `enum`.
|
|
25
|
-
*/
|
|
26
|
-
const?: string | number | boolean | null
|
|
27
|
-
/**
|
|
28
|
-
* OAS 3.1: specifies the media type of the schema content.
|
|
29
|
-
* When set to `'application/octet-stream'` on a `string` schema, the schema is treated as binary (`blob`).
|
|
30
|
-
*/
|
|
31
|
-
contentMediaType?: string
|
|
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>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Re-exported from `constants.ts` for backwards compatibility.
|
|
56
|
-
*/
|
|
57
|
-
export { httpMethods as HttpMethods } from '../constants.ts'
|
|
58
|
-
|
|
59
|
-
export type HttpMethod = OASHttpMethods
|
|
60
|
-
|
|
61
|
-
export type Document = OASDocument
|
|
62
|
-
|
|
63
|
-
export type Operation = OASOperation
|
|
64
|
-
|
|
65
|
-
export type DiscriminatorObject = OASDiscriminatorObject
|
|
66
|
-
|
|
67
|
-
export type ReferenceObject = OpenAPIV3.ReferenceObject
|
|
68
|
-
|
|
69
|
-
export type ResponseObject = OASResponseObject
|
|
70
|
-
|
|
71
|
-
export type MediaTypeObject = OASMediaTypeObject
|
package/src/oas/utils.ts
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { pascalCase, URLPath } from '@internals/utils'
|
|
3
|
-
import type { Config } from '@kubb/core'
|
|
4
|
-
import { bundle, loadConfig } from '@redocly/openapi-core'
|
|
5
|
-
import yaml from '@stoplight/yaml'
|
|
6
|
-
import { isRef } from 'oas/types'
|
|
7
|
-
import OASNormalize from 'oas-normalize'
|
|
8
|
-
import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
|
|
9
|
-
import { isPlainObject, mergeDeep } from 'remeda'
|
|
10
|
-
import swagger2openapi from 'swagger2openapi'
|
|
11
|
-
import { MERGE_DEFAULT_TITLE, MERGE_DEFAULT_VERSION, MERGE_OPENAPI_VERSION, structuralKeys } from '../constants.ts'
|
|
12
|
-
import { Oas } from './Oas.ts'
|
|
13
|
-
import type { contentType, Document, SchemaObject } from './types.ts'
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Narrows `doc` to a Swagger 2.0 document.
|
|
17
|
-
* Swagger 2.0 documents do not have an `openapi` version key.
|
|
18
|
-
*/
|
|
19
|
-
export function isOpenApiV2Document(doc: unknown): doc is OpenAPIV2.Document {
|
|
20
|
-
return !!doc && isPlainObject(doc) && !('openapi' in doc)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Narrows `doc` to an OpenAPI 3.x document.
|
|
25
|
-
* Any document that has an `openapi` key is considered 3.x (including 3.1).
|
|
26
|
-
*/
|
|
27
|
-
export function isOpenApiV3Document(doc: unknown): doc is OpenAPIV3.Document {
|
|
28
|
-
return !!doc && isPlainObject(doc) && 'openapi' in doc
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Narrows `doc` to an OpenAPI 3.1 document by checking the version prefix.
|
|
33
|
-
*/
|
|
34
|
-
export function isOpenApiV3_1Document(doc: unknown): doc is OpenAPIV3_1.Document {
|
|
35
|
-
return !!doc && isPlainObject(doc) && 'openapi' in (doc as object) && (doc as { openapi: string }).openapi.startsWith('3.1')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Returns `true` when a schema should be treated as nullable.
|
|
40
|
-
*
|
|
41
|
-
* Covers three nullable signals across OAS versions:
|
|
42
|
-
* - OAS 3.0: `nullable: true` or the vendor extension `x-nullable: true`.
|
|
43
|
-
* - OAS 3.1 / JSON Schema: `type: 'null'` or `type: ['null', ...]` (multi-type array).
|
|
44
|
-
*/
|
|
45
|
-
export function isNullable(schema?: SchemaObject & { 'x-nullable'?: boolean }): boolean {
|
|
46
|
-
const explicitNullable = schema?.nullable ?? schema?.['x-nullable']
|
|
47
|
-
if (explicitNullable === true) return true
|
|
48
|
-
|
|
49
|
-
const schemaType = schema?.type
|
|
50
|
-
if (schemaType === 'null') return true
|
|
51
|
-
if (Array.isArray(schemaType)) return schemaType.includes('null')
|
|
52
|
-
|
|
53
|
-
return false
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Narrows `obj` to an OpenAPI `$ref` pointer object.
|
|
58
|
-
* Delegates to the `oas` package's own `isRef` helper.
|
|
59
|
-
*/
|
|
60
|
-
export function isReference(obj?: unknown): obj is OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject {
|
|
61
|
-
return !!obj && isRef(obj as object)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Returns `true` when `obj` is a schema that carries a structured OAS 3.x `discriminator`
|
|
66
|
-
* object — as opposed to a plain-string discriminator found in some Swagger 2 specs.
|
|
67
|
-
*/
|
|
68
|
-
export function isDiscriminator(obj?: unknown): obj is SchemaObject & { discriminator: OpenAPIV3.DiscriminatorObject } {
|
|
69
|
-
const record = obj as Record<string, unknown>
|
|
70
|
-
return !!obj && !!record['discriminator'] && typeof record['discriminator'] !== 'string'
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Loads, dereferences, and wraps a raw OpenAPI document into an `Oas` instance.
|
|
75
|
-
*
|
|
76
|
-
* When given a file path string with `canBundle: true` (the default), Redocly's
|
|
77
|
-
* bundler resolves all external `$ref`s first. Swagger 2.0 documents are
|
|
78
|
-
* automatically up-converted to OpenAPI 3.0 via `swagger2openapi`.
|
|
79
|
-
*/
|
|
80
|
-
export async function parse(
|
|
81
|
-
pathOrApi: string | Document,
|
|
82
|
-
{ oasClass = Oas, canBundle = true, enablePaths = true }: { oasClass?: typeof Oas; canBundle?: boolean; enablePaths?: boolean } = {},
|
|
83
|
-
): Promise<Oas> {
|
|
84
|
-
if (typeof pathOrApi === 'string' && canBundle) {
|
|
85
|
-
const config = await loadConfig()
|
|
86
|
-
const bundleResults = await bundle({ ref: pathOrApi, config, base: pathOrApi })
|
|
87
|
-
|
|
88
|
-
return parse(bundleResults.bundle.parsed as string, { oasClass, canBundle, enablePaths })
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const oasNormalize = new OASNormalize(pathOrApi, {
|
|
92
|
-
enablePaths,
|
|
93
|
-
colorizeErrors: true,
|
|
94
|
-
})
|
|
95
|
-
const document = (await oasNormalize.load()) as Document
|
|
96
|
-
|
|
97
|
-
if (isOpenApiV2Document(document)) {
|
|
98
|
-
const { openapi } = await swagger2openapi.convertObj(document, {
|
|
99
|
-
anchors: true,
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
return new oasClass(openapi as Document)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return new oasClass(document)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Deep-merges multiple OpenAPI documents into a single `Oas` instance.
|
|
110
|
-
*
|
|
111
|
-
* Each document is parsed independently (without path dereferencing) and then
|
|
112
|
-
* recursively merged using `remeda`'s `mergeDeep`. The result is re-parsed so
|
|
113
|
-
* the returned `Oas` instance is fully initialized.
|
|
114
|
-
*
|
|
115
|
-
* Throws when the input array is empty — at least one document is required.
|
|
116
|
-
*/
|
|
117
|
-
export async function merge(pathOrApi: Array<string | Document>, { oasClass = Oas }: { oasClass?: typeof Oas } = {}): Promise<Oas> {
|
|
118
|
-
const instances = await Promise.all(pathOrApi.map((p) => parse(p, { oasClass, enablePaths: false, canBundle: false })))
|
|
119
|
-
|
|
120
|
-
if (instances.length === 0) {
|
|
121
|
-
throw new Error('No OAS instances provided for merging.')
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const seed: Document = {
|
|
125
|
-
openapi: MERGE_OPENAPI_VERSION,
|
|
126
|
-
info: { title: MERGE_DEFAULT_TITLE, version: MERGE_DEFAULT_VERSION },
|
|
127
|
-
paths: {},
|
|
128
|
-
components: { schemas: {} },
|
|
129
|
-
} as Document
|
|
130
|
-
|
|
131
|
-
const merged = instances.reduce((acc, current) => mergeDeep(acc, current.document as Document), seed)
|
|
132
|
-
|
|
133
|
-
return parse(merged, { oasClass })
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Constructs an `Oas` instance from a Kubb `Config` object.
|
|
138
|
-
*
|
|
139
|
-
* Handles all three input forms supported by `Config`:
|
|
140
|
-
* - `data` — an inline YAML string, JSON string, or pre-parsed object.
|
|
141
|
-
* - Array — multiple file paths that are deep-merged via {@link merge}.
|
|
142
|
-
* - `path` — a local file path or a remote URL.
|
|
143
|
-
*/
|
|
144
|
-
export function parseFromConfig(config: Config, oasClass: typeof Oas = Oas): Promise<Oas> {
|
|
145
|
-
if ('data' in config.input) {
|
|
146
|
-
if (typeof config.input.data === 'object') {
|
|
147
|
-
return parse(structuredClone(config.input.data) as Document, { oasClass })
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const api: string = yaml.parse(config.input.data as string)
|
|
152
|
-
return parse(api, { oasClass })
|
|
153
|
-
} catch {
|
|
154
|
-
return parse(config.input.data as string, { oasClass })
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (Array.isArray(config.input)) {
|
|
159
|
-
return merge(
|
|
160
|
-
config.input.map((input) => path.resolve(config.root, input.path)),
|
|
161
|
-
{ oasClass },
|
|
162
|
-
)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (new URLPath(config.input.path).isURL) {
|
|
166
|
-
return parse(config.input.path, { oasClass })
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return parse(path.resolve(config.root, config.input.path), { oasClass })
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Flattens a single-member or keyword-only `allOf` into its parent schema.
|
|
174
|
-
*
|
|
175
|
-
* Flattening is only performed when every `allOf` member is a "plain fragment" —
|
|
176
|
-
* i.e. contains no structural composition keywords (see `structuralKeys`) and no
|
|
177
|
-
* `$ref`. When any member carries structural meaning, the schema is returned unchanged.
|
|
178
|
-
*
|
|
179
|
-
* Keyword values from allOf members are merged into the parent on a first-write basis:
|
|
180
|
-
* outer schema values always win over fragment values.
|
|
181
|
-
*/
|
|
182
|
-
export function flattenSchema(schema: SchemaObject | null): SchemaObject | null {
|
|
183
|
-
if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null
|
|
184
|
-
if (schema.allOf.some((item) => isRef(item))) return schema
|
|
185
|
-
|
|
186
|
-
const isPlainFragment = (item: SchemaObject) => !Object.keys(item).some((key) => structuralKeys.has(key as 'properties'))
|
|
187
|
-
if (!schema.allOf.every((item) => isPlainFragment(item as SchemaObject))) return schema
|
|
188
|
-
|
|
189
|
-
const merged: SchemaObject = { ...schema }
|
|
190
|
-
delete merged.allOf
|
|
191
|
-
|
|
192
|
-
for (const fragment of schema.allOf as SchemaObject[]) {
|
|
193
|
-
for (const [key, value] of Object.entries(fragment)) {
|
|
194
|
-
if (merged[key as keyof typeof merged] === undefined) {
|
|
195
|
-
merged[key as keyof typeof merged] = value
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return merged
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Validates an OpenAPI document using `oas-normalize`.
|
|
205
|
-
* Enables path validation and colorized error output.
|
|
206
|
-
*/
|
|
207
|
-
export async function validate(document: Document) {
|
|
208
|
-
const oasNormalize = new OASNormalize(document, {
|
|
209
|
-
enablePaths: true,
|
|
210
|
-
colorizeErrors: true,
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
return oasNormalize.validate({
|
|
214
|
-
parser: {
|
|
215
|
-
validate: {
|
|
216
|
-
errors: { colorize: true },
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
})
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Discriminates the three component sources Kubb reads schemas from.
|
|
224
|
-
*/
|
|
225
|
-
type SchemaSourceMode = 'schemas' | 'responses' | 'requestBodies'
|
|
226
|
-
|
|
227
|
-
export type SchemaWithMetadata = {
|
|
228
|
-
schema: SchemaObject
|
|
229
|
-
source: SchemaSourceMode
|
|
230
|
-
originalName: string
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
type GetSchemasResult = {
|
|
234
|
-
schemas: Record<string, SchemaObject>
|
|
235
|
-
nameMapping: Map<string, string>
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` refs.
|
|
240
|
-
* Used by `sortSchemas` to build the dependency graph.
|
|
241
|
-
*/
|
|
242
|
-
function collectRefs(schema: unknown, refs = new Set<string>()): Set<string> {
|
|
243
|
-
if (Array.isArray(schema)) {
|
|
244
|
-
for (const item of schema) collectRefs(item, refs)
|
|
245
|
-
return refs
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (schema && typeof schema === 'object') {
|
|
249
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
250
|
-
if (key === '$ref' && typeof value === 'string') {
|
|
251
|
-
const match = value.match(/^#\/components\/schemas\/(.+)$/)
|
|
252
|
-
if (match) refs.add(match[1]!)
|
|
253
|
-
} else {
|
|
254
|
-
collectRefs(value, refs)
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return refs
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
264
|
-
*
|
|
265
|
-
* Schemas with no references come first; schemas that are referenced by others
|
|
266
|
-
* come before those that reference them. This ensures code generators emit
|
|
267
|
-
* referenced types before the types that depend on them.
|
|
268
|
-
*
|
|
269
|
-
* Cycles are silently skipped via the `stack` guard inside `visit`.
|
|
270
|
-
*/
|
|
271
|
-
export function sortSchemas(schemas: Record<string, SchemaObject>): Record<string, SchemaObject> {
|
|
272
|
-
const deps = new Map<string, string[]>()
|
|
273
|
-
|
|
274
|
-
for (const [name, schema] of Object.entries(schemas)) {
|
|
275
|
-
deps.set(name, Array.from(collectRefs(schema)))
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const sorted: string[] = []
|
|
279
|
-
const visited = new Set<string>()
|
|
280
|
-
|
|
281
|
-
function visit(name: string, stack: Set<string>) {
|
|
282
|
-
if (visited.has(name) || stack.has(name)) return
|
|
283
|
-
stack.add(name)
|
|
284
|
-
for (const child of deps.get(name) ?? []) {
|
|
285
|
-
if (deps.has(child)) visit(child, stack)
|
|
286
|
-
}
|
|
287
|
-
stack.delete(name)
|
|
288
|
-
visited.add(name)
|
|
289
|
-
sorted.push(name)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
for (const name of Object.keys(schemas)) {
|
|
293
|
-
visit(name, new Set())
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const result: Record<string, SchemaObject> = {}
|
|
297
|
-
for (const name of sorted) result[name] = schemas[name]!
|
|
298
|
-
return result
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Extracts the inline schema from a media-type `content` map.
|
|
303
|
-
*
|
|
304
|
-
* Prefers `preferredContentType` when provided; otherwise falls back to the
|
|
305
|
-
* first key in the map. Returns `null` when:
|
|
306
|
-
* - `content` is absent.
|
|
307
|
-
* - The target content-type has no `schema` entry.
|
|
308
|
-
* - The schema is a `$ref` (callers resolve refs separately).
|
|
309
|
-
*/
|
|
310
|
-
export function extractSchemaFromContent(content: Record<string, unknown> | undefined, preferredContentType?: contentType): SchemaObject | null {
|
|
311
|
-
if (!content) return null
|
|
312
|
-
|
|
313
|
-
const firstContentType = Object.keys(content)[0] ?? 'application/json'
|
|
314
|
-
const targetContentType = preferredContentType ?? firstContentType
|
|
315
|
-
const contentSchema = content[targetContentType] as { schema?: SchemaObject } | undefined
|
|
316
|
-
const schema = contentSchema?.schema
|
|
317
|
-
|
|
318
|
-
if (schema && '$ref' in schema) return null
|
|
319
|
-
return schema ?? null
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Returns the PascalCase suffix appended to a component name when resolving
|
|
324
|
-
* cross-source name collisions (schemas vs. responses vs. requestBodies).
|
|
325
|
-
*/
|
|
326
|
-
function getSemanticSuffix(source: SchemaSourceMode): string {
|
|
327
|
-
switch (source) {
|
|
328
|
-
case 'schemas':
|
|
329
|
-
return 'Schema'
|
|
330
|
-
case 'responses':
|
|
331
|
-
return 'Response'
|
|
332
|
-
case 'requestBodies':
|
|
333
|
-
return 'Request'
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Builds `GetSchemasResult` without any collision detection.
|
|
339
|
-
* Each schema is registered under its original component name and its full
|
|
340
|
-
* `#/components/<source>/<name>` ref path.
|
|
341
|
-
*/
|
|
342
|
-
export function legacyResolve(schemasWithMeta: SchemaWithMetadata[]): GetSchemasResult {
|
|
343
|
-
const schemas: Record<string, SchemaObject> = {}
|
|
344
|
-
const nameMapping = new Map<string, string>()
|
|
345
|
-
|
|
346
|
-
for (const item of schemasWithMeta) {
|
|
347
|
-
schemas[item.originalName] = item.schema
|
|
348
|
-
nameMapping.set(`#/components/${item.source}/${item.originalName}`, item.originalName)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return { schemas, nameMapping }
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Builds `GetSchemasResult` with automatic name-collision resolution.
|
|
356
|
-
*
|
|
357
|
-
* When two or more schemas normalize to the same PascalCase name:
|
|
358
|
-
* - If they share the same source, a numeric suffix (`2`, `3`, …) is appended.
|
|
359
|
-
* - If they come from different sources (schemas / responses / requestBodies),
|
|
360
|
-
* a semantic suffix (`Schema`, `Response`, `Request`) is appended.
|
|
361
|
-
*
|
|
362
|
-
* Non-colliding schemas are left unchanged.
|
|
363
|
-
*/
|
|
364
|
-
export function resolveCollisions(schemasWithMeta: SchemaWithMetadata[]): GetSchemasResult {
|
|
365
|
-
const schemas: Record<string, SchemaObject> = {}
|
|
366
|
-
const nameMapping = new Map<string, string>()
|
|
367
|
-
const normalizedNames = new Map<string, SchemaWithMetadata[]>()
|
|
368
|
-
|
|
369
|
-
for (const item of schemasWithMeta) {
|
|
370
|
-
const normalized = pascalCase(item.originalName)
|
|
371
|
-
const bucket = normalizedNames.get(normalized) ?? []
|
|
372
|
-
bucket.push(item)
|
|
373
|
-
normalizedNames.set(normalized, bucket)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
for (const [, items] of normalizedNames) {
|
|
377
|
-
if (items.length === 1) {
|
|
378
|
-
const item = items[0]!
|
|
379
|
-
schemas[item.originalName] = item.schema
|
|
380
|
-
nameMapping.set(`#/components/${item.source}/${item.originalName}`, item.originalName)
|
|
381
|
-
continue
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const sources = new Set(items.map((item) => item.source))
|
|
385
|
-
|
|
386
|
-
if (sources.size === 1) {
|
|
387
|
-
items.forEach((item, index) => {
|
|
388
|
-
const uniqueName = item.originalName + (index === 0 ? '' : (index + 1).toString())
|
|
389
|
-
schemas[uniqueName] = item.schema
|
|
390
|
-
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName)
|
|
391
|
-
})
|
|
392
|
-
} else {
|
|
393
|
-
items.forEach((item) => {
|
|
394
|
-
const uniqueName = item.originalName + getSemanticSuffix(item.source)
|
|
395
|
-
schemas[uniqueName] = item.schema
|
|
396
|
-
nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName)
|
|
397
|
-
})
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return { schemas, nameMapping }
|
|
402
|
-
}
|
package/src/utils.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { collect, createProperty, createSchema, narrowSchema } from '@kubb/ast'
|
|
2
|
-
import type { SchemaNode } from '@kubb/ast/types'
|
|
3
|
-
import type { KubbFile } from '@kubb/fabric-core/types'
|
|
4
|
-
import { SCALAR_PRIMITIVE_TYPES } from './constants.ts'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Extracts the schema name from a `$ref` string.
|
|
8
|
-
* For `#/components/schemas/Order` this returns `'Order'`.
|
|
9
|
-
* Falls back to the full ref string when no slash is present.
|
|
10
|
-
*/
|
|
11
|
-
export function extractRefName($ref: string): string {
|
|
12
|
-
return $ref.split('/').at(-1) ?? $ref
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Replaces the discriminator property's schema inside an `ObjectSchemaNode`
|
|
17
|
-
* with an enum of the given `values`.
|
|
18
|
-
*
|
|
19
|
-
* - When `enumName` is provided the enum is named, which lets printers emit a
|
|
20
|
-
* standalone enum declaration + type reference (e.g. `PetTypeEnum`).
|
|
21
|
-
* - When `enumName` is omitted the enum stays anonymous, so printers inline it
|
|
22
|
-
* as a literal union (e.g. `'dog'`).
|
|
23
|
-
*
|
|
24
|
-
* Returns the node unchanged when it is not an object or lacks the target property.
|
|
25
|
-
*/
|
|
26
|
-
export function applyDiscriminatorEnum({
|
|
27
|
-
node,
|
|
28
|
-
propertyName,
|
|
29
|
-
values,
|
|
30
|
-
enumName,
|
|
31
|
-
}: {
|
|
32
|
-
node: SchemaNode
|
|
33
|
-
propertyName: string
|
|
34
|
-
values: Array<string>
|
|
35
|
-
enumName?: string
|
|
36
|
-
}): SchemaNode {
|
|
37
|
-
if (node.type !== 'object' || !node.properties?.length) return node
|
|
38
|
-
|
|
39
|
-
const hasProperty = node.properties.some((prop) => prop.name === propertyName)
|
|
40
|
-
if (!hasProperty) return node
|
|
41
|
-
|
|
42
|
-
return createSchema({
|
|
43
|
-
...node,
|
|
44
|
-
properties: node.properties.map((prop) => {
|
|
45
|
-
if (prop.name !== propertyName) return prop
|
|
46
|
-
|
|
47
|
-
const enumSchema: SchemaNode = createSchema({
|
|
48
|
-
type: 'enum' as const,
|
|
49
|
-
primitive: 'string' as const,
|
|
50
|
-
enumValues: values,
|
|
51
|
-
name: enumName,
|
|
52
|
-
readOnly: prop.schema.readOnly,
|
|
53
|
-
writeOnly: prop.schema.writeOnly,
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
return createProperty({
|
|
57
|
-
...prop,
|
|
58
|
-
schema: enumSchema,
|
|
59
|
-
})
|
|
60
|
-
}),
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Merges consecutive anonymous (`name`-less) `ObjectSchemaNode`s in `members` into a
|
|
66
|
-
* single object by combining their `properties` arrays.
|
|
67
|
-
*
|
|
68
|
-
* Only adjacent pairs are merged — non-object or named nodes act as boundaries.
|
|
69
|
-
* This collapses patterns like `Address & { streetNumber } & { streetName }` into
|
|
70
|
-
* `Address & { streetNumber; streetName }`.
|
|
71
|
-
*/
|
|
72
|
-
export function mergeAdjacentAnonymousObjects(members: Array<SchemaNode>): Array<SchemaNode> {
|
|
73
|
-
return members.reduce<Array<SchemaNode>>((acc, member) => {
|
|
74
|
-
const obj = narrowSchema(member, 'object')
|
|
75
|
-
if (obj && !obj.name) {
|
|
76
|
-
const prev = acc[acc.length - 1]
|
|
77
|
-
const prevObj = prev ? narrowSchema(prev, 'object') : null
|
|
78
|
-
if (prevObj && !prevObj.name) {
|
|
79
|
-
acc[acc.length - 1] = createSchema({
|
|
80
|
-
...prevObj,
|
|
81
|
-
properties: [...(prevObj.properties ?? []), ...(obj.properties ?? [])],
|
|
82
|
-
}) as SchemaNode
|
|
83
|
-
return acc
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
acc.push(member)
|
|
87
|
-
return acc
|
|
88
|
-
}, [])
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Simplifies a union member list by removing `enum` nodes whose `primitive` type is
|
|
93
|
-
* already represented by a broader scalar node in the same union.
|
|
94
|
-
*
|
|
95
|
-
* For example `['placed', 'approved'] | string` collapses to `string` because
|
|
96
|
-
* `string` subsumes all string literals. `'' | string` similarly becomes `string`.
|
|
97
|
-
*
|
|
98
|
-
* Only scalar primitives (`string`, `number`, `integer`, `bigint`, `boolean`) are
|
|
99
|
-
* considered — object, array, and ref members are left untouched.
|
|
100
|
-
*
|
|
101
|
-
* Const-derived enums (those without an `enumType`, produced from an OpenAPI `const`
|
|
102
|
-
* keyword) are **never** removed — `'accepted' | string` must stay as-is because the
|
|
103
|
-
* literal is intentional.
|
|
104
|
-
*/
|
|
105
|
-
export function simplifyUnionMembers(members: Array<SchemaNode>): Array<SchemaNode> {
|
|
106
|
-
const scalarPrimitives = new Set(members.filter((m) => SCALAR_PRIMITIVE_TYPES.has(m.type as 'string')).map((m) => m.type as string))
|
|
107
|
-
if (!scalarPrimitives.size) return members
|
|
108
|
-
|
|
109
|
-
return members.filter((m) => {
|
|
110
|
-
if (m.type !== 'enum') return true
|
|
111
|
-
const prim = m.primitive
|
|
112
|
-
// Keep the enum if its primitive isn't fully subsumed.
|
|
113
|
-
if (!prim) return true
|
|
114
|
-
// Const-derived enums have no `enumType`; keep them as intentional literals.
|
|
115
|
-
if (!m.enumType) return true
|
|
116
|
-
// `number` subsumes `integer` literals and vice-versa for our purposes.
|
|
117
|
-
if (scalarPrimitives.has(prim)) return false
|
|
118
|
-
if ((prim === 'integer' || prim === 'number') && (scalarPrimitives.has('integer') || scalarPrimitives.has('number'))) return false
|
|
119
|
-
return true
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* `nameMapping`, and calls `resolve` to obtain the `{ name, path }` pair for
|
|
125
|
-
* each import. When `oas` is supplied, only `$ref`s that are resolvable in the
|
|
126
|
-
* spec are included; omit it to skip the existence check.
|
|
127
|
-
*
|
|
128
|
-
* This function is the pure, state-free alternative to `OasParser.getImports`.
|
|
129
|
-
* Because it receives `nameMapping` explicitly it can be called without holding
|
|
130
|
-
* a reference to the parser or the OAS instance.
|
|
131
|
-
*
|
|
132
|
-
* @example
|
|
133
|
-
* ```ts
|
|
134
|
-
* // Use adapter state directly — no parser reference needed
|
|
135
|
-
* const imports = getImports({
|
|
136
|
-
* node: schemaNode,
|
|
137
|
-
* nameMapping: adapter.options.nameMapping,
|
|
138
|
-
* resolve: (schemaName) => ({
|
|
139
|
-
* name: schemaManager.getName(schemaName, { type: 'type' }),
|
|
140
|
-
* path: schemaManager.getFile(schemaName).path,
|
|
141
|
-
* }),
|
|
142
|
-
* })
|
|
143
|
-
* ```
|
|
144
|
-
*/
|
|
145
|
-
export function getImports({
|
|
146
|
-
node,
|
|
147
|
-
nameMapping,
|
|
148
|
-
resolve,
|
|
149
|
-
}: {
|
|
150
|
-
node: SchemaNode
|
|
151
|
-
nameMapping: Map<string, string>
|
|
152
|
-
resolve: (schemaName: string) => { name: string; path: string } | undefined
|
|
153
|
-
}): Array<KubbFile.Import> {
|
|
154
|
-
return collect<KubbFile.Import>(node, {
|
|
155
|
-
schema(schemaNode): KubbFile.Import | undefined {
|
|
156
|
-
if (schemaNode.type !== 'ref' || !schemaNode.ref) return
|
|
157
|
-
|
|
158
|
-
const rawName = extractRefName(schemaNode.ref)
|
|
159
|
-
|
|
160
|
-
// Apply collision-resolved name if available.
|
|
161
|
-
const schemaName = nameMapping.get(rawName) ?? rawName
|
|
162
|
-
const result = resolve(schemaName)
|
|
163
|
-
if (!result) return
|
|
164
|
-
|
|
165
|
-
return { name: [result.name], path: result.path }
|
|
166
|
-
},
|
|
167
|
-
})
|
|
168
|
-
}
|