@kubb/adapter-oas 5.0.0-beta.2 → 5.0.0-beta.21
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/README.md +98 -0
- package/dist/index.cjs +492 -317
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -49
- package/dist/index.js +492 -314
- package/dist/index.js.map +1 -1
- package/extension.yaml +344 -0
- package/package.json +6 -4
- package/src/adapter.bench.ts +64 -0
- package/src/adapter.ts +76 -42
- package/src/discriminator.ts +57 -40
- package/src/factory.ts +1 -4
- package/src/index.ts +2 -2
- package/src/parser.ts +69 -37
- package/src/refs.ts +16 -4
- package/src/resolvers.ts +12 -13
- package/src/stream.ts +175 -0
package/src/discriminator.ts
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import { ast } from '@kubb/core'
|
|
2
|
+
import type { SchemaNodeByType } from '@kubb/ast'
|
|
2
3
|
|
|
3
|
-
type DiscriminatorTarget = {
|
|
4
|
+
export type DiscriminatorTarget = {
|
|
4
5
|
propertyName: string
|
|
5
6
|
enumValues: Array<string | number | boolean>
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
|
|
12
|
-
* enum value each union member is mapped to, then adds (or replaces) that property on the matching
|
|
13
|
-
* child object schema.
|
|
10
|
+
* Builds a map of child schema names → discriminator patch data by scanning the given
|
|
11
|
+
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
14
12
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* ```ts
|
|
19
|
-
* const { root } = parseOas(document, options)
|
|
20
|
-
* const next = applyDiscriminatorInheritance(root)
|
|
21
|
-
* ```
|
|
13
|
+
* Extracted from `applyDiscriminatorInheritance` so the streaming path can call it on a
|
|
14
|
+
* small pre-parsed subset of schemas (only the discriminator parents) rather than on all
|
|
15
|
+
* schemas at once.
|
|
22
16
|
*/
|
|
23
|
-
export function
|
|
17
|
+
export function buildDiscriminatorChildMap(schemas: ast.SchemaNode[]): Map<string, DiscriminatorTarget> {
|
|
24
18
|
const childMap = new Map<string, DiscriminatorTarget>()
|
|
25
19
|
|
|
26
|
-
for (const schema of
|
|
20
|
+
for (const schema of schemas) {
|
|
27
21
|
// Case 1: top-level schema is a union (oneOf/anyOf with discriminator)
|
|
28
22
|
// Case 2: top-level schema is an intersection wrapping a union (oneOf/anyOf + shared properties)
|
|
29
23
|
let unionNode = ast.narrowSchema(schema, 'union')
|
|
@@ -50,8 +44,8 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
|
|
|
50
44
|
const intersectionNode = ast.narrowSchema(member, 'intersection')
|
|
51
45
|
if (!intersectionNode?.members) continue
|
|
52
46
|
|
|
53
|
-
let refNode:
|
|
54
|
-
let objNode:
|
|
47
|
+
let refNode: SchemaNodeByType['ref'] | null = null
|
|
48
|
+
let objNode: SchemaNodeByType['object'] | null = null
|
|
55
49
|
|
|
56
50
|
for (const m of intersectionNode.members) {
|
|
57
51
|
refNode ??= ast.narrowSchema(m, 'ref')
|
|
@@ -61,24 +55,61 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
|
|
|
61
55
|
if (!refNode?.name || !objNode) continue
|
|
62
56
|
|
|
63
57
|
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName)
|
|
64
|
-
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') :
|
|
58
|
+
const enumNode = prop ? ast.narrowSchema(prop.schema, 'enum') : null
|
|
65
59
|
if (!enumNode?.enumValues?.length) continue
|
|
66
60
|
|
|
67
61
|
const enumValues = enumNode.enumValues.filter((v): v is string | number | boolean => v !== null)
|
|
68
62
|
if (!enumValues.length) continue
|
|
69
63
|
|
|
70
64
|
const existing = childMap.get(refNode.name)
|
|
71
|
-
if (existing) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
childMap.set(refNode.name, {
|
|
75
|
-
propertyName: discriminatorPropertyName,
|
|
76
|
-
enumValues: [...enumValues],
|
|
77
|
-
})
|
|
65
|
+
if (!existing) {
|
|
66
|
+
childMap.set(refNode.name, { propertyName: discriminatorPropertyName, enumValues: [...enumValues] })
|
|
67
|
+
continue
|
|
78
68
|
}
|
|
69
|
+
existing.enumValues.push(...enumValues)
|
|
79
70
|
}
|
|
80
71
|
}
|
|
81
72
|
|
|
73
|
+
return childMap
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
|
|
78
|
+
* the discriminant property). Used by the streaming path to apply patches inline per yield
|
|
79
|
+
* without buffering all schemas.
|
|
80
|
+
*/
|
|
81
|
+
export function patchDiscriminatorNode(node: ast.SchemaNode, entry: { propertyName: string; enumValues: Array<string | number | boolean> }): ast.SchemaNode {
|
|
82
|
+
const objectNode = ast.narrowSchema(node, 'object')
|
|
83
|
+
if (!objectNode) return node
|
|
84
|
+
|
|
85
|
+
const { propertyName, enumValues } = entry
|
|
86
|
+
const enumSchema = ast.createSchema({ type: 'enum', enumValues })
|
|
87
|
+
const newProp = ast.createProperty({ name: propertyName, required: true, schema: enumSchema })
|
|
88
|
+
|
|
89
|
+
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
|
|
90
|
+
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
|
|
91
|
+
|
|
92
|
+
return { ...objectNode, properties: newProperties }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Injects discriminator enum values into child schemas so they know which value identifies them.
|
|
97
|
+
*
|
|
98
|
+
* Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
|
|
99
|
+
* enum value each union member is mapped to, then adds (or replaces) that property on the matching
|
|
100
|
+
* child object schema.
|
|
101
|
+
*
|
|
102
|
+
* Returns a new `InputNode` — the original is never mutated.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* const { root } = parseOas(document, options)
|
|
107
|
+
* const next = applyDiscriminatorInheritance(root)
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNode {
|
|
111
|
+
const childMap = buildDiscriminatorChildMap(root.schemas)
|
|
112
|
+
|
|
82
113
|
if (childMap.size === 0) return root
|
|
83
114
|
|
|
84
115
|
return ast.transform(root, {
|
|
@@ -88,21 +119,7 @@ export function applyDiscriminatorInheritance(root: ast.InputNode): ast.InputNod
|
|
|
88
119
|
const entry = childMap.get(node.name)
|
|
89
120
|
if (!entry) return
|
|
90
121
|
|
|
91
|
-
|
|
92
|
-
if (!objectNode) return
|
|
93
|
-
|
|
94
|
-
const { propertyName, enumValues } = entry
|
|
95
|
-
const enumSchema = ast.createSchema({ type: 'enum', enumValues })
|
|
96
|
-
const newProp = ast.createProperty({
|
|
97
|
-
name: propertyName,
|
|
98
|
-
required: true,
|
|
99
|
-
schema: enumSchema,
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName)
|
|
103
|
-
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => (i === existingIdx ? newProp : p)) : [...objectNode.properties, newProp]
|
|
104
|
-
|
|
105
|
-
return { ...objectNode, properties: newProperties }
|
|
122
|
+
return patchDiscriminatorNode(node, entry)
|
|
106
123
|
},
|
|
107
124
|
})
|
|
108
125
|
}
|
package/src/factory.ts
CHANGED
|
@@ -74,10 +74,7 @@ export async function parseDocument(pathOrApi: string | Document, { canBundle =
|
|
|
74
74
|
* ```
|
|
75
75
|
*/
|
|
76
76
|
export async function mergeDocuments(pathOrApi: Array<string | Document>): Promise<Document> {
|
|
77
|
-
const documents:
|
|
78
|
-
for (const p of pathOrApi) {
|
|
79
|
-
documents.push(await parseDocument(p, { enablePaths: false, canBundle: false }))
|
|
80
|
-
}
|
|
77
|
+
const documents = await Promise.all(pathOrApi.map((p) => parseDocument(p, { enablePaths: false, canBundle: false })))
|
|
81
78
|
|
|
82
79
|
if (documents.length === 0) {
|
|
83
80
|
throw new Error('No OAS documents provided for merging.')
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { adapterOas, adapterOasName } from './adapter.ts'
|
|
2
|
-
export type {
|
|
3
|
-
export { mergeDocuments
|
|
2
|
+
export type { ValidateDocumentOptions } from './factory.ts'
|
|
3
|
+
export { mergeDocuments } from './factory.ts'
|
|
4
4
|
export type {
|
|
5
5
|
AdapterOas,
|
|
6
6
|
AdapterOasOptions,
|
package/src/parser.ts
CHANGED
|
@@ -30,6 +30,16 @@ export type OasParserContext = {
|
|
|
30
30
|
contentType?: ContentType
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The object returned by {@link createSchemaParser}.
|
|
35
|
+
* Contains parser functions bound to a specific document.
|
|
36
|
+
*/
|
|
37
|
+
export type SchemaParser = {
|
|
38
|
+
parseSchema: (entry: { schema: SchemaObject; name?: string | null }, options?: Partial<ast.ParserOptions>) => ast.SchemaNode
|
|
39
|
+
parseOperation: (options: ast.ParserOptions, operation: Operation) => ast.OperationNode
|
|
40
|
+
parseParameter: (options: ast.ParserOptions, param: Record<string, unknown>) => ast.ParameterNode
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
/**
|
|
34
44
|
* Pre-computed per-schema context passed to every schema converter.
|
|
35
45
|
*
|
|
@@ -76,9 +86,9 @@ function normalizeArrayEnum(schema: SchemaObject): SchemaObject {
|
|
|
76
86
|
* Each converter branch (`convertRef`, `convertAllOf`, etc.) mutually recursively calls `parseSchema`,
|
|
77
87
|
* made possible by hoisting of function declarations.
|
|
78
88
|
*
|
|
79
|
-
* @
|
|
89
|
+
* @internal
|
|
80
90
|
*/
|
|
81
|
-
function createSchemaParser(ctx: OasParserContext) {
|
|
91
|
+
export function createSchemaParser(ctx: OasParserContext) {
|
|
82
92
|
const document = ctx.document
|
|
83
93
|
|
|
84
94
|
// Branch handlers — each converts one OAS schema pattern to a SchemaNode.
|
|
@@ -89,6 +99,20 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
89
99
|
*/
|
|
90
100
|
const resolvingRefs = new Set<string>()
|
|
91
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Cache of already-resolved `$ref` schemas within this parser instance.
|
|
104
|
+
*
|
|
105
|
+
* Without this, the same referenced schema (e.g. `customer`) is fully re-expanded
|
|
106
|
+
* every time it appears as a `$ref` in a different parent schema. In heavily
|
|
107
|
+
* cross-referenced specs like Stripe (~1 400 schemas), this causes exponential
|
|
108
|
+
* blowup — `customer` alone may be referenced from dozens of top-level schemas,
|
|
109
|
+
* each triggering a fresh recursive expansion of its entire sub-tree.
|
|
110
|
+
*
|
|
111
|
+
* Memoizing by `$ref` path reduces the overall work from O(2^depth) to O(N)
|
|
112
|
+
* where N is the number of unique schema names.
|
|
113
|
+
*/
|
|
114
|
+
const resolvedRefCache = new Map<string, ast.SchemaNode | null>()
|
|
115
|
+
|
|
92
116
|
/**
|
|
93
117
|
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
94
118
|
*
|
|
@@ -98,19 +122,23 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
98
122
|
* Circular refs are detected via `resolvingRefs` and leave `schema` as `undefined`.
|
|
99
123
|
*/
|
|
100
124
|
function convertRef({ schema, name, nullable, defaultValue, rawOptions }: SchemaContext): ast.SchemaNode {
|
|
101
|
-
let resolvedSchema: ast.SchemaNode |
|
|
125
|
+
let resolvedSchema: ast.SchemaNode | null = null
|
|
102
126
|
const refPath = schema.$ref
|
|
103
127
|
if (refPath && !resolvingRefs.has(refPath)) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
if (!resolvedRefCache.has(refPath)) {
|
|
129
|
+
try {
|
|
130
|
+
const referenced = resolveRef<SchemaObject>(document, refPath)
|
|
131
|
+
if (referenced) {
|
|
132
|
+
resolvingRefs.add(refPath)
|
|
133
|
+
resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
|
|
134
|
+
resolvingRefs.delete(refPath)
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
// Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
|
|
110
138
|
}
|
|
111
|
-
|
|
112
|
-
// Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
|
|
139
|
+
resolvedRefCache.set(refPath, resolvedSchema)
|
|
113
140
|
}
|
|
141
|
+
resolvedSchema = resolvedRefCache.get(refPath) ?? null
|
|
114
142
|
}
|
|
115
143
|
|
|
116
144
|
return ast.createSchema({
|
|
@@ -150,6 +178,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
150
178
|
default: mergedDefault,
|
|
151
179
|
example: schema.example ?? memberNode.example,
|
|
152
180
|
pattern: schema.pattern ?? ('pattern' in memberNode ? memberNode.pattern : undefined),
|
|
181
|
+
format: schema.format ?? memberNode.format,
|
|
153
182
|
} as ast.DistributiveOmit<ast.SchemaNode, 'kind'>)
|
|
154
183
|
}
|
|
155
184
|
|
|
@@ -226,7 +255,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
226
255
|
|
|
227
256
|
return ast.createSchema({
|
|
228
257
|
type: 'intersection',
|
|
229
|
-
members: [...ast.
|
|
258
|
+
members: [...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...ast.mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
|
|
230
259
|
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
231
260
|
})
|
|
232
261
|
}
|
|
@@ -340,6 +369,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
340
369
|
title: schema.title,
|
|
341
370
|
description: schema.description,
|
|
342
371
|
deprecated: schema.deprecated,
|
|
372
|
+
format: schema.format,
|
|
343
373
|
})
|
|
344
374
|
}
|
|
345
375
|
|
|
@@ -470,6 +500,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
470
500
|
writeOnly: schema.writeOnly,
|
|
471
501
|
default: enumDefault,
|
|
472
502
|
example: schema.example,
|
|
503
|
+
format: schema.format,
|
|
473
504
|
}
|
|
474
505
|
|
|
475
506
|
const extensionKey = enumExtensionKeys.find((key) => key in schema)
|
|
@@ -517,15 +548,17 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
517
548
|
|
|
518
549
|
const resolvedChildName = ast.childName(name, propName)
|
|
519
550
|
const propNode = parseSchema({ schema: resolvedPropSchema, name: resolvedChildName }, rawOptions)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
551
|
+
const schemaNode = (() => {
|
|
552
|
+
const node = ast.setEnumName(propNode, name, propName, options.enumSuffix)
|
|
553
|
+
const tupleNode = ast.narrowSchema(node, 'tuple')
|
|
554
|
+
if (tupleNode?.items) {
|
|
555
|
+
const namedItems = tupleNode.items.map((item) => ast.setEnumName(item, name, propName, options.enumSuffix))
|
|
556
|
+
if (namedItems.some((item, i) => item !== tupleNode.items![i])) {
|
|
557
|
+
return { ...tupleNode, items: namedItems }
|
|
558
|
+
}
|
|
527
559
|
}
|
|
528
|
-
|
|
560
|
+
return node
|
|
561
|
+
})()
|
|
529
562
|
|
|
530
563
|
return ast.createProperty({
|
|
531
564
|
name: propName,
|
|
@@ -539,18 +572,15 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
539
572
|
: []
|
|
540
573
|
|
|
541
574
|
const additionalProperties = schema.additionalProperties
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
type: typeOptionMap.get(options.unknownType)!,
|
|
552
|
-
})
|
|
553
|
-
}
|
|
575
|
+
const additionalPropertiesNode: ast.SchemaNode | boolean | undefined = (() => {
|
|
576
|
+
if (additionalProperties === true) return true
|
|
577
|
+
if (additionalProperties === false) return false
|
|
578
|
+
if (additionalProperties && Object.keys(additionalProperties).length > 0) {
|
|
579
|
+
return parseSchema({ schema: additionalProperties as SchemaObject }, rawOptions)
|
|
580
|
+
}
|
|
581
|
+
if (additionalProperties) return ast.createSchema({ type: typeOptionMap.get(options.unknownType)! })
|
|
582
|
+
return undefined
|
|
583
|
+
})()
|
|
554
584
|
|
|
555
585
|
const rawPatternProperties = 'patternProperties' in schema ? schema.patternProperties : undefined
|
|
556
586
|
|
|
@@ -616,7 +646,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
616
646
|
*/
|
|
617
647
|
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options }: SchemaContext): ast.SchemaNode {
|
|
618
648
|
const rawItems = schema.items as SchemaObject | undefined
|
|
619
|
-
const itemName = rawItems?.enum?.length && name ? ast.enumPropName(
|
|
649
|
+
const itemName = rawItems?.enum?.length && name ? ast.enumPropName(null, name, options.enumSuffix) : undefined
|
|
620
650
|
const items = rawItems ? [parseSchema({ schema: rawItems, name: itemName }, rawOptions)] : []
|
|
621
651
|
|
|
622
652
|
return ast.createSchema({
|
|
@@ -683,6 +713,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
683
713
|
description: schema.description,
|
|
684
714
|
deprecated: schema.deprecated,
|
|
685
715
|
nullable,
|
|
716
|
+
format: schema.format,
|
|
686
717
|
})
|
|
687
718
|
}
|
|
688
719
|
|
|
@@ -776,6 +807,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
776
807
|
name,
|
|
777
808
|
title: schema.title,
|
|
778
809
|
description: schema.description,
|
|
810
|
+
format: schema.format,
|
|
779
811
|
})
|
|
780
812
|
}
|
|
781
813
|
|
|
@@ -839,8 +871,8 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
839
871
|
* Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
|
|
840
872
|
* `$ref` entries are skipped since their flags live on the dereferenced target.
|
|
841
873
|
*/
|
|
842
|
-
function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string[] |
|
|
843
|
-
if (!schema?.properties) return
|
|
874
|
+
function collectPropertyKeysByFlag(schema: SchemaObject | null, flag: 'readOnly' | 'writeOnly'): string[] | null {
|
|
875
|
+
if (!schema?.properties) return null
|
|
844
876
|
|
|
845
877
|
const keys: string[] = []
|
|
846
878
|
for (const key in schema.properties) {
|
|
@@ -849,7 +881,7 @@ function createSchemaParser(ctx: OasParserContext) {
|
|
|
849
881
|
keys.push(key)
|
|
850
882
|
}
|
|
851
883
|
}
|
|
852
|
-
return keys.length ? keys :
|
|
884
|
+
return keys.length ? keys : null
|
|
853
885
|
}
|
|
854
886
|
|
|
855
887
|
/**
|
|
@@ -990,7 +1022,7 @@ export function parseOas(
|
|
|
990
1022
|
const baseOas = new BaseOas(document)
|
|
991
1023
|
const paths = baseOas.getPaths()
|
|
992
1024
|
|
|
993
|
-
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([
|
|
1025
|
+
const operations: Array<ast.OperationNode> = Object.entries(paths).flatMap(([, methods]) =>
|
|
994
1026
|
Object.entries(methods)
|
|
995
1027
|
.map(([, operation]) => (operation ? _parseOperation(mergedOptions, operation) : null))
|
|
996
1028
|
.filter((op): op is ast.OperationNode => op !== null),
|
package/src/refs.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isReference } from './guards.ts'
|
|
2
2
|
import type { Document } from './types.ts'
|
|
3
3
|
|
|
4
|
+
const _refCache = new WeakMap<Document, Map<string, unknown>>()
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Resolves a local JSON pointer reference from a document.
|
|
6
8
|
*
|
|
@@ -18,11 +20,19 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
|
|
|
18
20
|
if ($ref === '') {
|
|
19
21
|
return null
|
|
20
22
|
}
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
if (!$ref.startsWith('#')) return null
|
|
24
|
+
$ref = globalThis.decodeURIComponent($ref.substring(1))
|
|
25
|
+
|
|
26
|
+
let docCache = _refCache.get(document)
|
|
27
|
+
if (!docCache) {
|
|
28
|
+
docCache = new Map()
|
|
29
|
+
_refCache.set(document, docCache)
|
|
25
30
|
}
|
|
31
|
+
|
|
32
|
+
if (docCache.has($ref)) {
|
|
33
|
+
return docCache.get($ref) as T
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
const current = $ref
|
|
27
37
|
.split('/')
|
|
28
38
|
.filter(Boolean)
|
|
@@ -31,6 +41,8 @@ export function resolveRef<T = unknown>(document: Document, $ref: string): T | n
|
|
|
31
41
|
if (!current) {
|
|
32
42
|
throw new Error(`Could not find a definition for ${origRef}.`)
|
|
33
43
|
}
|
|
44
|
+
|
|
45
|
+
docCache.set($ref, current)
|
|
34
46
|
return current as T
|
|
35
47
|
}
|
|
36
48
|
|
package/src/resolvers.ts
CHANGED
|
@@ -305,27 +305,25 @@ export function extractSchemaFromContent(content: Record<string, unknown> | unde
|
|
|
305
305
|
/**
|
|
306
306
|
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
307
307
|
*/
|
|
308
|
-
function collectRefs(schema: unknown
|
|
308
|
+
function* collectRefs(schema: unknown): Generator<string, void, undefined> {
|
|
309
309
|
if (Array.isArray(schema)) {
|
|
310
|
-
for (const item of schema) collectRefs(item
|
|
311
|
-
return
|
|
310
|
+
for (const item of schema) yield* collectRefs(item)
|
|
311
|
+
return
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
if (schema && typeof schema === 'object') {
|
|
315
315
|
for (const key in schema) {
|
|
316
316
|
const value = (schema as Record<string, unknown>)[key]
|
|
317
|
-
if (key === '$ref' && typeof value === 'string') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
317
|
+
if (!(key === '$ref' && typeof value === 'string')) {
|
|
318
|
+
yield* collectRefs(value)
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
if (value.startsWith(SCHEMA_REF_PREFIX)) {
|
|
322
|
+
const name = value.slice(SCHEMA_REF_PREFIX.length)
|
|
323
|
+
if (name) yield name
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
|
-
|
|
328
|
-
return refs
|
|
329
327
|
}
|
|
330
328
|
|
|
331
329
|
/**
|
|
@@ -344,7 +342,7 @@ export function sortSchemas(schemas: Record<string, SchemaObject>): Record<strin
|
|
|
344
342
|
const deps = new Map<string, string[]>()
|
|
345
343
|
|
|
346
344
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
347
|
-
deps.set(name,
|
|
345
|
+
deps.set(name, [...new Set(collectRefs(schema))])
|
|
348
346
|
}
|
|
349
347
|
|
|
350
348
|
const sorted: string[] = []
|
|
@@ -514,6 +512,7 @@ export function buildSchemaNode(schema: SchemaObject, name: string | null | unde
|
|
|
514
512
|
writeOnly: schema.writeOnly,
|
|
515
513
|
default: defaultValue,
|
|
516
514
|
example: schema.example,
|
|
515
|
+
format: schema.format,
|
|
517
516
|
} as const
|
|
518
517
|
}
|
|
519
518
|
|
package/src/stream.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { ast } from '@kubb/core'
|
|
2
|
+
import type BaseOas from 'oas'
|
|
3
|
+
import { buildDiscriminatorChildMap, patchDiscriminatorNode } from './discriminator.ts'
|
|
4
|
+
import type { SchemaParser } from './parser.ts'
|
|
5
|
+
import { resolveServerUrl } from './resolvers.ts'
|
|
6
|
+
import type { DiscriminatorTarget } from './discriminator.ts'
|
|
7
|
+
import type { AdapterOas, Document, SchemaObject } from './types.ts'
|
|
8
|
+
|
|
9
|
+
export type PreScanResult = {
|
|
10
|
+
refAliasMap: Map<string, ast.SchemaNode>
|
|
11
|
+
enumNames: string[]
|
|
12
|
+
circularNames: string[]
|
|
13
|
+
discriminatorChildMap: Map<string, DiscriminatorTarget> | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads the server URL from the document's `servers` array at `serverIndex`,
|
|
18
|
+
* interpolating any `serverVariables` into the URL template.
|
|
19
|
+
*
|
|
20
|
+
* Returns `null` when `serverIndex` is omitted or out of range.
|
|
21
|
+
*
|
|
22
|
+
* @example Resolve the first server
|
|
23
|
+
* `resolveBaseUrl({ document, serverIndex: 0 })`
|
|
24
|
+
*
|
|
25
|
+
* @example Override a path variable
|
|
26
|
+
* `resolveBaseUrl({ document, serverIndex: 0, serverVariables: { version: 'v2' } })`
|
|
27
|
+
*/
|
|
28
|
+
export function resolveBaseUrl({
|
|
29
|
+
document,
|
|
30
|
+
serverIndex,
|
|
31
|
+
serverVariables,
|
|
32
|
+
}: {
|
|
33
|
+
document: Document
|
|
34
|
+
serverIndex?: number
|
|
35
|
+
serverVariables?: Record<string, string>
|
|
36
|
+
}): string | null {
|
|
37
|
+
const server = serverIndex !== undefined ? document.servers?.at(serverIndex) : undefined
|
|
38
|
+
return server?.url ? resolveServerUrl(server, serverVariables) : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parses every schema once to build the lookup structures that streaming needs upfront.
|
|
43
|
+
*
|
|
44
|
+
* Three things happen in this single pass:
|
|
45
|
+
* - `refAliasMap` records schemas that are pure `$ref` aliases so the streaming pass can inline them.
|
|
46
|
+
* - `enumNames` collects the names of every enum schema so plugins skip re-scanning the stream.
|
|
47
|
+
* - `circularNames` runs cycle detection, which requires all nodes in memory simultaneously.
|
|
48
|
+
* The `allNodes` array is local and drops out of scope as soon as this function returns.
|
|
49
|
+
*
|
|
50
|
+
* After this call, only `refAliasMap` and `discriminatorChildMap` stay alive in the adapter closure.
|
|
51
|
+
* Both are proportional to the number of aliases or discriminator parents, not total schema count.
|
|
52
|
+
*
|
|
53
|
+
* Each schema is parsed again during the streaming pass — this is intentional.
|
|
54
|
+
* Holding the parsed nodes in memory here would defeat the streaming memory benefit.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const { refAliasMap, enumNames, circularNames } = preScan({
|
|
59
|
+
* schemas,
|
|
60
|
+
* parseSchema,
|
|
61
|
+
* parserOptions,
|
|
62
|
+
* discriminator: 'strict',
|
|
63
|
+
* })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function preScan({
|
|
67
|
+
schemas,
|
|
68
|
+
parseSchema,
|
|
69
|
+
parserOptions,
|
|
70
|
+
discriminator,
|
|
71
|
+
}: {
|
|
72
|
+
schemas: Record<string, SchemaObject>
|
|
73
|
+
parseSchema: (entry: { schema: SchemaObject; name: string }, options: ast.ParserOptions) => ast.SchemaNode
|
|
74
|
+
parserOptions: ast.ParserOptions
|
|
75
|
+
discriminator: AdapterOas['options']['discriminator']
|
|
76
|
+
}): PreScanResult {
|
|
77
|
+
const allNodes: ast.SchemaNode[] = []
|
|
78
|
+
const refAliasMap = new Map<string, ast.SchemaNode>()
|
|
79
|
+
const enumNames: string[] = []
|
|
80
|
+
const discriminatorParentNodes: ast.SchemaNode[] = []
|
|
81
|
+
|
|
82
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
83
|
+
const node = parseSchema({ schema, name }, parserOptions)
|
|
84
|
+
allNodes.push(node)
|
|
85
|
+
if (node.type === 'ref' && node.name && node.name !== name) {
|
|
86
|
+
refAliasMap.set(name, node)
|
|
87
|
+
}
|
|
88
|
+
if (ast.narrowSchema(node, ast.schemaTypes.enum) && node.name) {
|
|
89
|
+
enumNames.push(node.name)
|
|
90
|
+
}
|
|
91
|
+
if (discriminator === 'inherit' && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) {
|
|
92
|
+
discriminatorParentNodes.push(node)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const circularNames = [...ast.findCircularSchemas(allNodes)]
|
|
97
|
+
const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null
|
|
98
|
+
|
|
99
|
+
return { refAliasMap, enumNames, circularNames, discriminatorChildMap }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates a lazy `InputStreamNode` from already-resolved adapter state.
|
|
104
|
+
*
|
|
105
|
+
* The schema and operation iterables each start a fresh parse pass on every
|
|
106
|
+
* `[Symbol.asyncIterator]()` call. This lets multiple plugins consume the same
|
|
107
|
+
* stream object independently without sharing a cursor or holding all nodes in memory.
|
|
108
|
+
*
|
|
109
|
+
* Ref aliases in `refAliasMap` are inlined during iteration: an alias entry is replaced
|
|
110
|
+
* with its target's parsed node (but keeps the alias name) so plugins never receive bare `ref` nodes.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* const streamNode = createInputStream({ schemas, parseSchema, parseOperation, baseOas, parserOptions, refAliasMap, discriminatorChildMap, meta })
|
|
115
|
+
* for await (const schema of streamNode.schemas) {
|
|
116
|
+
* // each call to for-await restarts from the first schema
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function createInputStream({
|
|
121
|
+
schemas,
|
|
122
|
+
parseSchema,
|
|
123
|
+
parseOperation,
|
|
124
|
+
baseOas,
|
|
125
|
+
parserOptions,
|
|
126
|
+
refAliasMap,
|
|
127
|
+
discriminatorChildMap,
|
|
128
|
+
meta,
|
|
129
|
+
}: {
|
|
130
|
+
schemas: Record<string, SchemaObject>
|
|
131
|
+
parseSchema: SchemaParser['parseSchema']
|
|
132
|
+
parseOperation: SchemaParser['parseOperation']
|
|
133
|
+
baseOas: BaseOas
|
|
134
|
+
parserOptions: ast.ParserOptions
|
|
135
|
+
refAliasMap: Map<string, ast.SchemaNode>
|
|
136
|
+
discriminatorChildMap: Map<string, DiscriminatorTarget> | null
|
|
137
|
+
meta: ast.InputMeta
|
|
138
|
+
}): ast.InputStreamNode {
|
|
139
|
+
const schemasIterable: AsyncIterable<ast.SchemaNode> = {
|
|
140
|
+
[Symbol.asyncIterator]() {
|
|
141
|
+
return (async function* () {
|
|
142
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
143
|
+
// Inline ref aliases: replace the alias entry with its target's parsed node
|
|
144
|
+
// (keeping the alias name). Skip the first parse entirely for alias entries
|
|
145
|
+
// since that result is never used.
|
|
146
|
+
const alias = refAliasMap.get(name)
|
|
147
|
+
if (alias?.name && schemas[alias.name]) {
|
|
148
|
+
yield { ...parseSchema({ schema: schemas[alias.name]!, name: alias.name }, parserOptions), name }
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const parsed = parseSchema({ schema, name }, parserOptions)
|
|
153
|
+
const node = discriminatorChildMap?.get(name) ? patchDiscriminatorNode(parsed, discriminatorChildMap.get(name)!) : parsed
|
|
154
|
+
yield node
|
|
155
|
+
}
|
|
156
|
+
})()
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const operationsIterable: AsyncIterable<ast.OperationNode> = {
|
|
161
|
+
[Symbol.asyncIterator]() {
|
|
162
|
+
return (async function* () {
|
|
163
|
+
for (const methods of Object.values(baseOas.getPaths())) {
|
|
164
|
+
for (const operation of Object.values(methods)) {
|
|
165
|
+
if (!operation) continue
|
|
166
|
+
const node = parseOperation(parserOptions, operation)
|
|
167
|
+
if (node) yield node
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
})()
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return ast.createStreamInput(schemasIterable, operationsIterable, meta)
|
|
175
|
+
}
|