@kubb/adapter-oas 5.0.0-beta.35 → 5.0.0-beta.37
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 +159 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +160 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +2 -1
- package/src/constants.ts +8 -0
- package/src/dialect.ts +3 -3
- package/src/factory.ts +37 -8
- package/src/parser.ts +11 -16
- package/src/refs.ts +12 -1
- package/src/resolvers.ts +27 -10
- package/src/schemaDiagnostics.ts +59 -0
- package/src/stream.ts +5 -3
- package/src/types.ts +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/adapter-oas",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.37",
|
|
4
4
|
"description": "OpenAPI and Swagger adapter for Kubb. Parses and validates OAS 2.0/3.x specifications into a @kubb/ast RootNode for downstream code generation plugins.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adapter",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"oas": "^32.1.18",
|
|
48
48
|
"oas-normalize": "^16.0.4",
|
|
49
49
|
"swagger2openapi": "^7.0.8",
|
|
50
|
-
"@kubb/core": "5.0.0-beta.
|
|
50
|
+
"@kubb/core": "5.0.0-beta.37"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/swagger2openapi": "^7.0.4",
|
package/src/adapter.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ast, createAdapter } from '@kubb/core'
|
|
|
3
3
|
import type { AdapterSource } from '@kubb/core'
|
|
4
4
|
import BaseOas from 'oas'
|
|
5
5
|
import { DEFAULT_PARSER_OPTIONS } from './constants.ts'
|
|
6
|
-
import { parseDocument, parseFromConfig, validateDocument } from './factory.ts'
|
|
6
|
+
import { assertInputExists, parseDocument, parseFromConfig, validateDocument } from './factory.ts'
|
|
7
7
|
import { createSchemaParser } from './parser.ts'
|
|
8
8
|
import { getSchemas } from './resolvers.ts'
|
|
9
9
|
import { createInputStream, preScan, resolveBaseUrl } from './stream.ts'
|
|
@@ -144,6 +144,7 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
|
|
|
144
144
|
return parsedDocument
|
|
145
145
|
},
|
|
146
146
|
async validate(input, options) {
|
|
147
|
+
await assertInputExists(input)
|
|
147
148
|
const document = await parseDocument(input)
|
|
148
149
|
await validateDocument(document, options)
|
|
149
150
|
},
|
package/src/constants.ts
CHANGED
|
@@ -79,6 +79,14 @@ export const structuralKeys = new Set(['properties', 'items', 'additionalPropert
|
|
|
79
79
|
* formatMap['float'] // 'number'
|
|
80
80
|
* ```
|
|
81
81
|
*/
|
|
82
|
+
/**
|
|
83
|
+
* Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
|
|
84
|
+
* `int64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
85
|
+
* special-cases in `parser.ts`. `isHandledFormat` reads it so the
|
|
86
|
+
* `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
|
|
87
|
+
*/
|
|
88
|
+
export const specialCasedFormats: ReadonlySet<string> = new Set(['int64', 'date-time', 'date', 'time'])
|
|
89
|
+
|
|
82
90
|
export const formatMap = {
|
|
83
91
|
uuid: 'uuid',
|
|
84
92
|
email: 'email',
|
package/src/dialect.ts
CHANGED
|
@@ -4,13 +4,13 @@ import { resolveRef } from './refs.ts'
|
|
|
4
4
|
import type { SchemaObject } from './types.ts'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* The OpenAPI / Swagger dialect
|
|
7
|
+
* The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
|
|
8
8
|
*
|
|
9
9
|
* Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
|
|
10
10
|
* decisions that differ between specs (nullability, `$ref`, discriminator, binary,
|
|
11
11
|
* ref resolution) so the converter pipeline and dispatch rules stay shared. A
|
|
12
|
-
* future adapter (e.g. AsyncAPI) ships its own dialect
|
|
13
|
-
* nullability, no discriminator object, binary via `contentEncoding`
|
|
12
|
+
* future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
|
|
13
|
+
* nullability, no discriminator object, binary via `contentEncoding` and reuses
|
|
14
14
|
* the rest unchanged.
|
|
15
15
|
*
|
|
16
16
|
* Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
|
package/src/factory.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import { mergeDeep, URLPath } from '@internals/utils'
|
|
2
|
+
import { exists, mergeDeep, URLPath } from '@internals/utils'
|
|
3
|
+
import { diagnosticCode, DiagnosticError } from '@kubb/core'
|
|
3
4
|
import type { AdapterSource } from '@kubb/core'
|
|
4
5
|
import { bundle, loadConfig } from '@redocly/openapi-core'
|
|
5
6
|
import OASNormalize from 'oas-normalize'
|
|
@@ -77,7 +78,13 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
|
|
|
77
78
|
const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { enablePaths: false, canBundle: false })))
|
|
78
79
|
|
|
79
80
|
if (documents.length === 0) {
|
|
80
|
-
throw new
|
|
81
|
+
throw new DiagnosticError({
|
|
82
|
+
code: diagnosticCode.inputRequired,
|
|
83
|
+
severity: 'error',
|
|
84
|
+
message: 'No OAS documents were provided for merging.',
|
|
85
|
+
help: 'Pass at least one path or document to `input.path`.',
|
|
86
|
+
location: { kind: 'config' },
|
|
87
|
+
})
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
const seed: Document = {
|
|
@@ -99,9 +106,9 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
|
|
|
99
106
|
* Creates a `Document` from an `AdapterSource`.
|
|
100
107
|
*
|
|
101
108
|
* Handles all three source types:
|
|
102
|
-
* - `{ type: 'path' }`
|
|
103
|
-
* - `{ type: 'paths' }`
|
|
104
|
-
* - `{ type: 'data' }`
|
|
109
|
+
* - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
|
|
110
|
+
* - `{ type: 'paths' }` merges multiple file paths into a single document.
|
|
111
|
+
* - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
|
|
105
112
|
*
|
|
106
113
|
* @example
|
|
107
114
|
* ```ts
|
|
@@ -109,7 +116,7 @@ export async function mergeDocuments(pathOrApi: Array<string | Document>): Promi
|
|
|
109
116
|
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
110
117
|
* ```
|
|
111
118
|
*/
|
|
112
|
-
export function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
119
|
+
export async function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
113
120
|
if (source.type === 'data') {
|
|
114
121
|
if (typeof source.data === 'object') {
|
|
115
122
|
return parseDocument(structuredClone(source.data) as Document)
|
|
@@ -127,7 +134,29 @@ export function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
|
127
134
|
return parseDocument(source.path)
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
|
|
137
|
+
const resolved = path.resolve(path.dirname(source.path), source.path)
|
|
138
|
+
await assertInputExists(resolved)
|
|
139
|
+
return parseDocument(resolved)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
144
|
+
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
145
|
+
* its parse error instead.
|
|
146
|
+
*/
|
|
147
|
+
export async function assertInputExists(input: string): Promise<void> {
|
|
148
|
+
if (new URLPath(input).isURL) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
if (!(await exists(input))) {
|
|
152
|
+
throw new DiagnosticError({
|
|
153
|
+
code: diagnosticCode.inputNotFound,
|
|
154
|
+
severity: 'error',
|
|
155
|
+
message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
|
|
156
|
+
help: 'Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.',
|
|
157
|
+
location: { kind: 'config' },
|
|
158
|
+
})
|
|
159
|
+
}
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
/**
|
|
@@ -157,6 +186,6 @@ export async function validateDocument(document: Document, { throwOnError = fals
|
|
|
157
186
|
throw error
|
|
158
187
|
}
|
|
159
188
|
|
|
160
|
-
// Validation failures are non-fatal
|
|
189
|
+
// Validation failures are non-fatal, mirror plugin-oas behavior
|
|
161
190
|
}
|
|
162
191
|
}
|
package/src/parser.ts
CHANGED
|
@@ -60,7 +60,7 @@ type SchemaContext = {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* One entry in this adapter's schema-dispatch table
|
|
63
|
+
* One entry in this adapter's schema-dispatch table, a {@link ast.DispatchRule} specialized
|
|
64
64
|
* to OAS schema context and Kubb `SchemaNode` output. See {@link ast.dispatch} for the contract
|
|
65
65
|
* a future adapter (e.g. AsyncAPI) follows: define a context type and an ordered rules table.
|
|
66
66
|
*/
|
|
@@ -97,7 +97,7 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
|
|
|
97
97
|
export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect = oasDialect) {
|
|
98
98
|
const document = ctx.document
|
|
99
99
|
|
|
100
|
-
// Branch handlers
|
|
100
|
+
// Branch handlers, each converts one OAS schema pattern to a SchemaNode.
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* Tracks `$ref` paths that are currently being resolved to prevent infinite
|
|
@@ -106,16 +106,12 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
|
|
|
106
106
|
const resolvingRefs = new Set<string>()
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Cache of
|
|
109
|
+
* Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
|
|
110
110
|
*
|
|
111
|
-
* Without
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* each triggering a fresh recursive expansion of its entire sub-tree.
|
|
116
|
-
*
|
|
117
|
-
* Memoizing by `$ref` path reduces the overall work from O(2^depth) to O(N)
|
|
118
|
-
* where N is the number of unique schema names.
|
|
111
|
+
* Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
|
|
112
|
+
* it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
|
|
113
|
+
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
114
|
+
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
119
115
|
*/
|
|
120
116
|
const resolvedRefCache = new Map<string, ast.SchemaNode | null>()
|
|
121
117
|
|
|
@@ -253,7 +249,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
|
|
|
253
249
|
|
|
254
250
|
if (schema.properties) {
|
|
255
251
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema
|
|
256
|
-
// Don't pass `name` here
|
|
252
|
+
// Don't pass `name` here, the result must stay anonymous so it can be merged with the
|
|
257
253
|
// adjacent synthetic object in `mergeAdjacentObjectsLazy`. Nested enum qualification
|
|
258
254
|
// happens upstream via `convertObject`'s `setEnumName` propagation.
|
|
259
255
|
allOfMembers.push(parseSchema({ schema: schemaWithoutAllOf }, rawOptions))
|
|
@@ -495,7 +491,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
|
|
|
495
491
|
const nullInEnum = schema.enum!.includes(null)
|
|
496
492
|
const filteredValues = (nullInEnum ? schema.enum!.filter((v) => v !== null) : schema.enum!) as Array<string | number | boolean>
|
|
497
493
|
|
|
498
|
-
// drf-spectacular `NullEnum` ({ enum: [null] }) is just `null
|
|
494
|
+
// drf-spectacular `NullEnum` ({ enum: [null] }) is just `null`. An empty enum node would
|
|
499
495
|
// render as `never` (plugin-ts) / invalid `z.enum([])` (plugin-zod). Mirror the `const: null`
|
|
500
496
|
// branch so it renders as a clean `null` (not `z.null().nullable()`).
|
|
501
497
|
if (nullInEnum && filteredValues.length === 0) {
|
|
@@ -777,7 +773,7 @@ export function createSchemaParser(ctx: OasParserContext, dialect: OasDialect =
|
|
|
777
773
|
/**
|
|
778
774
|
* Ordered schema-dispatch table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
779
775
|
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
780
|
-
* `type`. The first matching rule that produces a node wins
|
|
776
|
+
* `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
|
|
781
777
|
* match/convert/fall-through contract.
|
|
782
778
|
*/
|
|
783
779
|
const schemaRules: Array<SchemaRule> = [
|
|
@@ -1058,8 +1054,7 @@ export function parseSchema(
|
|
|
1058
1054
|
* Parses an OpenAPI specification into Kubb's universal `InputNode` AST.
|
|
1059
1055
|
*
|
|
1060
1056
|
* This is the main entry point for `@kubb/adapter-oas`. It converts OpenAPI/Swagger specs into a spec-agnostic tree
|
|
1061
|
-
* that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here
|
|
1062
|
-
* the tree is a pure data structure representing all schemas and operations.
|
|
1057
|
+
* that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here, * the tree is a pure data structure representing all schemas and operations.
|
|
1063
1058
|
*
|
|
1064
1059
|
* Returns the AST root and a `nameMapping` for resolving schema references.
|
|
1065
1060
|
*
|
package/src/refs.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Diagnostic, diagnosticCode, Diagnostics } from '@kubb/core'
|
|
1
2
|
import { isReference } from './guards.ts'
|
|
2
3
|
import type { Document } from './types.ts'
|
|
3
4
|
|
|
@@ -39,7 +40,17 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
|
|
|
39
40
|
.reduce((obj: unknown, key: string) => (obj as Record<string, unknown>)?.[key], document as unknown)
|
|
40
41
|
|
|
41
42
|
if (!current) {
|
|
42
|
-
|
|
43
|
+
const diagnostic: Diagnostic = {
|
|
44
|
+
code: diagnosticCode.refNotFound,
|
|
45
|
+
severity: 'error',
|
|
46
|
+
message: `Could not find a definition for ${origRef}.`,
|
|
47
|
+
help: 'Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.',
|
|
48
|
+
location: { kind: 'schema', pointer: origRef, ref: origRef },
|
|
49
|
+
}
|
|
50
|
+
// Report the unresolved ref into the active build and resolve to null, like any
|
|
51
|
+
// other unresolvable ref. The build collects it and keeps going.
|
|
52
|
+
Diagnostics.report(diagnostic)
|
|
53
|
+
return null
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
docCache.set($ref, current)
|
package/src/resolvers.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { pascalCase } from '@internals/utils'
|
|
2
|
+
import { diagnosticCode, DiagnosticError } from '@kubb/core'
|
|
2
3
|
import type { ast } from '@kubb/core'
|
|
3
4
|
import type { ParameterObject, ServerObject } from 'oas/types'
|
|
4
5
|
import { isRef } from 'oas/types'
|
|
5
6
|
import { matchesMimeType } from 'oas/utils'
|
|
6
|
-
import { formatMap, SCHEMA_REF_PREFIX, structuralKeys } from './constants.ts'
|
|
7
|
+
import { formatMap, SCHEMA_REF_PREFIX, specialCasedFormats, structuralKeys } from './constants.ts'
|
|
7
8
|
import { isReference } from './guards.ts'
|
|
8
9
|
import { dereferenceWithRef, resolveRef } from './refs.ts'
|
|
9
10
|
import type { ContentType, Document, MediaTypeObject, Operation, ResponseObject, SchemaObject } from './types.ts'
|
|
@@ -35,7 +36,13 @@ export function resolveServerUrl(server: ServerObject, overrides?: Record<string
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) {
|
|
38
|
-
throw new
|
|
39
|
+
throw new DiagnosticError({
|
|
40
|
+
code: diagnosticCode.invalidServerVariable,
|
|
41
|
+
severity: 'error',
|
|
42
|
+
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(', ')}.`,
|
|
43
|
+
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
44
|
+
location: { kind: 'document', pointer: '#/servers' },
|
|
45
|
+
})
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
url = url.replaceAll(`{${key}}`, value)
|
|
@@ -52,6 +59,16 @@ export function getSchemaType(format: string): ast.SchemaType | null {
|
|
|
52
59
|
return formatMap[format as keyof typeof formatMap] ?? null
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry and the
|
|
64
|
+
* `specialCasedFormats` `convertFormat` handles directly. False means the format falls back to the
|
|
65
|
+
* base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
|
|
66
|
+
* diagnostic in step with the parser as `formatMap` grows.
|
|
67
|
+
*/
|
|
68
|
+
export function isHandledFormat(format: string): boolean {
|
|
69
|
+
return getSchemaType(format) !== null || specialCasedFormats.has(format)
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
/**
|
|
56
73
|
* Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
57
74
|
* Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
|
|
@@ -224,7 +241,7 @@ export type GetSchemasResult = {
|
|
|
224
241
|
/**
|
|
225
242
|
* Flattens a keyword-only `allOf` into its parent schema.
|
|
226
243
|
*
|
|
227
|
-
* Only flattens when every member is a plain fragment
|
|
244
|
+
* Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
|
|
228
245
|
* (see `structuralKeys`). Outer schema values take precedence over fragment values.
|
|
229
246
|
* Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
|
|
230
247
|
*
|
|
@@ -234,7 +251,7 @@ export type GetSchemasResult = {
|
|
|
234
251
|
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
235
252
|
*
|
|
236
253
|
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
237
|
-
* // returned unchanged
|
|
254
|
+
* // returned unchanged, contains a $ref
|
|
238
255
|
* ```
|
|
239
256
|
*/
|
|
240
257
|
/**
|
|
@@ -274,7 +291,7 @@ export function flattenSchema(schema: SchemaObject | null): SchemaObject | null
|
|
|
274
291
|
/**
|
|
275
292
|
* Extracts the inline schema from a media-type `content` map.
|
|
276
293
|
*
|
|
277
|
-
* Prefers `preferredContentType` when given
|
|
294
|
+
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
278
295
|
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
279
296
|
*
|
|
280
297
|
* @example
|
|
@@ -512,9 +529,9 @@ export function buildSchemaNode(schema: SchemaObject, name: string | null | unde
|
|
|
512
529
|
/**
|
|
513
530
|
* Returns all request body content type keys for an operation.
|
|
514
531
|
*
|
|
515
|
-
* The requestBody is dereferenced
|
|
516
|
-
*
|
|
517
|
-
*
|
|
532
|
+
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
533
|
+
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
534
|
+
* available content types even for referenced bodies.
|
|
518
535
|
*
|
|
519
536
|
* @example
|
|
520
537
|
* ```ts
|
|
@@ -531,14 +548,14 @@ export function getRequestBodyContentTypes(document: Document, operation: Operat
|
|
|
531
548
|
if (!body) return []
|
|
532
549
|
|
|
533
550
|
// dereferenceWithRef keeps $ref but spreads all resolved fields (including `content`).
|
|
534
|
-
// Do not bail out on isReference
|
|
551
|
+
// Do not bail out on isReference, the content is already present on the merged object.
|
|
535
552
|
return body.content ? Object.keys(body.content) : []
|
|
536
553
|
}
|
|
537
554
|
|
|
538
555
|
/**
|
|
539
556
|
* Returns all response content type keys for an operation at a given status code.
|
|
540
557
|
*
|
|
541
|
-
* Response `$ref`s are resolved in
|
|
558
|
+
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
542
559
|
* so the returned list reflects the available content types even for referenced responses.
|
|
543
560
|
*
|
|
544
561
|
* @example
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { diagnosticCode, Diagnostics } from '@kubb/core'
|
|
2
|
+
import type { ast } from '@kubb/core'
|
|
3
|
+
import { isHandledFormat } from './resolvers.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
|
|
7
|
+
* top-level schema. Walks the node the parser produced during `preScan`, threading the RFC 6901
|
|
8
|
+
* pointer as it descends so a nested field reports against its full path
|
|
9
|
+
* (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
|
|
10
|
+
* resolved schema is reported under its own walk. Reports land in the active build run, are a
|
|
11
|
+
* no-op outside one, and repeats are deduped by the build.
|
|
12
|
+
*/
|
|
13
|
+
export function reportSchemaDiagnostics({ node, name }: { node: ast.SchemaNode; name: string }): void {
|
|
14
|
+
visit(node, `#/components/schemas/${name}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function visit(node: ast.SchemaNode, pointer: string): void {
|
|
18
|
+
if (node.deprecated) {
|
|
19
|
+
Diagnostics.report({
|
|
20
|
+
code: diagnosticCode.deprecated,
|
|
21
|
+
severity: 'info',
|
|
22
|
+
message: 'This schema is marked as deprecated.',
|
|
23
|
+
location: { kind: 'schema', pointer },
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof node.format === 'string' && !isHandledFormat(node.format)) {
|
|
28
|
+
Diagnostics.report({
|
|
29
|
+
code: diagnosticCode.unsupportedFormat,
|
|
30
|
+
severity: 'warning',
|
|
31
|
+
message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
|
|
32
|
+
help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
|
|
33
|
+
location: { kind: 'schema', pointer },
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (node.type === 'object') {
|
|
38
|
+
for (const property of node.properties) {
|
|
39
|
+
visit(property.schema, `${pointer}/properties/${property.name}`)
|
|
40
|
+
}
|
|
41
|
+
if (node.additionalProperties && typeof node.additionalProperties === 'object') {
|
|
42
|
+
visit(node.additionalProperties, `${pointer}/additionalProperties`)
|
|
43
|
+
}
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (node.type === 'array' || node.type === 'tuple') {
|
|
48
|
+
for (const item of node.items ?? []) {
|
|
49
|
+
visit(item, `${pointer}/items`)
|
|
50
|
+
}
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (node.type === 'union' || node.type === 'intersection') {
|
|
55
|
+
for (const member of node.members ?? []) {
|
|
56
|
+
visit(member, pointer)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/stream.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { SCHEMA_REF_PREFIX } from './constants.ts'
|
|
|
4
4
|
import { buildDiscriminatorChildMap, patchDiscriminatorNode } from './discriminator.ts'
|
|
5
5
|
import type { SchemaParser } from './parser.ts'
|
|
6
6
|
import { resolveServerUrl } from './resolvers.ts'
|
|
7
|
+
import { reportSchemaDiagnostics } from './schemaDiagnostics.ts'
|
|
7
8
|
import type { DiscriminatorTarget } from './discriminator.ts'
|
|
8
9
|
import type { AdapterOas, Document, SchemaObject } from './types.ts'
|
|
9
10
|
|
|
@@ -41,7 +42,7 @@ function createDedupePlan({
|
|
|
41
42
|
isCandidate: (node) => {
|
|
42
43
|
if (node.type === 'enum') return true
|
|
43
44
|
if (node.type !== 'object') return false
|
|
44
|
-
// Skip object shapes that are part of a circular chain
|
|
45
|
+
// Skip object shapes that are part of a circular chain, hoisting them would break the cycle.
|
|
45
46
|
if (node.name && circularSchemas.has(node.name)) return false
|
|
46
47
|
return !ast.containsCircularRef(node, { circularSchemas })
|
|
47
48
|
},
|
|
@@ -98,7 +99,7 @@ export function resolveBaseUrl({
|
|
|
98
99
|
* After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
|
|
99
100
|
* Both are proportional to the number of aliases or discriminator parents, not total schema count.
|
|
100
101
|
*
|
|
101
|
-
* Each schema is parsed again during the streaming pass
|
|
102
|
+
* Each schema is parsed again during the streaming pass. This is intentional.
|
|
102
103
|
* Holding the parsed nodes in memory here would defeat the streaming memory benefit.
|
|
103
104
|
*
|
|
104
105
|
* @example
|
|
@@ -136,6 +137,7 @@ export function preScan({
|
|
|
136
137
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
137
138
|
const node = parseSchema({ schema, name }, parserOptions)
|
|
138
139
|
allNodes.push(node)
|
|
140
|
+
reportSchemaDiagnostics({ node, name })
|
|
139
141
|
if (node.type === 'ref' && node.name && node.name !== name) {
|
|
140
142
|
refAliasMap.set(name, node)
|
|
141
143
|
}
|
|
@@ -153,7 +155,7 @@ export function preScan({
|
|
|
153
155
|
let dedupePlan: ast.DedupePlan | null = null
|
|
154
156
|
if (dedupe) {
|
|
155
157
|
// One extra parse pass over operations so duplicates in request/response bodies are seen.
|
|
156
|
-
// Reuses the already-parsed `allNodes` for schemas
|
|
158
|
+
// Reuses the already-parsed `allNodes` for schemas, no second schema parse.
|
|
157
159
|
const operationNodes: Array<ast.OperationNode> = []
|
|
158
160
|
for (const methods of Object.values(baseOas.getPaths())) {
|
|
159
161
|
for (const operation of Object.values(methods)) {
|
package/src/types.ts
CHANGED
|
@@ -172,9 +172,9 @@ export type AdapterOasOptions = {
|
|
|
172
172
|
serverVariables?: Record<string, string>
|
|
173
173
|
/**
|
|
174
174
|
* How the `discriminator` field on `oneOf`/`anyOf` schemas is interpreted.
|
|
175
|
-
* - `'strict'`
|
|
175
|
+
* - `'strict'` child schemas stay exactly as written. The discriminator
|
|
176
176
|
* narrows types at the call site but child shapes are not modified.
|
|
177
|
-
* - `'inherit'`
|
|
177
|
+
* - `'inherit'` Kubb propagates the discriminator property as a literal
|
|
178
178
|
* value into each child schema, so each branch's discriminator field is
|
|
179
179
|
* precisely typed.
|
|
180
180
|
*
|
|
@@ -185,9 +185,9 @@ export type AdapterOasOptions = {
|
|
|
185
185
|
* Collapse structurally identical schemas and enums into a single shared definition.
|
|
186
186
|
*
|
|
187
187
|
* Duplicated inline shapes (especially enums repeated across many properties) are hoisted
|
|
188
|
-
* into one named schema
|
|
189
|
-
* component
|
|
190
|
-
* `description` and `example` is ignored. Enabled by default
|
|
188
|
+
* into one named schema. Every other occurrence, and any structurally identical top-level
|
|
189
|
+
* component, becomes a `ref` to it. Equality is shape-only: documentation such as
|
|
190
|
+
* `description` and `example` is ignored. Enabled by default. Set to `false` to keep every
|
|
191
191
|
* occurrence inline and produce byte-for-byte identical output to earlier versions.
|
|
192
192
|
*
|
|
193
193
|
* @default true
|