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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/oas/types.ts DELETED
@@ -1,77 +0,0 @@
1
- // external packages
2
-
3
- import { httpMethods } from '@kubb/ast'
4
- import type { HttpMethod as AstHttpMethod } from '@kubb/ast/types'
5
- import type { Operation as OASOperation } from 'oas/operation'
6
- import type {
7
- DiscriminatorObject as OASDiscriminatorObject,
8
- OASDocument,
9
- MediaTypeObject as OASMediaTypeObject,
10
- ResponseObject as OASResponseObject,
11
- SchemaObject as OASSchemaObject,
12
- } from 'oas/types'
13
- import type { OpenAPIV3 } from 'openapi-types'
14
-
15
- export type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
16
-
17
- export type contentType = 'application/json' | (string & {})
18
-
19
- export type SchemaObject = OASSchemaObject & {
20
- /**
21
- * OAS 3.1 extension: allows marking a schema as nullable even when `type` does not include `'null'`.
22
- */
23
- 'x-nullable'?: boolean
24
- /**
25
- * OAS 3.1: constrains the schema to a single fixed value.
26
- * Semantically equivalent to a one-item `enum`.
27
- */
28
- const?: string | number | boolean | null
29
- /**
30
- * OAS 3.1: specifies the media type of the schema content.
31
- * When set to `'application/octet-stream'` on a `string` schema, the schema is treated as binary (`blob`).
32
- */
33
- contentMediaType?: string
34
- $ref?: string
35
- /**
36
- * OAS 3.1 / JSON Schema: positional items in a tuple schema.
37
- * Replaces the OAS 3.0 multi-item `items` array syntax.
38
- */
39
- prefixItems?: Array<SchemaObject | ReferenceObject>
40
- /**
41
- * JSON Schema: maps regex patterns to sub-schemas for additional property validation.
42
- */
43
- patternProperties?: Record<string, SchemaObject | boolean>
44
- /**
45
- * OAS 3.0 / JSON Schema: single-schema form.
46
- * The OAS base type already includes this, but we re-declare it here to ensure
47
- * the single-schema overload takes precedence over the multi-schema tuple form.
48
- */
49
- items?: SchemaObject | ReferenceObject
50
- /**
51
- * Enum values for this schema (narrowed from `unknown[]` in the base type).
52
- */
53
- enum?: Array<string | number | boolean | null>
54
- }
55
-
56
- /**
57
- * Canonical uppercase->lowercase HTTP method map.
58
- * Re-exported for backwards compatibility with previous adapter-oas API.
59
- */
60
- export const HttpMethods = Object.fromEntries(Object.entries(httpMethods).map(([lower, upper]) => [upper, lower])) as Record<
61
- Uppercase<AstHttpMethod>,
62
- Lowercase<AstHttpMethod>
63
- >
64
-
65
- export type HttpMethod = Lowercase<AstHttpMethod>
66
-
67
- export type Document = OASDocument
68
-
69
- export type Operation = OASOperation
70
-
71
- export type DiscriminatorObject = OASDiscriminatorObject
72
-
73
- export type ReferenceObject = OpenAPIV3.ReferenceObject
74
-
75
- export type ResponseObject = OASResponseObject
76
-
77
- export type MediaTypeObject = OASMediaTypeObject
package/src/oas/utils.ts DELETED
@@ -1,401 +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
- const semanticSuffixes: Record<SchemaSourceMode, string> = {
327
- schemas: 'Schema',
328
- responses: 'Response',
329
- requestBodies: 'Request',
330
- }
331
-
332
- function getSemanticSuffix(source: SchemaSourceMode): string {
333
- return semanticSuffixes[source]
334
- }
335
-
336
- /**
337
- * Builds `GetSchemasResult` without any collision detection.
338
- * Each schema is registered under its original component name and its full
339
- * `#/components/<source>/<name>` ref path.
340
- */
341
- export function legacyResolve(schemasWithMeta: SchemaWithMetadata[]): GetSchemasResult {
342
- const schemas: Record<string, SchemaObject> = {}
343
- const nameMapping = new Map<string, string>()
344
-
345
- for (const item of schemasWithMeta) {
346
- schemas[item.originalName] = item.schema
347
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, item.originalName)
348
- }
349
-
350
- return { schemas, nameMapping }
351
- }
352
-
353
- /**
354
- * Builds `GetSchemasResult` with automatic name-collision resolution.
355
- *
356
- * When two or more schemas normalize to the same PascalCase name:
357
- * - If they share the same source, a numeric suffix (`2`, `3`, …) is appended.
358
- * - If they come from different sources (schemas / responses / requestBodies),
359
- * a semantic suffix (`Schema`, `Response`, `Request`) is appended.
360
- *
361
- * Non-colliding schemas are left unchanged.
362
- */
363
- export function resolveCollisions(schemasWithMeta: SchemaWithMetadata[]): GetSchemasResult {
364
- const schemas: Record<string, SchemaObject> = {}
365
- const nameMapping = new Map<string, string>()
366
- const normalizedNames = new Map<string, SchemaWithMetadata[]>()
367
-
368
- for (const item of schemasWithMeta) {
369
- const normalized = pascalCase(item.originalName)
370
- const bucket = normalizedNames.get(normalized) ?? []
371
- bucket.push(item)
372
- normalizedNames.set(normalized, bucket)
373
- }
374
-
375
- for (const [, items] of normalizedNames) {
376
- if (items.length === 1) {
377
- const item = items[0]!
378
- schemas[item.originalName] = item.schema
379
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, item.originalName)
380
- continue
381
- }
382
-
383
- const sources = new Set(items.map((item) => item.source))
384
-
385
- if (sources.size === 1) {
386
- items.forEach((item, index) => {
387
- const uniqueName = item.originalName + (index === 0 ? '' : (index + 1).toString())
388
- schemas[uniqueName] = item.schema
389
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName)
390
- })
391
- } else {
392
- items.forEach((item) => {
393
- const uniqueName = item.originalName + getSemanticSuffix(item.source)
394
- schemas[uniqueName] = item.schema
395
- nameMapping.set(`#/components/${item.source}/${item.originalName}`, uniqueName)
396
- })
397
- }
398
- }
399
-
400
- return { schemas, nameMapping }
401
- }
@@ -1,65 +0,0 @@
1
- import { collect, extractRefName, narrowSchema, schemaTypes, transform } from '@kubb/ast'
2
- import type { SchemaNode } from '@kubb/ast/types'
3
- import type { KubbFile } from '@kubb/fabric-core/types'
4
-
5
- /**
6
- * Collects import entries for all `ref` schema nodes in `node`.
7
- */
8
- export function getImports({
9
- node,
10
- nameMapping,
11
- resolve,
12
- }: {
13
- node: SchemaNode
14
- nameMapping: Map<string, string>
15
- resolve: (schemaName: string) => { name: string; path: string } | undefined
16
- }): Array<KubbFile.Import> {
17
- return collect<KubbFile.Import>(node, {
18
- schema(schemaNode): KubbFile.Import | undefined {
19
- if (schemaNode.type !== 'ref' || !schemaNode.ref) return
20
-
21
- const rawName = extractRefName(schemaNode.ref)
22
- const schemaName = nameMapping.get(rawName) ?? rawName
23
- const result = resolve(schemaName)
24
- if (!result) return
25
-
26
- return { name: [result.name], path: result.path }
27
- },
28
- })
29
- }
30
-
31
- /**
32
- * Walks a schema tree and resolves `ref`/`enum` names through callbacks.
33
- */
34
- export function resolveRefs({
35
- node,
36
- nameMapping,
37
- resolveName,
38
- resolveEnumName,
39
- }: {
40
- node: SchemaNode
41
- nameMapping: Map<string, string>
42
- resolveName: (ref: string) => string | undefined
43
- resolveEnumName?: (name: string) => string | undefined
44
- }): SchemaNode {
45
- return transform(node, {
46
- schema(schemaNode) {
47
- const schemaRef = narrowSchema(schemaNode, schemaTypes.ref)
48
-
49
- if (schemaRef && (schemaRef.ref || schemaRef.name)) {
50
- const rawRef = schemaRef.ref ?? schemaRef.name!
51
- const resolved = resolveName(nameMapping.get(rawRef) ?? rawRef)
52
- if (resolved) {
53
- return { ...schemaNode, name: resolved }
54
- }
55
- }
56
-
57
- if (schemaNode.type === 'enum' && schemaNode.name) {
58
- const resolved = (resolveEnumName ?? resolveName)(schemaNode.name)
59
- if (resolved) {
60
- return { ...schemaNode, name: resolved }
61
- }
62
- }
63
- },
64
- }) as SchemaNode
65
- }