@kubb/adapter-oas 5.0.0-beta.63 → 5.0.0-beta.65
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 +86 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +87 -195
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
- package/src/adapter.bench.ts +0 -60
- package/src/adapter.ts +0 -207
- package/src/bundler.ts +0 -71
- package/src/constants.ts +0 -135
- package/src/dedupe.ts +0 -237
- package/src/dialect.ts +0 -42
- package/src/discriminator.ts +0 -132
- package/src/factory.ts +0 -180
- package/src/guards.ts +0 -68
- package/src/index.ts +0 -15
- package/src/mime.ts +0 -21
- package/src/operation.ts +0 -194
- package/src/parser.ts +0 -1129
- package/src/refs.ts +0 -86
- package/src/resolvers.ts +0 -590
- package/src/schemaDiagnostics.ts +0 -76
- package/src/stream.ts +0 -221
- package/src/types.ts +0 -235
- /package/dist/{chunk-C0LytTxp.js → rolldown-runtime-C0LytTxp.js} +0 -0
package/src/dedupe.ts
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { containsCircularRef, extractRefName } from '@kubb/ast/utils'
|
|
2
|
-
import { ast } from '@kubb/core'
|
|
3
|
-
import { SCHEMA_REF_PREFIX } from './constants.ts'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The destination a deduplicated shape points at: the shared schema name and the
|
|
7
|
-
* synthetic `$ref` path stored on the generated `ref` nodes.
|
|
8
|
-
*/
|
|
9
|
-
type Target = {
|
|
10
|
-
name: string
|
|
11
|
-
ref: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The result of {@link plan}: the shared definitions to prepend to the schema list, plus the
|
|
16
|
-
* rewriting behavior that repoints duplicate shapes at their shared target. The lookup maps stay
|
|
17
|
-
* private to the closure, so callers interact with one object instead of three collections.
|
|
18
|
-
*/
|
|
19
|
-
export type Plan = {
|
|
20
|
-
/**
|
|
21
|
-
* New top-level schema definitions created for inline shapes that had no existing named
|
|
22
|
-
* component. Nested duplicates inside each definition are already collapsed.
|
|
23
|
-
*/
|
|
24
|
-
extracted: Array<ast.SchemaNode>
|
|
25
|
-
/**
|
|
26
|
-
* Rewrites an operation or nested schema, replacing every duplicate sub-schema with a `ref` to
|
|
27
|
-
* its shared target. Replacing a node prunes its subtree, so nested duplicates inside a replaced
|
|
28
|
-
* shape are not visited again. A `ref` to a duplicate top-level schema is repointed at the first
|
|
29
|
-
* schema with the same content.
|
|
30
|
-
*/
|
|
31
|
-
apply<T extends ast.Node>(node: T): T
|
|
32
|
-
/**
|
|
33
|
-
* Rewrites a top-level schema. A schema whose content duplicates a different shared one becomes a
|
|
34
|
-
* `ref` alias to it (keeping its own name and docs). Otherwise its nested duplicates collapse
|
|
35
|
-
* while its own root is preserved.
|
|
36
|
-
*/
|
|
37
|
-
applyTopLevel(node: ast.SchemaNode): ast.SchemaNode
|
|
38
|
-
/**
|
|
39
|
-
* Whether a top-level name duplicates an earlier schema with the same content. Such a schema is
|
|
40
|
-
* never emitted, since every reference to it is repointed at the first schema with that content.
|
|
41
|
-
*/
|
|
42
|
-
isAlias(name: string): boolean
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Runtime state {@link plan} reads from the current parse: schema names that take part in a
|
|
47
|
-
* circular chain (never extracted, extracting them would break the cycle) and the names already in
|
|
48
|
-
* use (so extracted definitions resolve collisions).
|
|
49
|
-
*/
|
|
50
|
-
export type DedupeContext = {
|
|
51
|
-
circularSchemas: ReadonlySet<string>
|
|
52
|
-
usedNames: Set<string>
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Minimum occurrences before a shape is deduplicated.
|
|
57
|
-
*/
|
|
58
|
-
const MIN_OCCURRENCES = 2
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Builds the shared `ref` replacement for a duplicate occurrence, carrying the
|
|
62
|
-
* usage-slot and documentation fields that are not part of the shared type.
|
|
63
|
-
*/
|
|
64
|
-
function createRefNode(node: ast.SchemaNode, target: Target): ast.SchemaNode {
|
|
65
|
-
return ast.factory.createSchema({
|
|
66
|
-
type: 'ref',
|
|
67
|
-
name: target.name,
|
|
68
|
-
ref: target.ref,
|
|
69
|
-
optional: node.optional,
|
|
70
|
-
nullish: node.nullish,
|
|
71
|
-
readOnly: node.readOnly,
|
|
72
|
-
writeOnly: node.writeOnly,
|
|
73
|
-
deprecated: node.deprecated,
|
|
74
|
-
description: node.description,
|
|
75
|
-
default: node.default,
|
|
76
|
-
example: node.example,
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Strips usage-slot flags from an extracted definition and applies its name.
|
|
82
|
-
* A standalone definition is never optional, so `optional`/`nullish` are cleared.
|
|
83
|
-
*/
|
|
84
|
-
function cleanDefinition(node: ast.SchemaNode, name: string): ast.SchemaNode {
|
|
85
|
-
return { ...node, name, optional: undefined, nullish: undefined }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Returns `true` when a node is eligible for deduplication. Only enums and objects qualify, and
|
|
90
|
-
* object shapes that take part in a circular chain are rejected so recursive structures are not
|
|
91
|
-
* extracted (which would break the cycle).
|
|
92
|
-
*/
|
|
93
|
-
function isCandidate(node: ast.SchemaNode, circularSchemas: ReadonlySet<string>): boolean {
|
|
94
|
-
if (node.type === 'enum') return true
|
|
95
|
-
if (node.type !== 'object') return false
|
|
96
|
-
if (node.name && circularSchemas.has(node.name)) return false
|
|
97
|
-
return !containsCircularRef(node, { circularSchemas })
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Produces the name for an inline shape with no existing named component, resolving
|
|
102
|
-
* collisions against `usedNames`. Returns `null` for an unnamed shape, which stays inline.
|
|
103
|
-
*/
|
|
104
|
-
function nameFor(node: ast.SchemaNode, usedNames: Set<string>): string | null {
|
|
105
|
-
const base = node.name
|
|
106
|
-
if (!base) return null
|
|
107
|
-
|
|
108
|
-
let name = base
|
|
109
|
-
let counter = 2
|
|
110
|
-
while (usedNames.has(name)) {
|
|
111
|
-
name = `${base}${counter++}`
|
|
112
|
-
}
|
|
113
|
-
usedNames.add(name)
|
|
114
|
-
return name
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Scans a forest of schema and operation nodes and produces a {@link Plan}.
|
|
119
|
-
*
|
|
120
|
-
* A shape that occurs at least {@link MIN_OCCURRENCES} times is deduplicated: if any occurrence is
|
|
121
|
-
* a named top-level schema, the first one becomes the target (so other top-level duplicates and
|
|
122
|
-
* inline copies turn into references to it). Other top-level names with the same content are
|
|
123
|
-
* recorded as aliases. Otherwise a new definition is extracted using {@link nameFor}. The returned
|
|
124
|
-
* plan rewrites nodes against those decisions.
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* ```ts
|
|
128
|
-
* const dedupePlan = plan([...schemaNodes, ...operationNodes], { circularSchemas, usedNames })
|
|
129
|
-
* ```
|
|
130
|
-
*/
|
|
131
|
-
export function plan(roots: ReadonlyArray<ast.Node>, context: DedupeContext): Plan {
|
|
132
|
-
const { circularSchemas, usedNames } = context
|
|
133
|
-
|
|
134
|
-
const topLevelNodes = new Set<ast.SchemaNode>()
|
|
135
|
-
|
|
136
|
-
type Group = {
|
|
137
|
-
count: number
|
|
138
|
-
representative: ast.SchemaNode
|
|
139
|
-
topLevelNames: Array<string>
|
|
140
|
-
}
|
|
141
|
-
const groups = new Map<string, Group>()
|
|
142
|
-
|
|
143
|
-
for (const root of roots) {
|
|
144
|
-
if (root.kind === 'Schema') topLevelNodes.add(root)
|
|
145
|
-
for (const schemaNode of ast.collect<ast.SchemaNode>(root, { schema: (node) => node })) {
|
|
146
|
-
if (!isCandidate(schemaNode, circularSchemas)) continue
|
|
147
|
-
|
|
148
|
-
const signature = ast.signatureOf(schemaNode)
|
|
149
|
-
const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name
|
|
150
|
-
const group = groups.get(signature)
|
|
151
|
-
if (group) {
|
|
152
|
-
group.count++
|
|
153
|
-
if (isTopLevel) group.topLevelNames.push(schemaNode.name!)
|
|
154
|
-
} else {
|
|
155
|
-
groups.set(signature, { count: 1, representative: schemaNode, topLevelNames: isTopLevel ? [schemaNode.name!] : [] })
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const bySignature = new Map<string, Target>()
|
|
161
|
-
const byName = new Map<string, Target>()
|
|
162
|
-
const pendingExtractions: Array<{ name: string; representative: ast.SchemaNode }> = []
|
|
163
|
-
|
|
164
|
-
for (const [signature, group] of groups) {
|
|
165
|
-
if (group.count < MIN_OCCURRENCES) continue
|
|
166
|
-
|
|
167
|
-
const [firstName, ...duplicateNames] = group.topLevelNames
|
|
168
|
-
if (firstName) {
|
|
169
|
-
const target: Target = { name: firstName, ref: `${SCHEMA_REF_PREFIX}${firstName}` }
|
|
170
|
-
bySignature.set(signature, target)
|
|
171
|
-
for (const duplicate of duplicateNames) {
|
|
172
|
-
byName.set(duplicate, target)
|
|
173
|
-
}
|
|
174
|
-
continue
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const name = nameFor(group.representative, usedNames)
|
|
178
|
-
if (!name) continue
|
|
179
|
-
|
|
180
|
-
bySignature.set(signature, { name, ref: `${SCHEMA_REF_PREFIX}${name}` })
|
|
181
|
-
pendingExtractions.push({ name, representative: group.representative })
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Rewrites a node against the resolved targets. `skipRootMatch` keeps a definition's own root from
|
|
185
|
-
// turning into a reference to itself. Nested duplicates are still collapsed.
|
|
186
|
-
function rewrite<T extends ast.Node>(node: T, skipRootMatch: boolean): T {
|
|
187
|
-
if (bySignature.size === 0 && byName.size === 0) return node
|
|
188
|
-
|
|
189
|
-
const root = node
|
|
190
|
-
|
|
191
|
-
return ast.transform(node, {
|
|
192
|
-
schema(schemaNode) {
|
|
193
|
-
if (schemaNode.type === 'ref') {
|
|
194
|
-
const refName = schemaNode.ref ? extractRefName(schemaNode.ref) : schemaNode.name
|
|
195
|
-
const target = refName ? byName.get(refName) : undefined
|
|
196
|
-
|
|
197
|
-
return target ? { ...schemaNode, name: target.name, ref: target.ref } : undefined
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (skipRootMatch && schemaNode === root) return undefined
|
|
201
|
-
|
|
202
|
-
const target = bySignature.get(ast.signatureOf(schemaNode))
|
|
203
|
-
if (!target) return undefined
|
|
204
|
-
|
|
205
|
-
return createRefNode(schemaNode, target)
|
|
206
|
-
},
|
|
207
|
-
}) as T
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Build extracted definitions only after every target name is known, so nested
|
|
211
|
-
// duplicates inside a definition also resolve to refs.
|
|
212
|
-
const extracted = pendingExtractions.map(({ name, representative }) => cleanDefinition(rewrite(representative, true), name))
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
extracted,
|
|
216
|
-
apply<T extends ast.Node>(node: T): T {
|
|
217
|
-
return rewrite(node, false)
|
|
218
|
-
},
|
|
219
|
-
applyTopLevel(node: ast.SchemaNode): ast.SchemaNode {
|
|
220
|
-
const target = bySignature.get(ast.signatureOf(node))
|
|
221
|
-
if (target && target.name !== node.name) {
|
|
222
|
-
return ast.factory.createSchema({
|
|
223
|
-
type: 'ref',
|
|
224
|
-
name: node.name ?? null,
|
|
225
|
-
ref: target.ref,
|
|
226
|
-
description: node.description,
|
|
227
|
-
deprecated: node.deprecated,
|
|
228
|
-
})
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return rewrite(node, true)
|
|
232
|
-
},
|
|
233
|
-
isAlias(name: string): boolean {
|
|
234
|
-
return byName.has(name)
|
|
235
|
-
},
|
|
236
|
-
}
|
|
237
|
-
}
|
package/src/dialect.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { ast } from '@kubb/core'
|
|
2
|
-
import { plan } from './dedupe.ts'
|
|
3
|
-
import { isDiscriminator, isNullable, isReference } from './guards.ts'
|
|
4
|
-
import { resolveRef } from './refs.ts'
|
|
5
|
-
import type { SchemaObject } from './types.ts'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The OpenAPI / Swagger dialect, the default used by `@kubb/adapter-oas`.
|
|
9
|
-
*
|
|
10
|
-
* Implements the spec-agnostic {@link ast.SchemaDialect} contract: it isolates the
|
|
11
|
-
* decisions that differ between specs (nullability, `$ref`, discriminator, binary,
|
|
12
|
-
* ref resolution) so the converter pipeline and dispatch rules stay shared. A
|
|
13
|
-
* future adapter (e.g. AsyncAPI) ships its own dialect, `type: ['null', …]`
|
|
14
|
-
* nullability, no discriminator object, binary via `contentEncoding` and reuses
|
|
15
|
-
* the rest unchanged.
|
|
16
|
-
*
|
|
17
|
-
* Formats (`uuid`, `email`, dates, …) are intentionally NOT here: they are shared
|
|
18
|
-
* JSON Schema vocabulary, so the converters keep that common case.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* const parser = createSchemaParser(context) // uses oasDialect
|
|
23
|
-
* const parser = createSchemaParser(context, oasDialect) // explicit
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export const oasDialect = ast.defineDialect({
|
|
27
|
-
name: 'oas',
|
|
28
|
-
schema: {
|
|
29
|
-
isNullable,
|
|
30
|
-
isReference,
|
|
31
|
-
isDiscriminator,
|
|
32
|
-
isBinary: (schema: SchemaObject) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
|
|
33
|
-
resolveRef,
|
|
34
|
-
},
|
|
35
|
-
dedupe: { plan },
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The concrete dialect type for `@kubb/adapter-oas`. Keeps the OAS guard predicates
|
|
40
|
-
* (`isReference`, `isDiscriminator`) intact so converters narrow schemas after a check.
|
|
41
|
-
*/
|
|
42
|
-
export type OasDialect = typeof oasDialect
|
package/src/discriminator.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { ast } from '@kubb/core'
|
|
2
|
-
import type { SchemaNodeByType } from '@kubb/ast'
|
|
3
|
-
|
|
4
|
-
export type DiscriminatorTarget = {
|
|
5
|
-
propertyName: string
|
|
6
|
-
enumValues: Array<string | number | boolean>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Maps each child schema name to its discriminator patch data by scanning the given
|
|
11
|
-
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
12
|
-
*
|
|
13
|
-
* The streaming path calls this on a small pre-parsed subset of schemas (only the
|
|
14
|
-
* discriminator parents) rather than on all schemas at once.
|
|
15
|
-
*/
|
|
16
|
-
export function buildDiscriminatorChildMap(schemas: Array<ast.SchemaNode>): Map<string, DiscriminatorTarget> {
|
|
17
|
-
const childMap = new Map<string, DiscriminatorTarget>()
|
|
18
|
-
|
|
19
|
-
for (const schema of schemas) {
|
|
20
|
-
// Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
|
|
21
|
-
// Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
|
|
22
|
-
let unionNode = ast.narrowSchema(schema, 'union')
|
|
23
|
-
|
|
24
|
-
if (!unionNode) {
|
|
25
|
-
const intersectionMembers = ast.narrowSchema(schema, 'intersection')?.members
|
|
26
|
-
if (intersectionMembers) {
|
|
27
|
-
for (const m of intersectionMembers) {
|
|
28
|
-
const u = ast.narrowSchema(m, 'union')
|
|
29
|
-
if (u) {
|
|
30
|
-
unionNode = u
|
|
31
|
-
break
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!unionNode?.discriminatorPropertyName || !unionNode.members) continue
|
|
38
|
-
|
|
39
|
-
const { discriminatorPropertyName, members } = unionNode
|
|
40
|
-
|
|
41
|
-
for (const member of members) {
|
|
42
|
-
// Members with a discriminant value are intersections: [RefSchemaNode, ObjectSchemaNode]
|
|
43
|
-
const intersectionNode = ast.narrowSchema(member, 'intersection')
|
|
44
|
-
if (!intersectionNode?.members) continue
|
|
45
|
-
|
|
46
|
-
let refNode: SchemaNodeByType['ref'] | null = null
|
|
47
|
-
let objNode: SchemaNodeByType['object'] | null = null
|
|
48
|
-
|
|
49
|
-
for (const m of intersectionNode.members) {
|
|
50
|
-
refNode ??= ast.narrowSchema(m, 'ref')
|
|
51
|
-
objNode ??= ast.narrowSchema(m, 'object')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!refNode?.name || !objNode) continue
|
|
55
|
-
|
|
56
|
-
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
|
|
57
|
-
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
|
|
58
|
-
if (!enumNode?.enumValues?.length) continue
|
|
59
|
-
|
|
60
|
-
const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
|
|
61
|
-
if (!enumValues.length) continue
|
|
62
|
-
|
|
63
|
-
const existing = childMap.get(refNode.name)
|
|
64
|
-
if (!existing) {
|
|
65
|
-
childMap.set(refNode.name, { propertyName: discriminatorPropertyName, enumValues: [...enumValues] })
|
|
66
|
-
continue
|
|
67
|
-
}
|
|
68
|
-
existing.enumValues.push(...enumValues)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return childMap
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
|
|
77
|
-
* the discriminant property). Used by the streaming path to apply patches inline per yield
|
|
78
|
-
* without buffering all schemas.
|
|
79
|
-
*/
|
|
80
|
-
export function patchDiscriminatorNode(node: ast.SchemaNode, entry: { propertyName: string; enumValues: Array<string | number | boolean> }): ast.SchemaNode {
|
|
81
|
-
const objectNode = ast.narrowSchema(node, 'object')
|
|
82
|
-
if (!objectNode) return node
|
|
83
|
-
|
|
84
|
-
const { propertyName, enumValues } = entry
|
|
85
|
-
const enumSchema = ast.factory.createSchema({ type: 'enum', enumValues })
|
|
86
|
-
const newProp = ast.factory.createProperty({ name: propertyName, required: true, schema: enumSchema })
|
|
87
|
-
|
|
88
|
-
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
|
|
89
|
-
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
|
|
90
|
-
|
|
91
|
-
return { ...objectNode, properties: newProperties }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Creates a single-property object schema used as a discriminator literal.
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* ```ts
|
|
99
|
-
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
100
|
-
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
|
-
export function createDiscriminantNode({ propertyName, value }: { propertyName: string; value: string }): ast.SchemaNode {
|
|
104
|
-
return ast.factory.createSchema({
|
|
105
|
-
type: 'object',
|
|
106
|
-
primitive: 'object',
|
|
107
|
-
properties: [
|
|
108
|
-
ast.factory.createProperty({
|
|
109
|
-
name: propertyName,
|
|
110
|
-
schema: ast.factory.createSchema({
|
|
111
|
-
type: 'enum',
|
|
112
|
-
primitive: 'string',
|
|
113
|
-
enumValues: [value],
|
|
114
|
-
}),
|
|
115
|
-
required: true,
|
|
116
|
-
}),
|
|
117
|
-
],
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
123
|
-
*
|
|
124
|
-
* @example
|
|
125
|
-
* ```ts
|
|
126
|
-
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
127
|
-
* ```
|
|
128
|
-
*/
|
|
129
|
-
export function findDiscriminator(mapping: Record<string, string> | undefined, ref: string | undefined): string | null {
|
|
130
|
-
if (!mapping || !ref) return null
|
|
131
|
-
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null
|
|
132
|
-
}
|
package/src/factory.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { exists, mergeDeep, Url } from '@internals/utils'
|
|
3
|
-
import { Diagnostics } from '@kubb/core'
|
|
4
|
-
import type { AdapterSource } from '@kubb/core'
|
|
5
|
-
import { compileErrors, validate } from '@readme/openapi-parser'
|
|
6
|
-
import { parse } from 'yaml'
|
|
7
|
-
import { bundleDocument } from './bundler.ts'
|
|
8
|
-
import { MERGE_DEFAULT_TITLE, MERGE_DEFAULT_VERSION, MERGE_OPENAPI_VERSION } from './constants.ts'
|
|
9
|
-
import { isOpenApiV2Document } from './guards.ts'
|
|
10
|
-
import type { Document } from './types.ts'
|
|
11
|
-
|
|
12
|
-
export type ParseOptions = {
|
|
13
|
-
canBundle?: boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type ValidateDocumentOptions = {
|
|
17
|
-
throwOnError?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Loads and bundles an OpenAPI document, returning the raw `Document`.
|
|
22
|
-
*
|
|
23
|
-
* Accepts a file path string or an already-parsed document object. File paths and URLs are
|
|
24
|
-
* bundled via `api-ref-bundler`, hoisting external file schemas into named `components.schemas`
|
|
25
|
-
* entries so generators can emit named types and imports. Swagger 2.0 documents are
|
|
26
|
-
* automatically up-converted to OpenAPI 3.0 via `swagger2openapi`.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```ts
|
|
30
|
-
* const document = await parseDocument('./openapi.yaml')
|
|
31
|
-
* const document = await parse(rawDocumentObject, { canBundle: false })
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export async function parseDocument(pathOrApi: string | Document, { canBundle = true }: ParseOptions = {}): Promise<Document> {
|
|
35
|
-
if (typeof pathOrApi === 'string' && canBundle) {
|
|
36
|
-
const bundled = await bundleDocument(pathOrApi)
|
|
37
|
-
|
|
38
|
-
return parseDocument(bundled, { canBundle: false })
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// A string here is always inline YAML/JSON content: file paths and URLs are read and parsed by
|
|
42
|
-
// `bundleDocument` first. `yaml.parse` also parses JSON, since JSON is a subset of YAML.
|
|
43
|
-
const document = (typeof pathOrApi === 'string' ? parse(pathOrApi) : pathOrApi) as Document
|
|
44
|
-
|
|
45
|
-
if (isOpenApiV2Document(document)) {
|
|
46
|
-
const { default: swagger2openapi } = await import('swagger2openapi')
|
|
47
|
-
const { openapi } = await swagger2openapi.convertObj(document, {
|
|
48
|
-
anchors: true,
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
return openapi as Document
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return document
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Deep-merges multiple OpenAPI documents into a single `Document`.
|
|
59
|
-
*
|
|
60
|
-
* Each document is parsed independently, then deep-merged into one in array order.
|
|
61
|
-
* Throws when the input array is empty.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```ts
|
|
65
|
-
* const document = await mergeDocuments(['./pets.yaml', './orders.yaml'])
|
|
66
|
-
* ```
|
|
67
|
-
*/
|
|
68
|
-
export async function mergeDocuments(pathOrApi: Array<string | Document>): Promise<Document> {
|
|
69
|
-
const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { canBundle: false })))
|
|
70
|
-
|
|
71
|
-
if (documents.length === 0) {
|
|
72
|
-
throw new Diagnostics.Error({
|
|
73
|
-
code: Diagnostics.code.inputRequired,
|
|
74
|
-
severity: 'error',
|
|
75
|
-
message: 'No OAS documents were provided for merging.',
|
|
76
|
-
help: 'Pass at least one path or document to `input.path`.',
|
|
77
|
-
location: { kind: 'config' },
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const seed: Document = {
|
|
82
|
-
openapi: MERGE_OPENAPI_VERSION,
|
|
83
|
-
info: { title: MERGE_DEFAULT_TITLE, version: MERGE_DEFAULT_VERSION },
|
|
84
|
-
paths: {},
|
|
85
|
-
components: { schemas: {} },
|
|
86
|
-
} as Document
|
|
87
|
-
|
|
88
|
-
const merged = documents.reduce(
|
|
89
|
-
(acc, current) => mergeDeep(acc as Record<string, unknown>, current as Record<string, unknown>),
|
|
90
|
-
seed as Record<string, unknown>,
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
return parseDocument(merged as Document)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Creates a `Document` from an `AdapterSource`.
|
|
98
|
-
*
|
|
99
|
-
* Handles all three source types:
|
|
100
|
-
* - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
|
|
101
|
-
* - `{ type: 'paths' }` merges multiple file paths into a single document.
|
|
102
|
-
* - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* ```ts
|
|
106
|
-
* const document = await parseFromConfig({ type: 'path', path: './openapi.yaml' })
|
|
107
|
-
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
export async function parseFromConfig(source: AdapterSource): Promise<Document> {
|
|
111
|
-
if (source.type === 'data') {
|
|
112
|
-
if (typeof source.data === 'object') {
|
|
113
|
-
return parseDocument(structuredClone(source.data) as Document)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return parseDocument(source.data as string, { canBundle: false })
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (source.type === 'paths') {
|
|
120
|
-
return mergeDocuments(source.paths)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// type === 'path'
|
|
124
|
-
if (Url.canParse(source.path)) {
|
|
125
|
-
return parseDocument(source.path)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const resolved = path.resolve(path.dirname(source.path), source.path)
|
|
129
|
-
await assertInputExists(resolved)
|
|
130
|
-
return parseDocument(resolved)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
135
|
-
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
136
|
-
* its parse error instead.
|
|
137
|
-
*/
|
|
138
|
-
export async function assertInputExists(input: string): Promise<void> {
|
|
139
|
-
if (Url.canParse(input)) {
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
if (!(await exists(input))) {
|
|
143
|
-
throw new Diagnostics.Error({
|
|
144
|
-
code: Diagnostics.code.inputNotFound,
|
|
145
|
-
severity: 'error',
|
|
146
|
-
message: `Cannot read the file set in \`input.path\` (or via \`kubb generate PATH\`): ${input}`,
|
|
147
|
-
help: 'Check that the path exists and is readable, then set it in `input.path` or pass it as `kubb generate PATH`.',
|
|
148
|
-
location: { kind: 'config' },
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
|
|
155
|
-
*
|
|
156
|
-
* @example
|
|
157
|
-
* ```ts
|
|
158
|
-
* await validateDocument(document)
|
|
159
|
-
* ```
|
|
160
|
-
*/
|
|
161
|
-
export async function validateDocument(document: Document, { throwOnError = false }: ValidateDocumentOptions = {}): Promise<void> {
|
|
162
|
-
try {
|
|
163
|
-
// `validate` dereferences its input in place, so clone to keep the cached document intact.
|
|
164
|
-
const result = await validate(structuredClone(document), {
|
|
165
|
-
validate: {
|
|
166
|
-
errors: { colorize: true },
|
|
167
|
-
},
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
if (!result.valid) {
|
|
171
|
-
throw new Error(compileErrors(result))
|
|
172
|
-
}
|
|
173
|
-
} catch (error) {
|
|
174
|
-
if (throwOnError) {
|
|
175
|
-
throw error
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Validation failures are non-fatal, mirror plugin-oas behavior
|
|
179
|
-
}
|
|
180
|
-
}
|
package/src/guards.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { isPlainObject } from '@internals/utils'
|
|
2
|
-
import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
|
|
3
|
-
import type { DiscriminatorObject, SchemaObject } from './types.ts'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* if (isOpenApiV2Document(doc)) {
|
|
11
|
-
* // doc is OpenAPIV2.Document
|
|
12
|
-
* }
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
export function isOpenApiV2Document(doc: unknown): doc is OpenAPIV2.Document {
|
|
16
|
-
return !!doc && isPlainObject(doc) && !('openapi' in doc)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Returns `true` when a schema should be treated as nullable.
|
|
21
|
-
*
|
|
22
|
-
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
23
|
-
* `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```ts
|
|
27
|
-
* isNullable({ type: 'string', nullable: true }) // true
|
|
28
|
-
* isNullable({ type: ['string', 'null'] }) // true
|
|
29
|
-
* isNullable({ type: 'string' }) // false
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
export function isNullable(schema?: SchemaObject & { 'x-nullable'?: boolean }): boolean {
|
|
33
|
-
const explicitNullable = schema?.nullable ?? schema?.['x-nullable']
|
|
34
|
-
if (explicitNullable === true) return true
|
|
35
|
-
|
|
36
|
-
const schemaType = schema?.type
|
|
37
|
-
if (schemaType === 'null') return true
|
|
38
|
-
if (Array.isArray(schemaType)) return schemaType.includes('null')
|
|
39
|
-
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```ts
|
|
48
|
-
* isReference({ $ref: '#/components/schemas/Pet' }) // true
|
|
49
|
-
* isReference({ type: 'string' }) // false
|
|
50
|
-
* ```
|
|
51
|
-
*/
|
|
52
|
-
export function isReference(obj?: unknown): obj is OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject {
|
|
53
|
-
return !!obj && typeof obj === 'object' && '$ref' in obj
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object.
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* ```ts
|
|
61
|
-
* isDiscriminator({ discriminator: { propertyName: 'type', mapping: {} } }) // true
|
|
62
|
-
* isDiscriminator({ discriminator: 'type' }) // false (Swagger 2 string form)
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
export function isDiscriminator(obj?: unknown): obj is SchemaObject & { discriminator: DiscriminatorObject } {
|
|
66
|
-
const record = obj as Record<string, unknown>
|
|
67
|
-
return !!obj && !!record['discriminator'] && typeof record['discriminator'] !== 'string'
|
|
68
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export { adapterOas, adapterOasName } from './adapter.ts'
|
|
2
|
-
export type {
|
|
3
|
-
AdapterOas,
|
|
4
|
-
AdapterOasOptions,
|
|
5
|
-
AdapterOasResolvedOptions,
|
|
6
|
-
ContentType,
|
|
7
|
-
DiscriminatorObject,
|
|
8
|
-
Document,
|
|
9
|
-
HttpMethod,
|
|
10
|
-
MediaTypeObject,
|
|
11
|
-
Operation,
|
|
12
|
-
ReferenceObject,
|
|
13
|
-
ResponseObject,
|
|
14
|
-
SchemaObject,
|
|
15
|
-
} from './types.ts'
|
package/src/mime.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MIME type fragments that mark a media type as JSON-like.
|
|
3
|
-
*
|
|
4
|
-
* A content type is JSON when it contains any of these substrings. The `+json` entry catches
|
|
5
|
-
* structured-syntax suffixes such as `application/vnd.api+json`.
|
|
6
|
-
*/
|
|
7
|
-
const jsonMimeFragments = ['application/json', 'application/x-json', 'text/json', 'text/x-json', '+json'] as const
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Returns `true` when a media type string is JSON-like.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* isJsonMimeType('application/json') // true
|
|
15
|
-
* isJsonMimeType('application/vnd.api+json') // true
|
|
16
|
-
* isJsonMimeType('multipart/form-data') // false
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
export function isJsonMimeType(mimeType: string): boolean {
|
|
20
|
-
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment))
|
|
21
|
-
}
|