@kubb/adapter-oas 5.0.0-beta.62 → 5.0.0-beta.64

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.
@@ -1,76 +0,0 @@
1
- import { 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/${escapePointerToken(name)}`)
15
- }
16
-
17
- /**
18
- * Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
19
- * property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
20
- */
21
- function escapePointerToken(token: string): string {
22
- return token.replace(/~/g, '~0').replace(/\//g, '~1')
23
- }
24
-
25
- function visit(node: ast.SchemaNode, pointer: string): void {
26
- if (node.deprecated) {
27
- Diagnostics.report({
28
- code: Diagnostics.code.deprecated,
29
- severity: 'info',
30
- message: 'This schema is marked as deprecated.',
31
- location: { kind: 'schema', pointer },
32
- })
33
- }
34
-
35
- if (typeof node.format === 'string' && !isHandledFormat(node.format)) {
36
- Diagnostics.report({
37
- code: Diagnostics.code.unsupportedFormat,
38
- severity: 'warning',
39
- message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
40
- help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
41
- location: { kind: 'schema', pointer },
42
- })
43
- }
44
-
45
- if (node.type === 'object') {
46
- for (const property of node.properties) {
47
- visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`)
48
- }
49
- if (node.additionalProperties && typeof node.additionalProperties === 'object') {
50
- visit(node.additionalProperties, `${pointer}/additionalProperties`)
51
- }
52
- return
53
- }
54
-
55
- if (node.type === 'array') {
56
- for (const item of node.items ?? []) {
57
- visit(item, `${pointer}/items`)
58
- }
59
- return
60
- }
61
-
62
- if (node.type === 'tuple') {
63
- // Each tuple position has its own pointer, so index them. A shared `/items` would collapse
64
- // distinct diagnostics in the dedupe.
65
- for (const [index, item] of (node.items ?? []).entries()) {
66
- visit(item, `${pointer}/items/${index}`)
67
- }
68
- return
69
- }
70
-
71
- if (node.type === 'union' || node.type === 'intersection') {
72
- for (const [index, member] of (node.members ?? []).entries()) {
73
- visit(member, `${pointer}/members/${index}`)
74
- }
75
- }
76
- }
package/src/stream.ts DELETED
@@ -1,264 +0,0 @@
1
- import { containsCircularRef, findCircularSchemas } from '@kubb/ast/utils'
2
- import { ast } from '@kubb/core'
3
- import { SCHEMA_REF_PREFIX } from './constants.ts'
4
- import { buildDiscriminatorChildMap, patchDiscriminatorNode } from './discriminator.ts'
5
- import { getOperations } from './operation.ts'
6
- import type { SchemaParser } from './parser.ts'
7
- import { resolveServerUrl } from './resolvers.ts'
8
- import { reportSchemaDiagnostics } from './schemaDiagnostics.ts'
9
- import type { DiscriminatorTarget } from './discriminator.ts'
10
- import type { AdapterOas, Document, SchemaObject } from './types.ts'
11
-
12
- export type PreScanResult = {
13
- refAliasMap: Map<string, ast.SchemaNode>
14
- enumNames: Array<string>
15
- circularNames: Array<string>
16
- discriminatorChildMap: Map<string, DiscriminatorTarget> | null
17
- dedupePlan: ast.DedupePlan | null
18
- }
19
-
20
- /**
21
- * Reads the server URL from the document's `servers` array at `serverIndex`,
22
- * interpolating any `serverVariables` into the URL template.
23
- *
24
- * Returns `null` when `serverIndex` is omitted or out of range.
25
- *
26
- * @example Resolve the first server
27
- * `resolveBaseUrl({ document, serverIndex: 0 })`
28
- *
29
- * @example Override a path variable
30
- * `resolveBaseUrl({ document, serverIndex: 0, serverVariables: { version: 'v2' } })`
31
- */
32
- export function resolveBaseUrl({
33
- document,
34
- serverIndex,
35
- serverVariables,
36
- }: {
37
- document: Document
38
- serverIndex?: number
39
- serverVariables?: Record<string, string>
40
- }): string | null {
41
- const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
42
- return server?.url ? resolveServerUrl(server, serverVariables) : null
43
- }
44
-
45
- /**
46
- * Parses every schema once to build the lookup structures that streaming needs upfront.
47
- *
48
- * Three things happen in this single pass:
49
- * - `refAliasMap` records schemas that are pure `$ref` aliases so the streaming pass can inline them.
50
- * - `enumNames` collects the names of every enum schema so plugins skip re-scanning the stream.
51
- * - `circularNames` runs cycle detection, which requires all nodes in memory simultaneously.
52
- * The `allNodes` array is local and drops out of scope as soon as this function returns.
53
- *
54
- * After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
55
- * Both are proportional to the number of aliases or discriminator parents, not total schema count.
56
- *
57
- * Each schema is parsed again during the streaming pass. This is intentional.
58
- * Holding the parsed nodes in memory here would defeat the streaming memory benefit.
59
- *
60
- * @example
61
- * ```ts
62
- * const { refAliasMap, enumNames, circularNames } = preScan({
63
- * schemas,
64
- * parseSchema,
65
- * parserOptions,
66
- * discriminator: 'strict',
67
- * })
68
- * ```
69
- */
70
- export function preScan({
71
- schemas,
72
- parseSchema,
73
- parseOperation,
74
- document,
75
- parserOptions,
76
- discriminator,
77
- dedupe,
78
- }: {
79
- schemas: Record<string, SchemaObject>
80
- parseSchema: (entry: { schema: SchemaObject; name: string }, options: ast.ParserOptions) => ast.SchemaNode
81
- parseOperation: SchemaParser['parseOperation']
82
- document: Document
83
- parserOptions: ast.ParserOptions
84
- discriminator: AdapterOas['options']['discriminator']
85
- dedupe: boolean
86
- }): PreScanResult {
87
- const allNodes: Array<ast.SchemaNode> = []
88
- const refAliasMap = new Map<string, ast.SchemaNode>()
89
- const enumNames: Array<string> = []
90
- const discriminatorParentNodes: Array<ast.SchemaNode> = []
91
-
92
- for (const [name, schema] of Object.entries(schemas)) {
93
- const node = parseSchema({ schema, name }, parserOptions)
94
- allNodes.push(node)
95
- reportSchemaDiagnostics({ node, name })
96
- if (node.type === 'ref' && node.name && node.name !== name) {
97
- refAliasMap.set(name, node)
98
- }
99
- if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) {
100
- enumNames.push(node.name)
101
- }
102
- if (discriminator === 'inherit' && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) {
103
- discriminatorParentNodes.push(node)
104
- }
105
- }
106
-
107
- const circularNames = [...findCircularSchemas(allNodes)]
108
- const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null
109
-
110
- let dedupePlan: ast.DedupePlan | null = null
111
- if (dedupe) {
112
- // One extra parse pass over operations so duplicates in request/response bodies are seen.
113
- // Reuses the already-parsed `allNodes` for schemas, no second schema parse.
114
- const operationNodes: Array<ast.OperationNode> = []
115
- for (const operation of getOperations(document)) {
116
- const operationNode = parseOperation(parserOptions, operation)
117
- if (operationNode) operationNodes.push(operationNode)
118
- }
119
-
120
- // Only enums and objects are candidates, and object shapes that reference a circular schema are
121
- // rejected to avoid hoisting recursive structures. Inline shapes reuse their context-derived
122
- // name (collision-resolved against existing component names). Shapes without a name stay inline.
123
- const circularSchemas = new Set(circularNames)
124
- const usedNames = new Set(Object.keys(schemas))
125
-
126
- dedupePlan = ast.buildDedupePlan([...allNodes, ...operationNodes], {
127
- isCandidate: (node) => {
128
- if (node.type === 'enum') return true
129
- if (node.type !== 'object') return false
130
- // Skip object shapes that are part of a circular chain, hoisting them would break the cycle.
131
- if (node.name && circularSchemas.has(node.name)) return false
132
- return !containsCircularRef(node, { circularSchemas })
133
- },
134
- nameFor: (node) => {
135
- const base = node.name
136
- if (!base) return null
137
-
138
- let name = base
139
- let counter = 2
140
- while (usedNames.has(name)) {
141
- name = `${base}${counter++}`
142
- }
143
- usedNames.add(name)
144
- return name
145
- },
146
- refFor: (name) => `${SCHEMA_REF_PREFIX}${name}`,
147
- })
148
-
149
- for (const definition of dedupePlan.hoisted) {
150
- if (definition.type === 'enum' && definition.name) enumNames.push(definition.name)
151
- }
152
- }
153
-
154
- // Enum names that duplicate an earlier schema's content are never emitted, so they are not
155
- // advertised to plugins either.
156
- const aliasNames = dedupePlan?.aliasNames
157
- const emittedEnumNames = aliasNames && aliasNames.size > 0 ? enumNames.filter((name) => !aliasNames.has(name)) : enumNames
158
-
159
- return { refAliasMap, enumNames: emittedEnumNames, circularNames, discriminatorChildMap, dedupePlan }
160
- }
161
-
162
- /**
163
- * Creates a lazy `InputNode<true>` from already-resolved adapter state.
164
- *
165
- * The schema and operation iterables each start a fresh parse pass on every
166
- * `[Symbol.asyncIterator]()` call. This lets multiple plugins consume the same
167
- * stream object independently without sharing a cursor or holding all nodes in memory.
168
- *
169
- * Ref aliases in `refAliasMap` are inlined during iteration: an alias entry is replaced
170
- * with its target's parsed node (but keeps the alias name) so plugins never receive bare `ref` nodes.
171
- *
172
- * @example
173
- * ```ts
174
- * const streamNode = createInputStream({ schemas, parseSchema, parseOperation, document, parserOptions, refAliasMap, discriminatorChildMap, meta })
175
- * for await (const schema of streamNode.schemas) {
176
- * // each call to for-await restarts from the first schema
177
- * }
178
- * ```
179
- */
180
- export function createInputStream({
181
- schemas,
182
- parseSchema,
183
- parseOperation,
184
- document,
185
- parserOptions,
186
- refAliasMap,
187
- discriminatorChildMap,
188
- dedupePlan,
189
- meta,
190
- }: {
191
- schemas: Record<string, SchemaObject>
192
- parseSchema: SchemaParser['parseSchema']
193
- parseOperation: SchemaParser['parseOperation']
194
- document: Document
195
- parserOptions: ast.ParserOptions
196
- refAliasMap: Map<string, ast.SchemaNode>
197
- discriminatorChildMap: Map<string, DiscriminatorTarget> | null
198
- dedupePlan: ast.DedupePlan | null
199
- meta: ast.InputMeta
200
- }): ast.InputNode<true> {
201
- // Rewrites a top-level schema against the dedupe plan: a structurally identical sibling
202
- // becomes a `ref` alias to the canonical one (keeping its own name). Otherwise nested
203
- // duplicates are collapsed while the schema's own root is preserved.
204
- const rewriteTopLevelSchema = (node: ast.SchemaNode): ast.SchemaNode => {
205
- if (!dedupePlan) return node
206
-
207
- const canonical = dedupePlan.canonicalBySignature.get(ast.signatureOf(node))
208
- if (canonical && canonical.name !== node.name) {
209
- return ast.factory.createSchema({
210
- type: 'ref',
211
- name: node.name ?? null,
212
- ref: canonical.ref,
213
- description: node.description,
214
- deprecated: node.deprecated,
215
- })
216
- }
217
-
218
- return ast.applyDedupe(node, dedupePlan, true)
219
- }
220
-
221
- const schemasIterable: AsyncIterable<ast.SchemaNode> = {
222
- [Symbol.asyncIterator]() {
223
- return (async function* () {
224
- // Hoisted canonical definitions are emitted first so the schema list owns the shared shapes.
225
- if (dedupePlan) {
226
- for (const definition of dedupePlan.hoisted) yield definition
227
- }
228
-
229
- for (const [name, schema] of Object.entries(schemas)) {
230
- // A top-level schema whose content duplicates an earlier one is not emitted: every
231
- // ref to it is repointed at the first schema with that content, so its model would
232
- // be dead code.
233
- if (dedupePlan?.aliasNames.has(name)) continue
234
-
235
- // Inline ref aliases: replace the alias entry with its target's parsed node
236
- // (keeping the alias name). Skip the first parse entirely for alias entries
237
- // since that result is never used.
238
- const alias = refAliasMap.get(name)
239
- if (alias?.name && schemas[alias.name]) {
240
- yield rewriteTopLevelSchema({ ...parseSchema({ schema: schemas[alias.name]!, name: alias.name }, parserOptions), name })
241
- continue
242
- }
243
-
244
- const parsed = parseSchema({ schema, name }, parserOptions)
245
- const node = discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)!) : parsed
246
- yield rewriteTopLevelSchema(node)
247
- }
248
- })()
249
- },
250
- }
251
-
252
- const operationsIterable: AsyncIterable<ast.OperationNode> = {
253
- [Symbol.asyncIterator]() {
254
- return (async function* () {
255
- for (const operation of getOperations(document)) {
256
- const node = parseOperation(parserOptions, operation)
257
- if (node) yield dedupePlan ? ast.applyDedupe(node, dedupePlan) : node
258
- }
259
- })()
260
- },
261
- }
262
-
263
- return ast.factory.createInput({ stream: true, schemas: schemasIterable, operations: operationsIterable, meta })
264
- }
package/src/types.ts DELETED
@@ -1,235 +0,0 @@
1
- import type { AdapterFactoryOptions } from '@kubb/core'
2
- import type { ast } from '@kubb/core'
3
- import type { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema'
4
- import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
5
- import type { Operation } from './operation.ts'
6
-
7
- export type { Operation }
8
-
9
- /**
10
- * Media type used to pick a schema from an operation's request or response.
11
- *
12
- * @example
13
- * ```ts
14
- * const ct: ContentType = 'application/vnd.api+json'
15
- * ```
16
- */
17
- export type ContentType = 'application/json' | (string & {})
18
-
19
- /**
20
- * Extended OpenAPI 3.0 schema object that includes OpenAPI 3.1 and JSON Schema fields.
21
- * The parser uses these additional fields to handle newer spec versions.
22
- *
23
- * @example
24
- * ```ts
25
- * const schema: SchemaObject = {
26
- * type: 'string',
27
- * const: 'dog',
28
- * contentMediaType: 'application/octet-stream',
29
- * }
30
- * ```
31
- */
32
- export type SchemaObject = {
33
- externalDocs?: unknown
34
- xml?: unknown
35
- $schema?: string
36
- deprecated?: boolean
37
- example?: unknown
38
- examples?: Array<unknown>
39
- nullable?: boolean
40
- readOnly?: boolean
41
- writeOnly?: boolean
42
- discriminator?: DiscriminatorObject
43
- 'x-readme-ref-name'?: string
44
- /**
45
- * OAS 3.0 vendor extension: marks a schema as nullable without using `type: ['null', ...]`.
46
- */
47
- 'x-nullable'?: boolean
48
- /**
49
- * OAS 3.1: constrains the schema to a single fixed value (equivalent to a one-item `enum`).
50
- */
51
- const?: string | number | boolean | null
52
- /**
53
- * OAS 3.1: media type of the schema content. `'application/octet-stream'` on a `string` schema maps to `blob`.
54
- */
55
- contentMediaType?: string
56
- $ref?: string
57
- /**
58
- * OAS 3.1: positional tuple items, replacing the multi-item `items` array from OAS 3.0.
59
- */
60
- prefixItems?: Array<SchemaObject | ReferenceObject>
61
- /**
62
- * JSON Schema: maps regex patterns to sub-schemas for validating additional properties.
63
- */
64
- patternProperties?: Record<string, SchemaObject | boolean>
65
- /**
66
- * Single-schema form of `items`. Narrowed from the base type to take precedence over the tuple overload.
67
- */
68
- items?: SchemaObject | ReferenceObject
69
- /**
70
- * Allowed values for this schema.
71
- */
72
- enum?: Array<string | number | boolean | null>
73
- } & (OpenAPIV3.SchemaObject | OpenAPIV3_1.SchemaObject | JSONSchema4 | JSONSchema6 | JSONSchema7)
74
-
75
- /**
76
- * HTTP method in the lowercase form an OpenAPI path item uses for its keys.
77
- */
78
- export type HttpMethod = Lowercase<ast.HttpMethod>
79
-
80
- /**
81
- * Normalized OpenAPI document after parsing.
82
- */
83
- export type Document = (OpenAPIV3.Document | OpenAPIV3_1.Document) & Record<string, unknown>
84
-
85
- /**
86
- * Single operation object (the `get`/`post`/… entry on a path item) plus any vendor extensions.
87
- */
88
- export type OperationObject = (OpenAPIV3.OperationObject | OpenAPIV3_1.OperationObject) & Record<string, unknown>
89
-
90
- /**
91
- * Path item object holding the operations and shared parameters for a single URL path.
92
- */
93
- export type PathItemObject = OpenAPIV3.PathItemObject | OpenAPIV3_1.PathItemObject
94
-
95
- /**
96
- * Discriminator object for `oneOf`/`anyOf` schemas in OpenAPI.
97
- */
98
- export type DiscriminatorObject = OpenAPIV3.DiscriminatorObject | OpenAPIV3_1.DiscriminatorObject
99
-
100
- /**
101
- * OpenAPI reference object pointing to a schema definition via `$ref`.
102
- */
103
- export type ReferenceObject = OpenAPIV3.ReferenceObject
104
-
105
- /**
106
- * OpenAPI response object holding the content and headers for one status code.
107
- */
108
- export type ResponseObject = OpenAPIV3.ResponseObject | OpenAPIV3_1.ResponseObject
109
-
110
- /**
111
- * OpenAPI request body object that maps content types to their media type objects.
112
- */
113
- export type RequestBodyObject = OpenAPIV3.RequestBodyObject | OpenAPIV3_1.RequestBodyObject
114
-
115
- /**
116
- * OpenAPI media type object that maps a content-type string to its schema.
117
- */
118
- export type MediaTypeObject = OpenAPIV3.MediaTypeObject | OpenAPIV3_1.MediaTypeObject
119
-
120
- /**
121
- * OpenAPI parameter object, narrowed so `in` is always one of the four valid locations.
122
- */
123
- export type ParameterObject = {
124
- in: 'cookie' | 'header' | 'path' | 'query'
125
- } & (OpenAPIV3.ParameterObject | OpenAPIV3_1.ParameterObject)
126
-
127
- /**
128
- * OpenAPI server object describing a base URL and its `{variable}` substitutions.
129
- */
130
- export type ServerObject = OpenAPIV3.ServerObject | OpenAPIV3_1.ServerObject
131
-
132
- /**
133
- * Configuration for the OpenAPI adapter: spec validation, content-type selection,
134
- * server URL resolution, and how schema types are derived.
135
- *
136
- * @example
137
- * ```ts
138
- * adapterOas({
139
- * validate: false,
140
- * dateType: 'date',
141
- * serverIndex: 0,
142
- * serverVariables: { env: 'prod' },
143
- * })
144
- * ```
145
- */
146
- export type AdapterOasOptions = {
147
- /**
148
- * Validate the OpenAPI spec with `@readme/openapi-parser` before parsing.
149
- * Set to `false` only when you have a known-invalid spec you still want to
150
- * generate from.
151
- *
152
- * @default true
153
- */
154
- validate?: boolean
155
- /**
156
- * Preferred media type when an operation defines several. Defaults to the
157
- * first JSON-compatible media type found in the spec.
158
- */
159
- contentType?: ContentType
160
- /**
161
- * Index into the `servers` array from your OpenAPI spec. Used to compute the
162
- * base URL for plugins that need it. Most projects pick `0` for the primary
163
- * server. Omit to leave `baseURL` undefined.
164
- */
165
- serverIndex?: number
166
- /**
167
- * Override values for `{variable}` placeholders in the selected server URL.
168
- * Only used when `serverIndex` is set. Variables you do not provide use
169
- * their `default` value from the spec.
170
- *
171
- * @example
172
- * ```ts
173
- * // spec server: "https://api.{env}.example.com"
174
- * serverVariables: { env: 'prod' }
175
- * // → baseURL: "https://api.prod.example.com"
176
- * ```
177
- */
178
- serverVariables?: Record<string, string>
179
- /**
180
- * How the `discriminator` field on `oneOf`/`anyOf` schemas is interpreted.
181
- * - `'strict'` child schemas stay exactly as written. The discriminator
182
- * narrows types at the call site but child shapes are not modified.
183
- * - `'inherit'` Kubb propagates the discriminator property as a literal
184
- * value into each child schema, so each branch's discriminator field is
185
- * precisely typed.
186
- *
187
- * @default 'strict'
188
- */
189
- discriminator?: 'strict' | 'inherit'
190
- /**
191
- * Collapse structurally identical schemas and enums into a single shared definition.
192
- *
193
- * Duplicated inline shapes (especially enums repeated across many properties) are hoisted
194
- * into one named schema. Every other occurrence, and any structurally identical top-level
195
- * component, becomes a `ref` to it. Equality is shape-only, so documentation such as
196
- * `description` and `example` is ignored. Set to `false` to keep every occurrence inline
197
- * and produce byte-for-byte identical output to earlier versions.
198
- *
199
- * @default true
200
- */
201
- dedupe?: boolean
202
- } & Partial<ast.ParserOptions>
203
-
204
- /**
205
- * Adapter options after defaults have been applied and schema name collisions resolved.
206
- */
207
- export type AdapterOasResolvedOptions = {
208
- validate: boolean
209
- contentType: AdapterOasOptions['contentType']
210
- serverIndex: AdapterOasOptions['serverIndex']
211
- serverVariables: AdapterOasOptions['serverVariables']
212
- discriminator: NonNullable<AdapterOasOptions['discriminator']>
213
- dedupe: NonNullable<AdapterOasOptions['dedupe']>
214
- dateType: NonNullable<AdapterOasOptions['dateType']>
215
- integerType: NonNullable<AdapterOasOptions['integerType']>
216
- unknownType: NonNullable<AdapterOasOptions['unknownType']>
217
- emptySchemaType: NonNullable<AdapterOasOptions['emptySchemaType']>
218
- enumSuffix: AdapterOasOptions['enumSuffix']
219
- /**
220
- * Map from original `$ref` paths to their collision-resolved schema names.
221
- * Populated after each `parse()` call.
222
- *
223
- * @example
224
- * ```ts
225
- * nameMapping.get('#/components/schemas/Order') // 'Order'
226
- * nameMapping.get('#/components/responses/Order') // 'OrderResponse'
227
- * ```
228
- */
229
- nameMapping: Map<string, string>
230
- }
231
-
232
- /**
233
- * `@kubb/core` adapter factory type for the OpenAPI adapter.
234
- */
235
- export type AdapterOas = AdapterFactoryOptions<'oas', AdapterOasOptions, AdapterOasResolvedOptions, Document>