@kubb/adapter-oas 5.0.0-beta.99 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/index.cjs +1217 -934
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +1209 -926
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "./rolldown-runtime-C0LytTxp.js";
|
|
2
|
-
import { ast,
|
|
2
|
+
import { ast, findCircularSchemasFromGraph, narrowSchema, resolveRefName } from "@kubb/ast";
|
|
3
3
|
import { Diagnostics, createAdapter } from "@kubb/core";
|
|
4
|
-
import path from "node:path";
|
|
5
4
|
import { access, readFile } from "node:fs/promises";
|
|
6
|
-
import
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parse } from "yaml";
|
|
7
7
|
import { upgrade } from "@scalar/openapi-upgrader";
|
|
8
8
|
import { bundle } from "api-ref-bundler";
|
|
9
|
-
import {
|
|
9
|
+
import { childName, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
|
|
10
10
|
//#region src/constants.ts
|
|
11
11
|
/**
|
|
12
12
|
* Default parser options applied when no explicit options are provided.
|
|
@@ -14,8 +14,8 @@ import { parse } from "yaml";
|
|
|
14
14
|
const DEFAULT_PARSER_OPTIONS = {
|
|
15
15
|
dateType: "string",
|
|
16
16
|
integerType: "bigint",
|
|
17
|
-
unknownType: "
|
|
18
|
-
emptySchemaType: "
|
|
17
|
+
unknownType: "unknown",
|
|
18
|
+
emptySchemaType: "unknown",
|
|
19
19
|
enumSuffix: "enum"
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
@@ -60,17 +60,32 @@ const structuralKeys = /* @__PURE__ */ new Set([
|
|
|
60
60
|
]);
|
|
61
61
|
/**
|
|
62
62
|
* Formats `convertFormat` maps to a dedicated type without going through `formatMap`:
|
|
63
|
-
* `int64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
63
|
+
* `int64`, `uint64` and the date/time family. Keep this in sync with the `convertFormat`
|
|
64
64
|
* special-cases in `parser.ts`. `isHandledFormat` reads it so the
|
|
65
65
|
* `KUBB_UNSUPPORTED_FORMAT` diagnostic and the parser agree on what is handled.
|
|
66
66
|
*/
|
|
67
67
|
const specialCasedFormats = /* @__PURE__ */ new Set([
|
|
68
68
|
"int64",
|
|
69
|
+
"uint64",
|
|
69
70
|
"date-time",
|
|
70
71
|
"date",
|
|
71
72
|
"time"
|
|
72
73
|
]);
|
|
73
74
|
/**
|
|
75
|
+
* Formats that describe a number, whether they resolve through `formatMap` or through the
|
|
76
|
+
* `convertFormat` special cases. On a `type: 'string'` schema these do not make the value a
|
|
77
|
+
* number: gRPC-gateway and other ProtoJSON producers send 64-bit integers as JSON strings.
|
|
78
|
+
*
|
|
79
|
+
* @see https://protobuf.dev/programming-guides/json/#int64-strings
|
|
80
|
+
*/
|
|
81
|
+
const numericFormats = /* @__PURE__ */ new Set([
|
|
82
|
+
"int32",
|
|
83
|
+
"int64",
|
|
84
|
+
"uint64",
|
|
85
|
+
"float",
|
|
86
|
+
"double"
|
|
87
|
+
]);
|
|
88
|
+
/**
|
|
74
89
|
* Static map from OAS `format` strings to Kubb `SchemaType` values.
|
|
75
90
|
*
|
|
76
91
|
* Only formats whose AST type differs from the OAS `type` field appear here.
|
|
@@ -104,7 +119,7 @@ const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
|
|
|
104
119
|
*/
|
|
105
120
|
const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
|
|
106
121
|
//#endregion
|
|
107
|
-
//#region src/discriminator.ts
|
|
122
|
+
//#region src/emit/discriminator/propagate.ts
|
|
108
123
|
/**
|
|
109
124
|
* Maps each child schema name to its discriminator patch data by scanning the given
|
|
110
125
|
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
@@ -180,42 +195,6 @@ function patchDiscriminatorNode(node, entry) {
|
|
|
180
195
|
properties: newProperties
|
|
181
196
|
};
|
|
182
197
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Creates a single-property object schema used as a discriminator literal.
|
|
185
|
-
*
|
|
186
|
-
* @example
|
|
187
|
-
* ```ts
|
|
188
|
-
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
189
|
-
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
190
|
-
* ```
|
|
191
|
-
*/
|
|
192
|
-
function createDiscriminantNode({ propertyName, value }) {
|
|
193
|
-
return ast.factory.createSchema({
|
|
194
|
-
type: "object",
|
|
195
|
-
primitive: "object",
|
|
196
|
-
properties: [ast.factory.createProperty({
|
|
197
|
-
name: propertyName,
|
|
198
|
-
schema: ast.factory.createSchema({
|
|
199
|
-
type: "enum",
|
|
200
|
-
primitive: "string",
|
|
201
|
-
enumValues: [value]
|
|
202
|
-
}),
|
|
203
|
-
required: true
|
|
204
|
-
})]
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
209
|
-
*
|
|
210
|
-
* @example
|
|
211
|
-
* ```ts
|
|
212
|
-
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
213
|
-
* ```
|
|
214
|
-
*/
|
|
215
|
-
function findDiscriminator(mapping, ref) {
|
|
216
|
-
if (!mapping || !ref) return null;
|
|
217
|
-
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
218
|
-
}
|
|
219
198
|
//#endregion
|
|
220
199
|
//#region ../../internals/utils/src/casing.ts
|
|
221
200
|
/**
|
|
@@ -244,6 +223,20 @@ function pascalCase(text, { prefix = "", suffix = "" } = {}) {
|
|
|
244
223
|
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, true);
|
|
245
224
|
}
|
|
246
225
|
//#endregion
|
|
226
|
+
//#region ../../internals/utils/src/errors.ts
|
|
227
|
+
/**
|
|
228
|
+
* Extracts a human-readable message from any thrown value.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```ts
|
|
232
|
+
* getErrorMessage(new Error('oops')) // 'oops'
|
|
233
|
+
* getErrorMessage('plain string') // 'plain string'
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
function getErrorMessage(value) {
|
|
237
|
+
return value instanceof Error ? value.message : String(value);
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
247
240
|
//#region ../../internals/utils/src/runtime.ts
|
|
248
241
|
/**
|
|
249
242
|
* Detects the JavaScript runtime executing the current process and exposes its name and version.
|
|
@@ -344,21 +337,104 @@ async function read(path) {
|
|
|
344
337
|
return readFile(path, { encoding: "utf8" });
|
|
345
338
|
}
|
|
346
339
|
//#endregion
|
|
347
|
-
//#region src/
|
|
340
|
+
//#region src/load/source.ts
|
|
348
341
|
const urlRegExp = /^https?:\/+/i;
|
|
342
|
+
/**
|
|
343
|
+
* Node reports every connection failure as `TypeError: fetch failed` and keeps the useful part
|
|
344
|
+
* (`connect ECONNREFUSED 127.0.0.1:8000`) on `cause`, one level deeper again when a host resolves
|
|
345
|
+
* to several addresses and the attempts collect into an `AggregateError`.
|
|
346
|
+
*/
|
|
347
|
+
function describeFetchFailure(error) {
|
|
348
|
+
if (error instanceof AggregateError && error.errors.length > 0) return describeFetchFailure(error.errors[0]);
|
|
349
|
+
if (error instanceof Error && error.cause instanceof Error) return describeFetchFailure(error.cause) || error.message;
|
|
350
|
+
return getErrorMessage(error);
|
|
351
|
+
}
|
|
352
|
+
function helpForStatus(status) {
|
|
353
|
+
if (status === 401 || status === 403) return "The server refused the request. Kubb sends no credentials, so serve the document without authentication or download it and set `input` to the local file.";
|
|
354
|
+
if (status === 404) return "Check the URL. Open it in a browser or with `curl` to confirm it serves the OpenAPI document.";
|
|
355
|
+
if (status >= 500) return "The server failed while serving the document. Check that it is healthy, then run Kubb again.";
|
|
356
|
+
return "Open the URL in a browser or with `curl` to see what the server returns, then point `input` at a URL that serves the OpenAPI document.";
|
|
357
|
+
}
|
|
358
|
+
async function fetchSource(url) {
|
|
359
|
+
try {
|
|
360
|
+
return await fetch(url);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
throw new Diagnostics.Error({
|
|
363
|
+
code: Diagnostics.code.inputUnreachable,
|
|
364
|
+
severity: "error",
|
|
365
|
+
message: `Cannot reach ${url.href}: ${describeFetchFailure(error)}`,
|
|
366
|
+
help: "Check that the host is running and reachable from this machine. For a local server, start it and confirm the port matches the one in `input`.",
|
|
367
|
+
location: { kind: "config" },
|
|
368
|
+
cause: error instanceof Error ? error : void 0
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
349
372
|
async function readSource(sourcePath) {
|
|
350
373
|
if (urlRegExp.test(sourcePath)) {
|
|
351
374
|
const url = new URL(sourcePath);
|
|
352
|
-
const response = await
|
|
353
|
-
if (!response.ok)
|
|
375
|
+
const response = await fetchSource(url);
|
|
376
|
+
if (!response.ok) {
|
|
377
|
+
const status = response.statusText ? `${response.status} ${response.statusText}` : String(response.status);
|
|
378
|
+
throw new Diagnostics.Error({
|
|
379
|
+
code: Diagnostics.code.inputRequestFailed,
|
|
380
|
+
severity: "error",
|
|
381
|
+
message: `The server at ${url.href} answered with HTTP ${status} instead of the OpenAPI document.`,
|
|
382
|
+
help: helpForStatus(response.status),
|
|
383
|
+
location: { kind: "config" }
|
|
384
|
+
});
|
|
385
|
+
}
|
|
354
386
|
return response.text();
|
|
355
387
|
}
|
|
356
388
|
return read(sourcePath);
|
|
357
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Reads and parses one source file or URL referenced during bundling: YAML/JSON is parsed into an
|
|
392
|
+
* object, Markdown is returned as-is (bundled inline rather than dereferenced).
|
|
393
|
+
*
|
|
394
|
+
* JSON is valid YAML, so `yaml`'s `parse` handles both, but its general-purpose parser (comments,
|
|
395
|
+
* anchors, block scalars, multi-document streams) does much more work than `JSON.parse` needs to.
|
|
396
|
+
* `JSON.parse` runs first and fails fast on the first non-JSON character, so a real YAML document
|
|
397
|
+
* falls through to `parse` at negligible cost.
|
|
398
|
+
*/
|
|
358
399
|
async function resolveSource(sourcePath) {
|
|
359
400
|
const data = await readSource(sourcePath);
|
|
360
401
|
if (sourcePath.toLowerCase().endsWith(".md")) return data;
|
|
361
|
-
|
|
402
|
+
try {
|
|
403
|
+
return JSON.parse(data);
|
|
404
|
+
} catch {
|
|
405
|
+
return parse(data);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
410
|
+
* URLs are skipped: a remote input reports `KUBB_INPUT_REQUEST_FAILED` or `KUBB_INPUT_UNREACHABLE`
|
|
411
|
+
* from the request itself. A malformed but readable file is left for `parseDocument` to surface
|
|
412
|
+
* its parse error instead.
|
|
413
|
+
*/
|
|
414
|
+
async function assertInputExists(input) {
|
|
415
|
+
if (URL.canParse(input)) return;
|
|
416
|
+
if (!await exists(input)) throw new Diagnostics.Error({
|
|
417
|
+
code: Diagnostics.code.inputNotFound,
|
|
418
|
+
severity: "error",
|
|
419
|
+
message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
|
|
420
|
+
help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
|
|
421
|
+
location: { kind: "config" }
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/load/normalize.ts
|
|
426
|
+
/**
|
|
427
|
+
* True when `node` contains a `$ref` pointing outside the current document (a relative path,
|
|
428
|
+
* absolute path, or URL). An internal `#/...` fragment does not count.
|
|
429
|
+
*
|
|
430
|
+
* `Object.values` reads array elements and object property values alike, so the same recursion
|
|
431
|
+
* walks both without a separate array branch.
|
|
432
|
+
*/
|
|
433
|
+
function hasExternalRef(node) {
|
|
434
|
+
if (!node || typeof node !== "object") return false;
|
|
435
|
+
const ref = node.$ref;
|
|
436
|
+
if (typeof ref === "string" && !ref.startsWith("#")) return true;
|
|
437
|
+
return Object.values(node).some(hasExternalRef);
|
|
362
438
|
}
|
|
363
439
|
/**
|
|
364
440
|
* Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
|
|
@@ -368,6 +444,11 @@ async function resolveSource(sourcePath) {
|
|
|
368
444
|
* can then emit a named type with an import instead of inlining the shape. Sources are read with
|
|
369
445
|
* the Bun-aware `read` util for local YAML and JSON files, and with `fetch` for HTTP(S) URLs.
|
|
370
446
|
*
|
|
447
|
+
* A document with no `$ref` outside itself has nothing to bundle, so it skips `api-ref-bundler`
|
|
448
|
+
* and returns as parsed. `bundle` only rewrites external refs into internal ones; on an
|
|
449
|
+
* all-internal document it is a no-op that still walks the whole tree to confirm that, which
|
|
450
|
+
* costs real time on a large spec.
|
|
451
|
+
*
|
|
371
452
|
* @example Local file
|
|
372
453
|
* `const document = await bundleDocument('./openapi.yaml')`
|
|
373
454
|
*
|
|
@@ -384,7 +465,8 @@ async function bundleDocument(pathOrUrl) {
|
|
|
384
465
|
cache.set(key, result);
|
|
385
466
|
return result;
|
|
386
467
|
};
|
|
387
|
-
await resolver(pathOrUrl);
|
|
468
|
+
const root = await resolver(pathOrUrl);
|
|
469
|
+
if (typeof root === "object" && root !== null && !hasExternalRef(root)) return root;
|
|
388
470
|
return await bundle(pathOrUrl, resolver);
|
|
389
471
|
}
|
|
390
472
|
/**
|
|
@@ -425,17 +507,20 @@ async function parseFromConfig(source) {
|
|
|
425
507
|
return parseDocument(resolved);
|
|
426
508
|
}
|
|
427
509
|
/**
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
510
|
+
* Asserts the parsed input is an OpenAPI or Swagger document.
|
|
511
|
+
*
|
|
512
|
+
* {@link validateDocument} keeps spec violations non-fatal so imperfect but usable documents still
|
|
513
|
+
* generate. That leniency also swallowed input that is not a document at all, which then produced
|
|
514
|
+
* an empty build with a success exit code. A missing version field is the one failure that cannot
|
|
515
|
+
* be a usable document, so it is fatal regardless of the `validate` option.
|
|
516
|
+
*/
|
|
517
|
+
function assertDocument(document) {
|
|
518
|
+
if (document && ("openapi" in document || "swagger" in document)) return;
|
|
519
|
+
throw new Diagnostics.Error({
|
|
520
|
+
code: Diagnostics.code.invalidDocument,
|
|
436
521
|
severity: "error",
|
|
437
|
-
message:
|
|
438
|
-
help: "
|
|
522
|
+
message: "The resolved `input` is not an OpenAPI or Swagger document: it declares no `openapi` or `swagger` version.",
|
|
523
|
+
help: "Point `input` at a document that declares `openapi` or `swagger`. If you pass an object, pass the spec itself rather than a wrapper such as `{ path }` or `{ data }`.",
|
|
439
524
|
location: { kind: "config" }
|
|
440
525
|
});
|
|
441
526
|
}
|
|
@@ -448,6 +533,7 @@ async function assertInputExists(input) {
|
|
|
448
533
|
* ```
|
|
449
534
|
*/
|
|
450
535
|
async function validateDocument(document, { throwOnError = false } = {}) {
|
|
536
|
+
const { compileErrors, validate } = await import("@readme/openapi-parser");
|
|
451
537
|
try {
|
|
452
538
|
const result = await validate(structuredClone(document), { validate: { errors: { colorize: true } } });
|
|
453
539
|
if (!result.valid) throw new Error(compileErrors(result));
|
|
@@ -516,97 +602,232 @@ const jsonMimeFragments = [
|
|
|
516
602
|
function isJsonMimeType(mimeType) {
|
|
517
603
|
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
|
|
518
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Picks a media-type entry from a `content` map: the first JSON-like media type, falling back to
|
|
607
|
+
* the first declared one. Returns `false` when `content` has no entries.
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* ```ts
|
|
611
|
+
* pickContentEntry({ 'application/xml': xmlEntry, 'application/json': jsonEntry })
|
|
612
|
+
* // ['application/json', jsonEntry]
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
615
|
+
function pickContentEntry(content) {
|
|
616
|
+
const mediaTypes = Object.keys(content);
|
|
617
|
+
const available = mediaTypes.find(isJsonMimeType) ?? mediaTypes[0];
|
|
618
|
+
return available ? [available, content[available]] : false;
|
|
619
|
+
}
|
|
519
620
|
//#endregion
|
|
520
|
-
//#region src/
|
|
521
|
-
const _refCache = /* @__PURE__ */ new WeakMap();
|
|
621
|
+
//#region src/model/components.ts
|
|
522
622
|
/**
|
|
523
|
-
*
|
|
623
|
+
* Extracts the inline schema from a media-type `content` map.
|
|
524
624
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
* build there is no sink to collect it, so it throws instead.
|
|
625
|
+
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
626
|
+
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
528
627
|
*
|
|
529
628
|
* @example
|
|
530
629
|
* ```ts
|
|
531
|
-
*
|
|
630
|
+
* extractSchemaFromContent(operation.content, 'application/json')
|
|
631
|
+
* // SchemaObject | null
|
|
532
632
|
* ```
|
|
533
633
|
*/
|
|
534
|
-
function
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
634
|
+
function extractSchemaFromContent(content, preferredContentType) {
|
|
635
|
+
if (!content) return null;
|
|
636
|
+
const firstContentType = Object.keys(content)[0] ?? "application/json";
|
|
637
|
+
const schema = content[preferredContentType ?? firstContentType]?.schema;
|
|
638
|
+
if (isReference(schema)) return null;
|
|
639
|
+
return schema ?? null;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
643
|
+
*/
|
|
644
|
+
function* collectRefs(schema) {
|
|
645
|
+
if (Array.isArray(schema)) {
|
|
646
|
+
for (const item of schema) yield* collectRefs(item);
|
|
647
|
+
return;
|
|
544
648
|
}
|
|
545
|
-
if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
pointer: origRef,
|
|
556
|
-
ref: origRef
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
|
|
560
|
-
return null;
|
|
649
|
+
if (schema && typeof schema === "object") for (const key in schema) {
|
|
650
|
+
const value = schema[key];
|
|
651
|
+
if (!(key === "$ref" && typeof value === "string")) {
|
|
652
|
+
yield* collectRefs(value);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (value.startsWith("#/components/schemas/")) {
|
|
656
|
+
const name = value.slice(21);
|
|
657
|
+
if (name) yield name;
|
|
658
|
+
}
|
|
561
659
|
}
|
|
562
|
-
docCache.set($ref, current);
|
|
563
|
-
return current;
|
|
564
660
|
}
|
|
565
661
|
/**
|
|
566
|
-
*
|
|
662
|
+
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
567
663
|
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
664
|
+
* Referenced schemas appear before the schemas that depend on them, so code generators
|
|
665
|
+
* can emit types in the correct order. Cycles are silently skipped.
|
|
570
666
|
*
|
|
571
667
|
* @example
|
|
572
668
|
* ```ts
|
|
573
|
-
*
|
|
574
|
-
* //
|
|
669
|
+
* const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
|
|
670
|
+
* // Pet appears before Order when Order.$ref points at Pet
|
|
575
671
|
* ```
|
|
576
672
|
*/
|
|
577
|
-
function
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
673
|
+
function sortSchemas(schemas) {
|
|
674
|
+
const deps = /* @__PURE__ */ new Map();
|
|
675
|
+
for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
|
|
676
|
+
const sorted = [];
|
|
677
|
+
const visited = /* @__PURE__ */ new Set();
|
|
678
|
+
function visit(name, stack) {
|
|
679
|
+
if (visited.has(name) || stack.has(name)) return;
|
|
680
|
+
stack.add(name);
|
|
681
|
+
for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
|
|
682
|
+
stack.delete(name);
|
|
683
|
+
visited.add(name);
|
|
684
|
+
sorted.push(name);
|
|
685
|
+
}
|
|
686
|
+
for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
|
|
687
|
+
const result = {};
|
|
688
|
+
for (const name of sorted) result[name] = schemas[name];
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
const semanticSuffixes = {
|
|
692
|
+
schemas: "Schema",
|
|
693
|
+
responses: "Response",
|
|
694
|
+
requestBodies: "Request"
|
|
695
|
+
};
|
|
696
|
+
/**
|
|
697
|
+
* Picks the collision suffix for one name-colliding schema: none when the name is unique,
|
|
698
|
+
* a semantic suffix (`Schema`, `Response`, `Request`) when the collision spans sources, otherwise
|
|
699
|
+
* a numeric suffix (`2`, `3`, …) for same-source collisions.
|
|
700
|
+
*/
|
|
701
|
+
function collisionSuffix({ isSingle, hasMultipleSources, source, index }) {
|
|
702
|
+
if (isSingle) return "";
|
|
703
|
+
if (hasMultipleSources) return semanticSuffixes[source];
|
|
704
|
+
if (index === 0) return "";
|
|
705
|
+
return String(index + 1);
|
|
584
706
|
}
|
|
585
707
|
/**
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
708
|
+
* Collects component schemas from one or more sources and resolves name collisions.
|
|
709
|
+
*
|
|
710
|
+
* Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
|
|
711
|
+
* topologically sorted by `$ref` dependency so generators emit types in the correct order.
|
|
712
|
+
*
|
|
713
|
+
* When two or more schemas normalize to the same PascalCase name:
|
|
714
|
+
* - Same source → numeric suffix (`2`, `3`, …).
|
|
715
|
+
* - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
|
|
589
716
|
*
|
|
590
717
|
* @example
|
|
591
718
|
* ```ts
|
|
592
|
-
*
|
|
719
|
+
* const { schemas, renames } = getSchemas(document, { contentType: 'application/json' }, refs)
|
|
593
720
|
* ```
|
|
594
721
|
*/
|
|
595
|
-
function
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
722
|
+
function getSchemas(document, { contentType }, refs) {
|
|
723
|
+
const components = document.components;
|
|
724
|
+
function resolveSchemaRef(schema) {
|
|
725
|
+
if (!isReference(schema)) return schema;
|
|
726
|
+
const resolved = refs.resolve(schema.$ref);
|
|
727
|
+
return resolved && !isReference(resolved) ? resolved : schema;
|
|
728
|
+
}
|
|
729
|
+
const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
|
|
730
|
+
schema: resolveSchemaRef(schema),
|
|
731
|
+
source: "schemas",
|
|
732
|
+
originalName: name
|
|
733
|
+
})), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
734
|
+
const schema = extractSchemaFromContent(item.content, contentType);
|
|
735
|
+
return schema ? [{
|
|
736
|
+
schema: resolveSchemaRef(schema),
|
|
737
|
+
source,
|
|
738
|
+
originalName: name
|
|
739
|
+
}] : [];
|
|
740
|
+
}))];
|
|
741
|
+
const normalizedNames = /* @__PURE__ */ new Map();
|
|
742
|
+
for (const item of candidates) {
|
|
743
|
+
const key = pascalCase(item.originalName);
|
|
744
|
+
const bucket = normalizedNames.get(key) ?? [];
|
|
745
|
+
bucket.push(item);
|
|
746
|
+
normalizedNames.set(key, bucket);
|
|
747
|
+
}
|
|
748
|
+
const schemas = {};
|
|
749
|
+
const renames = /* @__PURE__ */ new Map();
|
|
750
|
+
for (const [, items] of normalizedNames) {
|
|
751
|
+
const isSingle = items.length === 1;
|
|
752
|
+
const hasMultipleSources = !isSingle && new Set(items.map((item) => item.source)).size > 1;
|
|
753
|
+
items.forEach((item, index) => {
|
|
754
|
+
const suffix = collisionSuffix({
|
|
755
|
+
isSingle,
|
|
756
|
+
hasMultipleSources,
|
|
757
|
+
source: item.source,
|
|
758
|
+
index
|
|
759
|
+
});
|
|
760
|
+
const uniqueName = item.originalName + suffix;
|
|
761
|
+
schemas[uniqueName] = item.schema;
|
|
762
|
+
if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
schemas: sortSchemas(schemas),
|
|
767
|
+
renames
|
|
768
|
+
};
|
|
601
769
|
}
|
|
602
770
|
//#endregion
|
|
603
|
-
//#region src/
|
|
771
|
+
//#region src/model/server.ts
|
|
604
772
|
/**
|
|
605
|
-
*
|
|
606
|
-
*
|
|
773
|
+
* Reads the server URL from the document's `servers` array at `server.index`,
|
|
774
|
+
* interpolating any `server.variables` into the URL template.
|
|
775
|
+
*
|
|
776
|
+
* Returns `null` when `server.index` is omitted or out of range.
|
|
777
|
+
*
|
|
778
|
+
* @example Resolve the first server
|
|
779
|
+
* `resolveBaseUrl({ document, server: { index: 0 } })`
|
|
780
|
+
*
|
|
781
|
+
* @example Override a path variable
|
|
782
|
+
* `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
|
|
607
783
|
*/
|
|
608
|
-
function
|
|
609
|
-
|
|
784
|
+
function resolveBaseUrl({ document, server }) {
|
|
785
|
+
const index = server?.index;
|
|
786
|
+
const entry = index !== void 0 ? document.servers?.at(index) : void 0;
|
|
787
|
+
return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
791
|
+
* Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
|
|
792
|
+
* Throws if an override value is not in the variable's `enum` list.
|
|
793
|
+
*
|
|
794
|
+
* @example
|
|
795
|
+
* ```ts
|
|
796
|
+
* resolveServerUrl(
|
|
797
|
+
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
798
|
+
* { env: 'prod' },
|
|
799
|
+
* )
|
|
800
|
+
* // 'https://prod.api.example.com'
|
|
801
|
+
* ```
|
|
802
|
+
*/
|
|
803
|
+
function resolveServerUrl(server, overrides) {
|
|
804
|
+
if (!server.variables) return server.url;
|
|
805
|
+
let url = server.url;
|
|
806
|
+
for (const [key, variable] of Object.entries(server.variables)) {
|
|
807
|
+
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
808
|
+
if (value === void 0) continue;
|
|
809
|
+
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Diagnostics.Error({
|
|
810
|
+
code: Diagnostics.code.invalidServerVariable,
|
|
811
|
+
severity: "error",
|
|
812
|
+
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
813
|
+
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
814
|
+
location: {
|
|
815
|
+
kind: "document",
|
|
816
|
+
pointer: "#/servers"
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
url = url.replaceAll(`{${key}}`, value);
|
|
820
|
+
}
|
|
821
|
+
return url;
|
|
822
|
+
}
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/operation.ts
|
|
825
|
+
/**
|
|
826
|
+
* Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
|
|
827
|
+
* with no leading or trailing dash.
|
|
828
|
+
*/
|
|
829
|
+
function slugify(value) {
|
|
830
|
+
return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
610
831
|
}
|
|
611
832
|
/**
|
|
612
833
|
* Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
|
|
@@ -625,26 +846,28 @@ function getResponseStatusCodes({ schema }) {
|
|
|
625
846
|
return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
|
|
626
847
|
}
|
|
627
848
|
/**
|
|
628
|
-
* Returns the response object for a status code, resolving a `$ref`
|
|
849
|
+
* Returns the response object for a status code, resolving a `$ref` through `refs`. `false` when absent.
|
|
629
850
|
*/
|
|
630
|
-
function getResponseByStatusCode({
|
|
851
|
+
function getResponseByStatusCode({ operation, refs, statusCode }) {
|
|
631
852
|
const responses = operation.schema.responses;
|
|
632
853
|
if (!responses || isReference(responses)) return false;
|
|
633
|
-
return
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
854
|
+
return refs.deref(responses[statusCode]) ?? false;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Resolves the operation's request body, dereferencing a `$ref` through `refs`. Returns `null`
|
|
858
|
+
* when the operation has no request body or it cannot be resolved.
|
|
859
|
+
*/
|
|
860
|
+
function getRequestBody({ operation, refs }) {
|
|
861
|
+
return refs.deref(operation.schema.requestBody);
|
|
638
862
|
}
|
|
639
863
|
/**
|
|
640
|
-
* Resolves the request body (
|
|
864
|
+
* Resolves the request body (a `$ref` through `refs`) and returns its content map, or
|
|
641
865
|
* `undefined` when the operation has no request body.
|
|
642
866
|
*/
|
|
643
|
-
function getRequestBodyContent({
|
|
644
|
-
return
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
key: "requestBody"
|
|
867
|
+
function getRequestBodyContent({ operation, refs }) {
|
|
868
|
+
return getRequestBody({
|
|
869
|
+
operation,
|
|
870
|
+
refs
|
|
648
871
|
})?.content;
|
|
649
872
|
}
|
|
650
873
|
/**
|
|
@@ -652,25 +875,23 @@ function getRequestBodyContent({ document, operation }) {
|
|
|
652
875
|
* Otherwise picks the first JSON-like media type, then the first declared one, as a
|
|
653
876
|
* `[mediaType, object]` tuple.
|
|
654
877
|
*/
|
|
655
|
-
function getRequestContent({
|
|
878
|
+
function getRequestContent({ operation, refs, mediaType }) {
|
|
656
879
|
const content = getRequestBodyContent({
|
|
657
|
-
|
|
658
|
-
|
|
880
|
+
operation,
|
|
881
|
+
refs
|
|
659
882
|
});
|
|
660
883
|
if (!content) return false;
|
|
661
884
|
if (mediaType) return mediaType in content ? content[mediaType] : false;
|
|
662
|
-
|
|
663
|
-
const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
|
|
664
|
-
return available ? [available, content[available]] : false;
|
|
885
|
+
return pickContentEntry(content);
|
|
665
886
|
}
|
|
666
887
|
/**
|
|
667
888
|
* Returns the primary request content type. Prefers a JSON-like media type (the last one wins
|
|
668
889
|
* when several are declared), then the first declared one, defaulting to `'application/json'`.
|
|
669
890
|
*/
|
|
670
|
-
function getRequestContentType({
|
|
891
|
+
function getRequestContentType({ operation, refs }) {
|
|
671
892
|
const content = getRequestBodyContent({
|
|
672
|
-
|
|
673
|
-
|
|
893
|
+
operation,
|
|
894
|
+
refs
|
|
674
895
|
});
|
|
675
896
|
const mediaTypes = content ? Object.keys(content) : [];
|
|
676
897
|
let result = mediaTypes[0] ?? "application/json";
|
|
@@ -683,22 +904,18 @@ function getRequestContentType({ document, operation }) {
|
|
|
683
904
|
*
|
|
684
905
|
* @example
|
|
685
906
|
* ```ts
|
|
686
|
-
* for (const operation of getOperations(document)) {
|
|
907
|
+
* for (const operation of getOperations(document, refs)) {
|
|
687
908
|
* parseOperation(options, operation)
|
|
688
909
|
* }
|
|
689
910
|
* ```
|
|
690
911
|
*/
|
|
691
|
-
function getOperations(document) {
|
|
912
|
+
function getOperations(document, refs) {
|
|
692
913
|
const operations = [];
|
|
693
914
|
const paths = document.paths;
|
|
694
915
|
if (!paths) return operations;
|
|
695
916
|
for (const path of Object.keys(paths)) {
|
|
696
917
|
if (path.startsWith("x-")) continue;
|
|
697
|
-
const pathItem =
|
|
698
|
-
document,
|
|
699
|
-
container: paths,
|
|
700
|
-
key: path
|
|
701
|
-
});
|
|
918
|
+
const pathItem = refs.deref(paths[path]);
|
|
702
919
|
if (!pathItem) continue;
|
|
703
920
|
const item = pathItem;
|
|
704
921
|
for (const method of Object.keys(item)) {
|
|
@@ -708,75 +925,25 @@ function getOperations(document) {
|
|
|
708
925
|
operations.push({
|
|
709
926
|
path,
|
|
710
927
|
method,
|
|
711
|
-
schema
|
|
928
|
+
schema,
|
|
929
|
+
pathItem
|
|
712
930
|
});
|
|
713
931
|
}
|
|
714
932
|
}
|
|
715
933
|
return operations;
|
|
716
934
|
}
|
|
717
935
|
//#endregion
|
|
718
|
-
//#region src/
|
|
719
|
-
/**
|
|
720
|
-
* Reads the server URL from the document's `servers` array at `server.index`,
|
|
721
|
-
* interpolating any `server.variables` into the URL template.
|
|
722
|
-
*
|
|
723
|
-
* Returns `null` when `server.index` is omitted or out of range.
|
|
724
|
-
*
|
|
725
|
-
* @example Resolve the first server
|
|
726
|
-
* `resolveBaseUrl({ document, server: { index: 0 } })`
|
|
727
|
-
*
|
|
728
|
-
* @example Override a path variable
|
|
729
|
-
* `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
|
|
730
|
-
*/
|
|
731
|
-
function resolveBaseUrl({ document, server }) {
|
|
732
|
-
const index = server?.index;
|
|
733
|
-
const entry = index !== void 0 ? document.servers?.at(index) : void 0;
|
|
734
|
-
return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
738
|
-
* Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
|
|
739
|
-
* Throws if an override value is not in the variable's `enum` list.
|
|
740
|
-
*
|
|
741
|
-
* @example
|
|
742
|
-
* ```ts
|
|
743
|
-
* resolveServerUrl(
|
|
744
|
-
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
745
|
-
* { env: 'prod' },
|
|
746
|
-
* )
|
|
747
|
-
* // 'https://prod.api.example.com'
|
|
748
|
-
* ```
|
|
749
|
-
*/
|
|
750
|
-
function resolveServerUrl(server, overrides) {
|
|
751
|
-
if (!server.variables) return server.url;
|
|
752
|
-
let url = server.url;
|
|
753
|
-
for (const [key, variable] of Object.entries(server.variables)) {
|
|
754
|
-
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
755
|
-
if (value === void 0) continue;
|
|
756
|
-
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Diagnostics.Error({
|
|
757
|
-
code: Diagnostics.code.invalidServerVariable,
|
|
758
|
-
severity: "error",
|
|
759
|
-
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
760
|
-
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
761
|
-
location: {
|
|
762
|
-
kind: "document",
|
|
763
|
-
pointer: "#/servers"
|
|
764
|
-
}
|
|
765
|
-
});
|
|
766
|
-
url = url.replaceAll(`{${key}}`, value);
|
|
767
|
-
}
|
|
768
|
-
return url;
|
|
769
|
-
}
|
|
936
|
+
//#region src/emit/schemaShape.ts
|
|
770
937
|
/**
|
|
771
938
|
* Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
|
|
772
|
-
* Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
|
|
939
|
+
* Formats not in `formatMap` (e.g., `int64`, `uint64`, `date-time`) are handled separately by parser options.
|
|
773
940
|
*/
|
|
774
941
|
function getSchemaType(format) {
|
|
775
942
|
return formatMap[format] ?? null;
|
|
776
943
|
}
|
|
777
944
|
/**
|
|
778
945
|
* Whether the parser maps `format` to a dedicated type. True for any `formatMap` entry, plus the
|
|
779
|
-
* `specialCasedFormats` that `convertFormat` handles directly. False means the format falls back to
|
|
946
|
+
* `specialCasedFormats` that `convertFormat` handles directly (int64, uint64, date-time, date, time). False means the format falls back to
|
|
780
947
|
* the base type, which is what `KUBB_UNSUPPORTED_FORMAT` flags. Reading both sources keeps the
|
|
781
948
|
* diagnostic in step with the parser as `formatMap` grows.
|
|
782
949
|
*/
|
|
@@ -793,96 +960,57 @@ function getPrimitiveType(type) {
|
|
|
793
960
|
return "string";
|
|
794
961
|
}
|
|
795
962
|
/**
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
* Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
|
|
799
|
-
*
|
|
800
|
-
* @example
|
|
801
|
-
* ```ts
|
|
802
|
-
* getParameters(document, operation)
|
|
803
|
-
* // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
|
|
804
|
-
* ```
|
|
963
|
+
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
964
|
+
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
805
965
|
*/
|
|
806
|
-
function
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
966
|
+
function getDateType(options, format) {
|
|
967
|
+
if (!options.dateType) return null;
|
|
968
|
+
if (format === "date-time") {
|
|
969
|
+
if (options.dateType === "date") return {
|
|
970
|
+
type: "date",
|
|
971
|
+
representation: "date"
|
|
972
|
+
};
|
|
973
|
+
if (options.dateType === "stringOffset") return {
|
|
974
|
+
type: "datetime",
|
|
975
|
+
offset: true
|
|
976
|
+
};
|
|
977
|
+
if (options.dateType === "stringLocal") return {
|
|
978
|
+
type: "datetime",
|
|
979
|
+
local: true
|
|
980
|
+
};
|
|
981
|
+
return {
|
|
982
|
+
type: "datetime",
|
|
983
|
+
offset: false
|
|
984
|
+
};
|
|
824
985
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
for (const key in responses) derefInPlace({
|
|
834
|
-
document,
|
|
835
|
-
container: responses,
|
|
836
|
-
key
|
|
837
|
-
});
|
|
986
|
+
if (format === "date") return {
|
|
987
|
+
type: "date",
|
|
988
|
+
representation: options.dateType === "date" ? "date" : "string"
|
|
989
|
+
};
|
|
990
|
+
return {
|
|
991
|
+
type: "time",
|
|
992
|
+
representation: options.dateType === "date" ? "date" : "string"
|
|
993
|
+
};
|
|
838
994
|
}
|
|
839
995
|
/**
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
* @example
|
|
845
|
-
* ```ts
|
|
846
|
-
* getResponseSchema(document, operation, 200) // SchemaObject
|
|
847
|
-
* getResponseSchema(document, operation, '4XX') // {}
|
|
848
|
-
* ```
|
|
996
|
+
* Reads a schema's numeric `exclusiveMinimum`/`exclusiveMaximum` bounds (the OAS 3.1 numeric
|
|
997
|
+
* form). Either key is `undefined` when absent or, for the legacy OAS 3.0 boolean form, not a
|
|
998
|
+
* number.
|
|
849
999
|
*/
|
|
850
|
-
function
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
statusCode
|
|
856
|
-
}), options.contentType);
|
|
857
|
-
if (responseBody === false) return {};
|
|
858
|
-
const schema = responseBody.schema;
|
|
859
|
-
if (!schema) return {};
|
|
860
|
-
return dereferenceWithRef(document, schema);
|
|
1000
|
+
function getExclusiveBounds(schema) {
|
|
1001
|
+
return {
|
|
1002
|
+
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
1003
|
+
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
|
|
1004
|
+
};
|
|
861
1005
|
}
|
|
862
1006
|
/**
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
* ```ts
|
|
867
|
-
* getRequestSchema(document, operation) // SchemaObject | null
|
|
868
|
-
* ```
|
|
1007
|
+
* Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
|
|
1008
|
+
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
1009
|
+
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
869
1010
|
*/
|
|
870
|
-
function
|
|
871
|
-
if (
|
|
872
|
-
|
|
873
|
-
document,
|
|
874
|
-
operation,
|
|
875
|
-
mediaType: options.contentType
|
|
876
|
-
});
|
|
877
|
-
if (requestBody === false) return null;
|
|
878
|
-
const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
|
|
879
|
-
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
880
|
-
if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
|
|
881
|
-
type: "string",
|
|
882
|
-
contentMediaType: "application/octet-stream"
|
|
883
|
-
};
|
|
884
|
-
if (!schema) return null;
|
|
885
|
-
return dereferenceWithRef(document, schema);
|
|
1011
|
+
function extractExamples(schema) {
|
|
1012
|
+
if (Array.isArray(schema.examples)) return schema.examples;
|
|
1013
|
+
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
886
1014
|
}
|
|
887
1015
|
/**
|
|
888
1016
|
* Returns `true` when `fragment` carries any JSON Schema keyword that makes it
|
|
@@ -891,8 +1019,7 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
891
1019
|
* A fragment with a structural keyword can't be safely merged into a parent schema.
|
|
892
1020
|
*/
|
|
893
1021
|
function hasStructuralKeywords(fragment) {
|
|
894
|
-
|
|
895
|
-
return false;
|
|
1022
|
+
return Object.keys(fragment).some((key) => structuralKeys.has(key));
|
|
896
1023
|
}
|
|
897
1024
|
/**
|
|
898
1025
|
* Flattens a keyword-only `allOf` into its parent schema.
|
|
@@ -923,192 +1050,15 @@ function flattenSchema(schema) {
|
|
|
923
1050
|
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
|
|
924
1051
|
return merged;
|
|
925
1052
|
}
|
|
1053
|
+
//#endregion
|
|
1054
|
+
//#region src/emit/createNode.ts
|
|
926
1055
|
/**
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
931
|
-
*
|
|
932
|
-
* @example
|
|
933
|
-
* ```ts
|
|
934
|
-
* extractSchemaFromContent(operation.content, 'application/json')
|
|
935
|
-
* // SchemaObject | null
|
|
936
|
-
* ```
|
|
937
|
-
*/
|
|
938
|
-
function extractSchemaFromContent(content, preferredContentType) {
|
|
939
|
-
if (!content) return null;
|
|
940
|
-
const firstContentType = Object.keys(content)[0] ?? "application/json";
|
|
941
|
-
const schema = content[preferredContentType ?? firstContentType]?.schema;
|
|
942
|
-
if (schema && "$ref" in schema) return null;
|
|
943
|
-
return schema ?? null;
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
947
|
-
*/
|
|
948
|
-
function* collectRefs(schema) {
|
|
949
|
-
if (Array.isArray(schema)) {
|
|
950
|
-
for (const item of schema) yield* collectRefs(item);
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
if (schema && typeof schema === "object") for (const key in schema) {
|
|
954
|
-
const value = schema[key];
|
|
955
|
-
if (!(key === "$ref" && typeof value === "string")) {
|
|
956
|
-
yield* collectRefs(value);
|
|
957
|
-
continue;
|
|
958
|
-
}
|
|
959
|
-
if (value.startsWith("#/components/schemas/")) {
|
|
960
|
-
const name = value.slice(21);
|
|
961
|
-
if (name) yield name;
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
967
|
-
*
|
|
968
|
-
* Referenced schemas appear before the schemas that depend on them, so code generators
|
|
969
|
-
* can emit types in the correct order. Cycles are silently skipped.
|
|
970
|
-
*
|
|
971
|
-
* @example
|
|
972
|
-
* ```ts
|
|
973
|
-
* const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
|
|
974
|
-
* // Pet appears before Order when Order.$ref points at Pet
|
|
975
|
-
* ```
|
|
976
|
-
*/
|
|
977
|
-
function sortSchemas(schemas) {
|
|
978
|
-
const deps = /* @__PURE__ */ new Map();
|
|
979
|
-
for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
|
|
980
|
-
const sorted = [];
|
|
981
|
-
const visited = /* @__PURE__ */ new Set();
|
|
982
|
-
function visit(name, stack) {
|
|
983
|
-
if (visited.has(name) || stack.has(name)) return;
|
|
984
|
-
stack.add(name);
|
|
985
|
-
for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
|
|
986
|
-
stack.delete(name);
|
|
987
|
-
visited.add(name);
|
|
988
|
-
sorted.push(name);
|
|
989
|
-
}
|
|
990
|
-
for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
|
|
991
|
-
const result = {};
|
|
992
|
-
for (const name of sorted) result[name] = schemas[name];
|
|
993
|
-
return result;
|
|
994
|
-
}
|
|
995
|
-
const semanticSuffixes = {
|
|
996
|
-
schemas: "Schema",
|
|
997
|
-
responses: "Response",
|
|
998
|
-
requestBodies: "Request"
|
|
999
|
-
};
|
|
1000
|
-
function resolveSchemaRef(document, schema) {
|
|
1001
|
-
if (!isReference(schema)) return schema;
|
|
1002
|
-
const resolved = resolveRef(document, schema.$ref);
|
|
1003
|
-
return resolved && !isReference(resolved) ? resolved : schema;
|
|
1004
|
-
}
|
|
1005
|
-
/**
|
|
1006
|
-
* Collects component schemas from one or more sources and resolves name collisions.
|
|
1007
|
-
*
|
|
1008
|
-
* Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
|
|
1009
|
-
* topologically sorted by `$ref` dependency so generators emit types in the correct order.
|
|
1010
|
-
*
|
|
1011
|
-
* When two or more schemas normalize to the same PascalCase name:
|
|
1012
|
-
* - Same source → numeric suffix (`2`, `3`, …).
|
|
1013
|
-
* - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
|
|
1014
|
-
*
|
|
1015
|
-
* @example
|
|
1016
|
-
* ```ts
|
|
1017
|
-
* const { schemas, renames } = getSchemas(document, { contentType: 'application/json' })
|
|
1018
|
-
* ```
|
|
1019
|
-
*/
|
|
1020
|
-
function getSchemas(document, { contentType }) {
|
|
1021
|
-
const components = document.components;
|
|
1022
|
-
const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
|
|
1023
|
-
schema: resolveSchemaRef(document, schema),
|
|
1024
|
-
source: "schemas",
|
|
1025
|
-
originalName: name
|
|
1026
|
-
})), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
1027
|
-
const schema = extractSchemaFromContent(item.content, contentType);
|
|
1028
|
-
return schema ? [{
|
|
1029
|
-
schema: resolveSchemaRef(document, schema),
|
|
1030
|
-
source,
|
|
1031
|
-
originalName: name
|
|
1032
|
-
}] : [];
|
|
1033
|
-
}))];
|
|
1034
|
-
const normalizedNames = /* @__PURE__ */ new Map();
|
|
1035
|
-
for (const item of candidates) {
|
|
1036
|
-
const key = pascalCase(item.originalName);
|
|
1037
|
-
const bucket = normalizedNames.get(key) ?? [];
|
|
1038
|
-
bucket.push(item);
|
|
1039
|
-
normalizedNames.set(key, bucket);
|
|
1040
|
-
}
|
|
1041
|
-
const schemas = {};
|
|
1042
|
-
const renames = /* @__PURE__ */ new Map();
|
|
1043
|
-
for (const [, items] of normalizedNames) {
|
|
1044
|
-
const isSingle = items.length === 1;
|
|
1045
|
-
let hasMultipleSources = false;
|
|
1046
|
-
if (!isSingle) {
|
|
1047
|
-
const firstSource = items[0].source;
|
|
1048
|
-
for (const item of items) if (item.source !== firstSource) {
|
|
1049
|
-
hasMultipleSources = true;
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
items.forEach((item, index) => {
|
|
1054
|
-
const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index === 0 ? "" : String(index + 1);
|
|
1055
|
-
const uniqueName = item.originalName + suffix;
|
|
1056
|
-
schemas[uniqueName] = item.schema;
|
|
1057
|
-
if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
return {
|
|
1061
|
-
schemas: sortSchemas(schemas),
|
|
1062
|
-
renames
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
1067
|
-
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
1068
|
-
*/
|
|
1069
|
-
function getDateType(options, format) {
|
|
1070
|
-
if (!options.dateType) return null;
|
|
1071
|
-
if (format === "date-time") {
|
|
1072
|
-
if (options.dateType === "date") return {
|
|
1073
|
-
type: "date",
|
|
1074
|
-
representation: "date"
|
|
1075
|
-
};
|
|
1076
|
-
if (options.dateType === "stringOffset") return {
|
|
1077
|
-
type: "datetime",
|
|
1078
|
-
offset: true
|
|
1079
|
-
};
|
|
1080
|
-
if (options.dateType === "stringLocal") return {
|
|
1081
|
-
type: "datetime",
|
|
1082
|
-
local: true
|
|
1083
|
-
};
|
|
1084
|
-
return {
|
|
1085
|
-
type: "datetime",
|
|
1086
|
-
offset: false
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
if (format === "date") return {
|
|
1090
|
-
type: "date",
|
|
1091
|
-
representation: options.dateType === "date" ? "date" : "string"
|
|
1092
|
-
};
|
|
1093
|
-
return {
|
|
1094
|
-
type: "time",
|
|
1095
|
-
representation: options.dateType === "date" ? "date" : "string"
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
/**
|
|
1099
|
-
* Collects the shared metadata fields passed to every `createSchema` call.
|
|
1056
|
+
* Builds a schema node from a converter's base context plus its type-specific fields. Every
|
|
1057
|
+
* converter needs the same metadata fields (`title`, `description`, `examples`, ...) alongside
|
|
1058
|
+
* whatever makes its node distinct; this folds both into one call.
|
|
1100
1059
|
*/
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
1104
|
-
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
1105
|
-
*/
|
|
1106
|
-
function extractExamples(schema) {
|
|
1107
|
-
if (Array.isArray(schema.examples)) return schema.examples;
|
|
1108
|
-
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
1109
|
-
}
|
|
1110
|
-
function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
1111
|
-
return {
|
|
1060
|
+
function createNode({ schema, name, nullable, defaultValue }, extras) {
|
|
1061
|
+
return ast.factory.createSchema({
|
|
1112
1062
|
name,
|
|
1113
1063
|
nullable,
|
|
1114
1064
|
title: schema.title,
|
|
@@ -1118,123 +1068,164 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
|
1118
1068
|
writeOnly: schema.writeOnly,
|
|
1119
1069
|
default: defaultValue,
|
|
1120
1070
|
examples: extractExamples(schema),
|
|
1121
|
-
format: schema.format
|
|
1122
|
-
|
|
1071
|
+
format: schema.format,
|
|
1072
|
+
...extras
|
|
1073
|
+
});
|
|
1123
1074
|
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/emit/discriminator/preserve.ts
|
|
1124
1077
|
/**
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1127
|
-
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
1128
|
-
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
1129
|
-
* available content types even for referenced bodies.
|
|
1078
|
+
* Creates a single-property object schema used as a discriminator literal.
|
|
1130
1079
|
*
|
|
1131
1080
|
* @example
|
|
1132
1081
|
* ```ts
|
|
1133
|
-
*
|
|
1134
|
-
* // ['
|
|
1082
|
+
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
1083
|
+
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
1135
1084
|
* ```
|
|
1136
1085
|
*/
|
|
1137
|
-
function
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1086
|
+
function createDiscriminantNode({ propertyName, value }) {
|
|
1087
|
+
return ast.factory.createSchema({
|
|
1088
|
+
type: "object",
|
|
1089
|
+
primitive: "object",
|
|
1090
|
+
properties: [ast.factory.createProperty({
|
|
1091
|
+
name: propertyName,
|
|
1092
|
+
schema: ast.factory.createSchema({
|
|
1093
|
+
type: "enum",
|
|
1094
|
+
primitive: "string",
|
|
1095
|
+
enumValues: [value]
|
|
1096
|
+
}),
|
|
1097
|
+
required: true
|
|
1098
|
+
})]
|
|
1099
|
+
});
|
|
1142
1100
|
}
|
|
1143
1101
|
/**
|
|
1144
|
-
* Returns
|
|
1145
|
-
*
|
|
1146
|
-
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
1147
|
-
* so the returned list reflects the available content types even for referenced responses.
|
|
1102
|
+
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
1148
1103
|
*
|
|
1149
1104
|
* @example
|
|
1150
1105
|
* ```ts
|
|
1151
|
-
*
|
|
1152
|
-
* // ['application/json', 'application/xml']
|
|
1106
|
+
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
1153
1107
|
* ```
|
|
1154
1108
|
*/
|
|
1155
|
-
function
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
document,
|
|
1159
|
-
operation,
|
|
1160
|
-
statusCode
|
|
1161
|
-
});
|
|
1162
|
-
if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
|
|
1163
|
-
const body = responseObj;
|
|
1164
|
-
return body.content ? Object.keys(body.content) : [];
|
|
1165
|
-
}
|
|
1166
|
-
//#endregion
|
|
1167
|
-
//#region src/converters.ts
|
|
1168
|
-
/**
|
|
1169
|
-
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
1170
|
-
*
|
|
1171
|
-
* This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
|
|
1172
|
-
* from the array to its items sub-schema, so they are valid for downstream processing.
|
|
1173
|
-
*
|
|
1174
|
-
* @note A defensive measure for non-compliant specs.
|
|
1175
|
-
*/
|
|
1176
|
-
function normalizeArrayEnum(schema) {
|
|
1177
|
-
const normalizedItems = {
|
|
1178
|
-
...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
|
|
1179
|
-
enum: schema.enum
|
|
1180
|
-
};
|
|
1181
|
-
const { enum: _enum, ...schemaWithoutEnum } = schema;
|
|
1182
|
-
return {
|
|
1183
|
-
...schemaWithoutEnum,
|
|
1184
|
-
items: normalizedItems
|
|
1185
|
-
};
|
|
1109
|
+
function findDiscriminator(mapping, ref) {
|
|
1110
|
+
if (!mapping || !ref) return null;
|
|
1111
|
+
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
1186
1112
|
}
|
|
1187
1113
|
/**
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1114
|
+
* Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
|
|
1115
|
+
* node with either the shared-properties slice carrying that value, or a synthetic discriminant
|
|
1116
|
+
* literal. The referenced child schema's own definition is left untouched — narrowing happens only
|
|
1117
|
+
* at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
|
|
1118
|
+
* which additionally patches the child schema's own definition in a post-pass).
|
|
1190
1119
|
*/
|
|
1191
|
-
function
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1120
|
+
function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, rawOptions, name, refs }) {
|
|
1121
|
+
function pickDiscriminatorPropertyNode(node, propertyName) {
|
|
1122
|
+
const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
|
|
1123
|
+
if (!discriminatorProperty) return null;
|
|
1124
|
+
return ast.factory.createSchema({
|
|
1125
|
+
type: "object",
|
|
1126
|
+
primitive: "object",
|
|
1127
|
+
properties: [discriminatorProperty]
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
function implicitDiscriminantValue(member) {
|
|
1131
|
+
if (!discriminator || discriminator.mapping || !isReference(member)) return null;
|
|
1132
|
+
const value = extractRefName(member.$ref);
|
|
1133
|
+
if (!value) return null;
|
|
1134
|
+
const variant = refs.resolve(member.$ref, { report: false });
|
|
1135
|
+
if (!variant) return null;
|
|
1136
|
+
const propertyName = discriminator.propertyName;
|
|
1137
|
+
const seen = /* @__PURE__ */ new Set([member.$ref]);
|
|
1138
|
+
function constrains(v) {
|
|
1139
|
+
const prop = v.properties?.[propertyName];
|
|
1140
|
+
const resolved = prop && isReference(prop) ? refs.resolve(prop.$ref, { report: false }) : prop;
|
|
1141
|
+
if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
|
|
1142
|
+
const composition = v.allOf ?? v.oneOf ?? v.anyOf;
|
|
1143
|
+
if (!composition) return false;
|
|
1144
|
+
return composition.some((m) => {
|
|
1145
|
+
if (!isReference(m)) return constrains(m);
|
|
1146
|
+
if (seen.has(m.$ref)) return false;
|
|
1147
|
+
seen.add(m.$ref);
|
|
1148
|
+
const r = refs.resolve(m.$ref, { report: false });
|
|
1149
|
+
return r ? constrains(r) : false;
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
return constrains(variant) ? null : value;
|
|
1153
|
+
}
|
|
1154
|
+
return unionMembers.map((s) => {
|
|
1155
|
+
const ref = isReference(s) ? s.$ref : void 0;
|
|
1156
|
+
const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
|
|
1157
|
+
const memberNode = parse({
|
|
1158
|
+
schema: s,
|
|
1159
|
+
name
|
|
1160
|
+
}, rawOptions);
|
|
1161
|
+
if (!discriminatorValue || !discriminator) return memberNode;
|
|
1162
|
+
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.applyMacros(sharedPropertiesNode, [macroDiscriminatorEnum({
|
|
1163
|
+
propertyName: discriminator.propertyName,
|
|
1164
|
+
values: [discriminatorValue]
|
|
1165
|
+
})], { depth: "shallow" }), discriminator.propertyName) : void 0;
|
|
1166
|
+
return ast.factory.createSchema({
|
|
1167
|
+
type: "intersection",
|
|
1168
|
+
members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
|
|
1169
|
+
propertyName: discriminator.propertyName,
|
|
1170
|
+
value: discriminatorValue
|
|
1171
|
+
})]
|
|
1172
|
+
});
|
|
1201
1173
|
});
|
|
1202
1174
|
}
|
|
1203
1175
|
/**
|
|
1204
|
-
*
|
|
1205
|
-
*
|
|
1176
|
+
* Filters the discriminated members out of an `allOf` list: an `allOf` member that `$ref`s a
|
|
1177
|
+
* discriminated union's parent, where this schema is itself one of that union's children, is
|
|
1178
|
+
* dropped from `members` and its discriminant value collected instead — the same synthetic
|
|
1179
|
+
* literal `narrowUnionMembers` produces, so the emitted node stays a plain intersection rather
|
|
1180
|
+
* than nesting the whole parent union one level deeper.
|
|
1206
1181
|
*/
|
|
1207
|
-
function
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1182
|
+
function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
1183
|
+
const discriminantValues = [];
|
|
1184
|
+
return {
|
|
1185
|
+
members: allOfMembers.filter((item) => {
|
|
1186
|
+
if (!isReference(item) || !name) return true;
|
|
1187
|
+
const deref = refs.resolve(item.$ref);
|
|
1188
|
+
if (!deref || !isDiscriminator(deref)) return true;
|
|
1189
|
+
const parentUnion = deref.oneOf ?? deref.anyOf;
|
|
1190
|
+
if (!parentUnion) return true;
|
|
1191
|
+
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1192
|
+
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1193
|
+
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1194
|
+
if (inOneOf || inMapping) {
|
|
1195
|
+
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
|
|
1196
|
+
if (discriminatorValue) discriminantValues.push({
|
|
1197
|
+
propertyName: deref.discriminator.propertyName,
|
|
1198
|
+
value: discriminatorValue
|
|
1199
|
+
});
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
return true;
|
|
1203
|
+
}),
|
|
1204
|
+
discriminantValues
|
|
1205
|
+
};
|
|
1219
1206
|
}
|
|
1207
|
+
//#endregion
|
|
1208
|
+
//#region src/emit/converters/composition.ts
|
|
1220
1209
|
/**
|
|
1221
1210
|
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
1222
1211
|
*
|
|
1223
1212
|
* The resolved schema is stored in `node.schema`. Usage-site sibling fields
|
|
1224
1213
|
* (description, readOnly, nullable, etc.) are stored directly on the ref node.
|
|
1225
1214
|
* Use `syncSchemaRef(node)` in printers to get a merged view of both.
|
|
1226
|
-
* Circular refs are detected in `
|
|
1215
|
+
* Circular refs are detected in `refs.resolveNode` and leave `schema` as `null`.
|
|
1227
1216
|
*/
|
|
1228
|
-
function convertRef({ schema, name, nullable, defaultValue, rawOptions, document,
|
|
1217
|
+
function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, parse, refs, renames }) {
|
|
1229
1218
|
const refPath = schema.$ref;
|
|
1230
|
-
const resolvedSchema = refPath ?
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1219
|
+
const resolvedSchema = refPath ? refs.resolveNode(refPath, parse, rawOptions) : null;
|
|
1220
|
+
const ctx = {
|
|
1221
|
+
schema,
|
|
1222
|
+
name,
|
|
1223
|
+
nullable,
|
|
1224
|
+
defaultValue
|
|
1225
|
+
};
|
|
1226
|
+
if (refPath && document.components && !refs.exists(refPath)) return createNode(ctx, { type: "unknown" });
|
|
1235
1227
|
const targetName = renames?.get(schema.$ref);
|
|
1236
|
-
return
|
|
1237
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1228
|
+
return createNode(ctx, {
|
|
1238
1229
|
type: "ref",
|
|
1239
1230
|
name: extractRefName(schema.$ref),
|
|
1240
1231
|
ref: schema.$ref,
|
|
@@ -1245,7 +1236,7 @@ function convertRef({ schema, name, nullable, defaultValue, rawOptions, document
|
|
|
1245
1236
|
/**
|
|
1246
1237
|
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
1247
1238
|
*/
|
|
1248
|
-
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
1239
|
+
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
|
|
1249
1240
|
if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
|
|
1250
1241
|
const [memberSchema] = schema.allOf;
|
|
1251
1242
|
const memberNode = parse({
|
|
@@ -1270,26 +1261,12 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1270
1261
|
format: schema.format ?? memberNode.format
|
|
1271
1262
|
});
|
|
1272
1263
|
}
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
if (!parentUnion) return true;
|
|
1280
|
-
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1281
|
-
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1282
|
-
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1283
|
-
if (inOneOf || inMapping) {
|
|
1284
|
-
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
|
|
1285
|
-
if (discriminatorValue) filteredDiscriminantValues.push({
|
|
1286
|
-
propertyName: deref.discriminator.propertyName,
|
|
1287
|
-
value: discriminatorValue
|
|
1288
|
-
});
|
|
1289
|
-
return false;
|
|
1290
|
-
}
|
|
1291
|
-
return true;
|
|
1292
|
-
}).map((s) => parse({
|
|
1264
|
+
const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
|
|
1265
|
+
allOfMembers: schema.allOf,
|
|
1266
|
+
name,
|
|
1267
|
+
refs
|
|
1268
|
+
});
|
|
1269
|
+
const allOfMembers = discriminatedAllOf.map((s) => parse({
|
|
1293
1270
|
schema: s,
|
|
1294
1271
|
name
|
|
1295
1272
|
}, rawOptions));
|
|
@@ -1300,7 +1277,7 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1300
1277
|
if (missingRequired.length) {
|
|
1301
1278
|
const resolvedMembers = schema.allOf.flatMap((item) => {
|
|
1302
1279
|
if (!isReference(item)) return [item];
|
|
1303
|
-
const deref =
|
|
1280
|
+
const deref = refs.resolve(item.$ref);
|
|
1304
1281
|
return deref && !isReference(deref) ? [deref] : [];
|
|
1305
1282
|
});
|
|
1306
1283
|
for (const key of missingRequired) for (const resolved of resolvedMembers) {
|
|
@@ -1323,61 +1300,33 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1323
1300
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
|
|
1324
1301
|
allOfMembers.push(parse({ schema: schemaWithoutAllOf }, rawOptions));
|
|
1325
1302
|
}
|
|
1326
|
-
for (const { propertyName, value } of
|
|
1303
|
+
for (const { propertyName, value } of discriminantValues) allOfMembers.push(createDiscriminantNode({
|
|
1327
1304
|
propertyName,
|
|
1328
1305
|
value
|
|
1329
1306
|
}));
|
|
1330
|
-
return
|
|
1307
|
+
return createNode({
|
|
1308
|
+
schema,
|
|
1309
|
+
name,
|
|
1310
|
+
nullable,
|
|
1311
|
+
defaultValue
|
|
1312
|
+
}, {
|
|
1331
1313
|
type: "intersection",
|
|
1332
|
-
members: [...mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))]
|
|
1333
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1314
|
+
members: [...mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))]
|
|
1334
1315
|
});
|
|
1335
1316
|
}
|
|
1336
1317
|
/**
|
|
1337
1318
|
* Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
|
|
1338
1319
|
*/
|
|
1339
|
-
function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
properties: [discriminatorProperty]
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
function resolveRefSilent($ref) {
|
|
1350
|
-
if (!$ref.startsWith("#")) return null;
|
|
1351
|
-
return decodeURIComponent($ref.substring(1)).split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document) ?? null;
|
|
1352
|
-
}
|
|
1353
|
-
function implicitDiscriminantValue(member) {
|
|
1354
|
-
if (!discriminator || discriminator.mapping || !isReference(member)) return null;
|
|
1355
|
-
const value = extractRefName(member.$ref);
|
|
1356
|
-
if (!value) return null;
|
|
1357
|
-
const variant = resolveRefSilent(member.$ref);
|
|
1358
|
-
if (!variant) return null;
|
|
1359
|
-
const propertyName = discriminator.propertyName;
|
|
1360
|
-
const seen = /* @__PURE__ */ new Set([member.$ref]);
|
|
1361
|
-
function constrains(v) {
|
|
1362
|
-
const prop = v.properties?.[propertyName];
|
|
1363
|
-
const resolved = prop && isReference(prop) ? resolveRefSilent(prop.$ref) : prop;
|
|
1364
|
-
if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
|
|
1365
|
-
const composition = v.allOf ?? v.oneOf ?? v.anyOf;
|
|
1366
|
-
if (!composition) return false;
|
|
1367
|
-
return composition.some((m) => {
|
|
1368
|
-
if (!isReference(m)) return constrains(m);
|
|
1369
|
-
if (seen.has(m.$ref)) return false;
|
|
1370
|
-
seen.add(m.$ref);
|
|
1371
|
-
const r = resolveRefSilent(m.$ref);
|
|
1372
|
-
return r ? constrains(r) : false;
|
|
1373
|
-
});
|
|
1374
|
-
}
|
|
1375
|
-
return constrains(variant) ? null : value;
|
|
1376
|
-
}
|
|
1320
|
+
function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
|
|
1321
|
+
const ctx = {
|
|
1322
|
+
schema,
|
|
1323
|
+
name,
|
|
1324
|
+
nullable,
|
|
1325
|
+
defaultValue
|
|
1326
|
+
};
|
|
1377
1327
|
const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
|
|
1378
1328
|
const strategy = schema.oneOf ? "one" : "any";
|
|
1379
|
-
const
|
|
1380
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1329
|
+
const unionExtras = {
|
|
1381
1330
|
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
|
|
1382
1331
|
strategy
|
|
1383
1332
|
};
|
|
@@ -1388,41 +1337,29 @@ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1388
1337
|
name
|
|
1389
1338
|
}, rawOptions) : void 0;
|
|
1390
1339
|
if (sharedPropertiesNode || discriminator) {
|
|
1391
|
-
const members =
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.applyMacros(sharedPropertiesNode, [macroDiscriminatorEnum({
|
|
1400
|
-
propertyName: discriminator.propertyName,
|
|
1401
|
-
values: [discriminatorValue]
|
|
1402
|
-
})], { depth: "shallow" }), discriminator.propertyName) : void 0;
|
|
1403
|
-
return ast.factory.createSchema({
|
|
1404
|
-
type: "intersection",
|
|
1405
|
-
members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
|
|
1406
|
-
propertyName: discriminator.propertyName,
|
|
1407
|
-
value: discriminatorValue
|
|
1408
|
-
})]
|
|
1409
|
-
});
|
|
1340
|
+
const members = narrowUnionMembers({
|
|
1341
|
+
unionMembers,
|
|
1342
|
+
discriminator,
|
|
1343
|
+
sharedPropertiesNode,
|
|
1344
|
+
parse,
|
|
1345
|
+
rawOptions,
|
|
1346
|
+
name,
|
|
1347
|
+
refs
|
|
1410
1348
|
});
|
|
1411
|
-
const unionNode =
|
|
1349
|
+
const unionNode = createNode(ctx, {
|
|
1412
1350
|
type: "union",
|
|
1413
|
-
...
|
|
1351
|
+
...unionExtras,
|
|
1414
1352
|
members
|
|
1415
1353
|
});
|
|
1416
1354
|
if (!sharedPropertiesNode) return unionNode;
|
|
1417
|
-
return
|
|
1355
|
+
return createNode(ctx, {
|
|
1418
1356
|
type: "intersection",
|
|
1419
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1420
1357
|
members: [unionNode, sharedPropertiesNode]
|
|
1421
1358
|
});
|
|
1422
1359
|
}
|
|
1423
|
-
const unionNode =
|
|
1360
|
+
const unionNode = createNode(ctx, {
|
|
1424
1361
|
type: "union",
|
|
1425
|
-
...
|
|
1362
|
+
...unionExtras,
|
|
1426
1363
|
members: unionMembers.map((s) => parse({
|
|
1427
1364
|
schema: s,
|
|
1428
1365
|
name
|
|
@@ -1431,60 +1368,127 @@ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1431
1368
|
return ast.applyMacros(unionNode, [macroSimplifyUnion], { depth: "shallow" });
|
|
1432
1369
|
}
|
|
1433
1370
|
/**
|
|
1371
|
+
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
1372
|
+
* Only called once the multi-type rule's `match` has confirmed more than one non-`null` type
|
|
1373
|
+
* remains; a single remaining type (e.g. `['string', 'null']`) is handled as that type instead,
|
|
1374
|
+
* with nullability already folded in.
|
|
1375
|
+
*/
|
|
1376
|
+
function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
|
|
1377
|
+
const types = schema.type;
|
|
1378
|
+
const nonNullTypes = types.filter((t) => t !== "null");
|
|
1379
|
+
return createNode({
|
|
1380
|
+
schema,
|
|
1381
|
+
name,
|
|
1382
|
+
nullable: types.includes("null") || nullable || void 0,
|
|
1383
|
+
defaultValue
|
|
1384
|
+
}, {
|
|
1385
|
+
type: "union",
|
|
1386
|
+
members: nonNullTypes.map((t) => {
|
|
1387
|
+
return parse({
|
|
1388
|
+
schema: {
|
|
1389
|
+
...schema,
|
|
1390
|
+
type: t
|
|
1391
|
+
},
|
|
1392
|
+
name
|
|
1393
|
+
}, rawOptions);
|
|
1394
|
+
})
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
//#endregion
|
|
1398
|
+
//#region src/emit/converters/scalar.ts
|
|
1399
|
+
/**
|
|
1400
|
+
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
1401
|
+
*
|
|
1402
|
+
* This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
|
|
1403
|
+
* from the array to its items sub-schema, so they are valid for downstream processing.
|
|
1404
|
+
*
|
|
1405
|
+
* @note A defensive measure for non-compliant specs.
|
|
1406
|
+
*/
|
|
1407
|
+
function normalizeArrayEnum(schema) {
|
|
1408
|
+
const normalizedItems = {
|
|
1409
|
+
...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
|
|
1410
|
+
enum: schema.enum
|
|
1411
|
+
};
|
|
1412
|
+
const { enum: _enum, ...schemaWithoutEnum } = schema;
|
|
1413
|
+
return {
|
|
1414
|
+
...schemaWithoutEnum,
|
|
1415
|
+
items: normalizedItems
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
|
|
1420
|
+
* and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
|
|
1421
|
+
*/
|
|
1422
|
+
function createNullNode(schema, name, nullable) {
|
|
1423
|
+
return ast.factory.createSchema({
|
|
1424
|
+
type: "null",
|
|
1425
|
+
primitive: "null",
|
|
1426
|
+
name,
|
|
1427
|
+
title: schema.title,
|
|
1428
|
+
description: schema.description,
|
|
1429
|
+
deprecated: schema.deprecated,
|
|
1430
|
+
nullable,
|
|
1431
|
+
format: schema.format
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1434
1435
|
* Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
|
|
1435
1436
|
*/
|
|
1436
1437
|
function convertConst({ schema, name, nullable, defaultValue }) {
|
|
1437
1438
|
const constValue = schema.const;
|
|
1438
1439
|
if (constValue === null) return createNullNode(schema, name);
|
|
1439
1440
|
const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
|
|
1440
|
-
return
|
|
1441
|
+
return createNode({
|
|
1442
|
+
schema,
|
|
1443
|
+
name,
|
|
1444
|
+
nullable,
|
|
1445
|
+
defaultValue
|
|
1446
|
+
}, {
|
|
1441
1447
|
type: "enum",
|
|
1442
1448
|
primitive: constPrimitive,
|
|
1443
|
-
enumValues: [constValue]
|
|
1444
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1449
|
+
enumValues: [constValue]
|
|
1445
1450
|
});
|
|
1446
1451
|
}
|
|
1447
1452
|
/**
|
|
1448
|
-
* Converts a format-annotated schema into a special-type `SchemaNode`.
|
|
1449
|
-
*
|
|
1453
|
+
* Converts a format-annotated schema into a special-type `SchemaNode`. Only called once the
|
|
1454
|
+
* `format` rule's `match` has confirmed the format is handled (see `isHandledFormat`) and, for
|
|
1455
|
+
* a date-ish format, that `dateType` is not `false`.
|
|
1450
1456
|
*/
|
|
1451
|
-
function convertFormat(
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1457
|
+
function convertFormat(context) {
|
|
1458
|
+
const { schema, name, nullable, defaultValue, options, type } = context;
|
|
1459
|
+
const ctx = {
|
|
1460
|
+
schema,
|
|
1461
|
+
name,
|
|
1462
|
+
nullable,
|
|
1463
|
+
defaultValue
|
|
1464
|
+
};
|
|
1465
|
+
if (type === "string" && numericFormats.has(schema.format)) return convertString(context);
|
|
1466
|
+
if (schema.format === "int64" || schema.format === "uint64") return createNode(ctx, {
|
|
1454
1467
|
type: options.integerType === "bigint" ? "bigint" : "integer",
|
|
1455
1468
|
primitive: "integer",
|
|
1456
|
-
...base,
|
|
1457
1469
|
min: schema.minimum,
|
|
1458
1470
|
max: schema.maximum,
|
|
1459
|
-
|
|
1460
|
-
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
|
|
1471
|
+
...getExclusiveBounds(schema)
|
|
1461
1472
|
});
|
|
1462
1473
|
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
|
|
1463
1474
|
const dateType = getDateType(options, schema.format);
|
|
1464
|
-
if (
|
|
1465
|
-
if (dateType.type === "datetime") return ast.factory.createSchema({
|
|
1466
|
-
...base,
|
|
1475
|
+
if (dateType.type === "datetime") return createNode(ctx, {
|
|
1467
1476
|
primitive: "string",
|
|
1468
1477
|
type: "datetime",
|
|
1469
1478
|
offset: dateType.offset,
|
|
1470
1479
|
local: dateType.local
|
|
1471
1480
|
});
|
|
1472
|
-
return
|
|
1473
|
-
...base,
|
|
1481
|
+
return createNode(ctx, {
|
|
1474
1482
|
primitive: "string",
|
|
1475
1483
|
type: dateType.type,
|
|
1476
1484
|
representation: dateType.representation
|
|
1477
1485
|
});
|
|
1478
1486
|
}
|
|
1479
1487
|
const specialType = getSchemaType(schema.format);
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
const hasLength = specialType === "url" || specialType === "uuid" || specialType === "email";
|
|
1483
|
-
return ast.factory.createSchema({
|
|
1484
|
-
...base,
|
|
1485
|
-
primitive: specialPrimitive,
|
|
1488
|
+
return createNode(ctx, {
|
|
1489
|
+
primitive: specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string",
|
|
1486
1490
|
type: specialType,
|
|
1487
|
-
...
|
|
1491
|
+
...specialType === "url" || specialType === "uuid" || specialType === "email" ? {
|
|
1488
1492
|
min: schema.minLength,
|
|
1489
1493
|
max: schema.maxLength
|
|
1490
1494
|
} : {}
|
|
@@ -1504,21 +1508,28 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
|
|
|
1504
1508
|
const enumNullable = nullable || nullInEnum || void 0;
|
|
1505
1509
|
const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
|
|
1506
1510
|
const enumPrimitive = getPrimitiveType(type);
|
|
1507
|
-
const
|
|
1511
|
+
const ctx = {
|
|
1512
|
+
schema,
|
|
1513
|
+
name,
|
|
1514
|
+
nullable: enumNullable,
|
|
1515
|
+
defaultValue: enumDefault
|
|
1516
|
+
};
|
|
1517
|
+
const enumExtras = {
|
|
1508
1518
|
type: "enum",
|
|
1509
|
-
primitive: enumPrimitive
|
|
1510
|
-
...buildSchemaNode(schema, name, enumNullable, enumDefault)
|
|
1519
|
+
primitive: enumPrimitive
|
|
1511
1520
|
};
|
|
1512
1521
|
const extensionKey = enumExtensionKeys.find((key) => key in schema);
|
|
1513
1522
|
const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
|
|
1514
1523
|
if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
|
|
1515
|
-
|
|
1524
|
+
let enumPrimitiveType = "string";
|
|
1525
|
+
if (enumPrimitive === "number" || enumPrimitive === "integer") enumPrimitiveType = "number";
|
|
1526
|
+
else if (enumPrimitive === "boolean") enumPrimitiveType = "boolean";
|
|
1516
1527
|
const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
|
|
1517
1528
|
const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
|
|
1518
1529
|
const uniqueValues = [...new Set(filteredValues)];
|
|
1519
1530
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1520
|
-
return
|
|
1521
|
-
...
|
|
1531
|
+
return createNode(ctx, {
|
|
1532
|
+
...enumExtras,
|
|
1522
1533
|
primitive: enumPrimitiveType,
|
|
1523
1534
|
namedEnumValues: uniqueValues.map((value, index) => ({
|
|
1524
1535
|
name: String(rawEnumNames?.[index] ?? value),
|
|
@@ -1532,12 +1543,103 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
|
|
|
1532
1543
|
})
|
|
1533
1544
|
});
|
|
1534
1545
|
}
|
|
1535
|
-
return
|
|
1536
|
-
...
|
|
1546
|
+
return createNode(ctx, {
|
|
1547
|
+
...enumExtras,
|
|
1537
1548
|
enumValues: [...new Set(filteredValues)]
|
|
1538
1549
|
});
|
|
1539
1550
|
}
|
|
1540
1551
|
/**
|
|
1552
|
+
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
1553
|
+
*/
|
|
1554
|
+
function convertString({ schema, name, nullable, defaultValue }) {
|
|
1555
|
+
return createNode({
|
|
1556
|
+
schema,
|
|
1557
|
+
name,
|
|
1558
|
+
nullable,
|
|
1559
|
+
defaultValue
|
|
1560
|
+
}, {
|
|
1561
|
+
type: "string",
|
|
1562
|
+
primitive: "string",
|
|
1563
|
+
min: schema.minLength,
|
|
1564
|
+
max: schema.maxLength,
|
|
1565
|
+
pattern: schema.pattern
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1569
|
+
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
1570
|
+
*/
|
|
1571
|
+
function convertNumeric({ schema, name, nullable, defaultValue }, type) {
|
|
1572
|
+
return createNode({
|
|
1573
|
+
schema,
|
|
1574
|
+
name,
|
|
1575
|
+
nullable,
|
|
1576
|
+
defaultValue
|
|
1577
|
+
}, {
|
|
1578
|
+
type,
|
|
1579
|
+
primitive: type,
|
|
1580
|
+
min: schema.minimum,
|
|
1581
|
+
max: schema.maximum,
|
|
1582
|
+
...getExclusiveBounds(schema),
|
|
1583
|
+
multipleOf: schema.multipleOf
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Converts a `type: 'boolean'` schema.
|
|
1588
|
+
*/
|
|
1589
|
+
function convertBoolean({ schema, name, nullable, defaultValue }) {
|
|
1590
|
+
return createNode({
|
|
1591
|
+
schema,
|
|
1592
|
+
name,
|
|
1593
|
+
nullable,
|
|
1594
|
+
defaultValue
|
|
1595
|
+
}, {
|
|
1596
|
+
type: "boolean",
|
|
1597
|
+
primitive: "boolean"
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
|
|
1602
|
+
* into a `blob` node.
|
|
1603
|
+
*/
|
|
1604
|
+
function convertBinary({ schema, name, nullable, defaultValue }) {
|
|
1605
|
+
return createNode({
|
|
1606
|
+
schema,
|
|
1607
|
+
name,
|
|
1608
|
+
nullable,
|
|
1609
|
+
defaultValue
|
|
1610
|
+
}, {
|
|
1611
|
+
type: "blob",
|
|
1612
|
+
primitive: "string"
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
//#endregion
|
|
1616
|
+
//#region src/emit/converters/structural.ts
|
|
1617
|
+
/**
|
|
1618
|
+
* Resolves a `true` or empty-object map schema (`additionalProperties`/`patternProperties`) to
|
|
1619
|
+
* `options.unknownType`, otherwise parses it as a regular schema.
|
|
1620
|
+
*/
|
|
1621
|
+
function resolveMapSchema(mapSchema, options, parse, rawOptions) {
|
|
1622
|
+
if (mapSchema === true || typeof mapSchema === "object" && Object.keys(mapSchema).length === 0) return ast.factory.createSchema({ type: options.unknownType });
|
|
1623
|
+
return parse({ schema: mapSchema }, rawOptions);
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Names the inline enums on a property's schema, and on each item when the property is a tuple, from
|
|
1627
|
+
* the parent and property name. Wraps `macroEnumName` at the property construction site.
|
|
1628
|
+
*/
|
|
1629
|
+
function nameEnums(node, options) {
|
|
1630
|
+
const macro = macroEnumName(options);
|
|
1631
|
+
const named = ast.applyMacros(node, [macro], { depth: "shallow" });
|
|
1632
|
+
const tupleNode = ast.narrowSchema(named, "tuple");
|
|
1633
|
+
if (tupleNode?.items) {
|
|
1634
|
+
const namedItems = tupleNode.items.map((item) => ast.applyMacros(item, [macro], { depth: "shallow" }));
|
|
1635
|
+
if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
|
|
1636
|
+
...tupleNode,
|
|
1637
|
+
items: namedItems
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
return named;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1541
1643
|
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
1542
1644
|
*/
|
|
1543
1645
|
function convertObject({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
@@ -1563,146 +1665,84 @@ function convertObject({ schema, name, nullable, defaultValue, rawOptions, optio
|
|
|
1563
1665
|
});
|
|
1564
1666
|
}) : [];
|
|
1565
1667
|
const additionalProperties = schema.additionalProperties;
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
if (additionalProperties) return ast.factory.createSchema({ type: options.unknownType });
|
|
1571
|
-
})();
|
|
1668
|
+
let additionalPropertiesNode;
|
|
1669
|
+
if (additionalProperties === true) additionalPropertiesNode = true;
|
|
1670
|
+
else if (additionalProperties) additionalPropertiesNode = resolveMapSchema(additionalProperties, options, parse, rawOptions);
|
|
1671
|
+
else additionalPropertiesNode = additionalProperties;
|
|
1572
1672
|
const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
|
|
1573
|
-
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern,
|
|
1574
|
-
const objectNode =
|
|
1673
|
+
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, resolveMapSchema(patternSchema, options, parse, rawOptions)])) : void 0;
|
|
1674
|
+
const objectNode = createNode({
|
|
1675
|
+
schema,
|
|
1676
|
+
name,
|
|
1677
|
+
nullable,
|
|
1678
|
+
defaultValue
|
|
1679
|
+
}, {
|
|
1575
1680
|
type: "object",
|
|
1576
1681
|
primitive: "object",
|
|
1577
1682
|
properties,
|
|
1578
1683
|
additionalProperties: additionalPropertiesNode,
|
|
1579
1684
|
patternProperties,
|
|
1580
1685
|
minProperties: schema.minProperties,
|
|
1581
|
-
maxProperties: schema.maxProperties
|
|
1582
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1686
|
+
maxProperties: schema.maxProperties
|
|
1583
1687
|
});
|
|
1584
1688
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
1585
1689
|
const discPropName = schema.discriminator.propertyName;
|
|
1586
1690
|
const values = Object.keys(schema.discriminator.mapping);
|
|
1587
1691
|
const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
|
|
1588
|
-
return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
|
|
1589
|
-
propertyName: discPropName,
|
|
1590
|
-
values,
|
|
1591
|
-
enumName
|
|
1592
|
-
})], { depth: "shallow" });
|
|
1593
|
-
}
|
|
1594
|
-
return objectNode;
|
|
1595
|
-
}
|
|
1596
|
-
/**
|
|
1597
|
-
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
1598
|
-
*/
|
|
1599
|
-
function convertTuple({ schema, name, nullable, defaultValue, rawOptions, parse }) {
|
|
1600
|
-
const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
|
|
1601
|
-
const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? ast.factory.createSchema({ type: "any" }) : parse({ schema: schema.items }, rawOptions);
|
|
1602
|
-
return ast.factory.createSchema({
|
|
1603
|
-
type: "tuple",
|
|
1604
|
-
primitive: "array",
|
|
1605
|
-
items: tupleItems,
|
|
1606
|
-
rest,
|
|
1607
|
-
min: schema.minItems,
|
|
1608
|
-
max: schema.maxItems,
|
|
1609
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* Converts a `type: 'array'` schema into an `ArraySchemaNode`.
|
|
1614
|
-
*/
|
|
1615
|
-
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1616
|
-
const rawItems = schema.items;
|
|
1617
|
-
const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name;
|
|
1618
|
-
const items = rawItems ? [parse({
|
|
1619
|
-
schema: rawItems,
|
|
1620
|
-
name: itemName
|
|
1621
|
-
}, rawOptions)] : [];
|
|
1622
|
-
return ast.factory.createSchema({
|
|
1623
|
-
type: "array",
|
|
1624
|
-
primitive: "array",
|
|
1625
|
-
items,
|
|
1626
|
-
min: schema.minItems,
|
|
1627
|
-
max: schema.maxItems,
|
|
1628
|
-
unique: schema.uniqueItems ?? void 0,
|
|
1629
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
/**
|
|
1633
|
-
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
1634
|
-
*/
|
|
1635
|
-
function convertString({ schema, name, nullable, defaultValue }) {
|
|
1636
|
-
return ast.factory.createSchema({
|
|
1637
|
-
type: "string",
|
|
1638
|
-
primitive: "string",
|
|
1639
|
-
min: schema.minLength,
|
|
1640
|
-
max: schema.maxLength,
|
|
1641
|
-
pattern: schema.pattern,
|
|
1642
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1643
|
-
});
|
|
1644
|
-
}
|
|
1645
|
-
/**
|
|
1646
|
-
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
1647
|
-
*/
|
|
1648
|
-
function convertNumeric({ schema, name, nullable, defaultValue }, type) {
|
|
1649
|
-
return ast.factory.createSchema({
|
|
1650
|
-
type,
|
|
1651
|
-
primitive: type,
|
|
1652
|
-
min: schema.minimum,
|
|
1653
|
-
max: schema.maximum,
|
|
1654
|
-
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
1655
|
-
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
|
|
1656
|
-
multipleOf: schema.multipleOf,
|
|
1657
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1658
|
-
});
|
|
1659
|
-
}
|
|
1660
|
-
/**
|
|
1661
|
-
* Converts a `type: 'boolean'` schema.
|
|
1662
|
-
*/
|
|
1663
|
-
function convertBoolean({ schema, name, nullable, defaultValue }) {
|
|
1664
|
-
return ast.factory.createSchema({
|
|
1665
|
-
type: "boolean",
|
|
1666
|
-
primitive: "boolean",
|
|
1667
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1668
|
-
});
|
|
1692
|
+
return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
|
|
1693
|
+
propertyName: discPropName,
|
|
1694
|
+
values,
|
|
1695
|
+
enumName
|
|
1696
|
+
})], { depth: "shallow" });
|
|
1697
|
+
}
|
|
1698
|
+
return objectNode;
|
|
1669
1699
|
}
|
|
1670
1700
|
/**
|
|
1671
|
-
* Converts
|
|
1672
|
-
* into a `blob` node.
|
|
1701
|
+
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
1673
1702
|
*/
|
|
1674
|
-
function
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1703
|
+
function convertTuple({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1704
|
+
const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
|
|
1705
|
+
const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? ast.factory.createSchema({ type: options.unknownType }) : parse({ schema: schema.items }, rawOptions);
|
|
1706
|
+
return createNode({
|
|
1707
|
+
schema,
|
|
1708
|
+
name,
|
|
1709
|
+
nullable,
|
|
1710
|
+
defaultValue
|
|
1711
|
+
}, {
|
|
1712
|
+
type: "tuple",
|
|
1713
|
+
primitive: "array",
|
|
1714
|
+
items: tupleItems,
|
|
1715
|
+
rest,
|
|
1716
|
+
min: schema.minItems,
|
|
1717
|
+
max: schema.maxItems
|
|
1679
1718
|
});
|
|
1680
1719
|
}
|
|
1681
1720
|
/**
|
|
1682
|
-
* Converts
|
|
1683
|
-
*
|
|
1684
|
-
* Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parse`
|
|
1685
|
-
* falls through and handles it as that single type with nullability already folded in.
|
|
1721
|
+
* Converts a `type: 'array'` schema into an `ArraySchemaNode`.
|
|
1686
1722
|
*/
|
|
1687
|
-
function
|
|
1688
|
-
const
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1723
|
+
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1724
|
+
const rawItems = schema.items;
|
|
1725
|
+
const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name;
|
|
1726
|
+
const items = rawItems ? [parse({
|
|
1727
|
+
schema: rawItems,
|
|
1728
|
+
name: itemName
|
|
1729
|
+
}, rawOptions)] : [];
|
|
1730
|
+
return createNode({
|
|
1731
|
+
schema,
|
|
1732
|
+
name,
|
|
1733
|
+
nullable,
|
|
1734
|
+
defaultValue
|
|
1735
|
+
}, {
|
|
1736
|
+
type: "array",
|
|
1737
|
+
primitive: "array",
|
|
1738
|
+
items,
|
|
1739
|
+
min: schema.minItems,
|
|
1740
|
+
max: schema.maxItems,
|
|
1741
|
+
unique: schema.uniqueItems ?? void 0
|
|
1704
1742
|
});
|
|
1705
1743
|
}
|
|
1744
|
+
//#endregion
|
|
1745
|
+
//#region src/emit/parseSchema.ts
|
|
1706
1746
|
/**
|
|
1707
1747
|
* Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
1708
1748
|
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
@@ -1727,7 +1767,11 @@ const schemaRules = [
|
|
|
1727
1767
|
convert: convertConst
|
|
1728
1768
|
},
|
|
1729
1769
|
{
|
|
1730
|
-
match: ({ schema }) =>
|
|
1770
|
+
match: ({ schema, options }) => {
|
|
1771
|
+
if (!schema.format) return false;
|
|
1772
|
+
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") return options.dateType !== false;
|
|
1773
|
+
return isHandledFormat(schema.format);
|
|
1774
|
+
},
|
|
1731
1775
|
convert: convertFormat
|
|
1732
1776
|
},
|
|
1733
1777
|
{
|
|
@@ -1735,7 +1779,7 @@ const schemaRules = [
|
|
|
1735
1779
|
convert: convertBinary
|
|
1736
1780
|
},
|
|
1737
1781
|
{
|
|
1738
|
-
match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
|
|
1782
|
+
match: ({ schema }) => Array.isArray(schema.type) && schema.type.filter((t) => t !== "null").length > 1,
|
|
1739
1783
|
convert: convertMultiType
|
|
1740
1784
|
},
|
|
1741
1785
|
{
|
|
@@ -1784,72 +1828,306 @@ const schemaRules = [
|
|
|
1784
1828
|
}
|
|
1785
1829
|
];
|
|
1786
1830
|
//#endregion
|
|
1787
|
-
//#region src/
|
|
1831
|
+
//#region src/refs.ts
|
|
1832
|
+
const _refCache = /* @__PURE__ */ new WeakMap();
|
|
1788
1833
|
/**
|
|
1789
|
-
*
|
|
1834
|
+
* Walks a local `#/...` JSON pointer against `document`, memoized per document. `applicable` is
|
|
1835
|
+
* `false` for an empty or non-local ref (the caller should not treat that as a failed lookup).
|
|
1836
|
+
* Shared by `resolveRef`'s reporting walk and `createRefs().resolve`'s silent walk, so both use
|
|
1837
|
+
* the same trimming and caching instead of two separate implementations.
|
|
1838
|
+
*/
|
|
1839
|
+
function walkPointer(document, $ref) {
|
|
1840
|
+
const trimmed = $ref.trim();
|
|
1841
|
+
if (trimmed === "" || !trimmed.startsWith("#")) return {
|
|
1842
|
+
applicable: false,
|
|
1843
|
+
value: null
|
|
1844
|
+
};
|
|
1845
|
+
const pointer = globalThis.decodeURIComponent(trimmed.substring(1));
|
|
1846
|
+
let docCache = _refCache.get(document);
|
|
1847
|
+
if (!docCache) {
|
|
1848
|
+
docCache = /* @__PURE__ */ new Map();
|
|
1849
|
+
_refCache.set(document, docCache);
|
|
1850
|
+
}
|
|
1851
|
+
if (docCache.has(pointer)) return {
|
|
1852
|
+
applicable: true,
|
|
1853
|
+
value: docCache.get(pointer)
|
|
1854
|
+
};
|
|
1855
|
+
const current = pointer.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
|
|
1856
|
+
if (current) docCache.set(pointer, current);
|
|
1857
|
+
return {
|
|
1858
|
+
applicable: true,
|
|
1859
|
+
value: current ?? null
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Resolves a local JSON pointer reference from a document.
|
|
1790
1864
|
*
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1793
|
-
*
|
|
1794
|
-
* `parse` function passed to it, so this file only wires state to the converters.
|
|
1865
|
+
* Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
|
|
1866
|
+
* resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
|
|
1867
|
+
* build there is no sink to collect it, so it throws instead.
|
|
1795
1868
|
*
|
|
1796
|
-
* @
|
|
1869
|
+
* @example
|
|
1870
|
+
* ```ts
|
|
1871
|
+
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
|
|
1872
|
+
* ```
|
|
1797
1873
|
*/
|
|
1798
|
-
function
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1874
|
+
function resolveRef(document, $ref) {
|
|
1875
|
+
const { applicable, value } = walkPointer(document, $ref);
|
|
1876
|
+
if (!applicable) return null;
|
|
1877
|
+
if (value) return value;
|
|
1878
|
+
const diagnostic = {
|
|
1879
|
+
code: Diagnostics.code.refNotFound,
|
|
1880
|
+
severity: "error",
|
|
1881
|
+
message: `Could not find a definition for ${$ref}.`,
|
|
1882
|
+
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
1883
|
+
location: {
|
|
1884
|
+
kind: "schema",
|
|
1885
|
+
pointer: $ref,
|
|
1886
|
+
ref: $ref
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
|
|
1890
|
+
return null;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Resolves a `$ref` object while preserving the original `$ref` field on the result.
|
|
1894
|
+
*
|
|
1895
|
+
* Useful for parser flows that need both dereferenced fields and pointer
|
|
1896
|
+
* identity (for naming/import purposes). Non-reference values are returned as-is.
|
|
1897
|
+
*
|
|
1898
|
+
* @example
|
|
1899
|
+
* ```ts
|
|
1900
|
+
* dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
|
|
1901
|
+
* // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
|
|
1902
|
+
* ```
|
|
1903
|
+
*/
|
|
1904
|
+
function dereferenceWithRef(document, schema) {
|
|
1905
|
+
if (isReference(schema)) return {
|
|
1906
|
+
...schema,
|
|
1907
|
+
...resolveRef(document, schema.$ref),
|
|
1908
|
+
$ref: schema.$ref
|
|
1909
|
+
};
|
|
1910
|
+
return schema;
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Creates the `$ref` resolution service for one document.
|
|
1914
|
+
*
|
|
1915
|
+
* Replaces what used to be six overlapping resolvers (a reporting walk, a silent walk, an
|
|
1916
|
+
* existence check, and a resolve-then-parse-into-a-node step, each with its own cache) with one
|
|
1917
|
+
* pointer walk and one explicit `report` contract for a missing ref: `report: true` (the default)
|
|
1918
|
+
* reports a `refNotFound` diagnostic (or throws outside a build), `report: false` resolves to
|
|
1919
|
+
* `null` silently for a speculative lookup.
|
|
1920
|
+
*
|
|
1921
|
+
* @example
|
|
1922
|
+
* ```ts
|
|
1923
|
+
* const refs = createRefs(document)
|
|
1924
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet')
|
|
1925
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
|
|
1926
|
+
* refs.exists('#/components/schemas/Pet')
|
|
1927
|
+
* refs.resolveNode('#/components/schemas/Pet', parseSchema)
|
|
1928
|
+
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
1929
|
+
* ```
|
|
1930
|
+
*/
|
|
1931
|
+
function createRefs(document) {
|
|
1932
|
+
const resolvedNodeCache = /* @__PURE__ */ new Map();
|
|
1933
|
+
const existenceCache = /* @__PURE__ */ new Map();
|
|
1804
1934
|
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
1805
1935
|
/**
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
1811
|
-
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
1936
|
+
* Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
|
|
1937
|
+
* `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
|
|
1938
|
+
* outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
|
|
1939
|
+
* for a speculative lookup where a missing ref is not an error.
|
|
1812
1940
|
*/
|
|
1813
|
-
|
|
1941
|
+
function resolve(refPath, options) {
|
|
1942
|
+
if (options?.report === false) {
|
|
1943
|
+
const { applicable, value } = walkPointer(document, refPath);
|
|
1944
|
+
return applicable ? value : null;
|
|
1945
|
+
}
|
|
1946
|
+
return resolveRef(document, refPath);
|
|
1947
|
+
}
|
|
1814
1948
|
/**
|
|
1815
|
-
*
|
|
1949
|
+
* Returns `true` when a `$ref` path resolves to a component the document actually defines.
|
|
1816
1950
|
* A circular ref still resolves to an existing target, so this stays `true` for cycles and only
|
|
1817
|
-
* goes `false` for a `$ref` that points at a component the spec never declares.
|
|
1951
|
+
* goes `false` for a `$ref` that points at a component the spec never declares. Memoized.
|
|
1818
1952
|
*/
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
let exists = false;
|
|
1823
|
-
try {
|
|
1824
|
-
exists = !!resolveRef(document, refPath);
|
|
1825
|
-
} catch {
|
|
1826
|
-
exists = false;
|
|
1827
|
-
}
|
|
1828
|
-
refExistence.set(refPath, exists);
|
|
1829
|
-
}
|
|
1830
|
-
return refExistence.get(refPath) ?? false;
|
|
1953
|
+
function exists(refPath) {
|
|
1954
|
+
if (!existenceCache.has(refPath)) existenceCache.set(refPath, !!resolve(refPath, { report: false }));
|
|
1955
|
+
return existenceCache.get(refPath) ?? false;
|
|
1831
1956
|
}
|
|
1832
1957
|
/**
|
|
1833
|
-
* Resolves a `$ref` to its parsed node
|
|
1834
|
-
* Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
1835
|
-
* (e.g. a minimal document in a unit test).
|
|
1958
|
+
* Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
|
|
1959
|
+
* instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
1960
|
+
* resolved (e.g. a minimal document in a unit test).
|
|
1836
1961
|
*/
|
|
1837
|
-
function
|
|
1962
|
+
function resolveNode(refPath, parse, rawOptions) {
|
|
1838
1963
|
if (resolvingRefs.has(refPath)) return null;
|
|
1839
|
-
if (!
|
|
1964
|
+
if (!resolvedNodeCache.has(refPath)) {
|
|
1840
1965
|
let resolved = null;
|
|
1841
1966
|
try {
|
|
1842
|
-
const referenced =
|
|
1967
|
+
const referenced = resolve(refPath);
|
|
1843
1968
|
if (referenced) {
|
|
1844
1969
|
resolvingRefs.add(refPath);
|
|
1845
|
-
resolved =
|
|
1970
|
+
resolved = parse({ schema: referenced }, rawOptions);
|
|
1846
1971
|
resolvingRefs.delete(refPath);
|
|
1847
1972
|
}
|
|
1848
1973
|
} catch {}
|
|
1849
|
-
|
|
1974
|
+
resolvedNodeCache.set(refPath, resolved);
|
|
1850
1975
|
}
|
|
1851
|
-
return
|
|
1976
|
+
return resolvedNodeCache.get(refPath) ?? null;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
|
|
1980
|
+
* resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
|
|
1981
|
+
* `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
|
|
1982
|
+
* returned as-is.
|
|
1983
|
+
*
|
|
1984
|
+
* @example
|
|
1985
|
+
* ```ts
|
|
1986
|
+
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
1987
|
+
* ```
|
|
1988
|
+
*/
|
|
1989
|
+
function deref(value) {
|
|
1990
|
+
if (!isReference(value)) return value ? value : null;
|
|
1991
|
+
const resolved = resolve(value.$ref);
|
|
1992
|
+
return resolved && !isReference(resolved) ? resolved : null;
|
|
1852
1993
|
}
|
|
1994
|
+
return {
|
|
1995
|
+
resolve,
|
|
1996
|
+
exists,
|
|
1997
|
+
resolveNode,
|
|
1998
|
+
deref
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
//#endregion
|
|
2002
|
+
//#region src/model/operations.ts
|
|
2003
|
+
/**
|
|
2004
|
+
* Returns all parameters for an operation, merging path-level and operation-level entries.
|
|
2005
|
+
* Operation-level parameters override path-level ones with the same `in:name` key.
|
|
2006
|
+
* Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
|
|
2007
|
+
*
|
|
2008
|
+
* @example
|
|
2009
|
+
* ```ts
|
|
2010
|
+
* getParameters({ document, operation })
|
|
2011
|
+
* // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
|
|
2012
|
+
* ```
|
|
2013
|
+
*/
|
|
2014
|
+
function getParameters({ document, operation }) {
|
|
2015
|
+
const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
|
|
2016
|
+
const operationParams = resolveParams(operation.schema?.parameters || []);
|
|
2017
|
+
const pathLevelParams = resolveParams(operation.pathItem.parameters ?? []);
|
|
2018
|
+
const paramMap = /* @__PURE__ */ new Map();
|
|
2019
|
+
for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
2020
|
+
for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
2021
|
+
return Array.from(paramMap.values());
|
|
2022
|
+
}
|
|
2023
|
+
function getResponseBody(responseBody, contentType) {
|
|
2024
|
+
if (!responseBody) return false;
|
|
2025
|
+
if (isReference(responseBody)) return false;
|
|
2026
|
+
const body = responseBody;
|
|
2027
|
+
if (!body.content) return false;
|
|
2028
|
+
if (contentType) return contentType in body.content ? body.content[contentType] : false;
|
|
2029
|
+
const picked = pickContentEntry(body.content);
|
|
2030
|
+
return picked ? picked[1] : false;
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Returns the response schema for a given operation and HTTP status code.
|
|
2034
|
+
*
|
|
2035
|
+
* Returns an empty object `{}` when no response body schema is available.
|
|
2036
|
+
*
|
|
2037
|
+
* @example
|
|
2038
|
+
* ```ts
|
|
2039
|
+
* getResponseSchema({ document, operation, refs, statusCode: 200 }) // SchemaObject
|
|
2040
|
+
* getResponseSchema({ document, operation, refs, statusCode: '4XX' }) // {}
|
|
2041
|
+
* ```
|
|
2042
|
+
*/
|
|
2043
|
+
function getResponseSchema({ document, operation, refs, statusCode, options = {} }) {
|
|
2044
|
+
const responseBody = getResponseBody(getResponseByStatusCode({
|
|
2045
|
+
operation,
|
|
2046
|
+
refs,
|
|
2047
|
+
statusCode
|
|
2048
|
+
}), options.contentType);
|
|
2049
|
+
if (responseBody === false) return {};
|
|
2050
|
+
const schema = responseBody.schema;
|
|
2051
|
+
if (!schema) return {};
|
|
2052
|
+
return dereferenceWithRef(document, schema);
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Returns the request body schema for an operation, or `null` when absent.
|
|
2056
|
+
*
|
|
2057
|
+
* @example
|
|
2058
|
+
* ```ts
|
|
2059
|
+
* getRequestSchema({ document, operation, refs }) // SchemaObject | null
|
|
2060
|
+
* ```
|
|
2061
|
+
*/
|
|
2062
|
+
function getRequestSchema({ document, operation, refs, options = {} }) {
|
|
2063
|
+
const requestBody = getRequestContent({
|
|
2064
|
+
operation,
|
|
2065
|
+
refs,
|
|
2066
|
+
mediaType: options.contentType
|
|
2067
|
+
});
|
|
2068
|
+
if (requestBody === false) return null;
|
|
2069
|
+
const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
|
|
2070
|
+
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
2071
|
+
if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
|
|
2072
|
+
type: "string",
|
|
2073
|
+
contentMediaType: "application/octet-stream"
|
|
2074
|
+
};
|
|
2075
|
+
if (!schema) return null;
|
|
2076
|
+
return dereferenceWithRef(document, schema);
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Returns all request body content type keys for an operation, resolving a `$ref` requestBody
|
|
2080
|
+
* through `refs`.
|
|
2081
|
+
*
|
|
2082
|
+
* @example
|
|
2083
|
+
* ```ts
|
|
2084
|
+
* getRequestBodyContentTypes(operation, refs)
|
|
2085
|
+
* // ['application/json', 'multipart/form-data']
|
|
2086
|
+
* ```
|
|
2087
|
+
*/
|
|
2088
|
+
function getRequestBodyContentTypes(operation, refs) {
|
|
2089
|
+
const body = getRequestBody({
|
|
2090
|
+
operation,
|
|
2091
|
+
refs
|
|
2092
|
+
});
|
|
2093
|
+
return body?.content ? Object.keys(body.content) : [];
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Returns all response content type keys for an operation at a given status code, resolving the
|
|
2097
|
+
* response `$ref` through `refs`.
|
|
2098
|
+
*
|
|
2099
|
+
* @example
|
|
2100
|
+
* ```ts
|
|
2101
|
+
* getResponseBodyContentTypes(operation, refs, 200)
|
|
2102
|
+
* // ['application/json', 'application/xml']
|
|
2103
|
+
* ```
|
|
2104
|
+
*/
|
|
2105
|
+
function getResponseBodyContentTypes(operation, refs, statusCode) {
|
|
2106
|
+
const responseObj = getResponseByStatusCode({
|
|
2107
|
+
operation,
|
|
2108
|
+
refs,
|
|
2109
|
+
statusCode
|
|
2110
|
+
});
|
|
2111
|
+
if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
|
|
2112
|
+
const body = responseObj;
|
|
2113
|
+
return body.content ? Object.keys(body.content) : [];
|
|
2114
|
+
}
|
|
2115
|
+
//#endregion
|
|
2116
|
+
//#region src/parser.ts
|
|
2117
|
+
/**
|
|
2118
|
+
* Creates the schema and operation converters bound to one OpenAPI document.
|
|
2119
|
+
*
|
|
2120
|
+
* Takes the `$ref` service for this document (shared with the rest of the pipeline, see
|
|
2121
|
+
* `adapter.ts`) and owns the `parseSchema` recursion seam, then dispatches each schema through
|
|
2122
|
+
* the ordered `schemaRules` table from `emit/parseSchema.ts`. Every converter is a standalone
|
|
2123
|
+
* function that recurses through the `parse` function passed to it, so this file only wires
|
|
2124
|
+
* state to the converters.
|
|
2125
|
+
*
|
|
2126
|
+
* @internal
|
|
2127
|
+
*/
|
|
2128
|
+
function createSchemaParser(ctx) {
|
|
2129
|
+
const document = ctx.document;
|
|
2130
|
+
const refs = ctx.refs;
|
|
1853
2131
|
/**
|
|
1854
2132
|
* Converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
1855
2133
|
*
|
|
@@ -1878,15 +2156,10 @@ function createSchemaParser(ctx) {
|
|
|
1878
2156
|
options,
|
|
1879
2157
|
parse: parseSchema,
|
|
1880
2158
|
document,
|
|
1881
|
-
|
|
1882
|
-
refExists,
|
|
2159
|
+
refs,
|
|
1883
2160
|
renames: ctx.renames
|
|
1884
2161
|
};
|
|
1885
|
-
for (const rule of schemaRules)
|
|
1886
|
-
if (!rule.match(context)) continue;
|
|
1887
|
-
const node = rule.convert(context);
|
|
1888
|
-
if (node) return node;
|
|
1889
|
-
}
|
|
2162
|
+
for (const rule of schemaRules) if (rule.match(context)) return rule.convert(context);
|
|
1890
2163
|
const emptyType = options.emptySchemaType;
|
|
1891
2164
|
return ast.factory.createSchema({
|
|
1892
2165
|
type: emptyType,
|
|
@@ -1923,10 +2196,14 @@ function createSchemaParser(ctx) {
|
|
|
1923
2196
|
}
|
|
1924
2197
|
/**
|
|
1925
2198
|
* Reads the inline `requestBody` metadata (description / required) that OAS exposes
|
|
1926
|
-
* outside the schema itself
|
|
2199
|
+
* outside the schema itself, resolving a `$ref` requestBody through `refs`. Returns an
|
|
2200
|
+
* empty object when the request body is missing or cannot be resolved.
|
|
1927
2201
|
*/
|
|
1928
2202
|
function getRequestBodyMeta(operation) {
|
|
1929
|
-
const body =
|
|
2203
|
+
const body = getRequestBody({
|
|
2204
|
+
operation,
|
|
2205
|
+
refs
|
|
2206
|
+
});
|
|
1930
2207
|
if (!body) return { required: false };
|
|
1931
2208
|
return {
|
|
1932
2209
|
description: body.description,
|
|
@@ -1952,12 +2229,20 @@ function createSchemaParser(ctx) {
|
|
|
1952
2229
|
function parseOperation(options, operation) {
|
|
1953
2230
|
const operationId = getOperationId(operation);
|
|
1954
2231
|
const operationName = operationId ? pascalCase(operationId) : void 0;
|
|
1955
|
-
const parameters = getParameters(
|
|
1956
|
-
|
|
2232
|
+
const parameters = getParameters({
|
|
2233
|
+
document,
|
|
2234
|
+
operation
|
|
2235
|
+
}).map((param) => parseParameter(options, param, operationName));
|
|
2236
|
+
const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(operation, refs);
|
|
1957
2237
|
const requestBodyMeta = getRequestBodyMeta(operation);
|
|
1958
2238
|
const requestBodyName = operationName ? `${operationName}Request` : void 0;
|
|
1959
2239
|
const content = allContentTypes.flatMap((ct) => {
|
|
1960
|
-
const schema = getRequestSchema(
|
|
2240
|
+
const schema = getRequestSchema({
|
|
2241
|
+
document,
|
|
2242
|
+
operation,
|
|
2243
|
+
refs,
|
|
2244
|
+
options: { contentType: ct }
|
|
2245
|
+
});
|
|
1961
2246
|
if (!schema) return [];
|
|
1962
2247
|
return [ast.factory.createContent({
|
|
1963
2248
|
contentType: ct,
|
|
@@ -1975,14 +2260,20 @@ function createSchemaParser(ctx) {
|
|
|
1975
2260
|
} : void 0;
|
|
1976
2261
|
const responses = getResponseStatusCodes(operation).map((statusCode) => {
|
|
1977
2262
|
const responseObj = getResponseByStatusCode({
|
|
1978
|
-
document,
|
|
1979
2263
|
operation,
|
|
2264
|
+
refs,
|
|
1980
2265
|
statusCode
|
|
1981
2266
|
});
|
|
1982
2267
|
const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
|
|
1983
2268
|
const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
|
|
1984
2269
|
const parseEntrySchema = (contentType) => {
|
|
1985
|
-
const raw = getResponseSchema(
|
|
2270
|
+
const raw = getResponseSchema({
|
|
2271
|
+
document,
|
|
2272
|
+
operation,
|
|
2273
|
+
refs,
|
|
2274
|
+
statusCode,
|
|
2275
|
+
options: { contentType }
|
|
2276
|
+
});
|
|
1986
2277
|
return {
|
|
1987
2278
|
schema: raw && Object.keys(raw).length > 0 ? parseSchema({
|
|
1988
2279
|
schema: raw,
|
|
@@ -1991,14 +2282,14 @@ function createSchemaParser(ctx) {
|
|
|
1991
2282
|
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
|
|
1992
2283
|
};
|
|
1993
2284
|
};
|
|
1994
|
-
const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(
|
|
2285
|
+
const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(operation, refs, statusCode)).map((contentType) => ast.factory.createContent({
|
|
1995
2286
|
contentType,
|
|
1996
2287
|
...parseEntrySchema(contentType)
|
|
1997
2288
|
}));
|
|
1998
2289
|
if (content.length === 0) content.push(ast.factory.createContent({
|
|
1999
2290
|
contentType: getRequestContentType({
|
|
2000
|
-
|
|
2001
|
-
|
|
2291
|
+
operation,
|
|
2292
|
+
refs
|
|
2002
2293
|
}) || "application/json",
|
|
2003
2294
|
...parseEntrySchema(ctx.contentType)
|
|
2004
2295
|
}));
|
|
@@ -2008,12 +2299,10 @@ function createSchemaParser(ctx) {
|
|
|
2008
2299
|
content
|
|
2009
2300
|
});
|
|
2010
2301
|
});
|
|
2011
|
-
const pathItem = document.paths?.[operation.path];
|
|
2012
|
-
const pathItemDoc = pathItem && !isReference(pathItem) ? pathItem : void 0;
|
|
2013
2302
|
const pickDoc = (key) => {
|
|
2014
2303
|
const own = operation.schema[key];
|
|
2015
2304
|
if (typeof own === "string") return own;
|
|
2016
|
-
const fallback =
|
|
2305
|
+
const fallback = operation.pathItem[key];
|
|
2017
2306
|
return typeof fallback === "string" ? fallback : void 0;
|
|
2018
2307
|
};
|
|
2019
2308
|
return ast.factory.createOperation({
|
|
@@ -2086,15 +2375,20 @@ function refPromotedEnums(node, promoted) {
|
|
|
2086
2375
|
//#endregion
|
|
2087
2376
|
//#region src/schemaDiagnostics.ts
|
|
2088
2377
|
/**
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2378
|
+
* Scans one freshly converted top-level schema in a single walk, so the post-convert pass never
|
|
2379
|
+
* sweeps the same nodes twice. It reports the advisory diagnostics (`KUBB_UNSUPPORTED_FORMAT`,
|
|
2380
|
+
* `KUBB_DEPRECATED`) and returns the names of every schema the node references, ready to feed the
|
|
2381
|
+
* circular-dependency graph.
|
|
2382
|
+
*
|
|
2383
|
+
* Walks the node the parser produced, threading the RFC 6901 pointer as it descends so a nested
|
|
2384
|
+
* field reports against its full path (`#/components/schemas/Pet/properties/owner/properties/name`).
|
|
2385
|
+
* Refs are recorded by name and not followed, so the resolved schema is reported under its own walk.
|
|
2386
|
+
* Reports land in the active build run, are a no-op outside one, and repeats are deduped by the build.
|
|
2095
2387
|
*/
|
|
2096
|
-
function
|
|
2097
|
-
|
|
2388
|
+
function scanSchema({ node, name }) {
|
|
2389
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2390
|
+
visit(node, `#/components/schemas/${escapePointerToken(name)}`, refs);
|
|
2391
|
+
return refs;
|
|
2098
2392
|
}
|
|
2099
2393
|
/**
|
|
2100
2394
|
* Escapes a single JSON pointer reference token per RFC 6901 (`~` → `~0`, `/` → `~1`), so a
|
|
@@ -2103,7 +2397,11 @@ function reportSchemaDiagnostics({ node, name }) {
|
|
|
2103
2397
|
function escapePointerToken(token) {
|
|
2104
2398
|
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
2105
2399
|
}
|
|
2106
|
-
function visit(node, pointer) {
|
|
2400
|
+
function visit(node, pointer, refs) {
|
|
2401
|
+
if (node.type === "ref") {
|
|
2402
|
+
const refName = resolveRefName(node);
|
|
2403
|
+
if (refName) refs.add(refName);
|
|
2404
|
+
}
|
|
2107
2405
|
if (node.deprecated) Diagnostics.report({
|
|
2108
2406
|
code: Diagnostics.code.deprecated,
|
|
2109
2407
|
severity: "info",
|
|
@@ -2124,19 +2422,19 @@ function visit(node, pointer) {
|
|
|
2124
2422
|
}
|
|
2125
2423
|
});
|
|
2126
2424
|
if (node.type === "object") {
|
|
2127
|
-
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}
|
|
2128
|
-
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties
|
|
2425
|
+
for (const property of node.properties) visit(property.schema, `${pointer}/properties/${escapePointerToken(property.name)}`, refs);
|
|
2426
|
+
if (node.additionalProperties && typeof node.additionalProperties === "object") visit(node.additionalProperties, `${pointer}/additionalProperties`, refs);
|
|
2129
2427
|
return;
|
|
2130
2428
|
}
|
|
2131
2429
|
if (node.type === "array") {
|
|
2132
|
-
for (const item of node.items ?? []) visit(item, `${pointer}/items
|
|
2430
|
+
for (const item of node.items ?? []) visit(item, `${pointer}/items`, refs);
|
|
2133
2431
|
return;
|
|
2134
2432
|
}
|
|
2135
2433
|
if (node.type === "tuple") {
|
|
2136
|
-
for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}
|
|
2434
|
+
for (const [index, item] of (node.items ?? []).entries()) visit(item, `${pointer}/items/${index}`, refs);
|
|
2137
2435
|
return;
|
|
2138
2436
|
}
|
|
2139
|
-
if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}
|
|
2437
|
+
if (node.type === "union" || node.type === "intersection") for (const [index, member] of (node.members ?? []).entries()) visit(member, `${pointer}/members/${index}`, refs);
|
|
2140
2438
|
}
|
|
2141
2439
|
//#endregion
|
|
2142
2440
|
//#region src/adapter.ts
|
|
@@ -2182,63 +2480,33 @@ const adapterOas = createAdapter((options) => {
|
|
|
2182
2480
|
enumSuffix
|
|
2183
2481
|
};
|
|
2184
2482
|
let parsedDocument = null;
|
|
2185
|
-
const
|
|
2186
|
-
|
|
2187
|
-
const schemaParserCache = /* @__PURE__ */ new WeakMap();
|
|
2188
|
-
function ensureDocument(source) {
|
|
2189
|
-
const cached = documentCache.get(source);
|
|
2190
|
-
if (cached) return cached;
|
|
2191
|
-
const promise = (async () => {
|
|
2192
|
-
const fresh = await parseFromConfig(source);
|
|
2193
|
-
if (validate) await validateDocument(fresh);
|
|
2194
|
-
parsedDocument = fresh;
|
|
2195
|
-
return fresh;
|
|
2196
|
-
})();
|
|
2197
|
-
documentCache.set(source, promise);
|
|
2198
|
-
return promise;
|
|
2199
|
-
}
|
|
2200
|
-
function ensureSchemas(document) {
|
|
2201
|
-
const cached = schemasCache.get(document);
|
|
2202
|
-
if (cached) return cached;
|
|
2203
|
-
const result = getSchemas(document, { contentType });
|
|
2204
|
-
schemasCache.set(document, result);
|
|
2205
|
-
return result;
|
|
2206
|
-
}
|
|
2207
|
-
function ensureSchemaParser({ document, renames }) {
|
|
2208
|
-
const cached = schemaParserCache.get(document);
|
|
2209
|
-
if (cached) return cached;
|
|
2210
|
-
const parser = createSchemaParser({
|
|
2211
|
-
document,
|
|
2212
|
-
contentType,
|
|
2213
|
-
renames
|
|
2214
|
-
});
|
|
2215
|
-
schemaParserCache.set(document, parser);
|
|
2216
|
-
return parser;
|
|
2217
|
-
}
|
|
2218
|
-
function parseInput({ document, schemas, parser }) {
|
|
2483
|
+
const inputCache = /* @__PURE__ */ new WeakMap();
|
|
2484
|
+
function parseInput({ document, refs, schemas, parser }) {
|
|
2219
2485
|
const { parseSchema, parseOperation } = parser;
|
|
2220
2486
|
const parsedByName = /* @__PURE__ */ new Map();
|
|
2221
2487
|
const refAliasMap = /* @__PURE__ */ new Map();
|
|
2222
2488
|
const enumNames = [];
|
|
2223
2489
|
const discriminatorParentNodes = [];
|
|
2490
|
+
const refGraph = /* @__PURE__ */ new Map();
|
|
2224
2491
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
2225
2492
|
const node = parseSchema({
|
|
2226
2493
|
schema,
|
|
2227
2494
|
name
|
|
2228
2495
|
}, parserOptions);
|
|
2229
2496
|
parsedByName.set(name, node);
|
|
2230
|
-
|
|
2497
|
+
const refs = scanSchema({
|
|
2231
2498
|
node,
|
|
2232
2499
|
name
|
|
2233
2500
|
});
|
|
2501
|
+
if (node.name) refGraph.set(node.name, refs);
|
|
2234
2502
|
if (node.type === "ref" && node.name && node.name !== name) refAliasMap.set(name, node);
|
|
2235
2503
|
if (narrowSchema(node, "enum") && node.name) enumNames.push(node.name);
|
|
2236
2504
|
if (discriminator === "propagate" && (schema.oneOf ?? schema.anyOf) && schema.discriminator?.propertyName) discriminatorParentNodes.push(node);
|
|
2237
2505
|
}
|
|
2238
|
-
const circularNames = [...
|
|
2506
|
+
const circularNames = [...findCircularSchemasFromGraph(refGraph)];
|
|
2239
2507
|
const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
|
|
2240
2508
|
const operationNodes = [];
|
|
2241
|
-
for (const operation of getOperations(document)) {
|
|
2509
|
+
for (const operation of getOperations(document, refs)) {
|
|
2242
2510
|
const operationNode = parseOperation(parserOptions, operation);
|
|
2243
2511
|
if (operationNode) operationNodes.push(operationNode);
|
|
2244
2512
|
}
|
|
@@ -2300,19 +2568,34 @@ const adapterOas = createAdapter((options) => {
|
|
|
2300
2568
|
},
|
|
2301
2569
|
async validate(input, options) {
|
|
2302
2570
|
await assertInputExists(input);
|
|
2303
|
-
|
|
2571
|
+
const document = await parseDocument(input);
|
|
2572
|
+
assertDocument(document);
|
|
2573
|
+
await validateDocument(document, options);
|
|
2304
2574
|
},
|
|
2305
2575
|
async parse(source) {
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
document
|
|
2310
|
-
|
|
2311
|
-
|
|
2576
|
+
const cached = inputCache.get(source);
|
|
2577
|
+
if (cached) return cached;
|
|
2578
|
+
const promise = (async () => {
|
|
2579
|
+
const document = await parseFromConfig(source);
|
|
2580
|
+
assertDocument(document);
|
|
2581
|
+
if (validate) await validateDocument(document);
|
|
2582
|
+
parsedDocument = document;
|
|
2583
|
+
const refs = createRefs(document);
|
|
2584
|
+
const { schemas, renames } = getSchemas(document, { contentType }, refs);
|
|
2585
|
+
return parseInput({
|
|
2312
2586
|
document,
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2587
|
+
refs,
|
|
2588
|
+
schemas,
|
|
2589
|
+
parser: createSchemaParser({
|
|
2590
|
+
document,
|
|
2591
|
+
refs,
|
|
2592
|
+
contentType,
|
|
2593
|
+
renames
|
|
2594
|
+
})
|
|
2595
|
+
});
|
|
2596
|
+
})();
|
|
2597
|
+
inputCache.set(source, promise);
|
|
2598
|
+
return promise;
|
|
2316
2599
|
}
|
|
2317
2600
|
};
|
|
2318
2601
|
});
|