@kubb/adapter-oas 5.0.0-beta.10 → 5.0.0-beta.100
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/LICENSE +17 -10
- package/README.md +26 -24
- package/dist/index.cjs +1576 -1249
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +846 -95
- package/dist/index.js +1573 -1241
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
- package/extension.yaml +0 -344
- package/src/adapter.ts +0 -130
- package/src/constants.ts +0 -122
- package/src/discriminator.ts +0 -108
- package/src/factory.ts +0 -162
- package/src/guards.ts +0 -68
- package/src/index.ts +0 -18
- package/src/parser.ts +0 -1002
- package/src/refs.ts +0 -74
- package/src/resolvers.ts +0 -544
- package/src/types.ts +0 -206
- /package/dist/{chunk--u3MIqq1.js → rolldown-runtime-C0LytTxp.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,23 +1,16 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import { ast,
|
|
1
|
+
import "./rolldown-runtime-C0LytTxp.js";
|
|
2
|
+
import { ast, findCircularSchemas, narrowSchema } from "@kubb/ast";
|
|
3
|
+
import { Diagnostics, createAdapter } from "@kubb/core";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import { access, readFile } from "node:fs/promises";
|
|
6
|
+
import { compileErrors, validate } from "@readme/openapi-parser";
|
|
7
|
+
import { upgrade } from "@scalar/openapi-upgrader";
|
|
8
|
+
import { bundle } from "api-ref-bundler";
|
|
9
|
+
import { parse } from "yaml";
|
|
10
|
+
import { childName, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
|
|
10
11
|
//#region src/constants.ts
|
|
11
12
|
/**
|
|
12
13
|
* Default parser options applied when no explicit options are provided.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```ts
|
|
16
|
-
* import { DEFAULT_PARSER_OPTIONS } from '@kubb/adapter-oas'
|
|
17
|
-
*
|
|
18
|
-
* const parser = createOasParser(oas)
|
|
19
|
-
* const root = parser.parse({ ...DEFAULT_PARSER_OPTIONS, dateType: 'date' })
|
|
20
|
-
* ```
|
|
21
14
|
*/
|
|
22
15
|
const DEFAULT_PARSER_OPTIONS = {
|
|
23
16
|
dateType: "string",
|
|
@@ -38,32 +31,26 @@ const DEFAULT_PARSER_OPTIONS = {
|
|
|
38
31
|
*/
|
|
39
32
|
const SCHEMA_REF_PREFIX = "#/components/schemas/";
|
|
40
33
|
/**
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
const MERGE_OPENAPI_VERSION = "3.0.0";
|
|
44
|
-
/**
|
|
45
|
-
* Fallback `info.title` placed in the stub document when merging multiple API files.
|
|
46
|
-
*/
|
|
47
|
-
const MERGE_DEFAULT_TITLE = "Merged API";
|
|
48
|
-
/**
|
|
49
|
-
* Fallback `info.version` placed in the stub document when merging multiple API files.
|
|
34
|
+
* HTTP methods that count as operations on an OpenAPI path item. Other keys
|
|
35
|
+
* (`parameters`, `summary`, `$ref`, vendor extensions) are skipped when iterating operations.
|
|
50
36
|
*/
|
|
51
|
-
const
|
|
37
|
+
const SUPPORTED_METHODS = /* @__PURE__ */ new Set([
|
|
38
|
+
"get",
|
|
39
|
+
"put",
|
|
40
|
+
"post",
|
|
41
|
+
"delete",
|
|
42
|
+
"options",
|
|
43
|
+
"head",
|
|
44
|
+
"patch",
|
|
45
|
+
"trace"
|
|
46
|
+
]);
|
|
52
47
|
/**
|
|
53
48
|
* Set of JSON Schema keywords that prevent a schema fragment from being inlined during `allOf` flattening.
|
|
54
49
|
*
|
|
55
50
|
* A fragment that contains any of these keys carries structural meaning of its own and must stay as a separate
|
|
56
51
|
* intersection member rather than being merged into the parent.
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```ts
|
|
60
|
-
* import { structuralKeys } from '@kubb/adapter-oas'
|
|
61
|
-
*
|
|
62
|
-
* const isStructural = Object.keys(fragment).some((key) => structuralKeys.has(key))
|
|
63
|
-
* // true when fragment has e.g. 'properties' or 'oneOf'
|
|
64
|
-
* ```
|
|
65
52
|
*/
|
|
66
|
-
const structuralKeys = new Set([
|
|
53
|
+
const structuralKeys = /* @__PURE__ */ new Set([
|
|
67
54
|
"properties",
|
|
68
55
|
"items",
|
|
69
56
|
"additionalProperties",
|
|
@@ -73,21 +60,24 @@ const structuralKeys = new Set([
|
|
|
73
60
|
"not"
|
|
74
61
|
]);
|
|
75
62
|
/**
|
|
63
|
+
* Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
|
|
64
|
+
* `int64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
65
|
+
* special-cases in `parser.ts`. `isHandledFormat` reads it so the
|
|
66
|
+
* `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
|
|
67
|
+
*/
|
|
68
|
+
const specialCasedFormats = /* @__PURE__ */ new Set([
|
|
69
|
+
"int64",
|
|
70
|
+
"date-time",
|
|
71
|
+
"date",
|
|
72
|
+
"time"
|
|
73
|
+
]);
|
|
74
|
+
/**
|
|
76
75
|
* Static map from OAS `format` strings to Kubb `SchemaType` values.
|
|
77
76
|
*
|
|
78
77
|
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
79
|
-
* Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled
|
|
80
|
-
* in the parser. `ipv4` and `ipv6` map to their own dedicated schema types
|
|
81
|
-
* `idn-hostname` map to `'url'` as the closest generic string-format type.
|
|
82
|
-
*
|
|
83
|
-
* @example
|
|
84
|
-
* ```ts
|
|
85
|
-
* import { formatMap } from '@kubb/adapter-oas'
|
|
86
|
-
*
|
|
87
|
-
* formatMap['uuid'] // 'uuid'
|
|
88
|
-
* formatMap['binary'] // 'blob'
|
|
89
|
-
* formatMap['float'] // 'number'
|
|
90
|
-
* ```
|
|
78
|
+
* Formats that depend on runtime options (`int64`, `date-time`, `date`, `time`) are handled
|
|
79
|
+
* separately in the parser. `ipv4` and `ipv6` map to their own dedicated schema types. `hostname`
|
|
80
|
+
* and `idn-hostname` map to `'url'` as the closest generic string-format type.
|
|
91
81
|
*/
|
|
92
82
|
const formatMap = {
|
|
93
83
|
uuid: "uuid",
|
|
@@ -108,44 +98,24 @@ const formatMap = {
|
|
|
108
98
|
};
|
|
109
99
|
/**
|
|
110
100
|
* Vendor extension keys that attach human-readable labels to enum values, checked in priority order.
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* ```ts
|
|
114
|
-
* import { enumExtensionKeys } from '@kubb/adapter-oas'
|
|
115
|
-
*
|
|
116
|
-
* const key = enumExtensionKeys.find((k) => k in schema) // 'x-enumNames' | 'x-enum-varnames' | undefined
|
|
117
|
-
* ```
|
|
118
101
|
*/
|
|
119
102
|
const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
|
|
120
103
|
/**
|
|
121
|
-
*
|
|
122
|
-
* Replaces a plain object lookup with a `Map` for explicit key membership testing via `.has()`.
|
|
104
|
+
* Vendor extension keys that attach human-readable descriptions to enum values, checked in priority order.
|
|
123
105
|
*/
|
|
124
|
-
const
|
|
125
|
-
["any", ast.schemaTypes.any],
|
|
126
|
-
["unknown", ast.schemaTypes.unknown],
|
|
127
|
-
["void", ast.schemaTypes.void]
|
|
128
|
-
]);
|
|
106
|
+
const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
|
|
129
107
|
//#endregion
|
|
130
108
|
//#region src/discriminator.ts
|
|
131
109
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* Finds every union schema in `input.schemas` that has a `discriminatorPropertyName`, collects the
|
|
135
|
-
* enum value each union member is mapped to, then adds (or replaces) that property on the matching
|
|
136
|
-
* child object schema.
|
|
137
|
-
*
|
|
138
|
-
* Returns a new `InputNode` — the original is never mutated.
|
|
110
|
+
* Maps each child schema name to its discriminator patch data by scanning the given
|
|
111
|
+
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
139
112
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* const { root } = parseOas(document, options)
|
|
143
|
-
* const next = applyDiscriminatorInheritance(root)
|
|
144
|
-
* ```
|
|
113
|
+
* Called on a small pre-parsed subset of schemas (only the discriminator parents)
|
|
114
|
+
* rather than on all schemas at once.
|
|
145
115
|
*/
|
|
146
|
-
function
|
|
116
|
+
function buildDiscriminatorChildMap(schemas) {
|
|
147
117
|
const childMap = /* @__PURE__ */ new Map();
|
|
148
|
-
for (const schema of
|
|
118
|
+
for (const schema of schemas) {
|
|
149
119
|
let unionNode = ast.narrowSchema(schema, "union");
|
|
150
120
|
if (!unionNode) {
|
|
151
121
|
const intersectionMembers = ast.narrowSchema(schema, "intersection")?.members;
|
|
@@ -162,527 +132,285 @@ function applyDiscriminatorInheritance(root) {
|
|
|
162
132
|
for (const member of members) {
|
|
163
133
|
const intersectionNode = ast.narrowSchema(member, "intersection");
|
|
164
134
|
if (!intersectionNode?.members) continue;
|
|
165
|
-
let refNode;
|
|
166
|
-
let objNode;
|
|
135
|
+
let refNode = null;
|
|
136
|
+
let objNode = null;
|
|
167
137
|
for (const m of intersectionNode.members) {
|
|
168
138
|
refNode ??= ast.narrowSchema(m, "ref");
|
|
169
139
|
objNode ??= ast.narrowSchema(m, "object");
|
|
170
140
|
}
|
|
171
141
|
if (!refNode?.name || !objNode) continue;
|
|
172
142
|
const prop = objNode.properties.find((p) => p.name === discriminatorPropertyName);
|
|
173
|
-
const enumNode = prop ? ast.narrowSchema(prop.schema, "enum") :
|
|
143
|
+
const enumNode = prop ? ast.narrowSchema(prop.schema, "enum") : null;
|
|
174
144
|
if (!enumNode?.enumValues?.length) continue;
|
|
175
145
|
const enumValues = enumNode.enumValues.filter((v) => v !== null);
|
|
176
146
|
if (!enumValues.length) continue;
|
|
177
147
|
const existing = childMap.get(refNode.name);
|
|
178
|
-
if (existing)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
148
|
+
if (!existing) {
|
|
149
|
+
childMap.set(refNode.name, {
|
|
150
|
+
propertyName: discriminatorPropertyName,
|
|
151
|
+
enumValues: [...enumValues]
|
|
152
|
+
});
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
existing.enumValues.push(...enumValues);
|
|
183
156
|
}
|
|
184
157
|
}
|
|
185
|
-
|
|
186
|
-
return ast.transform(root, { schema(node, { parent }) {
|
|
187
|
-
if (parent?.kind !== "Input" || !node.name) return;
|
|
188
|
-
const entry = childMap.get(node.name);
|
|
189
|
-
if (!entry) return;
|
|
190
|
-
const objectNode = ast.narrowSchema(node, "object");
|
|
191
|
-
if (!objectNode) return;
|
|
192
|
-
const { propertyName, enumValues } = entry;
|
|
193
|
-
const enumSchema = ast.createSchema({
|
|
194
|
-
type: "enum",
|
|
195
|
-
enumValues
|
|
196
|
-
});
|
|
197
|
-
const newProp = ast.createProperty({
|
|
198
|
-
name: propertyName,
|
|
199
|
-
required: true,
|
|
200
|
-
schema: enumSchema
|
|
201
|
-
});
|
|
202
|
-
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
|
|
203
|
-
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
|
|
204
|
-
return {
|
|
205
|
-
...objectNode,
|
|
206
|
-
properties: newProperties
|
|
207
|
-
};
|
|
208
|
-
} });
|
|
209
|
-
}
|
|
210
|
-
//#endregion
|
|
211
|
-
//#region ../../internals/utils/src/casing.ts
|
|
212
|
-
/**
|
|
213
|
-
* Shared implementation for camelCase and PascalCase conversion.
|
|
214
|
-
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
215
|
-
* and capitalizes each word according to `pascal`.
|
|
216
|
-
*
|
|
217
|
-
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
218
|
-
*/
|
|
219
|
-
function toCamelOrPascal(text, pascal) {
|
|
220
|
-
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
221
|
-
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
222
|
-
if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
|
|
223
|
-
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
224
|
-
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
158
|
+
return childMap;
|
|
225
159
|
}
|
|
226
160
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
* Segments are joined with `/` to form a file path.
|
|
230
|
-
*
|
|
231
|
-
* Only splits on dots followed by a letter so that version numbers
|
|
232
|
-
* embedded in operationIds (e.g. `v2025.0`) are kept intact.
|
|
233
|
-
*
|
|
234
|
-
* Empty segments are filtered before joining. They arise when the text starts with
|
|
235
|
-
* a dot followed immediately by a letter (e.g. `..Schema` splits into `['..', 'Schema']`
|
|
236
|
-
* and `'..'` transforms to an empty string). Without this filter the join would produce
|
|
237
|
-
* a leading `/`, which `path.resolve` would interpret as an absolute path, allowing
|
|
238
|
-
* generated files to escape the configured output directory.
|
|
161
|
+
* Patches a single top-level `SchemaNode` with its discriminator entry (adds or replaces
|
|
162
|
+
* the discriminant property).
|
|
239
163
|
*/
|
|
240
|
-
function
|
|
241
|
-
const
|
|
242
|
-
|
|
164
|
+
function patchDiscriminatorNode(node, entry) {
|
|
165
|
+
const objectNode = ast.narrowSchema(node, "object");
|
|
166
|
+
if (!objectNode) return node;
|
|
167
|
+
const { propertyName, enumValues } = entry;
|
|
168
|
+
const enumSchema = ast.factory.createSchema({
|
|
169
|
+
type: "enum",
|
|
170
|
+
enumValues
|
|
171
|
+
});
|
|
172
|
+
const newProp = ast.factory.createProperty({
|
|
173
|
+
name: propertyName,
|
|
174
|
+
required: true,
|
|
175
|
+
schema: enumSchema
|
|
176
|
+
});
|
|
177
|
+
const existingIdx = objectNode.properties.findIndex((p) => p.name === propertyName);
|
|
178
|
+
const newProperties = existingIdx >= 0 ? objectNode.properties.map((p, i) => i === existingIdx ? newProp : p) : [...objectNode.properties, newProp];
|
|
179
|
+
return {
|
|
180
|
+
...objectNode,
|
|
181
|
+
properties: newProperties
|
|
182
|
+
};
|
|
243
183
|
}
|
|
244
184
|
/**
|
|
245
|
-
*
|
|
246
|
-
* When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
|
|
185
|
+
* Creates a single-property object schema used as a discriminator literal.
|
|
247
186
|
*
|
|
248
187
|
* @example
|
|
249
|
-
*
|
|
250
|
-
*
|
|
188
|
+
* ```ts
|
|
189
|
+
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
190
|
+
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
191
|
+
* ```
|
|
251
192
|
*/
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
*/
|
|
267
|
-
function pascalCase(text, { isFile, prefix = "", suffix = "" } = {}) {
|
|
268
|
-
if (isFile) return applyToFileParts(text, (part, isLast) => isLast ? pascalCase(part, {
|
|
269
|
-
prefix,
|
|
270
|
-
suffix
|
|
271
|
-
}) : camelCase(part));
|
|
272
|
-
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
193
|
+
function createDiscriminantNode({ propertyName, value }) {
|
|
194
|
+
return ast.factory.createSchema({
|
|
195
|
+
type: "object",
|
|
196
|
+
primitive: "object",
|
|
197
|
+
properties: [ast.factory.createProperty({
|
|
198
|
+
name: propertyName,
|
|
199
|
+
schema: ast.factory.createSchema({
|
|
200
|
+
type: "enum",
|
|
201
|
+
primitive: "string",
|
|
202
|
+
enumValues: [value]
|
|
203
|
+
}),
|
|
204
|
+
required: true
|
|
205
|
+
})]
|
|
206
|
+
});
|
|
273
207
|
}
|
|
274
|
-
//#endregion
|
|
275
|
-
//#region ../../internals/utils/src/object.ts
|
|
276
208
|
/**
|
|
277
|
-
* Returns `
|
|
209
|
+
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
278
210
|
*
|
|
279
211
|
* @example
|
|
280
212
|
* ```ts
|
|
281
|
-
*
|
|
282
|
-
* isPlainObject([]) // false
|
|
283
|
-
* isPlainObject(null) // false
|
|
213
|
+
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
284
214
|
* ```
|
|
285
215
|
*/
|
|
286
|
-
function
|
|
287
|
-
|
|
216
|
+
function findDiscriminator(mapping, ref) {
|
|
217
|
+
if (!mapping || !ref) return null;
|
|
218
|
+
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
288
219
|
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region ../../internals/utils/src/casing.ts
|
|
289
222
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
223
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
224
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
225
|
+
* and capitalizes each word according to `pascal`.
|
|
292
226
|
*
|
|
293
|
-
*
|
|
294
|
-
* ```ts
|
|
295
|
-
* mergeDeep({ a: { x: 1 } }, { a: { y: 2 } })
|
|
296
|
-
* // { a: { x: 1, y: 2 } }
|
|
297
|
-
* ```
|
|
227
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
298
228
|
*/
|
|
299
|
-
function
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
result[key] = sv !== null && typeof sv === "object" && !Array.isArray(sv) && tv !== null && typeof tv === "object" && !Array.isArray(tv) ? mergeDeep(tv, sv) : sv;
|
|
305
|
-
}
|
|
306
|
-
return result;
|
|
229
|
+
function toCamelOrPascal(text, pascal) {
|
|
230
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
231
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
232
|
+
return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
|
|
233
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
307
234
|
}
|
|
308
|
-
//#endregion
|
|
309
|
-
//#region ../../internals/utils/src/reserved.ts
|
|
310
|
-
/**
|
|
311
|
-
* JavaScript and Java reserved words.
|
|
312
|
-
* @link https://github.com/jonschlinkert/reserved/blob/master/index.js
|
|
313
|
-
*/
|
|
314
|
-
const reservedWords = new Set([
|
|
315
|
-
"abstract",
|
|
316
|
-
"arguments",
|
|
317
|
-
"boolean",
|
|
318
|
-
"break",
|
|
319
|
-
"byte",
|
|
320
|
-
"case",
|
|
321
|
-
"catch",
|
|
322
|
-
"char",
|
|
323
|
-
"class",
|
|
324
|
-
"const",
|
|
325
|
-
"continue",
|
|
326
|
-
"debugger",
|
|
327
|
-
"default",
|
|
328
|
-
"delete",
|
|
329
|
-
"do",
|
|
330
|
-
"double",
|
|
331
|
-
"else",
|
|
332
|
-
"enum",
|
|
333
|
-
"eval",
|
|
334
|
-
"export",
|
|
335
|
-
"extends",
|
|
336
|
-
"false",
|
|
337
|
-
"final",
|
|
338
|
-
"finally",
|
|
339
|
-
"float",
|
|
340
|
-
"for",
|
|
341
|
-
"function",
|
|
342
|
-
"goto",
|
|
343
|
-
"if",
|
|
344
|
-
"implements",
|
|
345
|
-
"import",
|
|
346
|
-
"in",
|
|
347
|
-
"instanceof",
|
|
348
|
-
"int",
|
|
349
|
-
"interface",
|
|
350
|
-
"let",
|
|
351
|
-
"long",
|
|
352
|
-
"native",
|
|
353
|
-
"new",
|
|
354
|
-
"null",
|
|
355
|
-
"package",
|
|
356
|
-
"private",
|
|
357
|
-
"protected",
|
|
358
|
-
"public",
|
|
359
|
-
"return",
|
|
360
|
-
"short",
|
|
361
|
-
"static",
|
|
362
|
-
"super",
|
|
363
|
-
"switch",
|
|
364
|
-
"synchronized",
|
|
365
|
-
"this",
|
|
366
|
-
"throw",
|
|
367
|
-
"throws",
|
|
368
|
-
"transient",
|
|
369
|
-
"true",
|
|
370
|
-
"try",
|
|
371
|
-
"typeof",
|
|
372
|
-
"var",
|
|
373
|
-
"void",
|
|
374
|
-
"volatile",
|
|
375
|
-
"while",
|
|
376
|
-
"with",
|
|
377
|
-
"yield",
|
|
378
|
-
"Array",
|
|
379
|
-
"Date",
|
|
380
|
-
"hasOwnProperty",
|
|
381
|
-
"Infinity",
|
|
382
|
-
"isFinite",
|
|
383
|
-
"isNaN",
|
|
384
|
-
"isPrototypeOf",
|
|
385
|
-
"length",
|
|
386
|
-
"Math",
|
|
387
|
-
"name",
|
|
388
|
-
"NaN",
|
|
389
|
-
"Number",
|
|
390
|
-
"Object",
|
|
391
|
-
"prototype",
|
|
392
|
-
"String",
|
|
393
|
-
"toString",
|
|
394
|
-
"undefined",
|
|
395
|
-
"valueOf"
|
|
396
|
-
]);
|
|
397
235
|
/**
|
|
398
|
-
*
|
|
236
|
+
* Converts `text` to PascalCase.
|
|
399
237
|
*
|
|
400
|
-
* @example
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* ```
|
|
238
|
+
* @example Word boundaries
|
|
239
|
+
* `pascalCase('hello-world') // 'HelloWorld'`
|
|
240
|
+
*
|
|
241
|
+
* @example With a suffix
|
|
242
|
+
* `pascalCase('tag', { suffix: 'schema' }) // 'TagSchema'`
|
|
406
243
|
*/
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
244
|
+
function pascalCase(text, { prefix = "", suffix = "" } = {}) {
|
|
245
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
410
246
|
}
|
|
411
247
|
//#endregion
|
|
412
|
-
//#region ../../internals/utils/src/
|
|
248
|
+
//#region ../../internals/utils/src/runtime.ts
|
|
413
249
|
/**
|
|
414
|
-
*
|
|
250
|
+
* Detects the JavaScript runtime executing the current process and exposes its name and version.
|
|
415
251
|
*
|
|
416
|
-
* @
|
|
417
|
-
* const p = new URLPath('/pet/{petId}')
|
|
418
|
-
* p.URL // '/pet/:petId'
|
|
419
|
-
* p.template // '`/pet/${petId}`'
|
|
252
|
+
* Prefer the shared {@link runtime} instance over constructing your own.
|
|
420
253
|
*/
|
|
421
|
-
var
|
|
422
|
-
/**
|
|
423
|
-
* The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
|
|
424
|
-
*/
|
|
425
|
-
path;
|
|
426
|
-
#options;
|
|
427
|
-
constructor(path, options = {}) {
|
|
428
|
-
this.path = path;
|
|
429
|
-
this.#options = options;
|
|
430
|
-
}
|
|
431
|
-
/** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
|
|
432
|
-
*
|
|
433
|
-
* @example
|
|
434
|
-
* ```ts
|
|
435
|
-
* new URLPath('/pet/{petId}').URL // '/pet/:petId'
|
|
436
|
-
* ```
|
|
437
|
-
*/
|
|
438
|
-
get URL() {
|
|
439
|
-
return this.toURLPath();
|
|
440
|
-
}
|
|
441
|
-
/** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
|
|
442
|
-
*
|
|
443
|
-
* @example
|
|
444
|
-
* ```ts
|
|
445
|
-
* new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
|
|
446
|
-
* new URLPath('/pet/{petId}').isURL // false
|
|
447
|
-
* ```
|
|
448
|
-
*/
|
|
449
|
-
get isURL() {
|
|
450
|
-
try {
|
|
451
|
-
return !!new URL(this.path).href;
|
|
452
|
-
} catch {
|
|
453
|
-
return false;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
254
|
+
var Runtime = class {
|
|
456
255
|
/**
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
* @example
|
|
460
|
-
* new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
|
|
461
|
-
* new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
|
|
462
|
-
*/
|
|
463
|
-
get template() {
|
|
464
|
-
return this.toTemplateString();
|
|
465
|
-
}
|
|
466
|
-
/** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
|
|
256
|
+
* `true` when the current process is running under Bun.
|
|
467
257
|
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* // { url: '/pet/:petId', params: { petId: 'petId' } }
|
|
472
|
-
* ```
|
|
473
|
-
*/
|
|
474
|
-
get object() {
|
|
475
|
-
return this.toObject();
|
|
476
|
-
}
|
|
477
|
-
/** Returns a map of path parameter names, or `undefined` when the path has no parameters.
|
|
258
|
+
* Detection keys off the global `Bun` object rather than `process.versions`,
|
|
259
|
+
* because Bun polyfills `process.versions.node` for Node compatibility and would
|
|
260
|
+
* otherwise look like Node.
|
|
478
261
|
*
|
|
479
262
|
* @example
|
|
480
263
|
* ```ts
|
|
481
|
-
*
|
|
482
|
-
*
|
|
264
|
+
* if (runtime.isBun) {
|
|
265
|
+
* await Bun.write(path, data)
|
|
266
|
+
* }
|
|
483
267
|
* ```
|
|
484
268
|
*/
|
|
485
|
-
get
|
|
486
|
-
return
|
|
487
|
-
}
|
|
488
|
-
#transformParam(raw) {
|
|
489
|
-
const param = isValidVarName(raw) ? raw : camelCase(raw);
|
|
490
|
-
return this.#options.casing === "camelcase" ? camelCase(param) : param;
|
|
269
|
+
get isBun() {
|
|
270
|
+
return typeof Bun !== "undefined";
|
|
491
271
|
}
|
|
492
272
|
/**
|
|
493
|
-
*
|
|
273
|
+
* `true` when the current process is running under Deno.
|
|
494
274
|
*/
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const raw = match[1];
|
|
498
|
-
fn(raw, this.#transformParam(raw));
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
toObject({ type = "path", replacer, stringify } = {}) {
|
|
502
|
-
const object = {
|
|
503
|
-
url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
|
|
504
|
-
params: this.getParams()
|
|
505
|
-
};
|
|
506
|
-
if (stringify) {
|
|
507
|
-
if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
|
|
508
|
-
if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
|
|
509
|
-
return `{ url: '${object.url}' }`;
|
|
510
|
-
}
|
|
511
|
-
return object;
|
|
275
|
+
get isDeno() {
|
|
276
|
+
return typeof globalThis.Deno !== "undefined";
|
|
512
277
|
}
|
|
513
278
|
/**
|
|
514
|
-
*
|
|
515
|
-
* An optional `replacer` can transform each extracted parameter name before interpolation.
|
|
279
|
+
* `true` when the current process is running under Node.
|
|
516
280
|
*
|
|
517
|
-
*
|
|
518
|
-
* new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
|
|
281
|
+
* Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
|
|
519
282
|
*/
|
|
520
|
-
|
|
521
|
-
return
|
|
522
|
-
if (i % 2 === 0) return part;
|
|
523
|
-
const param = this.#transformParam(part);
|
|
524
|
-
return `\${${replacer ? replacer(param) : param}}`;
|
|
525
|
-
}).join("")}\``;
|
|
283
|
+
get isNode() {
|
|
284
|
+
return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
|
|
526
285
|
}
|
|
527
286
|
/**
|
|
528
|
-
*
|
|
529
|
-
* An optional `replacer` transforms each parameter name in both key and value positions.
|
|
530
|
-
* Returns `undefined` when no path parameters are found.
|
|
287
|
+
* Name of the runtime executing the current process.
|
|
531
288
|
*
|
|
532
289
|
* @example
|
|
533
290
|
* ```ts
|
|
534
|
-
*
|
|
535
|
-
* // { petId: 'petId', tagId: 'tagId' }
|
|
291
|
+
* runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
|
|
536
292
|
* ```
|
|
537
293
|
*/
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
this
|
|
541
|
-
|
|
542
|
-
params[key] = key;
|
|
543
|
-
});
|
|
544
|
-
return Object.keys(params).length > 0 ? params : void 0;
|
|
294
|
+
get name() {
|
|
295
|
+
if (this.isBun) return "bun";
|
|
296
|
+
if (this.isDeno) return "deno";
|
|
297
|
+
return "node";
|
|
545
298
|
}
|
|
546
|
-
/**
|
|
299
|
+
/**
|
|
300
|
+
* Version of the active runtime, or an empty string when it cannot be read.
|
|
547
301
|
*
|
|
548
302
|
* @example
|
|
549
303
|
* ```ts
|
|
550
|
-
*
|
|
304
|
+
* runtime.version // '1.3.11' under Bun, '22.22.2' under Node
|
|
551
305
|
* ```
|
|
552
306
|
*/
|
|
553
|
-
|
|
554
|
-
|
|
307
|
+
get version() {
|
|
308
|
+
if (this.isBun) return process.versions.bun ?? "";
|
|
309
|
+
if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
|
|
310
|
+
return process.versions?.node ?? "";
|
|
555
311
|
}
|
|
556
312
|
};
|
|
557
|
-
//#endregion
|
|
558
|
-
//#region src/guards.ts
|
|
559
|
-
/**
|
|
560
|
-
* Returns `true` when `doc` is a Swagger 2.0 document (no `openapi` key).
|
|
561
|
-
*
|
|
562
|
-
* @example
|
|
563
|
-
* ```ts
|
|
564
|
-
* if (isOpenApiV2Document(doc)) {
|
|
565
|
-
* // doc is OpenAPIV2.Document
|
|
566
|
-
* }
|
|
567
|
-
* ```
|
|
568
|
-
*/
|
|
569
|
-
function isOpenApiV2Document(doc) {
|
|
570
|
-
return !!doc && isPlainObject(doc) && !("openapi" in doc);
|
|
571
|
-
}
|
|
572
313
|
/**
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
576
|
-
* `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
|
|
577
|
-
*
|
|
578
|
-
* @example
|
|
579
|
-
* ```ts
|
|
580
|
-
* isNullable({ type: 'string', nullable: true }) // true
|
|
581
|
-
* isNullable({ type: ['string', 'null'] }) // true
|
|
582
|
-
* isNullable({ type: 'string' }) // false
|
|
583
|
-
* ```
|
|
314
|
+
* Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
|
|
584
315
|
*/
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (schemaType === "null") return true;
|
|
589
|
-
if (Array.isArray(schemaType)) return schemaType.includes("null");
|
|
590
|
-
return false;
|
|
591
|
-
}
|
|
316
|
+
const runtime = new Runtime();
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region ../../internals/utils/src/fs.ts
|
|
592
319
|
/**
|
|
593
|
-
*
|
|
320
|
+
* Resolves to `true` when the file or directory at `path` exists.
|
|
321
|
+
* Uses `Bun.file().exists()` when running under Bun, `fs.access` otherwise.
|
|
594
322
|
*
|
|
595
323
|
* @example
|
|
596
324
|
* ```ts
|
|
597
|
-
*
|
|
598
|
-
*
|
|
325
|
+
* if (await exists('./kubb.config.ts')) {
|
|
326
|
+
* const content = await read('./kubb.config.ts')
|
|
327
|
+
* }
|
|
599
328
|
* ```
|
|
600
329
|
*/
|
|
601
|
-
function
|
|
602
|
-
|
|
330
|
+
async function exists(path) {
|
|
331
|
+
if (runtime.isBun) return Bun.file(path).exists();
|
|
332
|
+
return access(path).then(() => true, () => false);
|
|
603
333
|
}
|
|
604
334
|
/**
|
|
605
|
-
*
|
|
335
|
+
* Reads the file at `path` as a UTF-8 string.
|
|
336
|
+
* Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
|
|
606
337
|
*
|
|
607
338
|
* @example
|
|
608
339
|
* ```ts
|
|
609
|
-
*
|
|
610
|
-
* isDiscriminator({ discriminator: 'type' }) // false (Swagger 2 string form)
|
|
340
|
+
* const source = await read('./src/Pet.ts')
|
|
611
341
|
* ```
|
|
612
342
|
*/
|
|
613
|
-
function
|
|
614
|
-
|
|
615
|
-
return
|
|
343
|
+
async function read(path) {
|
|
344
|
+
if (runtime.isBun) return Bun.file(path).text();
|
|
345
|
+
return readFile(path, { encoding: "utf8" });
|
|
616
346
|
}
|
|
617
347
|
//#endregion
|
|
618
348
|
//#region src/factory.ts
|
|
349
|
+
const urlRegExp = /^https?:\/+/i;
|
|
350
|
+
async function readSource(sourcePath) {
|
|
351
|
+
if (urlRegExp.test(sourcePath)) {
|
|
352
|
+
const url = new URL(sourcePath);
|
|
353
|
+
const response = await fetch(url);
|
|
354
|
+
if (!response.ok) throw new Error(`Cannot fetch the OAS document at ${url.href} (HTTP ${response.status})`);
|
|
355
|
+
return response.text();
|
|
356
|
+
}
|
|
357
|
+
return read(sourcePath);
|
|
358
|
+
}
|
|
359
|
+
async function resolveSource(sourcePath) {
|
|
360
|
+
const data = await readSource(sourcePath);
|
|
361
|
+
if (sourcePath.toLowerCase().endsWith(".md")) return data;
|
|
362
|
+
return parse(data);
|
|
363
|
+
}
|
|
619
364
|
/**
|
|
620
|
-
*
|
|
365
|
+
* Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
|
|
621
366
|
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
367
|
+
* External file schemas are hoisted into named `components.schemas` entries, so a property
|
|
368
|
+
* pointing at `./schemas/User.yaml` ends up referencing `#/components/schemas/User`. Generators
|
|
369
|
+
* can then emit a named type with an import instead of inlining the shape. Sources are read with
|
|
370
|
+
* the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
|
|
625
371
|
*
|
|
626
|
-
* @example
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
372
|
+
* @example Local file
|
|
373
|
+
* `const document = await bundleDocument('./openapi.yaml')`
|
|
374
|
+
*
|
|
375
|
+
* @example Remote URL
|
|
376
|
+
* `const document = await bundleDocument('https://example.com/openapi.yaml')`
|
|
631
377
|
*/
|
|
632
|
-
async function
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}).load();
|
|
645
|
-
if (isOpenApiV2Document(document)) {
|
|
646
|
-
const { openapi } = await swagger2openapi.convertObj(document, { anchors: true });
|
|
647
|
-
return openapi;
|
|
648
|
-
}
|
|
649
|
-
return document;
|
|
378
|
+
async function bundleDocument(pathOrUrl) {
|
|
379
|
+
const cache = /* @__PURE__ */ new Map();
|
|
380
|
+
const resolver = (sourcePath) => {
|
|
381
|
+
const key = urlRegExp.test(sourcePath) ? new URL(sourcePath).href : sourcePath;
|
|
382
|
+
const cached = cache.get(key);
|
|
383
|
+
if (cached) return cached;
|
|
384
|
+
const result = resolveSource(sourcePath);
|
|
385
|
+
cache.set(key, result);
|
|
386
|
+
return result;
|
|
387
|
+
};
|
|
388
|
+
await resolver(pathOrUrl);
|
|
389
|
+
return await bundle(pathOrUrl, resolver);
|
|
650
390
|
}
|
|
651
391
|
/**
|
|
652
|
-
*
|
|
392
|
+
* Loads and bundles an OpenAPI document, returning the raw `Document`.
|
|
653
393
|
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
394
|
+
* A string is a file path or URL: it is bundled via `api-ref-bundler`, hoisting external file
|
|
395
|
+
* schemas into named `components.schemas` entries so generators can emit named types and imports.
|
|
396
|
+
* An object is treated as an already-parsed document. Swagger 2.0 and OpenAPI 3.0 documents are
|
|
397
|
+
* up-converted to OpenAPI 3.1 via `@scalar/openapi-upgrader`.
|
|
656
398
|
*
|
|
657
399
|
* @example
|
|
658
400
|
* ```ts
|
|
659
|
-
* const document = await
|
|
401
|
+
* const document = await parseDocument('./openapi.yaml')
|
|
402
|
+
* const document = await parseDocument(rawDocumentObject)
|
|
660
403
|
* ```
|
|
661
404
|
*/
|
|
662
|
-
async function
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
canBundle: false
|
|
666
|
-
})));
|
|
667
|
-
if (documents.length === 0) throw new Error("No OAS documents provided for merging.");
|
|
668
|
-
const seed = {
|
|
669
|
-
openapi: MERGE_OPENAPI_VERSION,
|
|
670
|
-
info: {
|
|
671
|
-
title: MERGE_DEFAULT_TITLE,
|
|
672
|
-
version: MERGE_DEFAULT_VERSION
|
|
673
|
-
},
|
|
674
|
-
paths: {},
|
|
675
|
-
components: { schemas: {} }
|
|
676
|
-
};
|
|
677
|
-
return parseDocument(documents.reduce((acc, current) => mergeDeep(acc, current), seed));
|
|
405
|
+
async function parseDocument(pathOrApi) {
|
|
406
|
+
if (typeof pathOrApi === "string") return parseDocument(await bundleDocument(pathOrApi));
|
|
407
|
+
return upgrade(pathOrApi, "3.1");
|
|
678
408
|
}
|
|
679
409
|
/**
|
|
680
410
|
* Creates a `Document` from an `AdapterSource`.
|
|
681
411
|
*
|
|
682
|
-
*
|
|
683
|
-
* - `{ type: '
|
|
684
|
-
* - `{ type: 'paths' }` — merges multiple file paths into a single document.
|
|
685
|
-
* - `{ type: 'data' }` — parses an inline string (YAML/JSON) or raw object.
|
|
412
|
+
* - `{ type: 'path' }` resolves and bundles a local file path or remote URL.
|
|
413
|
+
* - `{ type: 'data' }` parses an inline string (YAML/JSON) or raw object.
|
|
686
414
|
*
|
|
687
415
|
* @example
|
|
688
416
|
* ```ts
|
|
@@ -690,17 +418,30 @@ async function mergeDocuments(pathOrApi) {
|
|
|
690
418
|
* const document = await parseFromConfig({ type: 'data', data: '{"openapi":"3.0.0",...}' })
|
|
691
419
|
* ```
|
|
692
420
|
*/
|
|
693
|
-
function parseFromConfig(source) {
|
|
694
|
-
if (source.type === "data")
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
421
|
+
async function parseFromConfig(source) {
|
|
422
|
+
if (source.type === "data") return parseDocument(typeof source.data === "string" ? parse(source.data) : structuredClone(source.data));
|
|
423
|
+
if (URL.canParse(source.path)) return parseDocument(source.path);
|
|
424
|
+
const resolved = path.resolve(path.dirname(source.path), source.path);
|
|
425
|
+
await assertInputExists(resolved);
|
|
426
|
+
return parseDocument(resolved);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
430
|
+
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
431
|
+
* its parse error instead.
|
|
432
|
+
*/
|
|
433
|
+
async function assertInputExists(input) {
|
|
434
|
+
if (URL.canParse(input)) return;
|
|
435
|
+
if (!await exists(input)) throw new Diagnostics.Error({
|
|
436
|
+
code: Diagnostics.code.inputNotFound,
|
|
437
|
+
severity: "error",
|
|
438
|
+
message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
|
|
439
|
+
help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
|
|
440
|
+
location: { kind: "config" }
|
|
441
|
+
});
|
|
701
442
|
}
|
|
702
443
|
/**
|
|
703
|
-
* Validates an OpenAPI document using
|
|
444
|
+
* Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
|
|
704
445
|
*
|
|
705
446
|
* @example
|
|
706
447
|
* ```ts
|
|
@@ -709,34 +450,94 @@ function parseFromConfig(source) {
|
|
|
709
450
|
*/
|
|
710
451
|
async function validateDocument(document, { throwOnError = false } = {}) {
|
|
711
452
|
try {
|
|
712
|
-
await
|
|
713
|
-
|
|
714
|
-
colorizeErrors: true
|
|
715
|
-
}).validate({ parser: { validate: { errors: { colorize: true } } } });
|
|
453
|
+
const result = await validate(structuredClone(document), { validate: { errors: { colorize: true } } });
|
|
454
|
+
if (!result.valid) throw new Error(compileErrors(result));
|
|
716
455
|
} catch (error) {
|
|
717
456
|
if (throwOnError) throw error;
|
|
718
457
|
}
|
|
719
458
|
}
|
|
720
459
|
//#endregion
|
|
460
|
+
//#region src/oas.ts
|
|
461
|
+
/**
|
|
462
|
+
* Returns `true` when a schema should be treated as nullable.
|
|
463
|
+
*
|
|
464
|
+
* Recognizes all nullable signals across OAS versions: `nullable: true` (OAS 3.0),
|
|
465
|
+
* `x-nullable: true` (vendor extension), `type: 'null'`, and `type: ['null', ...]` (OAS 3.1).
|
|
466
|
+
*/
|
|
467
|
+
function isNullable(schema) {
|
|
468
|
+
if ((schema?.nullable ?? schema?.["x-nullable"]) === true) return true;
|
|
469
|
+
const schemaType = schema?.type;
|
|
470
|
+
if (schemaType === "null") return true;
|
|
471
|
+
if (Array.isArray(schemaType)) return schemaType.includes("null");
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Returns `true` when `obj` is an OpenAPI `$ref` pointer object.
|
|
476
|
+
*/
|
|
477
|
+
function isReference(obj) {
|
|
478
|
+
return !!obj && typeof obj === "object" && "$ref" in obj;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Returns `true` when `obj` is a schema with a structured OAS 3.x `discriminator` object,
|
|
482
|
+
* excluding the Swagger 2 string form.
|
|
483
|
+
*/
|
|
484
|
+
function isDiscriminator(obj) {
|
|
485
|
+
const record = obj;
|
|
486
|
+
return !!obj && !!record["discriminator"] && typeof record["discriminator"] !== "string";
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Returns `true` when a schema is a binary payload: an octet-stream string body.
|
|
490
|
+
*/
|
|
491
|
+
function isBinary(schema) {
|
|
492
|
+
return schema.type === "string" && schema.contentMediaType === "application/octet-stream";
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* MIME type fragments that mark a media type as JSON-like.
|
|
496
|
+
*
|
|
497
|
+
* A content type is JSON when it contains any of these substrings. The `+json` entry catches
|
|
498
|
+
* structured-syntax suffixes such as `application/vnd.api+json`.
|
|
499
|
+
*/
|
|
500
|
+
const jsonMimeFragments = [
|
|
501
|
+
"application/json",
|
|
502
|
+
"application/x-json",
|
|
503
|
+
"text/json",
|
|
504
|
+
"text/x-json",
|
|
505
|
+
"+json"
|
|
506
|
+
];
|
|
507
|
+
/**
|
|
508
|
+
* Returns `true` when a media type string is JSON-like.
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* ```ts
|
|
512
|
+
* isJsonMimeType('application/json') // true
|
|
513
|
+
* isJsonMimeType('application/vnd.api+json') // true
|
|
514
|
+
* isJsonMimeType('multipart/form-data') // false
|
|
515
|
+
* ```
|
|
516
|
+
*/
|
|
517
|
+
function isJsonMimeType(mimeType) {
|
|
518
|
+
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
721
521
|
//#region src/refs.ts
|
|
722
522
|
const _refCache = /* @__PURE__ */ new WeakMap();
|
|
723
523
|
/**
|
|
724
524
|
* Resolves a local JSON pointer reference from a document.
|
|
725
525
|
*
|
|
726
|
-
* Accepts `#/...` refs. Returns `null` for empty or non-local
|
|
727
|
-
*
|
|
526
|
+
* Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
|
|
527
|
+
* resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
|
|
528
|
+
* build there is no sink to collect it, so it throws instead.
|
|
728
529
|
*
|
|
729
530
|
* @example
|
|
730
531
|
* ```ts
|
|
731
|
-
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
|
|
532
|
+
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
|
|
732
533
|
* ```
|
|
733
534
|
*/
|
|
734
535
|
function resolveRef(document, $ref) {
|
|
735
536
|
const origRef = $ref;
|
|
736
537
|
$ref = $ref.trim();
|
|
737
538
|
if ($ref === "") return null;
|
|
738
|
-
if (
|
|
739
|
-
|
|
539
|
+
if (!$ref.startsWith("#")) return null;
|
|
540
|
+
$ref = globalThis.decodeURIComponent($ref.substring(1));
|
|
740
541
|
let docCache = _refCache.get(document);
|
|
741
542
|
if (!docCache) {
|
|
742
543
|
docCache = /* @__PURE__ */ new Map();
|
|
@@ -744,7 +545,21 @@ function resolveRef(document, $ref) {
|
|
|
744
545
|
}
|
|
745
546
|
if (docCache.has($ref)) return docCache.get($ref);
|
|
746
547
|
const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
|
|
747
|
-
if (!current)
|
|
548
|
+
if (!current) {
|
|
549
|
+
const diagnostic = {
|
|
550
|
+
code: Diagnostics.code.refNotFound,
|
|
551
|
+
severity: "error",
|
|
552
|
+
message: `Could not find a definition for ${origRef}.`,
|
|
553
|
+
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
554
|
+
location: {
|
|
555
|
+
kind: "schema",
|
|
556
|
+
pointer: origRef,
|
|
557
|
+
ref: origRef
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
748
563
|
docCache.set($ref, current);
|
|
749
564
|
return current;
|
|
750
565
|
}
|
|
@@ -768,59 +583,220 @@ function dereferenceWithRef(document, schema) {
|
|
|
768
583
|
};
|
|
769
584
|
return schema;
|
|
770
585
|
}
|
|
771
|
-
//#endregion
|
|
772
|
-
//#region src/resolvers.ts
|
|
773
586
|
/**
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
587
|
+
* Resolves a `$ref` slot in place: when `container[key]` holds a `$ref`, replaces it with the
|
|
588
|
+
* resolved value and returns that value. Returns `null` when the slot is empty, cannot be resolved,
|
|
589
|
+
* or is still a `$ref` after resolving. A non-`$ref` value is returned untouched, without writing.
|
|
777
590
|
*
|
|
778
591
|
* @example
|
|
779
592
|
* ```ts
|
|
780
|
-
*
|
|
781
|
-
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
782
|
-
* { env: 'prod' },
|
|
783
|
-
* )
|
|
784
|
-
* // 'https://prod.api.example.com'
|
|
593
|
+
* derefInPlace<ResponseObject>({ document, container: operation.schema.responses, key: '200' })
|
|
785
594
|
* ```
|
|
786
595
|
*/
|
|
787
|
-
function
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Error(`Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`);
|
|
794
|
-
url = url.replaceAll(`{${key}}`, value);
|
|
795
|
-
}
|
|
796
|
-
return url;
|
|
596
|
+
function derefInPlace({ document, container, key }) {
|
|
597
|
+
const value = container[key];
|
|
598
|
+
if (!isReference(value)) return value ? value : null;
|
|
599
|
+
const resolved = resolveRef(document, value.$ref);
|
|
600
|
+
container[key] = resolved;
|
|
601
|
+
return resolved && !isReference(resolved) ? resolved : null;
|
|
797
602
|
}
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region src/operation.ts
|
|
798
605
|
/**
|
|
799
|
-
*
|
|
800
|
-
*
|
|
606
|
+
* Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
|
|
607
|
+
* with no leading or trailing dash.
|
|
801
608
|
*/
|
|
802
|
-
function
|
|
803
|
-
return
|
|
609
|
+
function slugify(value) {
|
|
610
|
+
return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
804
611
|
}
|
|
805
612
|
/**
|
|
806
|
-
*
|
|
807
|
-
* Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
|
|
613
|
+
* Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
|
|
808
614
|
*/
|
|
809
|
-
function
|
|
810
|
-
|
|
811
|
-
if (
|
|
812
|
-
return
|
|
615
|
+
function getOperationId({ path, method, schema }) {
|
|
616
|
+
const { operationId } = schema;
|
|
617
|
+
if (typeof operationId === "string" && operationId.length > 0) return operationId;
|
|
618
|
+
return `${method}_${slugify(path).toLowerCase()}`;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Returns the declared response status codes, skipping `x-` extensions and non-object entries.
|
|
622
|
+
*/
|
|
623
|
+
function getResponseStatusCodes({ schema }) {
|
|
624
|
+
const responses = schema.responses;
|
|
625
|
+
if (!responses || isReference(responses)) return [];
|
|
626
|
+
return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Returns the response object for a status code, resolving a `$ref` in place. `false` when absent.
|
|
630
|
+
*/
|
|
631
|
+
function getResponseByStatusCode({ document, operation, statusCode }) {
|
|
632
|
+
const responses = operation.schema.responses;
|
|
633
|
+
if (!responses || isReference(responses)) return false;
|
|
634
|
+
return derefInPlace({
|
|
635
|
+
document,
|
|
636
|
+
container: responses,
|
|
637
|
+
key: statusCode
|
|
638
|
+
}) ?? false;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Resolves the request body (dereferencing a `$ref` in place) and returns its content map, or
|
|
642
|
+
* `undefined` when the operation has no request body.
|
|
643
|
+
*/
|
|
644
|
+
function getRequestBodyContent({ document, operation }) {
|
|
645
|
+
return derefInPlace({
|
|
646
|
+
document,
|
|
647
|
+
container: operation.schema,
|
|
648
|
+
key: "requestBody"
|
|
649
|
+
})?.content;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Returns the request body media type. With `mediaType` set, returns that entry or `false`.
|
|
653
|
+
* Otherwise picks the first JSON-like media type, then the first declared one, as a
|
|
654
|
+
* `[mediaType, object]` tuple.
|
|
655
|
+
*/
|
|
656
|
+
function getRequestContent({ document, operation, mediaType }) {
|
|
657
|
+
const content = getRequestBodyContent({
|
|
658
|
+
document,
|
|
659
|
+
operation
|
|
660
|
+
});
|
|
661
|
+
if (!content) return false;
|
|
662
|
+
if (mediaType) return mediaType in content ? content[mediaType] : false;
|
|
663
|
+
const mediaTypes = Object.keys(content);
|
|
664
|
+
const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
|
|
665
|
+
return available ? [available, content[available]] : false;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Returns the primary request content type. Prefers a JSON-like media type (the last one wins
|
|
669
|
+
* when several are declared), then the first declared one, defaulting to `'application/json'`.
|
|
670
|
+
*/
|
|
671
|
+
function getRequestContentType({ document, operation }) {
|
|
672
|
+
const content = getRequestBodyContent({
|
|
673
|
+
document,
|
|
674
|
+
operation
|
|
675
|
+
});
|
|
676
|
+
const mediaTypes = content ? Object.keys(content) : [];
|
|
677
|
+
let result = mediaTypes[0] ?? "application/json";
|
|
678
|
+
for (const mt of mediaTypes) if (isJsonMimeType(mt)) result = mt;
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Builds an `Operation` for every supported HTTP method on every path, in document order.
|
|
683
|
+
* `x-` path keys and unresolvable path-item `$ref`s are skipped.
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```ts
|
|
687
|
+
* for (const operation of getOperations(document)) {
|
|
688
|
+
* parseOperation(options, operation)
|
|
689
|
+
* }
|
|
690
|
+
* ```
|
|
691
|
+
*/
|
|
692
|
+
function getOperations(document) {
|
|
693
|
+
const operations = [];
|
|
694
|
+
const paths = document.paths;
|
|
695
|
+
if (!paths) return operations;
|
|
696
|
+
for (const path of Object.keys(paths)) {
|
|
697
|
+
if (path.startsWith("x-")) continue;
|
|
698
|
+
const pathItem = derefInPlace({
|
|
699
|
+
document,
|
|
700
|
+
container: paths,
|
|
701
|
+
key: path
|
|
702
|
+
});
|
|
703
|
+
if (!pathItem) continue;
|
|
704
|
+
const item = pathItem;
|
|
705
|
+
for (const method of Object.keys(item)) {
|
|
706
|
+
if (!SUPPORTED_METHODS.has(method)) continue;
|
|
707
|
+
const schema = item[method];
|
|
708
|
+
if (!schema || typeof schema !== "object") continue;
|
|
709
|
+
operations.push({
|
|
710
|
+
path,
|
|
711
|
+
method,
|
|
712
|
+
schema
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return operations;
|
|
717
|
+
}
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/resolvers.ts
|
|
720
|
+
/**
|
|
721
|
+
* Reads the server URL from the document's `servers` array at `server.index`,
|
|
722
|
+
* interpolating any `server.variables` into the URL template.
|
|
723
|
+
*
|
|
724
|
+
* Returns `null` when `server.index` is omitted or out of range.
|
|
725
|
+
*
|
|
726
|
+
* @example Resolve the first server
|
|
727
|
+
* `resolveBaseUrl({ document, server: { index: 0 } })`
|
|
728
|
+
*
|
|
729
|
+
* @example Override a path variable
|
|
730
|
+
* `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
|
|
731
|
+
*/
|
|
732
|
+
function resolveBaseUrl({ document, server }) {
|
|
733
|
+
const index = server?.index;
|
|
734
|
+
const entry = index !== void 0 ? document.servers?.at(index) : void 0;
|
|
735
|
+
return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
739
|
+
* Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
|
|
740
|
+
* Throws if an override value is not in the variable's `enum` list.
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```ts
|
|
744
|
+
* resolveServerUrl(
|
|
745
|
+
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
746
|
+
* { env: 'prod' },
|
|
747
|
+
* )
|
|
748
|
+
* // 'https://prod.api.example.com'
|
|
749
|
+
* ```
|
|
750
|
+
*/
|
|
751
|
+
function resolveServerUrl(server, overrides) {
|
|
752
|
+
if (!server.variables) return server.url;
|
|
753
|
+
let url = server.url;
|
|
754
|
+
for (const [key, variable] of Object.entries(server.variables)) {
|
|
755
|
+
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
756
|
+
if (value === void 0) continue;
|
|
757
|
+
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Diagnostics.Error({
|
|
758
|
+
code: Diagnostics.code.invalidServerVariable,
|
|
759
|
+
severity: "error",
|
|
760
|
+
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
761
|
+
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
762
|
+
location: {
|
|
763
|
+
kind: "document",
|
|
764
|
+
pointer: "#/servers"
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
url = url.replaceAll(`{${key}}`, value);
|
|
768
|
+
}
|
|
769
|
+
return url;
|
|
813
770
|
}
|
|
814
771
|
/**
|
|
815
|
-
*
|
|
772
|
+
* Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
|
|
773
|
+
* Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
|
|
816
774
|
*/
|
|
817
|
-
function
|
|
818
|
-
return
|
|
775
|
+
function getSchemaType(format) {
|
|
776
|
+
return formatMap[format] ?? null;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
|
|
780
|
+
* `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
|
|
781
|
+
* the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
|
|
782
|
+
* diagnostic in step with the parser as `formatMap` grows.
|
|
783
|
+
*/
|
|
784
|
+
function isHandledFormat(format) {
|
|
785
|
+
return getSchemaType(format) !== null || specialCasedFormats.has(format);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Converts an OAS primitive type string to its `PrimitiveSchemaType` equivalent.
|
|
789
|
+
* Numeric types (`number`, `integer`, `bigint`) pass through unchanged. `boolean` maps to `'boolean'`. Everything else becomes `'string'`.
|
|
790
|
+
*/
|
|
791
|
+
function getPrimitiveType(type) {
|
|
792
|
+
if (type === "number" || type === "integer" || type === "bigint") return type;
|
|
793
|
+
if (type === "boolean") return "boolean";
|
|
794
|
+
return "string";
|
|
819
795
|
}
|
|
820
796
|
/**
|
|
821
797
|
* Returns all parameters for an operation, merging path-level and operation-level entries.
|
|
822
798
|
* Operation-level parameters override path-level ones with the same `in:name` key.
|
|
823
|
-
* `$ref`
|
|
799
|
+
* Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
|
|
824
800
|
*
|
|
825
801
|
* @example
|
|
826
802
|
* ```ts
|
|
@@ -847,19 +823,19 @@ function getResponseBody(responseBody, contentType) {
|
|
|
847
823
|
if (!(contentType in body.content)) return false;
|
|
848
824
|
return body.content[contentType];
|
|
849
825
|
}
|
|
850
|
-
let availableContentType;
|
|
851
826
|
const contentTypes = Object.keys(body.content);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
827
|
+
const availableContentType = contentTypes.find(isJsonMimeType) ?? contentTypes[0];
|
|
828
|
+
if (!availableContentType) return false;
|
|
829
|
+
return body.content[availableContentType];
|
|
830
|
+
}
|
|
831
|
+
function resolveResponseRefs(document, operation) {
|
|
832
|
+
const responses = operation.schema.responses;
|
|
833
|
+
if (!responses) return;
|
|
834
|
+
for (const key in responses) derefInPlace({
|
|
835
|
+
document,
|
|
836
|
+
container: responses,
|
|
837
|
+
key
|
|
838
|
+
});
|
|
863
839
|
}
|
|
864
840
|
/**
|
|
865
841
|
* Returns the response schema for a given operation and HTTP status code.
|
|
@@ -873,16 +849,14 @@ function getResponseBody(responseBody, contentType) {
|
|
|
873
849
|
* ```
|
|
874
850
|
*/
|
|
875
851
|
function getResponseSchema(document, operation, statusCode, options = {}) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
883
|
-
const responseBody = getResponseBody(operation.getResponseByStatusCode(statusCode), options.contentType);
|
|
852
|
+
resolveResponseRefs(document, operation);
|
|
853
|
+
const responseBody = getResponseBody(getResponseByStatusCode({
|
|
854
|
+
document,
|
|
855
|
+
operation,
|
|
856
|
+
statusCode
|
|
857
|
+
}), options.contentType);
|
|
884
858
|
if (responseBody === false) return {};
|
|
885
|
-
const schema =
|
|
859
|
+
const schema = responseBody.schema;
|
|
886
860
|
if (!schema) return {};
|
|
887
861
|
return dereferenceWithRef(document, schema);
|
|
888
862
|
}
|
|
@@ -896,16 +870,35 @@ function getResponseSchema(document, operation, statusCode, options = {}) {
|
|
|
896
870
|
*/
|
|
897
871
|
function getRequestSchema(document, operation, options = {}) {
|
|
898
872
|
if (operation.schema.requestBody) operation.schema.requestBody = dereferenceWithRef(document, operation.schema.requestBody);
|
|
899
|
-
const requestBody =
|
|
873
|
+
const requestBody = getRequestContent({
|
|
874
|
+
document,
|
|
875
|
+
operation,
|
|
876
|
+
mediaType: options.contentType
|
|
877
|
+
});
|
|
900
878
|
if (requestBody === false) return null;
|
|
879
|
+
const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
|
|
901
880
|
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
881
|
+
if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
|
|
882
|
+
type: "string",
|
|
883
|
+
contentMediaType: "application/octet-stream"
|
|
884
|
+
};
|
|
902
885
|
if (!schema) return null;
|
|
903
886
|
return dereferenceWithRef(document, schema);
|
|
904
887
|
}
|
|
905
888
|
/**
|
|
889
|
+
* Returns `true` when `fragment` carries any JSON Schema keyword that makes it
|
|
890
|
+
* structurally significant on its own (see `structuralKeys`).
|
|
891
|
+
*
|
|
892
|
+
* A fragment with a structural keyword can't be safely merged into a parent schema.
|
|
893
|
+
*/
|
|
894
|
+
function hasStructuralKeywords(fragment) {
|
|
895
|
+
for (const key in fragment) if (structuralKeys.has(key)) return true;
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
906
899
|
* Flattens a keyword-only `allOf` into its parent schema.
|
|
907
900
|
*
|
|
908
|
-
* Only flattens when every member is a plain fragment
|
|
901
|
+
* Only flattens when every member is a plain fragment, with no `$ref` and no structural keywords
|
|
909
902
|
* (see `structuralKeys`). Outer schema values take precedence over fragment values.
|
|
910
903
|
* Returns `null` for a `null` input, and the original schema unchanged when flattening is unsafe.
|
|
911
904
|
*
|
|
@@ -913,35 +906,28 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
913
906
|
* ```ts
|
|
914
907
|
* flattenSchema({ allOf: [{ description: 'A pet' }], type: 'object', properties: {} })
|
|
915
908
|
* // { type: 'object', properties: {}, description: 'A pet' }
|
|
909
|
+
* ```
|
|
916
910
|
*
|
|
911
|
+
* @example
|
|
912
|
+
* ```ts
|
|
917
913
|
* flattenSchema({ allOf: [{ $ref: '#/components/schemas/Pet' }] })
|
|
918
|
-
* // returned unchanged
|
|
914
|
+
* // returned unchanged, contains a $ref
|
|
919
915
|
* ```
|
|
920
916
|
*/
|
|
921
|
-
/**
|
|
922
|
-
* Returns `true` when `fragment` carries any JSON Schema keyword that makes it
|
|
923
|
-
* structurally significant on its own (see `structuralKeys`).
|
|
924
|
-
*
|
|
925
|
-
* A fragment with a structural keyword can't be safely merged into a parent schema.
|
|
926
|
-
*/
|
|
927
|
-
function hasStructuralKeywords(fragment) {
|
|
928
|
-
for (const key in fragment) if (structuralKeys.has(key)) return true;
|
|
929
|
-
return false;
|
|
930
|
-
}
|
|
931
917
|
function flattenSchema(schema) {
|
|
932
918
|
if (!schema?.allOf || schema.allOf.length === 0) return schema ?? null;
|
|
933
919
|
const allOfFragments = schema.allOf;
|
|
934
|
-
if (allOfFragments.some((item) =>
|
|
920
|
+
if (allOfFragments.some((item) => isReference(item))) return schema;
|
|
935
921
|
if (allOfFragments.some(hasStructuralKeywords)) return schema;
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment))
|
|
922
|
+
const { allOf: _allOf, ...rest } = schema;
|
|
923
|
+
const merged = rest;
|
|
924
|
+
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
|
|
939
925
|
return merged;
|
|
940
926
|
}
|
|
941
927
|
/**
|
|
942
928
|
* Extracts the inline schema from a media-type `content` map.
|
|
943
929
|
*
|
|
944
|
-
* Prefers `preferredContentType` when given
|
|
930
|
+
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
945
931
|
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
946
932
|
*
|
|
947
933
|
* @example
|
|
@@ -960,21 +946,22 @@ function extractSchemaFromContent(content, preferredContentType) {
|
|
|
960
946
|
/**
|
|
961
947
|
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
962
948
|
*/
|
|
963
|
-
function collectRefs(schema
|
|
949
|
+
function* collectRefs(schema) {
|
|
964
950
|
if (Array.isArray(schema)) {
|
|
965
|
-
for (const item of schema) collectRefs(item
|
|
966
|
-
return
|
|
951
|
+
for (const item of schema) yield* collectRefs(item);
|
|
952
|
+
return;
|
|
967
953
|
}
|
|
968
954
|
if (schema && typeof schema === "object") for (const key in schema) {
|
|
969
955
|
const value = schema[key];
|
|
970
|
-
if (key === "$ref" && typeof value === "string") {
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
956
|
+
if (!(key === "$ref" && typeof value === "string")) {
|
|
957
|
+
yield* collectRefs(value);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (value.startsWith("#/components/schemas/")) {
|
|
961
|
+
const name = value.slice(21);
|
|
962
|
+
if (name) yield name;
|
|
963
|
+
}
|
|
976
964
|
}
|
|
977
|
-
return refs;
|
|
978
965
|
}
|
|
979
966
|
/**
|
|
980
967
|
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
@@ -990,7 +977,7 @@ function collectRefs(schema, refs = /* @__PURE__ */ new Set()) {
|
|
|
990
977
|
*/
|
|
991
978
|
function sortSchemas(schemas) {
|
|
992
979
|
const deps = /* @__PURE__ */ new Map();
|
|
993
|
-
for (const [name, schema] of Object.entries(schemas)) deps.set(name,
|
|
980
|
+
for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
|
|
994
981
|
const sorted = [];
|
|
995
982
|
const visited = /* @__PURE__ */ new Set();
|
|
996
983
|
function visit(name, stack) {
|
|
@@ -1011,9 +998,6 @@ const semanticSuffixes = {
|
|
|
1011
998
|
responses: "Response",
|
|
1012
999
|
requestBodies: "Request"
|
|
1013
1000
|
};
|
|
1014
|
-
function getSemanticSuffix(source) {
|
|
1015
|
-
return semanticSuffixes[source];
|
|
1016
|
-
}
|
|
1017
1001
|
function resolveSchemaRef(document, schema) {
|
|
1018
1002
|
if (!isReference(schema)) return schema;
|
|
1019
1003
|
const resolved = resolveRef(document, schema.$ref);
|
|
@@ -1031,7 +1015,7 @@ function resolveSchemaRef(document, schema) {
|
|
|
1031
1015
|
*
|
|
1032
1016
|
* @example
|
|
1033
1017
|
* ```ts
|
|
1034
|
-
* const { schemas,
|
|
1018
|
+
* const { schemas, renames } = getSchemas(document, { contentType: 'application/json' })
|
|
1035
1019
|
* ```
|
|
1036
1020
|
*/
|
|
1037
1021
|
function getSchemas(document, { contentType }) {
|
|
@@ -1056,32 +1040,32 @@ function getSchemas(document, { contentType }) {
|
|
|
1056
1040
|
normalizedNames.set(key, bucket);
|
|
1057
1041
|
}
|
|
1058
1042
|
const schemas = {};
|
|
1059
|
-
const
|
|
1043
|
+
const renames = /* @__PURE__ */ new Map();
|
|
1060
1044
|
for (const [, items] of normalizedNames) {
|
|
1061
1045
|
const isSingle = items.length === 1;
|
|
1062
1046
|
let hasMultipleSources = false;
|
|
1063
1047
|
if (!isSingle) {
|
|
1064
1048
|
const firstSource = items[0].source;
|
|
1065
|
-
for (
|
|
1049
|
+
for (const item of items) if (item.source !== firstSource) {
|
|
1066
1050
|
hasMultipleSources = true;
|
|
1067
1051
|
break;
|
|
1068
1052
|
}
|
|
1069
1053
|
}
|
|
1070
1054
|
items.forEach((item, index) => {
|
|
1071
|
-
const suffix = isSingle ? "" : hasMultipleSources ?
|
|
1055
|
+
const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index === 0 ? "" : String(index + 1);
|
|
1072
1056
|
const uniqueName = item.originalName + suffix;
|
|
1073
1057
|
schemas[uniqueName] = item.schema;
|
|
1074
|
-
|
|
1058
|
+
if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
1075
1059
|
});
|
|
1076
1060
|
}
|
|
1077
1061
|
return {
|
|
1078
1062
|
schemas: sortSchemas(schemas),
|
|
1079
|
-
|
|
1063
|
+
renames
|
|
1080
1064
|
};
|
|
1081
1065
|
}
|
|
1082
1066
|
/**
|
|
1083
1067
|
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
1084
|
-
* Returns `null` when `dateType: false`,
|
|
1068
|
+
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
1085
1069
|
*/
|
|
1086
1070
|
function getDateType(options, format) {
|
|
1087
1071
|
if (!options.dateType) return null;
|
|
@@ -1115,6 +1099,15 @@ function getDateType(options, format) {
|
|
|
1115
1099
|
/**
|
|
1116
1100
|
* Collects the shared metadata fields passed to every `createSchema` call.
|
|
1117
1101
|
*/
|
|
1102
|
+
/**
|
|
1103
|
+
* Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
|
|
1104
|
+
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
1105
|
+
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
1106
|
+
*/
|
|
1107
|
+
function extractExamples(schema) {
|
|
1108
|
+
if (Array.isArray(schema.examples)) return schema.examples;
|
|
1109
|
+
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
1110
|
+
}
|
|
1118
1111
|
function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
1119
1112
|
return {
|
|
1120
1113
|
name,
|
|
@@ -1125,15 +1118,16 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
|
1125
1118
|
readOnly: schema.readOnly,
|
|
1126
1119
|
writeOnly: schema.writeOnly,
|
|
1127
1120
|
default: defaultValue,
|
|
1128
|
-
|
|
1121
|
+
examples: extractExamples(schema),
|
|
1122
|
+
format: schema.format
|
|
1129
1123
|
};
|
|
1130
1124
|
}
|
|
1131
1125
|
/**
|
|
1132
1126
|
* Returns all request body content type keys for an operation.
|
|
1133
1127
|
*
|
|
1134
|
-
* The requestBody is dereferenced
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1128
|
+
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
1129
|
+
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
1130
|
+
* available content types even for referenced bodies.
|
|
1137
1131
|
*
|
|
1138
1132
|
* @example
|
|
1139
1133
|
* ```ts
|
|
@@ -1147,15 +1141,38 @@ function getRequestBodyContentTypes(document, operation) {
|
|
|
1147
1141
|
if (!body) return [];
|
|
1148
1142
|
return body.content ? Object.keys(body.content) : [];
|
|
1149
1143
|
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Returns all response content type keys for an operation at a given status code.
|
|
1146
|
+
*
|
|
1147
|
+
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
1148
|
+
* so the returned list reflects the available content types even for referenced responses.
|
|
1149
|
+
*
|
|
1150
|
+
* @example
|
|
1151
|
+
* ```ts
|
|
1152
|
+
* getResponseBodyContentTypes(document, operation, 200)
|
|
1153
|
+
* // ['application/json', 'application/xml']
|
|
1154
|
+
* ```
|
|
1155
|
+
*/
|
|
1156
|
+
function getResponseBodyContentTypes(document, operation, statusCode) {
|
|
1157
|
+
resolveResponseRefs(document, operation);
|
|
1158
|
+
const responseObj = getResponseByStatusCode({
|
|
1159
|
+
document,
|
|
1160
|
+
operation,
|
|
1161
|
+
statusCode
|
|
1162
|
+
});
|
|
1163
|
+
if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
|
|
1164
|
+
const body = responseObj;
|
|
1165
|
+
return body.content ? Object.keys(body.content) : [];
|
|
1166
|
+
}
|
|
1150
1167
|
//#endregion
|
|
1151
|
-
//#region src/
|
|
1168
|
+
//#region src/converters.ts
|
|
1152
1169
|
/**
|
|
1153
1170
|
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
1154
1171
|
*
|
|
1155
1172
|
* This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
|
|
1156
|
-
* from the array to its items sub-schema,
|
|
1173
|
+
* from the array to its items sub-schema, so they are valid for downstream processing.
|
|
1157
1174
|
*
|
|
1158
|
-
* @note
|
|
1175
|
+
* @note A defensive measure for non-compliant specs.
|
|
1159
1176
|
*/
|
|
1160
1177
|
function normalizeArrayEnum(schema) {
|
|
1161
1178
|
const normalizedItems = {
|
|
@@ -1169,490 +1186,677 @@ function normalizeArrayEnum(schema) {
|
|
|
1169
1186
|
};
|
|
1170
1187
|
}
|
|
1171
1188
|
/**
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1189
|
+
* Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
|
|
1190
|
+
* and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
|
|
1191
|
+
*/
|
|
1192
|
+
function createNullNode(schema, name, nullable) {
|
|
1193
|
+
return ast.factory.createSchema({
|
|
1194
|
+
type: "null",
|
|
1195
|
+
primitive: "null",
|
|
1196
|
+
name,
|
|
1197
|
+
title: schema.title,
|
|
1198
|
+
description: schema.description,
|
|
1199
|
+
deprecated: schema.deprecated,
|
|
1200
|
+
nullable,
|
|
1201
|
+
format: schema.format
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Names the inline enums on a property's schema, and on each item when the property is a tuple, from
|
|
1206
|
+
* the parent and property name. Wraps `macroEnumName` at the property construction site.
|
|
1207
|
+
*/
|
|
1208
|
+
function nameEnums(node, options) {
|
|
1209
|
+
const macro = macroEnumName(options);
|
|
1210
|
+
const named = ast.applyMacros(node, [macro], { depth: "shallow" });
|
|
1211
|
+
const tupleNode = ast.narrowSchema(named, "tuple");
|
|
1212
|
+
if (tupleNode?.items) {
|
|
1213
|
+
const namedItems = tupleNode.items.map((item) => ast.applyMacros(item, [macro], { depth: "shallow" }));
|
|
1214
|
+
if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
|
|
1215
|
+
...tupleNode,
|
|
1216
|
+
items: namedItems
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
return named;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
1177
1223
|
*
|
|
1178
|
-
*
|
|
1224
|
+
* The resolved schema is stored in `node.schema`. Usage-site sibling fields
|
|
1225
|
+
* (description, readOnly, nullable, etc.) are stored directly on the ref node.
|
|
1226
|
+
* Use `syncSchemaRef(node)` in printers to get a merged view of both.
|
|
1227
|
+
* Circular refs are detected in `resolveRefNode` and leave `schema` as `null`.
|
|
1179
1228
|
*/
|
|
1180
|
-
function
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1229
|
+
function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, resolveRefNode, refExists, renames }) {
|
|
1230
|
+
const refPath = schema.$ref;
|
|
1231
|
+
const resolvedSchema = refPath ? resolveRefNode(refPath, rawOptions) : null;
|
|
1232
|
+
if (refPath && document.components && !refExists(refPath)) return ast.factory.createSchema({
|
|
1233
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1234
|
+
type: "unknown"
|
|
1235
|
+
});
|
|
1236
|
+
const targetName = renames?.get(schema.$ref);
|
|
1237
|
+
return ast.factory.createSchema({
|
|
1238
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1239
|
+
type: "ref",
|
|
1240
|
+
name: extractRefName(schema.$ref),
|
|
1241
|
+
ref: schema.$ref,
|
|
1242
|
+
...targetName ? { targetName } : {},
|
|
1243
|
+
schema: resolvedSchema
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
1248
|
+
*/
|
|
1249
|
+
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, document }) {
|
|
1250
|
+
if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
|
|
1251
|
+
const [memberSchema] = schema.allOf;
|
|
1252
|
+
const memberNode = parse({
|
|
1253
|
+
schema: memberSchema,
|
|
1254
|
+
name
|
|
1255
|
+
}, rawOptions);
|
|
1256
|
+
const { kind: _kind, ...memberNodeProps } = memberNode;
|
|
1257
|
+
const mergedNullable = nullable || memberNode.nullable || void 0;
|
|
1258
|
+
const mergedDefault = schema.default === null && mergedNullable ? void 0 : schema.default ?? memberNode.default;
|
|
1259
|
+
return ast.factory.createSchema({
|
|
1260
|
+
...memberNodeProps,
|
|
1261
|
+
name,
|
|
1262
|
+
title: schema.title ?? memberNode.title,
|
|
1263
|
+
description: schema.description ?? memberNode.description,
|
|
1264
|
+
deprecated: schema.deprecated ?? memberNode.deprecated,
|
|
1265
|
+
nullable: mergedNullable,
|
|
1266
|
+
readOnly: schema.readOnly ?? memberNode.readOnly,
|
|
1267
|
+
writeOnly: schema.writeOnly ?? memberNode.writeOnly,
|
|
1268
|
+
default: mergedDefault,
|
|
1269
|
+
examples: extractExamples(schema) ?? memberNode.examples,
|
|
1270
|
+
pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0),
|
|
1271
|
+
format: schema.format ?? memberNode.format
|
|
1212
1272
|
});
|
|
1213
1273
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
if (
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
name,
|
|
1230
|
-
title: schema.title ?? memberNode.title,
|
|
1231
|
-
description: schema.description ?? memberNode.description,
|
|
1232
|
-
deprecated: schema.deprecated ?? memberNode.deprecated,
|
|
1233
|
-
nullable: mergedNullable,
|
|
1234
|
-
readOnly: schema.readOnly ?? memberNode.readOnly,
|
|
1235
|
-
writeOnly: schema.writeOnly ?? memberNode.writeOnly,
|
|
1236
|
-
default: mergedDefault,
|
|
1237
|
-
example: schema.example ?? memberNode.example,
|
|
1238
|
-
pattern: schema.pattern ?? ("pattern" in memberNode ? memberNode.pattern : void 0)
|
|
1274
|
+
const filteredDiscriminantValues = [];
|
|
1275
|
+
const allOfMembers = schema.allOf.filter((item) => {
|
|
1276
|
+
if (!isReference(item) || !name) return true;
|
|
1277
|
+
const deref = resolveRef(document, item.$ref);
|
|
1278
|
+
if (!deref || !isDiscriminator(deref)) return true;
|
|
1279
|
+
const parentUnion = deref.oneOf ?? deref.anyOf;
|
|
1280
|
+
if (!parentUnion) return true;
|
|
1281
|
+
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1282
|
+
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1283
|
+
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1284
|
+
if (inOneOf || inMapping) {
|
|
1285
|
+
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
|
|
1286
|
+
if (discriminatorValue) filteredDiscriminantValues.push({
|
|
1287
|
+
propertyName: deref.discriminator.propertyName,
|
|
1288
|
+
value: discriminatorValue
|
|
1239
1289
|
});
|
|
1290
|
+
return false;
|
|
1240
1291
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
const syntheticStart = allOfMembers.length;
|
|
1262
|
-
if (Array.isArray(schema.required) && schema.required.length) {
|
|
1263
|
-
const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
|
|
1264
|
-
const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
|
|
1265
|
-
if (missingRequired.length) {
|
|
1266
|
-
const resolvedMembers = schema.allOf.flatMap((item) => {
|
|
1267
|
-
if (!isReference(item)) return [item];
|
|
1268
|
-
const deref = resolveRef(document, item.$ref);
|
|
1269
|
-
return deref && !isReference(deref) ? [deref] : [];
|
|
1270
|
-
});
|
|
1271
|
-
for (const key of missingRequired) for (const resolved of resolvedMembers) if (resolved.properties?.[key]) {
|
|
1272
|
-
allOfMembers.push(parseSchema({ schema: {
|
|
1273
|
-
properties: { [key]: resolved.properties[key] },
|
|
1292
|
+
return true;
|
|
1293
|
+
}).map((s) => parse({
|
|
1294
|
+
schema: s,
|
|
1295
|
+
name
|
|
1296
|
+
}, rawOptions));
|
|
1297
|
+
const syntheticStart = allOfMembers.length;
|
|
1298
|
+
if (Array.isArray(schema.required) && schema.required.length) {
|
|
1299
|
+
const outerKeys = schema.properties ? new Set(Object.keys(schema.properties)) : /* @__PURE__ */ new Set();
|
|
1300
|
+
const missingRequired = schema.required.filter((key) => !outerKeys.has(key));
|
|
1301
|
+
if (missingRequired.length) {
|
|
1302
|
+
const resolvedMembers = schema.allOf.flatMap((item) => {
|
|
1303
|
+
if (!isReference(item)) return [item];
|
|
1304
|
+
const deref = resolveRef(document, item.$ref);
|
|
1305
|
+
return deref && !isReference(deref) ? [deref] : [];
|
|
1306
|
+
});
|
|
1307
|
+
for (const key of missingRequired) for (const resolved of resolvedMembers) {
|
|
1308
|
+
const prop = resolved.properties?.[key];
|
|
1309
|
+
if (prop) {
|
|
1310
|
+
const memberSchema = {
|
|
1311
|
+
properties: { [key]: prop },
|
|
1274
1312
|
required: [key]
|
|
1275
|
-
}
|
|
1313
|
+
};
|
|
1314
|
+
allOfMembers.push(parse({
|
|
1315
|
+
schema: memberSchema,
|
|
1316
|
+
name
|
|
1317
|
+
}, rawOptions));
|
|
1276
1318
|
break;
|
|
1277
1319
|
}
|
|
1278
1320
|
}
|
|
1279
1321
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1322
|
+
}
|
|
1323
|
+
if (schema.properties) {
|
|
1324
|
+
const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
|
|
1325
|
+
allOfMembers.push(parse({ schema: schemaWithoutAllOf }, rawOptions));
|
|
1326
|
+
}
|
|
1327
|
+
for (const { propertyName, value } of filteredDiscriminantValues) allOfMembers.push(createDiscriminantNode({
|
|
1328
|
+
propertyName,
|
|
1329
|
+
value
|
|
1330
|
+
}));
|
|
1331
|
+
return ast.factory.createSchema({
|
|
1332
|
+
type: "intersection",
|
|
1333
|
+
members: [...mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))],
|
|
1334
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
|
|
1339
|
+
*/
|
|
1340
|
+
function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, document }) {
|
|
1341
|
+
function pickDiscriminatorPropertyNode(node, propertyName) {
|
|
1342
|
+
const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
|
|
1343
|
+
if (!discriminatorProperty) return null;
|
|
1344
|
+
return ast.factory.createSchema({
|
|
1345
|
+
type: "object",
|
|
1346
|
+
primitive: "object",
|
|
1347
|
+
properties: [discriminatorProperty]
|
|
1292
1348
|
});
|
|
1293
1349
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1350
|
+
function resolveRefSilent($ref) {
|
|
1351
|
+
if (!$ref.startsWith("#")) return null;
|
|
1352
|
+
return decodeURIComponent($ref.substring(1)).split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document) ?? null;
|
|
1353
|
+
}
|
|
1354
|
+
function implicitDiscriminantValue(member) {
|
|
1355
|
+
if (!discriminator || discriminator.mapping || !isReference(member)) return null;
|
|
1356
|
+
const value = extractRefName(member.$ref);
|
|
1357
|
+
if (!value) return null;
|
|
1358
|
+
const variant = resolveRefSilent(member.$ref);
|
|
1359
|
+
if (!variant) return null;
|
|
1360
|
+
const propertyName = discriminator.propertyName;
|
|
1361
|
+
const seen = /* @__PURE__ */ new Set([member.$ref]);
|
|
1362
|
+
function constrains(v) {
|
|
1363
|
+
const prop = v.properties?.[propertyName];
|
|
1364
|
+
const resolved = prop && isReference(prop) ? resolveRefSilent(prop.$ref) : prop;
|
|
1365
|
+
if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
|
|
1366
|
+
const composition = v.allOf ?? v.oneOf ?? v.anyOf;
|
|
1367
|
+
if (!composition) return false;
|
|
1368
|
+
return composition.some((m) => {
|
|
1369
|
+
if (!isReference(m)) return constrains(m);
|
|
1370
|
+
if (seen.has(m.$ref)) return false;
|
|
1371
|
+
seen.add(m.$ref);
|
|
1372
|
+
const r = resolveRefSilent(m.$ref);
|
|
1373
|
+
return r ? constrains(r) : false;
|
|
1305
1374
|
});
|
|
1306
1375
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1376
|
+
return constrains(variant) ? null : value;
|
|
1377
|
+
}
|
|
1378
|
+
const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
|
|
1379
|
+
const strategy = schema.oneOf ? "one" : "any";
|
|
1380
|
+
const unionBase = {
|
|
1381
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1382
|
+
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
|
|
1383
|
+
strategy
|
|
1384
|
+
};
|
|
1385
|
+
const discriminator = isDiscriminator(schema) ? schema.discriminator : void 0;
|
|
1386
|
+
const { oneOf: _o, anyOf: _a, discriminator: _d, ...memberBaseSchema } = schema;
|
|
1387
|
+
const sharedPropertiesNode = schema.properties ? parse({
|
|
1388
|
+
schema: memberBaseSchema,
|
|
1389
|
+
name
|
|
1390
|
+
}, rawOptions) : void 0;
|
|
1391
|
+
if (sharedPropertiesNode || discriminator) {
|
|
1392
|
+
const members = unionMembers.map((s) => {
|
|
1393
|
+
const ref = isReference(s) ? s.$ref : void 0;
|
|
1394
|
+
const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
|
|
1395
|
+
const memberNode = parse({
|
|
1396
|
+
schema: s,
|
|
1319
1397
|
name
|
|
1320
1398
|
}, rawOptions);
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
if (!discriminatorValue || !discriminator) return memberNode;
|
|
1328
|
-
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.setDiscriminatorEnum({
|
|
1329
|
-
node: sharedPropertiesNode,
|
|
1330
|
-
propertyName: discriminator.propertyName,
|
|
1331
|
-
values: [discriminatorValue]
|
|
1332
|
-
}), discriminator.propertyName) : void 0;
|
|
1333
|
-
return ast.createSchema({
|
|
1334
|
-
type: "intersection",
|
|
1335
|
-
members: [memberNode, narrowedDiscriminatorNode ?? ast.createDiscriminantNode({
|
|
1336
|
-
propertyName: discriminator.propertyName,
|
|
1337
|
-
value: discriminatorValue
|
|
1338
|
-
})]
|
|
1339
|
-
});
|
|
1340
|
-
});
|
|
1341
|
-
const unionNode = ast.createSchema({
|
|
1342
|
-
type: "union",
|
|
1343
|
-
...unionBase,
|
|
1344
|
-
members
|
|
1345
|
-
});
|
|
1346
|
-
if (!sharedPropertiesNode) return unionNode;
|
|
1347
|
-
return ast.createSchema({
|
|
1399
|
+
if (!discriminatorValue || !discriminator) return memberNode;
|
|
1400
|
+
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.applyMacros(sharedPropertiesNode, [macroDiscriminatorEnum({
|
|
1401
|
+
propertyName: discriminator.propertyName,
|
|
1402
|
+
values: [discriminatorValue]
|
|
1403
|
+
})], { depth: "shallow" }), discriminator.propertyName) : void 0;
|
|
1404
|
+
return ast.factory.createSchema({
|
|
1348
1405
|
type: "intersection",
|
|
1349
|
-
|
|
1350
|
-
|
|
1406
|
+
members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
|
|
1407
|
+
propertyName: discriminator.propertyName,
|
|
1408
|
+
value: discriminatorValue
|
|
1409
|
+
})]
|
|
1351
1410
|
});
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1411
|
+
});
|
|
1412
|
+
const unionNode = ast.factory.createSchema({
|
|
1354
1413
|
type: "union",
|
|
1355
1414
|
...unionBase,
|
|
1356
|
-
members
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
|
|
1361
|
-
*/
|
|
1362
|
-
function convertConst({ schema, name, nullable, defaultValue }) {
|
|
1363
|
-
const constValue = schema.const;
|
|
1364
|
-
if (constValue === null) return ast.createSchema({
|
|
1365
|
-
type: "null",
|
|
1366
|
-
primitive: "null",
|
|
1367
|
-
name,
|
|
1368
|
-
title: schema.title,
|
|
1369
|
-
description: schema.description,
|
|
1370
|
-
deprecated: schema.deprecated
|
|
1415
|
+
members
|
|
1371
1416
|
});
|
|
1372
|
-
|
|
1373
|
-
return ast.createSchema({
|
|
1374
|
-
type: "
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1417
|
+
if (!sharedPropertiesNode) return unionNode;
|
|
1418
|
+
return ast.factory.createSchema({
|
|
1419
|
+
type: "intersection",
|
|
1420
|
+
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1421
|
+
members: [unionNode, sharedPropertiesNode]
|
|
1378
1422
|
});
|
|
1379
1423
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
type: "url",
|
|
1424
|
-
min: schema.minLength,
|
|
1425
|
-
max: schema.maxLength
|
|
1426
|
-
});
|
|
1427
|
-
if (specialType === "ipv4") return ast.createSchema({
|
|
1424
|
+
const unionNode = ast.factory.createSchema({
|
|
1425
|
+
type: "union",
|
|
1426
|
+
...unionBase,
|
|
1427
|
+
members: unionMembers.map((s) => parse({
|
|
1428
|
+
schema: s,
|
|
1429
|
+
name
|
|
1430
|
+
}, rawOptions))
|
|
1431
|
+
});
|
|
1432
|
+
return ast.applyMacros(unionNode, [macroSimplifyUnion], { depth: "shallow" });
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
|
|
1436
|
+
*/
|
|
1437
|
+
function convertConst({ schema, name, nullable, defaultValue }) {
|
|
1438
|
+
const constValue = schema.const;
|
|
1439
|
+
if (constValue === null) return createNullNode(schema, name);
|
|
1440
|
+
const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
|
|
1441
|
+
return ast.factory.createSchema({
|
|
1442
|
+
type: "enum",
|
|
1443
|
+
primitive: constPrimitive,
|
|
1444
|
+
enumValues: [constValue],
|
|
1445
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Converts a format-annotated schema into a special-type `SchemaNode`.
|
|
1450
|
+
* Returns `null` when the format should fall through to string handling (`dateType: false`).
|
|
1451
|
+
*/
|
|
1452
|
+
function convertFormat({ schema, name, nullable, defaultValue, options }) {
|
|
1453
|
+
const base = buildSchemaNode(schema, name, nullable, defaultValue);
|
|
1454
|
+
if (schema.format === "int64") return ast.factory.createSchema({
|
|
1455
|
+
type: options.integerType === "bigint" ? "bigint" : "integer",
|
|
1456
|
+
primitive: "integer",
|
|
1457
|
+
...base,
|
|
1458
|
+
min: schema.minimum,
|
|
1459
|
+
max: schema.maximum,
|
|
1460
|
+
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
1461
|
+
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
|
|
1462
|
+
});
|
|
1463
|
+
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
|
|
1464
|
+
const dateType = getDateType(options, schema.format);
|
|
1465
|
+
if (!dateType) return null;
|
|
1466
|
+
if (dateType.type === "datetime") return ast.factory.createSchema({
|
|
1428
1467
|
...base,
|
|
1429
1468
|
primitive: "string",
|
|
1430
|
-
type: "
|
|
1469
|
+
type: "datetime",
|
|
1470
|
+
offset: dateType.offset,
|
|
1471
|
+
local: dateType.local
|
|
1431
1472
|
});
|
|
1432
|
-
|
|
1473
|
+
return ast.factory.createSchema({
|
|
1433
1474
|
...base,
|
|
1434
1475
|
primitive: "string",
|
|
1435
|
-
type:
|
|
1476
|
+
type: dateType.type,
|
|
1477
|
+
representation: dateType.representation
|
|
1436
1478
|
});
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1479
|
+
}
|
|
1480
|
+
const specialType = getSchemaType(schema.format);
|
|
1481
|
+
if (!specialType) return null;
|
|
1482
|
+
const specialPrimitive = specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string";
|
|
1483
|
+
const hasLength = specialType === "url" || specialType === "uuid" || specialType === "email";
|
|
1484
|
+
return ast.factory.createSchema({
|
|
1485
|
+
...base,
|
|
1486
|
+
primitive: specialPrimitive,
|
|
1487
|
+
type: specialType,
|
|
1488
|
+
...hasLength ? {
|
|
1441
1489
|
min: schema.minLength,
|
|
1442
1490
|
max: schema.maxLength
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
example: schema.example
|
|
1475
|
-
};
|
|
1476
|
-
const extensionKey = enumExtensionKeys.find((key) => key in schema);
|
|
1477
|
-
if (extensionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
|
|
1478
|
-
const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
|
|
1479
|
-
const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
|
|
1480
|
-
const uniqueValues = [...new Set(filteredValues)];
|
|
1481
|
-
const seenNames = /* @__PURE__ */ new Set();
|
|
1482
|
-
return ast.createSchema({
|
|
1483
|
-
...enumBase,
|
|
1484
|
-
primitive: enumPrimitiveType,
|
|
1485
|
-
namedEnumValues: uniqueValues.map((value, index) => ({
|
|
1486
|
-
name: String(rawEnumNames?.[index] ?? value),
|
|
1487
|
-
value,
|
|
1488
|
-
primitive: enumPrimitiveType
|
|
1489
|
-
})).filter((entry) => {
|
|
1490
|
-
if (seenNames.has(entry.name)) return false;
|
|
1491
|
-
seenNames.add(entry.name);
|
|
1492
|
-
return true;
|
|
1493
|
-
})
|
|
1494
|
-
});
|
|
1495
|
-
}
|
|
1496
|
-
return ast.createSchema({
|
|
1491
|
+
} : {}
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Converts an `enum` schema into an `EnumSchemaNode`.
|
|
1496
|
+
*/
|
|
1497
|
+
function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
|
|
1498
|
+
if (type === "array") return parse({
|
|
1499
|
+
schema: normalizeArrayEnum(schema),
|
|
1500
|
+
name
|
|
1501
|
+
}, rawOptions);
|
|
1502
|
+
const nullInEnum = schema.enum.includes(null);
|
|
1503
|
+
const filteredValues = nullInEnum ? schema.enum.filter((v) => v !== null) : schema.enum;
|
|
1504
|
+
if (nullInEnum && filteredValues.length === 0) return createNullNode(schema, name);
|
|
1505
|
+
const enumNullable = nullable || nullInEnum || void 0;
|
|
1506
|
+
const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
|
|
1507
|
+
const enumPrimitive = getPrimitiveType(type);
|
|
1508
|
+
const enumBase = {
|
|
1509
|
+
type: "enum",
|
|
1510
|
+
primitive: enumPrimitive,
|
|
1511
|
+
...buildSchemaNode(schema, name, enumNullable, enumDefault)
|
|
1512
|
+
};
|
|
1513
|
+
const extensionKey = enumExtensionKeys.find((key) => key in schema);
|
|
1514
|
+
const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
|
|
1515
|
+
if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
|
|
1516
|
+
const enumPrimitiveType = enumPrimitive === "number" || enumPrimitive === "integer" ? "number" : enumPrimitive === "boolean" ? "boolean" : "string";
|
|
1517
|
+
const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
|
|
1518
|
+
const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
|
|
1519
|
+
const uniqueValues = [...new Set(filteredValues)];
|
|
1520
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1521
|
+
return ast.factory.createSchema({
|
|
1497
1522
|
...enumBase,
|
|
1498
|
-
|
|
1523
|
+
primitive: enumPrimitiveType,
|
|
1524
|
+
namedEnumValues: uniqueValues.map((value, index) => ({
|
|
1525
|
+
name: String(rawEnumNames?.[index] ?? value),
|
|
1526
|
+
value,
|
|
1527
|
+
primitive: enumPrimitiveType,
|
|
1528
|
+
description: rawEnumDescriptions?.[index]
|
|
1529
|
+
})).filter((entry) => {
|
|
1530
|
+
if (seenNames.has(entry.name)) return false;
|
|
1531
|
+
seenNames.add(entry.name);
|
|
1532
|
+
return true;
|
|
1533
|
+
})
|
|
1499
1534
|
});
|
|
1500
1535
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
}
|
|
1522
|
-
return ast.createProperty({
|
|
1523
|
-
name: propName,
|
|
1524
|
-
schema: {
|
|
1525
|
-
...schemaNode,
|
|
1526
|
-
nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
|
|
1527
|
-
},
|
|
1528
|
-
required
|
|
1529
|
-
});
|
|
1530
|
-
}) : [];
|
|
1531
|
-
const additionalProperties = schema.additionalProperties;
|
|
1532
|
-
let additionalPropertiesNode;
|
|
1533
|
-
if (additionalProperties === true) additionalPropertiesNode = true;
|
|
1534
|
-
else if (additionalProperties && Object.keys(additionalProperties).length > 0) additionalPropertiesNode = parseSchema({ schema: additionalProperties }, rawOptions);
|
|
1535
|
-
else if (additionalProperties === false) additionalPropertiesNode = false;
|
|
1536
|
-
else if (additionalProperties) additionalPropertiesNode = ast.createSchema({ type: typeOptionMap.get(options.unknownType) });
|
|
1537
|
-
const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
|
|
1538
|
-
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? ast.createSchema({ type: typeOptionMap.get(options.unknownType) }) : parseSchema({ schema: patternSchema }, rawOptions)])) : void 0;
|
|
1539
|
-
const objectNode = ast.createSchema({
|
|
1540
|
-
type: "object",
|
|
1541
|
-
primitive: "object",
|
|
1542
|
-
properties,
|
|
1543
|
-
additionalProperties: additionalPropertiesNode,
|
|
1544
|
-
patternProperties,
|
|
1545
|
-
minProperties: schema.minProperties,
|
|
1546
|
-
maxProperties: schema.maxProperties,
|
|
1547
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1536
|
+
return ast.factory.createSchema({
|
|
1537
|
+
...enumBase,
|
|
1538
|
+
enumValues: [...new Set(filteredValues)]
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
1543
|
+
*/
|
|
1544
|
+
function convertObject({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1545
|
+
const properties = schema.properties ? Object.entries(schema.properties).map(([propName, propSchema]) => {
|
|
1546
|
+
const required = Array.isArray(schema.required) ? schema.required.includes(propName) : !!schema.required;
|
|
1547
|
+
const resolvedPropSchema = propSchema;
|
|
1548
|
+
const propNullable = isNullable(resolvedPropSchema);
|
|
1549
|
+
const schemaNode = nameEnums(parse({
|
|
1550
|
+
schema: resolvedPropSchema,
|
|
1551
|
+
name: childName(name, propName)
|
|
1552
|
+
}, rawOptions), {
|
|
1553
|
+
parentName: name,
|
|
1554
|
+
propName,
|
|
1555
|
+
enumSuffix: options.enumSuffix
|
|
1548
1556
|
});
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
values,
|
|
1557
|
-
enumName
|
|
1558
|
-
});
|
|
1559
|
-
}
|
|
1560
|
-
return objectNode;
|
|
1561
|
-
}
|
|
1562
|
-
/**
|
|
1563
|
-
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
1564
|
-
*/
|
|
1565
|
-
function convertTuple({ schema, name, nullable, defaultValue, rawOptions }) {
|
|
1566
|
-
const tupleItems = (schema.prefixItems ?? []).map((item) => parseSchema({ schema: item }, rawOptions));
|
|
1567
|
-
const rest = schema.items ? parseSchema({ schema: schema.items }, rawOptions) : ast.createSchema({ type: "any" });
|
|
1568
|
-
return ast.createSchema({
|
|
1569
|
-
type: "tuple",
|
|
1570
|
-
primitive: "array",
|
|
1571
|
-
items: tupleItems,
|
|
1572
|
-
rest,
|
|
1573
|
-
min: schema.minItems,
|
|
1574
|
-
max: schema.maxItems,
|
|
1575
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1557
|
+
return ast.factory.createProperty({
|
|
1558
|
+
name: propName,
|
|
1559
|
+
schema: {
|
|
1560
|
+
...schemaNode,
|
|
1561
|
+
nullable: schemaNode.type === "null" ? void 0 : propNullable || void 0
|
|
1562
|
+
},
|
|
1563
|
+
required
|
|
1576
1564
|
});
|
|
1565
|
+
}) : [];
|
|
1566
|
+
const additionalProperties = schema.additionalProperties;
|
|
1567
|
+
const additionalPropertiesNode = (() => {
|
|
1568
|
+
if (additionalProperties === true) return true;
|
|
1569
|
+
if (additionalProperties === false) return false;
|
|
1570
|
+
if (additionalProperties && Object.keys(additionalProperties).length > 0) return parse({ schema: additionalProperties }, rawOptions);
|
|
1571
|
+
if (additionalProperties) return ast.factory.createSchema({ type: options.unknownType });
|
|
1572
|
+
})();
|
|
1573
|
+
const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
|
|
1574
|
+
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, patternSchema === true || typeof patternSchema === "object" && Object.keys(patternSchema).length === 0 ? ast.factory.createSchema({ type: options.unknownType }) : parse({ schema: patternSchema }, rawOptions)])) : void 0;
|
|
1575
|
+
const objectNode = ast.factory.createSchema({
|
|
1576
|
+
type: "object",
|
|
1577
|
+
primitive: "object",
|
|
1578
|
+
properties,
|
|
1579
|
+
additionalProperties: additionalPropertiesNode,
|
|
1580
|
+
patternProperties,
|
|
1581
|
+
minProperties: schema.minProperties,
|
|
1582
|
+
maxProperties: schema.maxProperties,
|
|
1583
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1584
|
+
});
|
|
1585
|
+
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
1586
|
+
const discPropName = schema.discriminator.propertyName;
|
|
1587
|
+
const values = Object.keys(schema.discriminator.mapping);
|
|
1588
|
+
const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
|
|
1589
|
+
return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
|
|
1590
|
+
propertyName: discPropName,
|
|
1591
|
+
values,
|
|
1592
|
+
enumName
|
|
1593
|
+
})], { depth: "shallow" });
|
|
1577
1594
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1595
|
+
return objectNode;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
1599
|
+
*/
|
|
1600
|
+
function convertTuple({ schema, name, nullable, defaultValue, rawOptions, parse }) {
|
|
1601
|
+
const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
|
|
1602
|
+
const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? ast.factory.createSchema({ type: "any" }) : parse({ schema: schema.items }, rawOptions);
|
|
1603
|
+
return ast.factory.createSchema({
|
|
1604
|
+
type: "tuple",
|
|
1605
|
+
primitive: "array",
|
|
1606
|
+
items: tupleItems,
|
|
1607
|
+
rest,
|
|
1608
|
+
min: schema.minItems,
|
|
1609
|
+
max: schema.maxItems,
|
|
1610
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Converts a `type: 'array'` schema into an `ArraySchemaNode`.
|
|
1615
|
+
*/
|
|
1616
|
+
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1617
|
+
const rawItems = schema.items;
|
|
1618
|
+
const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name;
|
|
1619
|
+
const items = rawItems ? [parse({
|
|
1620
|
+
schema: rawItems,
|
|
1621
|
+
name: itemName
|
|
1622
|
+
}, rawOptions)] : [];
|
|
1623
|
+
return ast.factory.createSchema({
|
|
1624
|
+
type: "array",
|
|
1625
|
+
primitive: "array",
|
|
1626
|
+
items,
|
|
1627
|
+
min: schema.minItems,
|
|
1628
|
+
max: schema.maxItems,
|
|
1629
|
+
unique: schema.uniqueItems ?? void 0,
|
|
1630
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
1635
|
+
*/
|
|
1636
|
+
function convertString({ schema, name, nullable, defaultValue }) {
|
|
1637
|
+
return ast.factory.createSchema({
|
|
1638
|
+
type: "string",
|
|
1639
|
+
primitive: "string",
|
|
1640
|
+
min: schema.minLength,
|
|
1641
|
+
max: schema.maxLength,
|
|
1642
|
+
pattern: schema.pattern,
|
|
1643
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
1648
|
+
*/
|
|
1649
|
+
function convertNumeric({ schema, name, nullable, defaultValue }, type) {
|
|
1650
|
+
return ast.factory.createSchema({
|
|
1651
|
+
type,
|
|
1652
|
+
primitive: type,
|
|
1653
|
+
min: schema.minimum,
|
|
1654
|
+
max: schema.maximum,
|
|
1655
|
+
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
1656
|
+
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
|
|
1657
|
+
multipleOf: schema.multipleOf,
|
|
1658
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Converts a `type: 'boolean'` schema.
|
|
1663
|
+
*/
|
|
1664
|
+
function convertBoolean({ schema, name, nullable, defaultValue }) {
|
|
1665
|
+
return ast.factory.createSchema({
|
|
1666
|
+
type: "boolean",
|
|
1667
|
+
primitive: "boolean",
|
|
1668
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
|
|
1673
|
+
* into a `blob` node.
|
|
1674
|
+
*/
|
|
1675
|
+
function convertBinary({ schema, name, nullable, defaultValue }) {
|
|
1676
|
+
return ast.factory.createSchema({
|
|
1677
|
+
type: "blob",
|
|
1678
|
+
primitive: "string",
|
|
1679
|
+
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
1684
|
+
*
|
|
1685
|
+
* Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parse`
|
|
1686
|
+
* falls through and handles it as that single type with nullability already folded in.
|
|
1687
|
+
*/
|
|
1688
|
+
function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
|
|
1689
|
+
const types = schema.type;
|
|
1690
|
+
const nonNullTypes = types.filter((t) => t !== "null");
|
|
1691
|
+
if (nonNullTypes.length <= 1) return null;
|
|
1692
|
+
const arrayNullable = types.includes("null") || nullable || void 0;
|
|
1693
|
+
return ast.factory.createSchema({
|
|
1694
|
+
type: "union",
|
|
1695
|
+
members: nonNullTypes.map((t) => {
|
|
1696
|
+
return parse({
|
|
1697
|
+
schema: {
|
|
1698
|
+
...schema,
|
|
1699
|
+
type: t
|
|
1700
|
+
},
|
|
1701
|
+
name
|
|
1702
|
+
}, rawOptions);
|
|
1703
|
+
}),
|
|
1704
|
+
...buildSchemaNode(schema, name, arrayNullable, defaultValue)
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
1709
|
+
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
1710
|
+
* `type`. The first matching rule that produces a node wins. See {@link SchemaRule} for the
|
|
1711
|
+
* match/convert/fall-through contract.
|
|
1712
|
+
*/
|
|
1713
|
+
const schemaRules = [
|
|
1714
|
+
{
|
|
1715
|
+
match: ({ schema }) => isReference(schema),
|
|
1716
|
+
convert: convertRef
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
match: ({ schema }) => !!schema.allOf?.length,
|
|
1720
|
+
convert: convertAllOf
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
match: ({ schema }) => !!(schema.oneOf?.length || schema.anyOf?.length),
|
|
1724
|
+
convert: convertUnion
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
match: ({ schema }) => "const" in schema && schema.const !== void 0,
|
|
1728
|
+
convert: convertConst
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
match: ({ schema }) => !!schema.format,
|
|
1732
|
+
convert: convertFormat
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
match: ({ schema }) => isBinary(schema),
|
|
1736
|
+
convert: convertBinary
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
|
|
1740
|
+
convert: convertMultiType
|
|
1741
|
+
},
|
|
1742
|
+
{
|
|
1743
|
+
match: ({ schema, type }) => !type && (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0),
|
|
1744
|
+
convert: convertString
|
|
1745
|
+
},
|
|
1746
|
+
{
|
|
1747
|
+
match: ({ schema, type }) => !type && (schema.minimum !== void 0 || schema.maximum !== void 0),
|
|
1748
|
+
convert: (ctx) => convertNumeric(ctx, "number")
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
match: ({ schema }) => !!schema.enum?.length,
|
|
1752
|
+
convert: convertEnum
|
|
1753
|
+
},
|
|
1754
|
+
{
|
|
1755
|
+
match: ({ schema, type }) => type === "object" || !!schema.properties || !!schema.additionalProperties || "patternProperties" in schema,
|
|
1756
|
+
convert: convertObject
|
|
1757
|
+
},
|
|
1758
|
+
{
|
|
1759
|
+
match: ({ schema }) => "prefixItems" in schema,
|
|
1760
|
+
convert: convertTuple
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
match: ({ schema, type }) => type === "array" || "items" in schema,
|
|
1764
|
+
convert: convertArray
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
match: ({ type }) => type === "string",
|
|
1768
|
+
convert: convertString
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
match: ({ type }) => type === "number",
|
|
1772
|
+
convert: (ctx) => convertNumeric(ctx, "number")
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
match: ({ type }) => type === "integer",
|
|
1776
|
+
convert: (ctx) => convertNumeric(ctx, "integer")
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
match: ({ type }) => type === "boolean",
|
|
1780
|
+
convert: convertBoolean
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
match: ({ type }) => type === "null",
|
|
1784
|
+
convert: ({ schema, name, nullable }) => createNullNode(schema, name, nullable)
|
|
1597
1785
|
}
|
|
1786
|
+
];
|
|
1787
|
+
//#endregion
|
|
1788
|
+
//#region src/parser.ts
|
|
1789
|
+
/**
|
|
1790
|
+
* Creates the schema and operation converters bound to one OpenAPI document.
|
|
1791
|
+
*
|
|
1792
|
+
* Owns the per-instance `$ref` state (cycle detection, resolved-node cache, existence cache) and
|
|
1793
|
+
* the `parseSchema` recursion seam, then dispatches each schema through the ordered `schemaRules`
|
|
1794
|
+
* table from `converters.ts`. Every converter is a standalone function that recurses through the
|
|
1795
|
+
* `parse` function passed to it, so this file only wires state to the converters.
|
|
1796
|
+
*
|
|
1797
|
+
* @internal
|
|
1798
|
+
*/
|
|
1799
|
+
function createSchemaParser(ctx) {
|
|
1800
|
+
const document = ctx.document;
|
|
1598
1801
|
/**
|
|
1599
|
-
*
|
|
1802
|
+
* Tracks `$ref` paths that are currently being resolved to prevent infinite
|
|
1803
|
+
* recursion when schemas contain circular references (e.g. `Pet → parent → Pet`).
|
|
1600
1804
|
*/
|
|
1601
|
-
|
|
1602
|
-
return ast.createSchema({
|
|
1603
|
-
type: "string",
|
|
1604
|
-
primitive: "string",
|
|
1605
|
-
min: schema.minLength,
|
|
1606
|
-
max: schema.maxLength,
|
|
1607
|
-
pattern: schema.pattern,
|
|
1608
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1805
|
+
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
1611
1806
|
/**
|
|
1612
|
-
*
|
|
1807
|
+
* Cache of `$ref` schemas already resolved in this parser instance, keyed by ref path.
|
|
1808
|
+
*
|
|
1809
|
+
* Without it, a shared schema (e.g. `customer`) is re-expanded for every `$ref` that points at
|
|
1810
|
+
* it. In cross-referenced specs like Stripe (~1400 schemas) that becomes exponential blowup,
|
|
1811
|
+
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
1812
|
+
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
1613
1813
|
*/
|
|
1614
|
-
|
|
1615
|
-
return ast.createSchema({
|
|
1616
|
-
type,
|
|
1617
|
-
primitive: type,
|
|
1618
|
-
min: schema.minimum,
|
|
1619
|
-
max: schema.maximum,
|
|
1620
|
-
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
1621
|
-
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
|
|
1622
|
-
multipleOf: schema.multipleOf,
|
|
1623
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1624
|
-
});
|
|
1625
|
-
}
|
|
1814
|
+
const resolvedRefCache = /* @__PURE__ */ new Map();
|
|
1626
1815
|
/**
|
|
1627
|
-
*
|
|
1816
|
+
* Memoized record of whether a `$ref` path resolves to a node the document actually defines.
|
|
1817
|
+
* A circular ref still resolves to an existing target, so this stays `true` for cycles and only
|
|
1818
|
+
* goes `false` for a `$ref` that points at a component the spec never declares.
|
|
1628
1819
|
*/
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1820
|
+
const refExistence = /* @__PURE__ */ new Map();
|
|
1821
|
+
function refExists(refPath) {
|
|
1822
|
+
if (!refExistence.has(refPath)) {
|
|
1823
|
+
let exists = false;
|
|
1824
|
+
try {
|
|
1825
|
+
exists = !!resolveRef(document, refPath);
|
|
1826
|
+
} catch {
|
|
1827
|
+
exists = false;
|
|
1828
|
+
}
|
|
1829
|
+
refExistence.set(refPath, exists);
|
|
1830
|
+
}
|
|
1831
|
+
return refExistence.get(refPath) ?? false;
|
|
1635
1832
|
}
|
|
1636
1833
|
/**
|
|
1637
|
-
*
|
|
1834
|
+
* Resolves a `$ref` to its parsed node, guarding against cycles and memoizing per instance.
|
|
1835
|
+
* Returns `null` when the ref is currently being resolved (a cycle) or cannot be resolved
|
|
1836
|
+
* (e.g. a minimal document in a unit test).
|
|
1638
1837
|
*/
|
|
1639
|
-
function
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1838
|
+
function resolveRefNode(refPath, rawOptions) {
|
|
1839
|
+
if (resolvingRefs.has(refPath)) return null;
|
|
1840
|
+
if (!resolvedRefCache.has(refPath)) {
|
|
1841
|
+
let resolved = null;
|
|
1842
|
+
try {
|
|
1843
|
+
const referenced = resolveRef(document, refPath);
|
|
1844
|
+
if (referenced) {
|
|
1845
|
+
resolvingRefs.add(refPath);
|
|
1846
|
+
resolved = parseSchema({ schema: referenced }, rawOptions);
|
|
1847
|
+
resolvingRefs.delete(refPath);
|
|
1848
|
+
}
|
|
1849
|
+
} catch {}
|
|
1850
|
+
resolvedRefCache.set(refPath, resolved);
|
|
1851
|
+
}
|
|
1852
|
+
return resolvedRefCache.get(refPath) ?? null;
|
|
1649
1853
|
}
|
|
1650
1854
|
/**
|
|
1651
|
-
*
|
|
1855
|
+
* Converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
1652
1856
|
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1857
|
+
* Builds the per-schema context, then walks the ordered {@link schemaRules} table and returns
|
|
1858
|
+
* the first converter that produces a node. When none match, falls back to the configured
|
|
1859
|
+
* `emptySchemaType`.
|
|
1656
1860
|
*/
|
|
1657
1861
|
function parseSchema({ schema, name }, rawOptions) {
|
|
1658
1862
|
const options = {
|
|
@@ -1665,80 +1869,57 @@ function createSchemaParser(ctx) {
|
|
|
1665
1869
|
name
|
|
1666
1870
|
}, rawOptions);
|
|
1667
1871
|
const nullable = isNullable(schema) || void 0;
|
|
1668
|
-
const
|
|
1669
|
-
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
1670
|
-
const ctx = {
|
|
1872
|
+
const context = {
|
|
1671
1873
|
schema,
|
|
1672
1874
|
name,
|
|
1673
1875
|
nullable,
|
|
1674
|
-
defaultValue,
|
|
1675
|
-
type,
|
|
1876
|
+
defaultValue: schema.default === null && nullable ? void 0 : schema.default,
|
|
1877
|
+
type: Array.isArray(schema.type) ? schema.type[0] : schema.type,
|
|
1676
1878
|
rawOptions,
|
|
1677
|
-
options
|
|
1879
|
+
options,
|
|
1880
|
+
parse: parseSchema,
|
|
1881
|
+
document,
|
|
1882
|
+
resolveRefNode,
|
|
1883
|
+
refExists,
|
|
1884
|
+
renames: ctx.renames
|
|
1678
1885
|
};
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
if (schema.format) {
|
|
1684
|
-
const formatResult = convertFormat(ctx);
|
|
1685
|
-
if (formatResult) return formatResult;
|
|
1886
|
+
for (const rule of schemaRules) {
|
|
1887
|
+
if (!rule.match(context)) continue;
|
|
1888
|
+
const node = rule.convert(context);
|
|
1889
|
+
if (node) return node;
|
|
1686
1890
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
primitive: "string",
|
|
1690
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1691
|
-
});
|
|
1692
|
-
if (Array.isArray(schema.type) && schema.type.length > 1) {
|
|
1693
|
-
const nonNullTypes = schema.type.filter((t) => t !== "null");
|
|
1694
|
-
const arrayNullable = schema.type.includes("null") || nullable || void 0;
|
|
1695
|
-
if (nonNullTypes.length > 1) return ast.createSchema({
|
|
1696
|
-
type: "union",
|
|
1697
|
-
members: nonNullTypes.map((t) => parseSchema({
|
|
1698
|
-
schema: {
|
|
1699
|
-
...schema,
|
|
1700
|
-
type: t
|
|
1701
|
-
},
|
|
1702
|
-
name
|
|
1703
|
-
}, rawOptions)),
|
|
1704
|
-
...buildSchemaNode(schema, name, arrayNullable, defaultValue)
|
|
1705
|
-
});
|
|
1706
|
-
}
|
|
1707
|
-
if (!type) {
|
|
1708
|
-
if (schema.minLength !== void 0 || schema.maxLength !== void 0 || schema.pattern !== void 0) return convertString(ctx);
|
|
1709
|
-
if (schema.minimum !== void 0 || schema.maximum !== void 0) return convertNumeric(ctx, "number");
|
|
1710
|
-
}
|
|
1711
|
-
if (schema.enum?.length) return convertEnum(ctx);
|
|
1712
|
-
if (type === "object" || schema.properties || schema.additionalProperties || "patternProperties" in schema) return convertObject(ctx);
|
|
1713
|
-
if ("prefixItems" in schema) return convertTuple(ctx);
|
|
1714
|
-
if (type === "array" || "items" in schema) return convertArray(ctx);
|
|
1715
|
-
if (type === "string") return convertString(ctx);
|
|
1716
|
-
if (type === "number") return convertNumeric(ctx, "number");
|
|
1717
|
-
if (type === "integer") return convertNumeric(ctx, "integer");
|
|
1718
|
-
if (type === "boolean") return convertBoolean(ctx);
|
|
1719
|
-
if (type === "null") return convertNull(ctx);
|
|
1720
|
-
const emptyType = typeOptionMap.get(options.emptySchemaType);
|
|
1721
|
-
return ast.createSchema({
|
|
1891
|
+
const emptyType = options.emptySchemaType;
|
|
1892
|
+
return ast.factory.createSchema({
|
|
1722
1893
|
type: emptyType,
|
|
1723
1894
|
name,
|
|
1724
1895
|
title: schema.title,
|
|
1725
|
-
description: schema.description
|
|
1896
|
+
description: schema.description,
|
|
1897
|
+
format: schema.format
|
|
1726
1898
|
});
|
|
1727
1899
|
}
|
|
1728
1900
|
/**
|
|
1729
1901
|
* Converts a dereferenced OAS parameter object into a `ParameterNode`.
|
|
1730
1902
|
*/
|
|
1731
|
-
function parseParameter(options, param) {
|
|
1903
|
+
function parseParameter(options, param, parentName) {
|
|
1732
1904
|
const required = param["required"] ?? false;
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1735
|
-
|
|
1905
|
+
const paramName = param["name"];
|
|
1906
|
+
const schemaName = parentName && paramName ? pascalCase(`${parentName} ${paramName}`) : void 0;
|
|
1907
|
+
const schema = param["schema"] ? parseSchema({
|
|
1908
|
+
schema: param["schema"],
|
|
1909
|
+
name: schemaName
|
|
1910
|
+
}, options) : ast.factory.createSchema({ type: options.unknownType });
|
|
1911
|
+
const style = param["style"];
|
|
1912
|
+
const explode = param["explode"];
|
|
1913
|
+
return ast.factory.createParameter({
|
|
1914
|
+
name: paramName,
|
|
1736
1915
|
in: param["in"],
|
|
1737
1916
|
schema: {
|
|
1738
1917
|
...schema,
|
|
1739
1918
|
description: param["description"] ?? schema.description
|
|
1740
1919
|
},
|
|
1741
|
-
required
|
|
1920
|
+
required,
|
|
1921
|
+
...style !== void 0 ? { style } : {},
|
|
1922
|
+
...explode !== void 0 ? { explode } : {}
|
|
1742
1923
|
});
|
|
1743
1924
|
}
|
|
1744
1925
|
/**
|
|
@@ -1754,73 +1935,97 @@ function createSchemaParser(ctx) {
|
|
|
1754
1935
|
};
|
|
1755
1936
|
}
|
|
1756
1937
|
/**
|
|
1757
|
-
* Reads the inline response object (not a `$ref`) and returns its description plus its `content` map.
|
|
1758
|
-
*/
|
|
1759
|
-
function getResponseMeta(responseObj) {
|
|
1760
|
-
if (typeof responseObj !== "object" || responseObj === null || Array.isArray(responseObj)) return {};
|
|
1761
|
-
const inline = responseObj;
|
|
1762
|
-
return {
|
|
1763
|
-
description: inline.description,
|
|
1764
|
-
content: inline.content
|
|
1765
|
-
};
|
|
1766
|
-
}
|
|
1767
|
-
/**
|
|
1768
1938
|
* Collects property names whose schema has a truthy boolean flag (`readOnly` or `writeOnly`).
|
|
1769
1939
|
* `$ref` entries are skipped since their flags live on the dereferenced target.
|
|
1770
1940
|
*/
|
|
1771
1941
|
function collectPropertyKeysByFlag(schema, flag) {
|
|
1772
|
-
if (!schema?.properties) return
|
|
1942
|
+
if (!schema?.properties) return null;
|
|
1773
1943
|
const keys = [];
|
|
1774
1944
|
for (const key in schema.properties) {
|
|
1775
1945
|
const prop = schema.properties[key];
|
|
1776
1946
|
if (prop && !isReference(prop) && prop[flag]) keys.push(key);
|
|
1777
1947
|
}
|
|
1778
|
-
return keys.length ? keys :
|
|
1948
|
+
return keys.length ? keys : null;
|
|
1779
1949
|
}
|
|
1780
1950
|
/**
|
|
1781
1951
|
* Converts an OAS `Operation` into an `OperationNode`.
|
|
1782
1952
|
*/
|
|
1783
1953
|
function parseOperation(options, operation) {
|
|
1784
|
-
const
|
|
1954
|
+
const operationId = getOperationId(operation);
|
|
1955
|
+
const operationName = operationId ? pascalCase(operationId) : void 0;
|
|
1956
|
+
const parameters = getParameters(document, operation).map((param) => parseParameter(options, param, operationName));
|
|
1785
1957
|
const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(document, operation);
|
|
1786
1958
|
const requestBodyMeta = getRequestBodyMeta(operation);
|
|
1959
|
+
const requestBodyName = operationName ? `${operationName}Request` : void 0;
|
|
1787
1960
|
const content = allContentTypes.flatMap((ct) => {
|
|
1788
1961
|
const schema = getRequestSchema(document, operation, { contentType: ct });
|
|
1789
1962
|
if (!schema) return [];
|
|
1790
|
-
return [{
|
|
1963
|
+
return [ast.factory.createContent({
|
|
1791
1964
|
contentType: ct,
|
|
1792
|
-
schema: ast.
|
|
1965
|
+
schema: ast.optionality(parseSchema({
|
|
1966
|
+
schema,
|
|
1967
|
+
name: requestBodyName
|
|
1968
|
+
}, options), requestBodyMeta.required),
|
|
1793
1969
|
keysToOmit: collectPropertyKeysByFlag(schema, "readOnly")
|
|
1794
|
-
}];
|
|
1970
|
+
})];
|
|
1795
1971
|
});
|
|
1796
1972
|
const requestBody = content.length > 0 || requestBodyMeta.description ? {
|
|
1797
1973
|
description: requestBodyMeta.description,
|
|
1798
1974
|
required: requestBodyMeta.required || void 0,
|
|
1799
1975
|
content: content.length > 0 ? content : void 0
|
|
1800
1976
|
} : void 0;
|
|
1801
|
-
const responses =
|
|
1802
|
-
const responseObj =
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1977
|
+
const responses = getResponseStatusCodes(operation).map((statusCode) => {
|
|
1978
|
+
const responseObj = getResponseByStatusCode({
|
|
1979
|
+
document,
|
|
1980
|
+
operation,
|
|
1981
|
+
statusCode
|
|
1982
|
+
});
|
|
1983
|
+
const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
|
|
1984
|
+
const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
|
|
1985
|
+
const parseEntrySchema = (contentType) => {
|
|
1986
|
+
const raw = getResponseSchema(document, operation, statusCode, { contentType });
|
|
1987
|
+
return {
|
|
1988
|
+
schema: raw && Object.keys(raw).length > 0 ? parseSchema({
|
|
1989
|
+
schema: raw,
|
|
1990
|
+
name: responseName
|
|
1991
|
+
}, options) : ast.factory.createSchema({ type: options.emptySchemaType }),
|
|
1992
|
+
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
|
|
1993
|
+
};
|
|
1994
|
+
};
|
|
1995
|
+
const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(document, operation, statusCode)).map((contentType) => ast.factory.createContent({
|
|
1996
|
+
contentType,
|
|
1997
|
+
...parseEntrySchema(contentType)
|
|
1998
|
+
}));
|
|
1999
|
+
if (content.length === 0) content.push(ast.factory.createContent({
|
|
2000
|
+
contentType: getRequestContentType({
|
|
2001
|
+
document,
|
|
2002
|
+
operation
|
|
2003
|
+
}) || "application/json",
|
|
2004
|
+
...parseEntrySchema(ctx.contentType)
|
|
2005
|
+
}));
|
|
2006
|
+
return ast.factory.createResponse({
|
|
1808
2007
|
statusCode,
|
|
1809
2008
|
description,
|
|
1810
|
-
|
|
1811
|
-
mediaType,
|
|
1812
|
-
keysToOmit: collectPropertyKeysByFlag(responseSchema, "writeOnly")
|
|
2009
|
+
content
|
|
1813
2010
|
});
|
|
1814
2011
|
});
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
|
|
2012
|
+
const pathItem = document.paths?.[operation.path];
|
|
2013
|
+
const pathItemDoc = pathItem && !isReference(pathItem) ? pathItem : void 0;
|
|
2014
|
+
const pickDoc = (key) => {
|
|
2015
|
+
const own = operation.schema[key];
|
|
2016
|
+
if (typeof own === "string") return own;
|
|
2017
|
+
const fallback = pathItemDoc?.[key];
|
|
2018
|
+
return typeof fallback === "string" ? fallback : void 0;
|
|
2019
|
+
};
|
|
2020
|
+
return ast.factory.createOperation({
|
|
2021
|
+
operationId,
|
|
2022
|
+
protocol: "http",
|
|
1818
2023
|
method: operation.method.toUpperCase(),
|
|
1819
|
-
path:
|
|
1820
|
-
tags: operation.
|
|
1821
|
-
summary:
|
|
1822
|
-
description:
|
|
1823
|
-
deprecated: operation.
|
|
2024
|
+
path: operation.path,
|
|
2025
|
+
tags: Array.isArray(operation.schema.tags) ? operation.schema.tags.map(String) : [],
|
|
2026
|
+
summary: pickDoc("summary") || void 0,
|
|
2027
|
+
description: pickDoc("description") || void 0,
|
|
2028
|
+
deprecated: operation.schema.deprecated || void 0,
|
|
1824
2029
|
parameters,
|
|
1825
2030
|
requestBody,
|
|
1826
2031
|
responses
|
|
@@ -1832,59 +2037,122 @@ function createSchemaParser(ctx) {
|
|
|
1832
2037
|
parseParameter
|
|
1833
2038
|
};
|
|
1834
2039
|
}
|
|
2040
|
+
//#endregion
|
|
2041
|
+
//#region src/promoteEnums.ts
|
|
1835
2042
|
/**
|
|
1836
|
-
*
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
1839
|
-
* that downstream plugins (`plugin-ts`, `plugin-zod`, etc.) consume for code generation. No code is generated here —
|
|
1840
|
-
* the tree is a pure data structure representing all schemas and operations.
|
|
1841
|
-
*
|
|
1842
|
-
* Returns the AST root and a `nameMapping` for resolving schema references.
|
|
1843
|
-
*
|
|
1844
|
-
* @example
|
|
1845
|
-
* ```ts
|
|
1846
|
-
* import { parseOas } from '@kubb/adapter-oas'
|
|
1847
|
-
*
|
|
1848
|
-
* const document = await parseFromConfig(config)
|
|
1849
|
-
* const { root, nameMapping } = parseOas(document, { dateType: 'date', contentType: 'application/json' })
|
|
1850
|
-
* ```
|
|
2043
|
+
* Collects inline enums to lift to the top level, keyed by the name the parser derived for them
|
|
2044
|
+
* (e.g. `PetStatusEnum`). An enum already defined as a top-level component is left as-is, and a
|
|
2045
|
+
* name that recurs maps to the first definition so each name yields one shared type.
|
|
1851
2046
|
*/
|
|
1852
|
-
function
|
|
1853
|
-
const
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2047
|
+
function collectInlineEnums(roots, topLevelNames) {
|
|
2048
|
+
const promoted = /* @__PURE__ */ new Map();
|
|
2049
|
+
for (const root of roots) {
|
|
2050
|
+
const isSchemaRoot = root.kind === "Schema";
|
|
2051
|
+
for (const node of ast.collect(root, { schema: (schemaNode) => schemaNode })) {
|
|
2052
|
+
if (node.type !== "enum" || !node.name) continue;
|
|
2053
|
+
if (isSchemaRoot && node === root) continue;
|
|
2054
|
+
if (topLevelNames.has(node.name)) continue;
|
|
2055
|
+
if (!promoted.has(node.name)) promoted.set(node.name, {
|
|
2056
|
+
...node,
|
|
2057
|
+
optional: void 0,
|
|
2058
|
+
nullish: void 0
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
return promoted;
|
|
2063
|
+
}
|
|
2064
|
+
/**
|
|
2065
|
+
* Replaces every promoted inline enum in `node` with a `ref` to its lifted definition, keeping the
|
|
2066
|
+
* occurrence's usage-slot and documentation fields.
|
|
2067
|
+
*/
|
|
2068
|
+
function refPromotedEnums(node, promoted) {
|
|
2069
|
+
if (promoted.size === 0) return node;
|
|
2070
|
+
return ast.transform(node, { schema(schemaNode) {
|
|
2071
|
+
if (schemaNode.type !== "enum" || !schemaNode.name || !promoted.has(schemaNode.name)) return void 0;
|
|
2072
|
+
return ast.factory.createSchema({
|
|
2073
|
+
type: "ref",
|
|
2074
|
+
name: schemaNode.name,
|
|
2075
|
+
ref: `${SCHEMA_REF_PREFIX}${schemaNode.name}`,
|
|
2076
|
+
optional: schemaNode.optional,
|
|
2077
|
+
nullish: schemaNode.nullish,
|
|
2078
|
+
readOnly: schemaNode.readOnly,
|
|
2079
|
+
writeOnly: schemaNode.writeOnly,
|
|
2080
|
+
deprecated: schemaNode.deprecated,
|
|
2081
|
+
description: schemaNode.description,
|
|
2082
|
+
default: schemaNode.default,
|
|
2083
|
+
examples: schemaNode.examples
|
|
2084
|
+
});
|
|
2085
|
+
} });
|
|
2086
|
+
}
|
|
2087
|
+
//#endregion
|
|
2088
|
+
//#region src/schemaDiagnostics.ts
|
|
2089
|
+
/**
|
|
2090
|
+
* Reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`, `KUBB_DEPRECATED`) for one
|
|
2091
|
+
* top-level schema. Walks the node the parser produced, threading the RFC 6901
|
|
2092
|
+
* pointer as it descends so a nested field reports against its full path
|
|
2093
|
+
* (`#/components/schemas/Pet/properties/owner/properties/name`). Refs are not followed, so the
|
|
2094
|
+
* resolved schema is reported under its own walk. Reports land in the active build run, are a
|
|
2095
|
+
* no-op outside one, and repeats are deduped by the build.
|
|
2096
|
+
*/
|
|
2097
|
+
function reportSchemaDiagnostics({ node, name }) {
|
|
2098
|
+
visit(node, `#/components/schemas/${escapePointerToken(name)}`);
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
|
|
2102
|
+
* property name with those characters maps to a distinct pointer instead of colliding in the dedupe.
|
|
2103
|
+
*/
|
|
2104
|
+
function escapePointerToken(token) {
|
|
2105
|
+
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2106
|
+
}
|
|
2107
|
+
function visit(node, pointer) {
|
|
2108
|
+
if (node.deprecated) Diagnostics.report({
|
|
2109
|
+
code: Diagnostics.code.deprecated,
|
|
2110
|
+
severity: "info",
|
|
2111
|
+
message: "This schema is marked as deprecated.",
|
|
2112
|
+
location: {
|
|
2113
|
+
kind: "schema",
|
|
2114
|
+
pointer
|
|
2115
|
+
}
|
|
1862
2116
|
});
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2117
|
+
if (typeof node.format === "string" && !isHandledFormat(node.format)) Diagnostics.report({
|
|
2118
|
+
code: Diagnostics.code.unsupportedFormat,
|
|
2119
|
+
severity: "warning",
|
|
2120
|
+
message: `Kubb does not map the format "${node.format}" to a specific type, so it falls back to the base type.`,
|
|
2121
|
+
help: `Use a format Kubb supports, or handle "${node.format}" with a custom parser or plugin.`,
|
|
2122
|
+
location: {
|
|
2123
|
+
kind: "schema",
|
|
2124
|
+
pointer
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
if (node.type === "object") {
|
|
2128
|
+
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`);
|
|
2129
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`);
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
if (node.type === "array") {
|
|
2133
|
+
for (const item of node.items ?? []) visit(item, `${pointer}/items`);
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
if (node.type === "tuple") {
|
|
2137
|
+
for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`);
|
|
1876
2141
|
}
|
|
1877
2142
|
//#endregion
|
|
1878
2143
|
//#region src/adapter.ts
|
|
1879
2144
|
/**
|
|
1880
|
-
*
|
|
2145
|
+
* The `name` of `@kubb/adapter-oas`, used to identify this adapter in a Kubb config.
|
|
1881
2146
|
*/
|
|
1882
2147
|
const adapterOasName = "oas";
|
|
1883
2148
|
/**
|
|
1884
|
-
*
|
|
2149
|
+
* Default Kubb adapter for OpenAPI 2.0, 3.0, and 3.1 specifications. Reads the
|
|
2150
|
+
* spec from `input` (a file path, URL, inline content, or parsed object), validates
|
|
2151
|
+
* it, resolves the base URL, and converts every schema and operation into the
|
|
2152
|
+
* universal AST that every downstream plugin consumes.
|
|
1885
2153
|
*
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
2154
|
+
* Configure once on `defineConfig`. The adapter's choices (date representation,
|
|
2155
|
+
* integer width, server URL) apply to every plugin in the build.
|
|
1888
2156
|
*
|
|
1889
2157
|
* @example
|
|
1890
2158
|
* ```ts
|
|
@@ -1893,99 +2161,163 @@ const adapterOasName = "oas";
|
|
|
1893
2161
|
* import { pluginTs } from '@kubb/plugin-ts'
|
|
1894
2162
|
*
|
|
1895
2163
|
* export default defineConfig({
|
|
1896
|
-
*
|
|
1897
|
-
*
|
|
2164
|
+
* input: './petStore.yaml',
|
|
2165
|
+
* output: { path: './src/gen' },
|
|
2166
|
+
* adapter: adapterOas({
|
|
2167
|
+
* server: { index: 0 },
|
|
2168
|
+
* discriminator: 'propagate',
|
|
2169
|
+
* dateType: 'date',
|
|
2170
|
+
* }),
|
|
1898
2171
|
* plugins: [pluginTs()],
|
|
1899
2172
|
* })
|
|
1900
2173
|
* ```
|
|
1901
2174
|
*/
|
|
1902
2175
|
const adapterOas = createAdapter((options) => {
|
|
1903
|
-
const { validate = true, contentType,
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2176
|
+
const { validate = true, contentType, server, discriminator = "preserve", enums = "inline", dateType = DEFAULT_PARSER_OPTIONS.dateType, integerType = DEFAULT_PARSER_OPTIONS.integerType, unknownType = DEFAULT_PARSER_OPTIONS.unknownType, enumSuffix = DEFAULT_PARSER_OPTIONS.enumSuffix, emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType } = options;
|
|
2177
|
+
const parserOptions = {
|
|
2178
|
+
...DEFAULT_PARSER_OPTIONS,
|
|
2179
|
+
dateType,
|
|
2180
|
+
integerType,
|
|
2181
|
+
unknownType,
|
|
2182
|
+
emptySchemaType,
|
|
2183
|
+
enumSuffix
|
|
2184
|
+
};
|
|
2185
|
+
let parsedDocument = null;
|
|
2186
|
+
const documentCache = /* @__PURE__ */ new WeakMap();
|
|
2187
|
+
const schemasCache = /* @__PURE__ */ new WeakMap();
|
|
2188
|
+
const schemaParserCache = /* @__PURE__ */ new WeakMap();
|
|
2189
|
+
function ensureDocument(source) {
|
|
2190
|
+
const cached = documentCache.get(source);
|
|
2191
|
+
if (cached) return cached;
|
|
2192
|
+
const promise = (async () => {
|
|
2193
|
+
const fresh = await parseFromConfig(source);
|
|
2194
|
+
if (validate) await validateDocument(fresh);
|
|
2195
|
+
parsedDocument = fresh;
|
|
2196
|
+
return fresh;
|
|
2197
|
+
})();
|
|
2198
|
+
documentCache.set(source, promise);
|
|
2199
|
+
return promise;
|
|
2200
|
+
}
|
|
2201
|
+
function ensureSchemas(document) {
|
|
2202
|
+
const cached = schemasCache.get(document);
|
|
2203
|
+
if (cached) return cached;
|
|
2204
|
+
const result = getSchemas(document, { contentType });
|
|
2205
|
+
schemasCache.set(document, result);
|
|
2206
|
+
return result;
|
|
2207
|
+
}
|
|
2208
|
+
function ensureSchemaParser({ document, renames }) {
|
|
2209
|
+
const cached = schemaParserCache.get(document);
|
|
2210
|
+
if (cached) return cached;
|
|
2211
|
+
const parser = createSchemaParser({
|
|
2212
|
+
document,
|
|
2213
|
+
contentType,
|
|
2214
|
+
renames
|
|
2215
|
+
});
|
|
2216
|
+
schemaParserCache.set(document, parser);
|
|
2217
|
+
return parser;
|
|
2218
|
+
}
|
|
2219
|
+
function parseInput({ document, schemas, parser }) {
|
|
2220
|
+
const { parseSchema, parseOperation } = parser;
|
|
2221
|
+
const parsedByName = /* @__PURE__ */ new Map();
|
|
2222
|
+
const refAliasMap = /* @__PURE__ */ new Map();
|
|
2223
|
+
const enumNames = [];
|
|
2224
|
+
const discriminatorParentNodes = [];
|
|
2225
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
2226
|
+
const node = parseSchema({
|
|
2227
|
+
schema,
|
|
2228
|
+
name
|
|
2229
|
+
}, parserOptions);
|
|
2230
|
+
parsedByName.set(name, node);
|
|
2231
|
+
reportSchemaDiagnostics({
|
|
2232
|
+
node,
|
|
2233
|
+
name
|
|
2234
|
+
});
|
|
2235
|
+
if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
|
|
2236
|
+
if (narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
|
|
2237
|
+
if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
2238
|
+
}
|
|
2239
|
+
const circularNames = [...findCircularSchemas([...parsedByName.values()])];
|
|
2240
|
+
const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
|
|
2241
|
+
const operationNodes = [];
|
|
2242
|
+
for (const operation of getOperations(document)) {
|
|
2243
|
+
const operationNode = parseOperation(parserOptions, operation);
|
|
2244
|
+
if (operationNode) operationNodes.push(operationNode);
|
|
2245
|
+
}
|
|
2246
|
+
let promotedEnums = null;
|
|
2247
|
+
if (enums === "root") {
|
|
2248
|
+
promotedEnums = collectInlineEnums([...parsedByName.values(), ...operationNodes], new Set(Object.keys(schemas)));
|
|
2249
|
+
for (const name of promotedEnums.keys()) enumNames.push(name);
|
|
2250
|
+
}
|
|
2251
|
+
const schemaNodes = promotedEnums ? [...promotedEnums.values()] : [];
|
|
2252
|
+
for (const name of Object.keys(schemas)) {
|
|
2253
|
+
const alias = refAliasMap.get(name);
|
|
2254
|
+
let node;
|
|
2255
|
+
if (alias?.name && parsedByName.has(alias.name)) node = {
|
|
2256
|
+
...parsedByName.get(alias.name),
|
|
2257
|
+
name
|
|
2258
|
+
};
|
|
2259
|
+
else {
|
|
2260
|
+
const parsed = parsedByName.get(name);
|
|
2261
|
+
const child = discriminatorChildMap?.get(name);
|
|
2262
|
+
node = child ? patchDiscriminatorNode(parsed, child) : parsed;
|
|
2263
|
+
}
|
|
2264
|
+
schemaNodes.push(promotedEnums ? refPromotedEnums(node, promotedEnums) : node);
|
|
2265
|
+
}
|
|
2266
|
+
const operations = promotedEnums ? operationNodes.map((node) => refPromotedEnums(node, promotedEnums)) : operationNodes;
|
|
2267
|
+
return ast.factory.createInput({
|
|
2268
|
+
schemas: schemaNodes,
|
|
2269
|
+
operations,
|
|
2270
|
+
meta: {
|
|
2271
|
+
title: document.info?.title,
|
|
2272
|
+
description: document.info?.description,
|
|
2273
|
+
version: document.info?.version,
|
|
2274
|
+
baseURL: resolveBaseUrl({
|
|
2275
|
+
document,
|
|
2276
|
+
server
|
|
2277
|
+
}),
|
|
2278
|
+
circularNames,
|
|
2279
|
+
enumNames
|
|
2280
|
+
}
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
1907
2283
|
return {
|
|
1908
2284
|
name: "oas",
|
|
1909
2285
|
get options() {
|
|
1910
2286
|
return {
|
|
1911
2287
|
validate,
|
|
1912
2288
|
contentType,
|
|
1913
|
-
|
|
1914
|
-
serverVariables,
|
|
2289
|
+
server,
|
|
1915
2290
|
discriminator,
|
|
2291
|
+
enums,
|
|
1916
2292
|
dateType,
|
|
1917
2293
|
integerType,
|
|
1918
2294
|
unknownType,
|
|
1919
2295
|
emptySchemaType,
|
|
1920
|
-
enumSuffix
|
|
1921
|
-
nameMapping
|
|
2296
|
+
enumSuffix
|
|
1922
2297
|
};
|
|
1923
2298
|
},
|
|
1924
2299
|
get document() {
|
|
1925
2300
|
return parsedDocument;
|
|
1926
2301
|
},
|
|
1927
|
-
get inputNode() {
|
|
1928
|
-
return inputNode;
|
|
1929
|
-
},
|
|
1930
2302
|
async validate(input, options) {
|
|
2303
|
+
await assertInputExists(input);
|
|
1931
2304
|
await validateDocument(await parseDocument(input), options);
|
|
1932
2305
|
},
|
|
1933
|
-
getImports(node, resolve) {
|
|
1934
|
-
return ast.collectImports({
|
|
1935
|
-
node,
|
|
1936
|
-
nameMapping,
|
|
1937
|
-
resolve: (schemaName) => {
|
|
1938
|
-
const result = resolve(schemaName);
|
|
1939
|
-
if (!result) return;
|
|
1940
|
-
return ast.createImport({
|
|
1941
|
-
name: [result.name],
|
|
1942
|
-
path: result.path
|
|
1943
|
-
});
|
|
1944
|
-
}
|
|
1945
|
-
});
|
|
1946
|
-
},
|
|
1947
2306
|
async parse(source) {
|
|
1948
|
-
const document = await
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
emptySchemaType,
|
|
1958
|
-
enumSuffix
|
|
1959
|
-
});
|
|
1960
|
-
const node = discriminator === "inherit" ? applyDiscriminatorInheritance(parsedRoot) : parsedRoot;
|
|
1961
|
-
nameMapping = parsedNameMapping;
|
|
1962
|
-
parsedDocument = document;
|
|
1963
|
-
inputNode = ast.createInput({
|
|
1964
|
-
...node,
|
|
1965
|
-
meta: {
|
|
1966
|
-
title: document.info?.title,
|
|
1967
|
-
description: document.info?.description,
|
|
1968
|
-
version: document.info?.version,
|
|
1969
|
-
baseURL
|
|
1970
|
-
}
|
|
2307
|
+
const document = await ensureDocument(source);
|
|
2308
|
+
const { schemas, renames } = ensureSchemas(document);
|
|
2309
|
+
return parseInput({
|
|
2310
|
+
document,
|
|
2311
|
+
schemas,
|
|
2312
|
+
parser: ensureSchemaParser({
|
|
2313
|
+
document,
|
|
2314
|
+
renames
|
|
2315
|
+
})
|
|
1971
2316
|
});
|
|
1972
|
-
return inputNode;
|
|
1973
2317
|
}
|
|
1974
2318
|
};
|
|
1975
2319
|
});
|
|
1976
2320
|
//#endregion
|
|
1977
|
-
|
|
1978
|
-
/**
|
|
1979
|
-
* Maps uppercase HTTP method names to lowercase for backwards compatibility.
|
|
1980
|
-
*
|
|
1981
|
-
* @example
|
|
1982
|
-
* ```ts
|
|
1983
|
-
* HttpMethods['GET'] // 'get'
|
|
1984
|
-
* HttpMethods['POST'] // 'post'
|
|
1985
|
-
* ```
|
|
1986
|
-
*/
|
|
1987
|
-
const HttpMethods = Object.fromEntries(Object.entries(ast.httpMethods).map(([lower, upper]) => [upper, lower]));
|
|
1988
|
-
//#endregion
|
|
1989
|
-
export { HttpMethods, adapterOas, adapterOasName, mergeDocuments };
|
|
2321
|
+
export { adapterOas, adapterOasName };
|
|
1990
2322
|
|
|
1991
2323
|
//# sourceMappingURL=index.js.map
|