@kubb/adapter-oas 5.0.0-beta.100 → 5.0.0-beta.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1069 -918
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +1069 -918
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import "./rolldown-runtime-C0LytTxp.js";
|
|
2
2
|
import { ast, findCircularSchemas, narrowSchema } 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";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parse } from "yaml";
|
|
6
7
|
import { compileErrors, validate } from "@readme/openapi-parser";
|
|
7
8
|
import { upgrade } from "@scalar/openapi-upgrader";
|
|
8
9
|
import { bundle } from "api-ref-bundler";
|
|
9
|
-
import { parse } from "yaml";
|
|
10
10
|
import { childName, enumPropName, extractRefName, macroDiscriminatorEnum, macroEnumName, macroSimplifyUnion, mergeAdjacentObjectsLazy } from "@kubb/kit";
|
|
11
11
|
//#region src/constants.ts
|
|
12
12
|
/**
|
|
@@ -105,7 +105,7 @@ const enumExtensionKeys = ["x-enumNames", "x-enum-varnames"];
|
|
|
105
105
|
*/
|
|
106
106
|
const enumDescriptionKeys = ["x-enumDescriptions", "x-enum-descriptions"];
|
|
107
107
|
//#endregion
|
|
108
|
-
//#region src/discriminator.ts
|
|
108
|
+
//#region src/emit/discriminator/propagate.ts
|
|
109
109
|
/**
|
|
110
110
|
* Maps each child schema name to its discriminator patch data by scanning the given
|
|
111
111
|
* top-level AST schema nodes for union schemas that carry a `discriminatorPropertyName`.
|
|
@@ -181,42 +181,6 @@ function patchDiscriminatorNode(node, entry) {
|
|
|
181
181
|
properties: newProperties
|
|
182
182
|
};
|
|
183
183
|
}
|
|
184
|
-
/**
|
|
185
|
-
* Creates a single-property object schema used as a discriminator literal.
|
|
186
|
-
*
|
|
187
|
-
* @example
|
|
188
|
-
* ```ts
|
|
189
|
-
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
190
|
-
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
191
|
-
* ```
|
|
192
|
-
*/
|
|
193
|
-
function createDiscriminantNode({ propertyName, value }) {
|
|
194
|
-
return ast.factory.createSchema({
|
|
195
|
-
type: "object",
|
|
196
|
-
primitive: "object",
|
|
197
|
-
properties: [ast.factory.createProperty({
|
|
198
|
-
name: propertyName,
|
|
199
|
-
schema: ast.factory.createSchema({
|
|
200
|
-
type: "enum",
|
|
201
|
-
primitive: "string",
|
|
202
|
-
enumValues: [value]
|
|
203
|
-
}),
|
|
204
|
-
required: true
|
|
205
|
-
})]
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
210
|
-
*
|
|
211
|
-
* @example
|
|
212
|
-
* ```ts
|
|
213
|
-
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
214
|
-
* ```
|
|
215
|
-
*/
|
|
216
|
-
function findDiscriminator(mapping, ref) {
|
|
217
|
-
if (!mapping || !ref) return null;
|
|
218
|
-
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
219
|
-
}
|
|
220
184
|
//#endregion
|
|
221
185
|
//#region ../../internals/utils/src/casing.ts
|
|
222
186
|
/**
|
|
@@ -345,7 +309,7 @@ async function read(path) {
|
|
|
345
309
|
return readFile(path, { encoding: "utf8" });
|
|
346
310
|
}
|
|
347
311
|
//#endregion
|
|
348
|
-
//#region src/
|
|
312
|
+
//#region src/load/source.ts
|
|
349
313
|
const urlRegExp = /^https?:\/+/i;
|
|
350
314
|
async function readSource(sourcePath) {
|
|
351
315
|
if (urlRegExp.test(sourcePath)) {
|
|
@@ -356,12 +320,33 @@ async function readSource(sourcePath) {
|
|
|
356
320
|
}
|
|
357
321
|
return read(sourcePath);
|
|
358
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Reads and parses one source file or URL referenced during bundling: YAML/JSON is parsed into an
|
|
325
|
+
* object, Markdown is returned as-is (bundled inline rather than dereferenced).
|
|
326
|
+
*/
|
|
359
327
|
async function resolveSource(sourcePath) {
|
|
360
328
|
const data = await readSource(sourcePath);
|
|
361
329
|
if (sourcePath.toLowerCase().endsWith(".md")) return data;
|
|
362
330
|
return parse(data);
|
|
363
331
|
}
|
|
364
332
|
/**
|
|
333
|
+
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
334
|
+
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
335
|
+
* its parse error instead.
|
|
336
|
+
*/
|
|
337
|
+
async function assertInputExists(input) {
|
|
338
|
+
if (URL.canParse(input)) return;
|
|
339
|
+
if (!await exists(input)) throw new Diagnostics.Error({
|
|
340
|
+
code: Diagnostics.code.inputNotFound,
|
|
341
|
+
severity: "error",
|
|
342
|
+
message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
|
|
343
|
+
help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
|
|
344
|
+
location: { kind: "config" }
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
//#endregion
|
|
348
|
+
//#region src/load/normalize.ts
|
|
349
|
+
/**
|
|
365
350
|
* Bundles a multi-file OpenAPI document into a single document via `api-ref-bundler`.
|
|
366
351
|
*
|
|
367
352
|
* External file schemas are hoisted into named `components.schemas` entries, so a property
|
|
@@ -426,21 +411,6 @@ async function parseFromConfig(source) {
|
|
|
426
411
|
return parseDocument(resolved);
|
|
427
412
|
}
|
|
428
413
|
/**
|
|
429
|
-
* Throws a coded `KUBB_INPUT_NOT_FOUND` diagnostic when a local input path does not exist.
|
|
430
|
-
* URLs are skipped, and a malformed but readable file is left for `parseDocument` to surface
|
|
431
|
-
* its parse error instead.
|
|
432
|
-
*/
|
|
433
|
-
async function assertInputExists(input) {
|
|
434
|
-
if (URL.canParse(input)) return;
|
|
435
|
-
if (!await exists(input)) throw new Diagnostics.Error({
|
|
436
|
-
code: Diagnostics.code.inputNotFound,
|
|
437
|
-
severity: "error",
|
|
438
|
-
message: `Cannot read the file set as \`input\` (or via \`kubb generate PATH\`): ${input}`,
|
|
439
|
-
help: "Check that the path exists and is readable, then set it as `input` or pass it as `kubb generate PATH`.",
|
|
440
|
-
location: { kind: "config" }
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
414
|
* Validates an OpenAPI document using `@readme/openapi-parser` with colorized error output.
|
|
445
415
|
*
|
|
446
416
|
* @example
|
|
@@ -517,135 +487,272 @@ const jsonMimeFragments = [
|
|
|
517
487
|
function isJsonMimeType(mimeType) {
|
|
518
488
|
return jsonMimeFragments.some((fragment) => mimeType.includes(fragment));
|
|
519
489
|
}
|
|
520
|
-
//#endregion
|
|
521
|
-
//#region src/refs.ts
|
|
522
|
-
const _refCache = /* @__PURE__ */ new WeakMap();
|
|
523
490
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
* Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
|
|
527
|
-
* resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
|
|
528
|
-
* build there is no sink to collect it, so it throws instead.
|
|
491
|
+
* Picks a media-type entry from a `content` map: the first JSON-like media type, falling back to
|
|
492
|
+
* the first declared one. Returns `false` when `content` has no entries.
|
|
529
493
|
*
|
|
530
494
|
* @example
|
|
531
495
|
* ```ts
|
|
532
|
-
*
|
|
496
|
+
* pickContentEntry({ 'application/xml': xmlEntry, 'application/json': jsonEntry })
|
|
497
|
+
* // ['application/json', jsonEntry]
|
|
533
498
|
* ```
|
|
534
499
|
*/
|
|
535
|
-
function
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (!$ref.startsWith("#")) return null;
|
|
540
|
-
$ref = globalThis.decodeURIComponent($ref.substring(1));
|
|
541
|
-
let docCache = _refCache.get(document);
|
|
542
|
-
if (!docCache) {
|
|
543
|
-
docCache = /* @__PURE__ */ new Map();
|
|
544
|
-
_refCache.set(document, docCache);
|
|
545
|
-
}
|
|
546
|
-
if (docCache.has($ref)) return docCache.get($ref);
|
|
547
|
-
const current = $ref.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
|
|
548
|
-
if (!current) {
|
|
549
|
-
const diagnostic = {
|
|
550
|
-
code: Diagnostics.code.refNotFound,
|
|
551
|
-
severity: "error",
|
|
552
|
-
message: `Could not find a definition for ${origRef}.`,
|
|
553
|
-
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
554
|
-
location: {
|
|
555
|
-
kind: "schema",
|
|
556
|
-
pointer: origRef,
|
|
557
|
-
ref: origRef
|
|
558
|
-
}
|
|
559
|
-
};
|
|
560
|
-
if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
|
|
561
|
-
return null;
|
|
562
|
-
}
|
|
563
|
-
docCache.set($ref, current);
|
|
564
|
-
return current;
|
|
500
|
+
function pickContentEntry(content) {
|
|
501
|
+
const mediaTypes = Object.keys(content);
|
|
502
|
+
const available = mediaTypes.find(isJsonMimeType) ?? mediaTypes[0];
|
|
503
|
+
return available ? [available, content[available]] : false;
|
|
565
504
|
}
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/model/components.ts
|
|
566
507
|
/**
|
|
567
|
-
*
|
|
508
|
+
* Extracts the inline schema from a media-type `content` map.
|
|
568
509
|
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
510
|
+
* Prefers `preferredContentType` when given, otherwise uses the first key in the map.
|
|
511
|
+
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
571
512
|
*
|
|
572
513
|
* @example
|
|
573
514
|
* ```ts
|
|
574
|
-
*
|
|
575
|
-
* //
|
|
515
|
+
* extractSchemaFromContent(operation.content, 'application/json')
|
|
516
|
+
* // SchemaObject | null
|
|
576
517
|
* ```
|
|
577
518
|
*/
|
|
578
|
-
function
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
519
|
+
function extractSchemaFromContent(content, preferredContentType) {
|
|
520
|
+
if (!content) return null;
|
|
521
|
+
const firstContentType = Object.keys(content)[0] ?? "application/json";
|
|
522
|
+
const schema = content[preferredContentType ?? firstContentType]?.schema;
|
|
523
|
+
if (isReference(schema)) return null;
|
|
524
|
+
return schema ?? null;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
528
|
+
*/
|
|
529
|
+
function* collectRefs(schema) {
|
|
530
|
+
if (Array.isArray(schema)) {
|
|
531
|
+
for (const item of schema) yield* collectRefs(item);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (schema && typeof schema === "object") for (const key in schema) {
|
|
535
|
+
const value = schema[key];
|
|
536
|
+
if (!(key === "$ref" && typeof value === "string")) {
|
|
537
|
+
yield* collectRefs(value);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (value.startsWith("#/components/schemas/")) {
|
|
541
|
+
const name = value.slice(21);
|
|
542
|
+
if (name) yield name;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
585
545
|
}
|
|
586
546
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
547
|
+
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
548
|
+
*
|
|
549
|
+
* Referenced schemas appear before the schemas that depend on them, so code generators
|
|
550
|
+
* can emit types in the correct order. Cycles are silently skipped.
|
|
590
551
|
*
|
|
591
552
|
* @example
|
|
592
553
|
* ```ts
|
|
593
|
-
*
|
|
554
|
+
* const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
|
|
555
|
+
* // Pet appears before Order when Order.$ref points at Pet
|
|
594
556
|
* ```
|
|
595
557
|
*/
|
|
596
|
-
function
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
558
|
+
function sortSchemas(schemas) {
|
|
559
|
+
const deps = /* @__PURE__ */ new Map();
|
|
560
|
+
for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
|
|
561
|
+
const sorted = [];
|
|
562
|
+
const visited = /* @__PURE__ */ new Set();
|
|
563
|
+
function visit(name, stack) {
|
|
564
|
+
if (visited.has(name) || stack.has(name)) return;
|
|
565
|
+
stack.add(name);
|
|
566
|
+
for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
|
|
567
|
+
stack.delete(name);
|
|
568
|
+
visited.add(name);
|
|
569
|
+
sorted.push(name);
|
|
570
|
+
}
|
|
571
|
+
for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
|
|
572
|
+
const result = {};
|
|
573
|
+
for (const name of sorted) result[name] = schemas[name];
|
|
574
|
+
return result;
|
|
602
575
|
}
|
|
603
|
-
|
|
604
|
-
|
|
576
|
+
const semanticSuffixes = {
|
|
577
|
+
schemas: "Schema",
|
|
578
|
+
responses: "Response",
|
|
579
|
+
requestBodies: "Request"
|
|
580
|
+
};
|
|
605
581
|
/**
|
|
606
|
-
*
|
|
607
|
-
*
|
|
582
|
+
* Picks the collision suffix for one name-colliding schema: none when the name is unique,
|
|
583
|
+
* a semantic suffix (`Schema`, `Response`, `Request`) when the collision spans sources, otherwise
|
|
584
|
+
* a numeric suffix (`2`, `3`, …) for same-source collisions.
|
|
608
585
|
*/
|
|
609
|
-
function
|
|
610
|
-
|
|
586
|
+
function collisionSuffix({ isSingle, hasMultipleSources, source, index }) {
|
|
587
|
+
if (isSingle) return "";
|
|
588
|
+
if (hasMultipleSources) return semanticSuffixes[source];
|
|
589
|
+
if (index === 0) return "";
|
|
590
|
+
return String(index + 1);
|
|
611
591
|
}
|
|
612
592
|
/**
|
|
613
|
-
*
|
|
593
|
+
* Collects component schemas from one or more sources and resolves name collisions.
|
|
594
|
+
*
|
|
595
|
+
* Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
|
|
596
|
+
* topologically sorted by `$ref` dependency so generators emit types in the correct order.
|
|
597
|
+
*
|
|
598
|
+
* When two or more schemas normalize to the same PascalCase name:
|
|
599
|
+
* - Same source → numeric suffix (`2`, `3`, …).
|
|
600
|
+
* - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* ```ts
|
|
604
|
+
* const { schemas, renames } = getSchemas(document, { contentType: 'application/json' }, refs)
|
|
605
|
+
* ```
|
|
614
606
|
*/
|
|
615
|
-
function
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
607
|
+
function getSchemas(document, { contentType }, refs) {
|
|
608
|
+
const components = document.components;
|
|
609
|
+
function resolveSchemaRef(schema) {
|
|
610
|
+
if (!isReference(schema)) return schema;
|
|
611
|
+
const resolved = refs.resolve(schema.$ref);
|
|
612
|
+
return resolved && !isReference(resolved) ? resolved : schema;
|
|
613
|
+
}
|
|
614
|
+
const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
|
|
615
|
+
schema: resolveSchemaRef(schema),
|
|
616
|
+
source: "schemas",
|
|
617
|
+
originalName: name
|
|
618
|
+
})), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
619
|
+
const schema = extractSchemaFromContent(item.content, contentType);
|
|
620
|
+
return schema ? [{
|
|
621
|
+
schema: resolveSchemaRef(schema),
|
|
622
|
+
source,
|
|
623
|
+
originalName: name
|
|
624
|
+
}] : [];
|
|
625
|
+
}))];
|
|
626
|
+
const normalizedNames = /* @__PURE__ */ new Map();
|
|
627
|
+
for (const item of candidates) {
|
|
628
|
+
const key = pascalCase(item.originalName);
|
|
629
|
+
const bucket = normalizedNames.get(key) ?? [];
|
|
630
|
+
bucket.push(item);
|
|
631
|
+
normalizedNames.set(key, bucket);
|
|
632
|
+
}
|
|
633
|
+
const schemas = {};
|
|
634
|
+
const renames = /* @__PURE__ */ new Map();
|
|
635
|
+
for (const [, items] of normalizedNames) {
|
|
636
|
+
const isSingle = items.length === 1;
|
|
637
|
+
const hasMultipleSources = !isSingle && new Set(items.map((item) => item.source)).size > 1;
|
|
638
|
+
items.forEach((item, index) => {
|
|
639
|
+
const suffix = collisionSuffix({
|
|
640
|
+
isSingle,
|
|
641
|
+
hasMultipleSources,
|
|
642
|
+
source: item.source,
|
|
643
|
+
index
|
|
644
|
+
});
|
|
645
|
+
const uniqueName = item.originalName + suffix;
|
|
646
|
+
schemas[uniqueName] = item.schema;
|
|
647
|
+
if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
schemas: sortSchemas(schemas),
|
|
652
|
+
renames
|
|
653
|
+
};
|
|
619
654
|
}
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/model/server.ts
|
|
620
657
|
/**
|
|
621
|
-
*
|
|
658
|
+
* Reads the server URL from the document's `servers` array at `server.index`,
|
|
659
|
+
* interpolating any `server.variables` into the URL template.
|
|
660
|
+
*
|
|
661
|
+
* Returns `null` when `server.index` is omitted or out of range.
|
|
662
|
+
*
|
|
663
|
+
* @example Resolve the first server
|
|
664
|
+
* `resolveBaseUrl({ document, server: { index: 0 } })`
|
|
665
|
+
*
|
|
666
|
+
* @example Override a path variable
|
|
667
|
+
* `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
|
|
622
668
|
*/
|
|
623
|
-
function
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
return
|
|
669
|
+
function resolveBaseUrl({ document, server }) {
|
|
670
|
+
const index = server?.index;
|
|
671
|
+
const entry = index !== void 0 ? document.servers?.at(index) : void 0;
|
|
672
|
+
return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
|
|
627
673
|
}
|
|
628
674
|
/**
|
|
629
|
-
*
|
|
675
|
+
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
676
|
+
* Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
|
|
677
|
+
* Throws if an override value is not in the variable's `enum` list.
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* ```ts
|
|
681
|
+
* resolveServerUrl(
|
|
682
|
+
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
683
|
+
* { env: 'prod' },
|
|
684
|
+
* )
|
|
685
|
+
* // 'https://prod.api.example.com'
|
|
686
|
+
* ```
|
|
630
687
|
*/
|
|
631
|
-
function
|
|
632
|
-
|
|
688
|
+
function resolveServerUrl(server, overrides) {
|
|
689
|
+
if (!server.variables) return server.url;
|
|
690
|
+
let url = server.url;
|
|
691
|
+
for (const [key, variable] of Object.entries(server.variables)) {
|
|
692
|
+
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
693
|
+
if (value === void 0) continue;
|
|
694
|
+
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Diagnostics.Error({
|
|
695
|
+
code: Diagnostics.code.invalidServerVariable,
|
|
696
|
+
severity: "error",
|
|
697
|
+
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
698
|
+
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
699
|
+
location: {
|
|
700
|
+
kind: "document",
|
|
701
|
+
pointer: "#/servers"
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
url = url.replaceAll(`{${key}}`, value);
|
|
705
|
+
}
|
|
706
|
+
return url;
|
|
707
|
+
}
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region src/operation.ts
|
|
710
|
+
/**
|
|
711
|
+
* Slugifies a path for the `operationId` fallback: non-alphanumerics collapse to single dashes,
|
|
712
|
+
* with no leading or trailing dash.
|
|
713
|
+
*/
|
|
714
|
+
function slugify(value) {
|
|
715
|
+
return value.replace(/[^a-zA-Z0-9]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Returns the operation's `operationId`, falling back to `<method>_<slugified-path>` when absent.
|
|
719
|
+
*/
|
|
720
|
+
function getOperationId({ path, method, schema }) {
|
|
721
|
+
const { operationId } = schema;
|
|
722
|
+
if (typeof operationId === "string" && operationId.length > 0) return operationId;
|
|
723
|
+
return `${method}_${slugify(path).toLowerCase()}`;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Returns the declared response status codes, skipping `x-` extensions and non-object entries.
|
|
727
|
+
*/
|
|
728
|
+
function getResponseStatusCodes({ schema }) {
|
|
729
|
+
const responses = schema.responses;
|
|
730
|
+
if (!responses || isReference(responses)) return [];
|
|
731
|
+
return Object.keys(responses).filter((key) => !key.startsWith("x-") && !!responses[key] && typeof responses[key] === "object");
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Returns the response object for a status code, resolving a `$ref` through `refs`. `false` when absent.
|
|
735
|
+
*/
|
|
736
|
+
function getResponseByStatusCode({ operation, refs, statusCode }) {
|
|
737
|
+
const responses = operation.schema.responses;
|
|
633
738
|
if (!responses || isReference(responses)) return false;
|
|
634
|
-
return
|
|
635
|
-
document,
|
|
636
|
-
container: responses,
|
|
637
|
-
key: statusCode
|
|
638
|
-
}) ?? false;
|
|
739
|
+
return refs.deref(responses[statusCode]) ?? false;
|
|
639
740
|
}
|
|
640
741
|
/**
|
|
641
|
-
* Resolves the request body
|
|
742
|
+
* Resolves the operation's request body, dereferencing a `$ref` through `refs`. Returns `null`
|
|
743
|
+
* when the operation has no request body or it cannot be resolved.
|
|
744
|
+
*/
|
|
745
|
+
function getRequestBody({ operation, refs }) {
|
|
746
|
+
return refs.deref(operation.schema.requestBody);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Resolves the request body (a `$ref` through `refs`) and returns its content map, or
|
|
642
750
|
* `undefined` when the operation has no request body.
|
|
643
751
|
*/
|
|
644
|
-
function getRequestBodyContent({
|
|
645
|
-
return
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
key: "requestBody"
|
|
752
|
+
function getRequestBodyContent({ operation, refs }) {
|
|
753
|
+
return getRequestBody({
|
|
754
|
+
operation,
|
|
755
|
+
refs
|
|
649
756
|
})?.content;
|
|
650
757
|
}
|
|
651
758
|
/**
|
|
@@ -653,25 +760,23 @@ function getRequestBodyContent({ document, operation }) {
|
|
|
653
760
|
* Otherwise picks the first JSON-like media type, then the first declared one, as a
|
|
654
761
|
* `[mediaType, object]` tuple.
|
|
655
762
|
*/
|
|
656
|
-
function getRequestContent({
|
|
763
|
+
function getRequestContent({ operation, refs, mediaType }) {
|
|
657
764
|
const content = getRequestBodyContent({
|
|
658
|
-
|
|
659
|
-
|
|
765
|
+
operation,
|
|
766
|
+
refs
|
|
660
767
|
});
|
|
661
768
|
if (!content) return false;
|
|
662
769
|
if (mediaType) return mediaType in content ? content[mediaType] : false;
|
|
663
|
-
|
|
664
|
-
const available = mediaTypes.find((mt) => isJsonMimeType(mt)) ?? mediaTypes[0];
|
|
665
|
-
return available ? [available, content[available]] : false;
|
|
770
|
+
return pickContentEntry(content);
|
|
666
771
|
}
|
|
667
772
|
/**
|
|
668
773
|
* Returns the primary request content type. Prefers a JSON-like media type (the last one wins
|
|
669
774
|
* when several are declared), then the first declared one, defaulting to `'application/json'`.
|
|
670
775
|
*/
|
|
671
|
-
function getRequestContentType({
|
|
776
|
+
function getRequestContentType({ operation, refs }) {
|
|
672
777
|
const content = getRequestBodyContent({
|
|
673
|
-
|
|
674
|
-
|
|
778
|
+
operation,
|
|
779
|
+
refs
|
|
675
780
|
});
|
|
676
781
|
const mediaTypes = content ? Object.keys(content) : [];
|
|
677
782
|
let result = mediaTypes[0] ?? "application/json";
|
|
@@ -684,22 +789,18 @@ function getRequestContentType({ document, operation }) {
|
|
|
684
789
|
*
|
|
685
790
|
* @example
|
|
686
791
|
* ```ts
|
|
687
|
-
* for (const operation of getOperations(document)) {
|
|
792
|
+
* for (const operation of getOperations(document, refs)) {
|
|
688
793
|
* parseOperation(options, operation)
|
|
689
794
|
* }
|
|
690
795
|
* ```
|
|
691
796
|
*/
|
|
692
|
-
function getOperations(document) {
|
|
797
|
+
function getOperations(document, refs) {
|
|
693
798
|
const operations = [];
|
|
694
799
|
const paths = document.paths;
|
|
695
800
|
if (!paths) return operations;
|
|
696
801
|
for (const path of Object.keys(paths)) {
|
|
697
802
|
if (path.startsWith("x-")) continue;
|
|
698
|
-
const pathItem =
|
|
699
|
-
document,
|
|
700
|
-
container: paths,
|
|
701
|
-
key: path
|
|
702
|
-
});
|
|
803
|
+
const pathItem = refs.deref(paths[path]);
|
|
703
804
|
if (!pathItem) continue;
|
|
704
805
|
const item = pathItem;
|
|
705
806
|
for (const method of Object.keys(item)) {
|
|
@@ -709,65 +810,15 @@ function getOperations(document) {
|
|
|
709
810
|
operations.push({
|
|
710
811
|
path,
|
|
711
812
|
method,
|
|
712
|
-
schema
|
|
813
|
+
schema,
|
|
814
|
+
pathItem
|
|
713
815
|
});
|
|
714
816
|
}
|
|
715
817
|
}
|
|
716
818
|
return operations;
|
|
717
819
|
}
|
|
718
820
|
//#endregion
|
|
719
|
-
//#region src/
|
|
720
|
-
/**
|
|
721
|
-
* Reads the server URL from the document's `servers` array at `server.index`,
|
|
722
|
-
* interpolating any `server.variables` into the URL template.
|
|
723
|
-
*
|
|
724
|
-
* Returns `null` when `server.index` is omitted or out of range.
|
|
725
|
-
*
|
|
726
|
-
* @example Resolve the first server
|
|
727
|
-
* `resolveBaseUrl({ document, server: { index: 0 } })`
|
|
728
|
-
*
|
|
729
|
-
* @example Override a path variable
|
|
730
|
-
* `resolveBaseUrl({ document, server: { index: 0, variables: { version: 'v2' } } })`
|
|
731
|
-
*/
|
|
732
|
-
function resolveBaseUrl({ document, server }) {
|
|
733
|
-
const index = server?.index;
|
|
734
|
-
const entry = index !== void 0 ? document.servers?.at(index) : void 0;
|
|
735
|
-
return entry?.url ? resolveServerUrl(entry, server?.variables) : null;
|
|
736
|
-
}
|
|
737
|
-
/**
|
|
738
|
-
* Replaces `{variable}` placeholders in an OpenAPI server URL with provided values.
|
|
739
|
-
* Resolution order: `overrides[key]` → `variable.default` → left unreplaced.
|
|
740
|
-
* Throws if an override value is not in the variable's `enum` list.
|
|
741
|
-
*
|
|
742
|
-
* @example
|
|
743
|
-
* ```ts
|
|
744
|
-
* resolveServerUrl(
|
|
745
|
-
* { url: 'https://{env}.api.example.com', variables: { env: { default: 'dev', enum: ['dev', 'prod'] } } },
|
|
746
|
-
* { env: 'prod' },
|
|
747
|
-
* )
|
|
748
|
-
* // 'https://prod.api.example.com'
|
|
749
|
-
* ```
|
|
750
|
-
*/
|
|
751
|
-
function resolveServerUrl(server, overrides) {
|
|
752
|
-
if (!server.variables) return server.url;
|
|
753
|
-
let url = server.url;
|
|
754
|
-
for (const [key, variable] of Object.entries(server.variables)) {
|
|
755
|
-
const value = overrides?.[key] ?? (variable.default != null ? String(variable.default) : void 0);
|
|
756
|
-
if (value === void 0) continue;
|
|
757
|
-
if (variable.enum?.length && !variable.enum.some((e) => String(e) === value)) throw new Diagnostics.Error({
|
|
758
|
-
code: Diagnostics.code.invalidServerVariable,
|
|
759
|
-
severity: "error",
|
|
760
|
-
message: `Invalid server variable value '${value}' for '${key}' when resolving ${server.url}. Valid values are: ${variable.enum.join(", ")}.`,
|
|
761
|
-
help: `Use one of the allowed enum values, or drop the enum on the '${key}' server variable.`,
|
|
762
|
-
location: {
|
|
763
|
-
kind: "document",
|
|
764
|
-
pointer: "#/servers"
|
|
765
|
-
}
|
|
766
|
-
});
|
|
767
|
-
url = url.replaceAll(`{${key}}`, value);
|
|
768
|
-
}
|
|
769
|
-
return url;
|
|
770
|
-
}
|
|
821
|
+
//#region src/emit/schemaShape.ts
|
|
771
822
|
/**
|
|
772
823
|
* Returns the Kubb `SchemaType` for a given OAS `format` string, or `null` if not found.
|
|
773
824
|
* Formats not in `formatMap` (e.g., `int64`, `date-time`) are handled separately by parser options.
|
|
@@ -794,96 +845,57 @@ function getPrimitiveType(type) {
|
|
|
794
845
|
return "string";
|
|
795
846
|
}
|
|
796
847
|
/**
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
* Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
|
|
800
|
-
*
|
|
801
|
-
* @example
|
|
802
|
-
* ```ts
|
|
803
|
-
* getParameters(document, operation)
|
|
804
|
-
* // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
|
|
805
|
-
* ```
|
|
848
|
+
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
849
|
+
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
806
850
|
*/
|
|
807
|
-
function
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
851
|
+
function getDateType(options, format) {
|
|
852
|
+
if (!options.dateType) return null;
|
|
853
|
+
if (format === "date-time") {
|
|
854
|
+
if (options.dateType === "date") return {
|
|
855
|
+
type: "date",
|
|
856
|
+
representation: "date"
|
|
857
|
+
};
|
|
858
|
+
if (options.dateType === "stringOffset") return {
|
|
859
|
+
type: "datetime",
|
|
860
|
+
offset: true
|
|
861
|
+
};
|
|
862
|
+
if (options.dateType === "stringLocal") return {
|
|
863
|
+
type: "datetime",
|
|
864
|
+
local: true
|
|
865
|
+
};
|
|
866
|
+
return {
|
|
867
|
+
type: "datetime",
|
|
868
|
+
offset: false
|
|
869
|
+
};
|
|
825
870
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
for (const key in responses) derefInPlace({
|
|
835
|
-
document,
|
|
836
|
-
container: responses,
|
|
837
|
-
key
|
|
838
|
-
});
|
|
871
|
+
if (format === "date") return {
|
|
872
|
+
type: "date",
|
|
873
|
+
representation: options.dateType === "date" ? "date" : "string"
|
|
874
|
+
};
|
|
875
|
+
return {
|
|
876
|
+
type: "time",
|
|
877
|
+
representation: options.dateType === "date" ? "date" : "string"
|
|
878
|
+
};
|
|
839
879
|
}
|
|
840
880
|
/**
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
* @example
|
|
846
|
-
* ```ts
|
|
847
|
-
* getResponseSchema(document, operation, 200) // SchemaObject
|
|
848
|
-
* getResponseSchema(document, operation, '4XX') // {}
|
|
849
|
-
* ```
|
|
881
|
+
* Reads a schema's numeric `exclusiveMinimum`/`exclusiveMaximum` bounds (the OAS 3.1 numeric
|
|
882
|
+
* form). Either key is `undefined` when absent or, for the legacy OAS 3.0 boolean form, not a
|
|
883
|
+
* number.
|
|
850
884
|
*/
|
|
851
|
-
function
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
statusCode
|
|
857
|
-
}), options.contentType);
|
|
858
|
-
if (responseBody === false) return {};
|
|
859
|
-
const schema = responseBody.schema;
|
|
860
|
-
if (!schema) return {};
|
|
861
|
-
return dereferenceWithRef(document, schema);
|
|
885
|
+
function getExclusiveBounds(schema) {
|
|
886
|
+
return {
|
|
887
|
+
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
888
|
+
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
|
|
889
|
+
};
|
|
862
890
|
}
|
|
863
891
|
/**
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
* ```ts
|
|
868
|
-
* getRequestSchema(document, operation) // SchemaObject | null
|
|
869
|
-
* ```
|
|
892
|
+
* Reads schema examples as an array. OAS 3.1 uses an `examples` array, but specs (including ones
|
|
893
|
+
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
894
|
+
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
870
895
|
*/
|
|
871
|
-
function
|
|
872
|
-
if (
|
|
873
|
-
|
|
874
|
-
document,
|
|
875
|
-
operation,
|
|
876
|
-
mediaType: options.contentType
|
|
877
|
-
});
|
|
878
|
-
if (requestBody === false) return null;
|
|
879
|
-
const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
|
|
880
|
-
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
881
|
-
if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
|
|
882
|
-
type: "string",
|
|
883
|
-
contentMediaType: "application/octet-stream"
|
|
884
|
-
};
|
|
885
|
-
if (!schema) return null;
|
|
886
|
-
return dereferenceWithRef(document, schema);
|
|
896
|
+
function extractExamples(schema) {
|
|
897
|
+
if (Array.isArray(schema.examples)) return schema.examples;
|
|
898
|
+
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
887
899
|
}
|
|
888
900
|
/**
|
|
889
901
|
* Returns `true` when `fragment` carries any JSON Schema keyword that makes it
|
|
@@ -892,8 +904,7 @@ function getRequestSchema(document, operation, options = {}) {
|
|
|
892
904
|
* A fragment with a structural keyword can't be safely merged into a parent schema.
|
|
893
905
|
*/
|
|
894
906
|
function hasStructuralKeywords(fragment) {
|
|
895
|
-
|
|
896
|
-
return false;
|
|
907
|
+
return Object.keys(fragment).some((key) => structuralKeys.has(key));
|
|
897
908
|
}
|
|
898
909
|
/**
|
|
899
910
|
* Flattens a keyword-only `allOf` into its parent schema.
|
|
@@ -924,192 +935,15 @@ function flattenSchema(schema) {
|
|
|
924
935
|
for (const fragment of allOfFragments) for (const [key, value] of Object.entries(fragment)) merged[key] ??= value;
|
|
925
936
|
return merged;
|
|
926
937
|
}
|
|
938
|
+
//#endregion
|
|
939
|
+
//#region src/emit/createNode.ts
|
|
927
940
|
/**
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* Returns `null` when `content` is absent, the schema is missing, or the schema is a `$ref`.
|
|
932
|
-
*
|
|
933
|
-
* @example
|
|
934
|
-
* ```ts
|
|
935
|
-
* extractSchemaFromContent(operation.content, 'application/json')
|
|
936
|
-
* // SchemaObject | null
|
|
937
|
-
* ```
|
|
938
|
-
*/
|
|
939
|
-
function extractSchemaFromContent(content, preferredContentType) {
|
|
940
|
-
if (!content) return null;
|
|
941
|
-
const firstContentType = Object.keys(content)[0] ?? "application/json";
|
|
942
|
-
const schema = content[preferredContentType ?? firstContentType]?.schema;
|
|
943
|
-
if (schema && "$ref" in schema) return null;
|
|
944
|
-
return schema ?? null;
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* Walks a schema tree and collects the names of all `#/components/schemas/<name>` `$ref`s.
|
|
948
|
-
*/
|
|
949
|
-
function* collectRefs(schema) {
|
|
950
|
-
if (Array.isArray(schema)) {
|
|
951
|
-
for (const item of schema) yield* collectRefs(item);
|
|
952
|
-
return;
|
|
953
|
-
}
|
|
954
|
-
if (schema && typeof schema === "object") for (const key in schema) {
|
|
955
|
-
const value = schema[key];
|
|
956
|
-
if (!(key === "$ref" && typeof value === "string")) {
|
|
957
|
-
yield* collectRefs(value);
|
|
958
|
-
continue;
|
|
959
|
-
}
|
|
960
|
-
if (value.startsWith("#/components/schemas/")) {
|
|
961
|
-
const name = value.slice(21);
|
|
962
|
-
if (name) yield name;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Returns a copy of `schemas` topologically sorted by `$ref` dependency.
|
|
968
|
-
*
|
|
969
|
-
* Referenced schemas appear before the schemas that depend on them, so code generators
|
|
970
|
-
* can emit types in the correct order. Cycles are silently skipped.
|
|
971
|
-
*
|
|
972
|
-
* @example
|
|
973
|
-
* ```ts
|
|
974
|
-
* const sorted = sortSchemas({ Order: orderSchema, Pet: petSchema })
|
|
975
|
-
* // Pet appears before Order when Order.$ref points at Pet
|
|
976
|
-
* ```
|
|
977
|
-
*/
|
|
978
|
-
function sortSchemas(schemas) {
|
|
979
|
-
const deps = /* @__PURE__ */ new Map();
|
|
980
|
-
for (const [name, schema] of Object.entries(schemas)) deps.set(name, [...new Set(collectRefs(schema))]);
|
|
981
|
-
const sorted = [];
|
|
982
|
-
const visited = /* @__PURE__ */ new Set();
|
|
983
|
-
function visit(name, stack) {
|
|
984
|
-
if (visited.has(name) || stack.has(name)) return;
|
|
985
|
-
stack.add(name);
|
|
986
|
-
for (const child of deps.get(name) ?? []) if (deps.has(child)) visit(child, stack);
|
|
987
|
-
stack.delete(name);
|
|
988
|
-
visited.add(name);
|
|
989
|
-
sorted.push(name);
|
|
990
|
-
}
|
|
991
|
-
for (const name of Object.keys(schemas)) visit(name, /* @__PURE__ */ new Set());
|
|
992
|
-
const result = {};
|
|
993
|
-
for (const name of sorted) result[name] = schemas[name];
|
|
994
|
-
return result;
|
|
995
|
-
}
|
|
996
|
-
const semanticSuffixes = {
|
|
997
|
-
schemas: "Schema",
|
|
998
|
-
responses: "Response",
|
|
999
|
-
requestBodies: "Request"
|
|
1000
|
-
};
|
|
1001
|
-
function resolveSchemaRef(document, schema) {
|
|
1002
|
-
if (!isReference(schema)) return schema;
|
|
1003
|
-
const resolved = resolveRef(document, schema.$ref);
|
|
1004
|
-
return resolved && !isReference(resolved) ? resolved : schema;
|
|
1005
|
-
}
|
|
1006
|
-
/**
|
|
1007
|
-
* Collects component schemas from one or more sources and resolves name collisions.
|
|
1008
|
-
*
|
|
1009
|
-
* Sources default to `['schemas', 'requestBodies', 'responses']`. Returned schemas are
|
|
1010
|
-
* topologically sorted by `$ref` dependency so generators emit types in the correct order.
|
|
1011
|
-
*
|
|
1012
|
-
* When two or more schemas normalize to the same PascalCase name:
|
|
1013
|
-
* - Same source → numeric suffix (`2`, `3`, …).
|
|
1014
|
-
* - Different sources → semantic suffix (`Schema`, `Response`, `Request`).
|
|
1015
|
-
*
|
|
1016
|
-
* @example
|
|
1017
|
-
* ```ts
|
|
1018
|
-
* const { schemas, renames } = getSchemas(document, { contentType: 'application/json' })
|
|
1019
|
-
* ```
|
|
1020
|
-
*/
|
|
1021
|
-
function getSchemas(document, { contentType }) {
|
|
1022
|
-
const components = document.components;
|
|
1023
|
-
const candidates = [...Object.entries(components?.schemas ?? {}).map(([name, schema]) => ({
|
|
1024
|
-
schema: resolveSchemaRef(document, schema),
|
|
1025
|
-
source: "schemas",
|
|
1026
|
-
originalName: name
|
|
1027
|
-
})), ...["responses", "requestBodies"].flatMap((source) => Object.entries(components?.[source] ?? {}).flatMap(([name, item]) => {
|
|
1028
|
-
const schema = extractSchemaFromContent(item.content, contentType);
|
|
1029
|
-
return schema ? [{
|
|
1030
|
-
schema: resolveSchemaRef(document, schema),
|
|
1031
|
-
source,
|
|
1032
|
-
originalName: name
|
|
1033
|
-
}] : [];
|
|
1034
|
-
}))];
|
|
1035
|
-
const normalizedNames = /* @__PURE__ */ new Map();
|
|
1036
|
-
for (const item of candidates) {
|
|
1037
|
-
const key = pascalCase(item.originalName);
|
|
1038
|
-
const bucket = normalizedNames.get(key) ?? [];
|
|
1039
|
-
bucket.push(item);
|
|
1040
|
-
normalizedNames.set(key, bucket);
|
|
1041
|
-
}
|
|
1042
|
-
const schemas = {};
|
|
1043
|
-
const renames = /* @__PURE__ */ new Map();
|
|
1044
|
-
for (const [, items] of normalizedNames) {
|
|
1045
|
-
const isSingle = items.length === 1;
|
|
1046
|
-
let hasMultipleSources = false;
|
|
1047
|
-
if (!isSingle) {
|
|
1048
|
-
const firstSource = items[0].source;
|
|
1049
|
-
for (const item of items) if (item.source !== firstSource) {
|
|
1050
|
-
hasMultipleSources = true;
|
|
1051
|
-
break;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
items.forEach((item, index) => {
|
|
1055
|
-
const suffix = isSingle ? "" : hasMultipleSources ? semanticSuffixes[item.source] : index === 0 ? "" : String(index + 1);
|
|
1056
|
-
const uniqueName = item.originalName + suffix;
|
|
1057
|
-
schemas[uniqueName] = item.schema;
|
|
1058
|
-
if (suffix) renames.set(`#/components/${item.source}/${item.originalName}`, uniqueName);
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
return {
|
|
1062
|
-
schemas: sortSchemas(schemas),
|
|
1063
|
-
renames
|
|
1064
|
-
};
|
|
1065
|
-
}
|
|
1066
|
-
/**
|
|
1067
|
-
* Resolves the AST type descriptor for a date/time format, honoring the `dateType` option.
|
|
1068
|
-
* Returns `null` when `dateType: false`, so the format falls through to `string`.
|
|
1069
|
-
*/
|
|
1070
|
-
function getDateType(options, format) {
|
|
1071
|
-
if (!options.dateType) return null;
|
|
1072
|
-
if (format === "date-time") {
|
|
1073
|
-
if (options.dateType === "date") return {
|
|
1074
|
-
type: "date",
|
|
1075
|
-
representation: "date"
|
|
1076
|
-
};
|
|
1077
|
-
if (options.dateType === "stringOffset") return {
|
|
1078
|
-
type: "datetime",
|
|
1079
|
-
offset: true
|
|
1080
|
-
};
|
|
1081
|
-
if (options.dateType === "stringLocal") return {
|
|
1082
|
-
type: "datetime",
|
|
1083
|
-
local: true
|
|
1084
|
-
};
|
|
1085
|
-
return {
|
|
1086
|
-
type: "datetime",
|
|
1087
|
-
offset: false
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
if (format === "date") return {
|
|
1091
|
-
type: "date",
|
|
1092
|
-
representation: options.dateType === "date" ? "date" : "string"
|
|
1093
|
-
};
|
|
1094
|
-
return {
|
|
1095
|
-
type: "time",
|
|
1096
|
-
representation: options.dateType === "date" ? "date" : "string"
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Collects the shared metadata fields passed to every `createSchema` call.
|
|
941
|
+
* Builds a schema node from a converter's base context plus its type-specific fields. Every
|
|
942
|
+
* converter needs the same metadata fields (`title`, `description`, `examples`, ...) alongside
|
|
943
|
+
* whatever makes its node distinct; this folds both into one call.
|
|
1101
944
|
*/
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
* labeled 3.1) still use the singular OAS 3.0 `example`, which the upgrader only converts on the
|
|
1105
|
-
* 3.0 -> 3.1 hop. Normalize both into one array so the AST node exposes only `examples`.
|
|
1106
|
-
*/
|
|
1107
|
-
function extractExamples(schema) {
|
|
1108
|
-
if (Array.isArray(schema.examples)) return schema.examples;
|
|
1109
|
-
return schema.example !== void 0 ? [schema.example] : void 0;
|
|
1110
|
-
}
|
|
1111
|
-
function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
1112
|
-
return {
|
|
945
|
+
function createNode({ schema, name, nullable, defaultValue }, extras) {
|
|
946
|
+
return ast.factory.createSchema({
|
|
1113
947
|
name,
|
|
1114
948
|
nullable,
|
|
1115
949
|
title: schema.title,
|
|
@@ -1119,123 +953,164 @@ function buildSchemaNode(schema, name, nullable, defaultValue) {
|
|
|
1119
953
|
writeOnly: schema.writeOnly,
|
|
1120
954
|
default: defaultValue,
|
|
1121
955
|
examples: extractExamples(schema),
|
|
1122
|
-
format: schema.format
|
|
1123
|
-
|
|
956
|
+
format: schema.format,
|
|
957
|
+
...extras
|
|
958
|
+
});
|
|
1124
959
|
}
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/emit/discriminator/preserve.ts
|
|
1125
962
|
/**
|
|
1126
|
-
*
|
|
1127
|
-
*
|
|
1128
|
-
* The requestBody is dereferenced in place when it is a `$ref` (the same mutation that
|
|
1129
|
-
* `getRequestSchema` already performs), so the returned list accurately reflects the
|
|
1130
|
-
* available content types even for referenced bodies.
|
|
963
|
+
* Creates a single-property object schema used as a discriminator literal.
|
|
1131
964
|
*
|
|
1132
965
|
* @example
|
|
1133
966
|
* ```ts
|
|
1134
|
-
*
|
|
1135
|
-
* // ['
|
|
967
|
+
* createDiscriminantNode({ propertyName: 'type', value: 'dog' })
|
|
968
|
+
* // -> { type: 'object', properties: [{ name: 'type', required: true, schema: enum('dog') }] }
|
|
1136
969
|
* ```
|
|
1137
970
|
*/
|
|
1138
|
-
function
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
971
|
+
function createDiscriminantNode({ propertyName, value }) {
|
|
972
|
+
return ast.factory.createSchema({
|
|
973
|
+
type: "object",
|
|
974
|
+
primitive: "object",
|
|
975
|
+
properties: [ast.factory.createProperty({
|
|
976
|
+
name: propertyName,
|
|
977
|
+
schema: ast.factory.createSchema({
|
|
978
|
+
type: "enum",
|
|
979
|
+
primitive: "string",
|
|
980
|
+
enumValues: [value]
|
|
981
|
+
}),
|
|
982
|
+
required: true
|
|
983
|
+
})]
|
|
984
|
+
});
|
|
1143
985
|
}
|
|
1144
986
|
/**
|
|
1145
|
-
* Returns
|
|
1146
|
-
*
|
|
1147
|
-
* Response `$ref`s are resolved in place first (the same mutation `getResponseSchema` performs),
|
|
1148
|
-
* so the returned list reflects the available content types even for referenced responses.
|
|
987
|
+
* Returns the discriminator key whose mapping value matches `ref`, or `null` when there is no match.
|
|
1149
988
|
*
|
|
1150
989
|
* @example
|
|
1151
990
|
* ```ts
|
|
1152
|
-
*
|
|
1153
|
-
* // ['application/json', 'application/xml']
|
|
991
|
+
* findDiscriminator({ dog: '#/components/schemas/Dog' }, '#/components/schemas/Dog') // 'dog'
|
|
1154
992
|
* ```
|
|
1155
993
|
*/
|
|
1156
|
-
function
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
document,
|
|
1160
|
-
operation,
|
|
1161
|
-
statusCode
|
|
1162
|
-
});
|
|
1163
|
-
if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
|
|
1164
|
-
const body = responseObj;
|
|
1165
|
-
return body.content ? Object.keys(body.content) : [];
|
|
1166
|
-
}
|
|
1167
|
-
//#endregion
|
|
1168
|
-
//#region src/converters.ts
|
|
1169
|
-
/**
|
|
1170
|
-
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
1171
|
-
*
|
|
1172
|
-
* This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
|
|
1173
|
-
* from the array to its items sub-schema, so they are valid for downstream processing.
|
|
1174
|
-
*
|
|
1175
|
-
* @note A defensive measure for non-compliant specs.
|
|
1176
|
-
*/
|
|
1177
|
-
function normalizeArrayEnum(schema) {
|
|
1178
|
-
const normalizedItems = {
|
|
1179
|
-
...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
|
|
1180
|
-
enum: schema.enum
|
|
1181
|
-
};
|
|
1182
|
-
const { enum: _enum, ...schemaWithoutEnum } = schema;
|
|
1183
|
-
return {
|
|
1184
|
-
...schemaWithoutEnum,
|
|
1185
|
-
items: normalizedItems
|
|
1186
|
-
};
|
|
994
|
+
function findDiscriminator(mapping, ref) {
|
|
995
|
+
if (!mapping || !ref) return null;
|
|
996
|
+
return Object.entries(mapping).find(([, value]) => value === ref)?.[0] ?? null;
|
|
1187
997
|
}
|
|
1188
998
|
/**
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
999
|
+
* Narrows each `oneOf`/`anyOf` member with its discriminant value, intersecting the member's own
|
|
1000
|
+
* node with either the shared-properties slice carrying that value, or a synthetic discriminant
|
|
1001
|
+
* literal. The referenced child schema's own definition is left untouched — narrowing happens only
|
|
1002
|
+
* at this union usage site, which is what makes this mode "preserve" (as opposed to `propagate`,
|
|
1003
|
+
* which additionally patches the child schema's own definition in a post-pass).
|
|
1191
1004
|
*/
|
|
1192
|
-
function
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1005
|
+
function narrowUnionMembers({ unionMembers, discriminator, sharedPropertiesNode, parse, rawOptions, name, refs }) {
|
|
1006
|
+
function pickDiscriminatorPropertyNode(node, propertyName) {
|
|
1007
|
+
const discriminatorProperty = ast.narrowSchema(node, "object")?.properties?.find((property) => property.name === propertyName);
|
|
1008
|
+
if (!discriminatorProperty) return null;
|
|
1009
|
+
return ast.factory.createSchema({
|
|
1010
|
+
type: "object",
|
|
1011
|
+
primitive: "object",
|
|
1012
|
+
properties: [discriminatorProperty]
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
function implicitDiscriminantValue(member) {
|
|
1016
|
+
if (!discriminator || discriminator.mapping || !isReference(member)) return null;
|
|
1017
|
+
const value = extractRefName(member.$ref);
|
|
1018
|
+
if (!value) return null;
|
|
1019
|
+
const variant = refs.resolve(member.$ref, { report: false });
|
|
1020
|
+
if (!variant) return null;
|
|
1021
|
+
const propertyName = discriminator.propertyName;
|
|
1022
|
+
const seen = /* @__PURE__ */ new Set([member.$ref]);
|
|
1023
|
+
function constrains(v) {
|
|
1024
|
+
const prop = v.properties?.[propertyName];
|
|
1025
|
+
const resolved = prop && isReference(prop) ? refs.resolve(prop.$ref, { report: false }) : prop;
|
|
1026
|
+
if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
|
|
1027
|
+
const composition = v.allOf ?? v.oneOf ?? v.anyOf;
|
|
1028
|
+
if (!composition) return false;
|
|
1029
|
+
return composition.some((m) => {
|
|
1030
|
+
if (!isReference(m)) return constrains(m);
|
|
1031
|
+
if (seen.has(m.$ref)) return false;
|
|
1032
|
+
seen.add(m.$ref);
|
|
1033
|
+
const r = refs.resolve(m.$ref, { report: false });
|
|
1034
|
+
return r ? constrains(r) : false;
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
return constrains(variant) ? null : value;
|
|
1038
|
+
}
|
|
1039
|
+
return unionMembers.map((s) => {
|
|
1040
|
+
const ref = isReference(s) ? s.$ref : void 0;
|
|
1041
|
+
const discriminatorValue = findDiscriminator(discriminator?.mapping, ref) ?? implicitDiscriminantValue(s);
|
|
1042
|
+
const memberNode = parse({
|
|
1043
|
+
schema: s,
|
|
1044
|
+
name
|
|
1045
|
+
}, rawOptions);
|
|
1046
|
+
if (!discriminatorValue || !discriminator) return memberNode;
|
|
1047
|
+
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.applyMacros(sharedPropertiesNode, [macroDiscriminatorEnum({
|
|
1048
|
+
propertyName: discriminator.propertyName,
|
|
1049
|
+
values: [discriminatorValue]
|
|
1050
|
+
})], { depth: "shallow" }), discriminator.propertyName) : void 0;
|
|
1051
|
+
return ast.factory.createSchema({
|
|
1052
|
+
type: "intersection",
|
|
1053
|
+
members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
|
|
1054
|
+
propertyName: discriminator.propertyName,
|
|
1055
|
+
value: discriminatorValue
|
|
1056
|
+
})]
|
|
1057
|
+
});
|
|
1202
1058
|
});
|
|
1203
1059
|
}
|
|
1204
1060
|
/**
|
|
1205
|
-
*
|
|
1206
|
-
*
|
|
1061
|
+
* Filters the discriminated members out of an `allOf` list: an `allOf` member that `$ref`s a
|
|
1062
|
+
* discriminated union's parent, where this schema is itself one of that union's children, is
|
|
1063
|
+
* dropped from `members` and its discriminant value collected instead — the same synthetic
|
|
1064
|
+
* literal `narrowUnionMembers` produces, so the emitted node stays a plain intersection rather
|
|
1065
|
+
* than nesting the whole parent union one level deeper.
|
|
1207
1066
|
*/
|
|
1208
|
-
function
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1067
|
+
function extractDiscriminatedAllOfMembers({ allOfMembers, name, refs }) {
|
|
1068
|
+
const discriminantValues = [];
|
|
1069
|
+
return {
|
|
1070
|
+
members: allOfMembers.filter((item) => {
|
|
1071
|
+
if (!isReference(item) || !name) return true;
|
|
1072
|
+
const deref = refs.resolve(item.$ref);
|
|
1073
|
+
if (!deref || !isDiscriminator(deref)) return true;
|
|
1074
|
+
const parentUnion = deref.oneOf ?? deref.anyOf;
|
|
1075
|
+
if (!parentUnion) return true;
|
|
1076
|
+
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1077
|
+
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1078
|
+
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1079
|
+
if (inOneOf || inMapping) {
|
|
1080
|
+
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
|
|
1081
|
+
if (discriminatorValue) discriminantValues.push({
|
|
1082
|
+
propertyName: deref.discriminator.propertyName,
|
|
1083
|
+
value: discriminatorValue
|
|
1084
|
+
});
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
return true;
|
|
1088
|
+
}),
|
|
1089
|
+
discriminantValues
|
|
1090
|
+
};
|
|
1220
1091
|
}
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region src/emit/converters/composition.ts
|
|
1221
1094
|
/**
|
|
1222
1095
|
* Converts a `$ref` schema into a `RefSchemaNode`.
|
|
1223
1096
|
*
|
|
1224
1097
|
* The resolved schema is stored in `node.schema`. Usage-site sibling fields
|
|
1225
1098
|
* (description, readOnly, nullable, etc.) are stored directly on the ref node.
|
|
1226
1099
|
* Use `syncSchemaRef(node)` in printers to get a merged view of both.
|
|
1227
|
-
* Circular refs are detected in `
|
|
1100
|
+
* Circular refs are detected in `refs.resolveNode` and leave `schema` as `null`.
|
|
1228
1101
|
*/
|
|
1229
|
-
function convertRef({ schema, name, nullable, defaultValue, rawOptions, document,
|
|
1102
|
+
function convertRef({ schema, name, nullable, defaultValue, rawOptions, document, parse, refs, renames }) {
|
|
1230
1103
|
const refPath = schema.$ref;
|
|
1231
|
-
const resolvedSchema = refPath ?
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1104
|
+
const resolvedSchema = refPath ? refs.resolveNode(refPath, parse, rawOptions) : null;
|
|
1105
|
+
const ctx = {
|
|
1106
|
+
schema,
|
|
1107
|
+
name,
|
|
1108
|
+
nullable,
|
|
1109
|
+
defaultValue
|
|
1110
|
+
};
|
|
1111
|
+
if (refPath && document.components && !refs.exists(refPath)) return createNode(ctx, { type: "unknown" });
|
|
1236
1112
|
const targetName = renames?.get(schema.$ref);
|
|
1237
|
-
return
|
|
1238
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1113
|
+
return createNode(ctx, {
|
|
1239
1114
|
type: "ref",
|
|
1240
1115
|
name: extractRefName(schema.$ref),
|
|
1241
1116
|
ref: schema.$ref,
|
|
@@ -1246,7 +1121,7 @@ function convertRef({ schema, name, nullable, defaultValue, rawOptions, document
|
|
|
1246
1121
|
/**
|
|
1247
1122
|
* Converts an `allOf` schema into a flattened node or an `IntersectionSchemaNode`.
|
|
1248
1123
|
*/
|
|
1249
|
-
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
1124
|
+
function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
|
|
1250
1125
|
if (schema.allOf.length === 1 && !schema.properties && !(Array.isArray(schema.required) && schema.required.length) && schema.additionalProperties === void 0) {
|
|
1251
1126
|
const [memberSchema] = schema.allOf;
|
|
1252
1127
|
const memberNode = parse({
|
|
@@ -1271,26 +1146,12 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1271
1146
|
format: schema.format ?? memberNode.format
|
|
1272
1147
|
});
|
|
1273
1148
|
}
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
if (!parentUnion) return true;
|
|
1281
|
-
const childRef = `${SCHEMA_REF_PREFIX}${name}`;
|
|
1282
|
-
const inOneOf = parentUnion.some((oneOfItem) => isReference(oneOfItem) && oneOfItem.$ref === childRef);
|
|
1283
|
-
const inMapping = Object.values(deref.discriminator.mapping ?? {}).some((v) => v === childRef);
|
|
1284
|
-
if (inOneOf || inMapping) {
|
|
1285
|
-
const discriminatorValue = findDiscriminator(deref.discriminator.mapping, childRef);
|
|
1286
|
-
if (discriminatorValue) filteredDiscriminantValues.push({
|
|
1287
|
-
propertyName: deref.discriminator.propertyName,
|
|
1288
|
-
value: discriminatorValue
|
|
1289
|
-
});
|
|
1290
|
-
return false;
|
|
1291
|
-
}
|
|
1292
|
-
return true;
|
|
1293
|
-
}).map((s) => parse({
|
|
1149
|
+
const { members: discriminatedAllOf, discriminantValues } = extractDiscriminatedAllOfMembers({
|
|
1150
|
+
allOfMembers: schema.allOf,
|
|
1151
|
+
name,
|
|
1152
|
+
refs
|
|
1153
|
+
});
|
|
1154
|
+
const allOfMembers = discriminatedAllOf.map((s) => parse({
|
|
1294
1155
|
schema: s,
|
|
1295
1156
|
name
|
|
1296
1157
|
}, rawOptions));
|
|
@@ -1301,7 +1162,7 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1301
1162
|
if (missingRequired.length) {
|
|
1302
1163
|
const resolvedMembers = schema.allOf.flatMap((item) => {
|
|
1303
1164
|
if (!isReference(item)) return [item];
|
|
1304
|
-
const deref =
|
|
1165
|
+
const deref = refs.resolve(item.$ref);
|
|
1305
1166
|
return deref && !isReference(deref) ? [deref] : [];
|
|
1306
1167
|
});
|
|
1307
1168
|
for (const key of missingRequired) for (const resolved of resolvedMembers) {
|
|
@@ -1324,61 +1185,33 @@ function convertAllOf({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1324
1185
|
const { allOf: _allOf, ...schemaWithoutAllOf } = schema;
|
|
1325
1186
|
allOfMembers.push(parse({ schema: schemaWithoutAllOf }, rawOptions));
|
|
1326
1187
|
}
|
|
1327
|
-
for (const { propertyName, value } of
|
|
1188
|
+
for (const { propertyName, value } of discriminantValues) allOfMembers.push(createDiscriminantNode({
|
|
1328
1189
|
propertyName,
|
|
1329
1190
|
value
|
|
1330
1191
|
}));
|
|
1331
|
-
return
|
|
1192
|
+
return createNode({
|
|
1193
|
+
schema,
|
|
1194
|
+
name,
|
|
1195
|
+
nullable,
|
|
1196
|
+
defaultValue
|
|
1197
|
+
}, {
|
|
1332
1198
|
type: "intersection",
|
|
1333
|
-
members: [...mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))]
|
|
1334
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1199
|
+
members: [...mergeAdjacentObjectsLazy(allOfMembers.slice(0, syntheticStart)), ...mergeAdjacentObjectsLazy(allOfMembers.slice(syntheticStart))]
|
|
1335
1200
|
});
|
|
1336
1201
|
}
|
|
1337
1202
|
/**
|
|
1338
1203
|
* Converts a `oneOf` / `anyOf` schema into a `UnionSchemaNode`.
|
|
1339
1204
|
*/
|
|
1340
|
-
function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
properties: [discriminatorProperty]
|
|
1348
|
-
});
|
|
1349
|
-
}
|
|
1350
|
-
function resolveRefSilent($ref) {
|
|
1351
|
-
if (!$ref.startsWith("#")) return null;
|
|
1352
|
-
return decodeURIComponent($ref.substring(1)).split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document) ?? null;
|
|
1353
|
-
}
|
|
1354
|
-
function implicitDiscriminantValue(member) {
|
|
1355
|
-
if (!discriminator || discriminator.mapping || !isReference(member)) return null;
|
|
1356
|
-
const value = extractRefName(member.$ref);
|
|
1357
|
-
if (!value) return null;
|
|
1358
|
-
const variant = resolveRefSilent(member.$ref);
|
|
1359
|
-
if (!variant) return null;
|
|
1360
|
-
const propertyName = discriminator.propertyName;
|
|
1361
|
-
const seen = /* @__PURE__ */ new Set([member.$ref]);
|
|
1362
|
-
function constrains(v) {
|
|
1363
|
-
const prop = v.properties?.[propertyName];
|
|
1364
|
-
const resolved = prop && isReference(prop) ? resolveRefSilent(prop.$ref) : prop;
|
|
1365
|
-
if (resolved && (Array.isArray(resolved.enum) || resolved.const !== void 0)) return true;
|
|
1366
|
-
const composition = v.allOf ?? v.oneOf ?? v.anyOf;
|
|
1367
|
-
if (!composition) return false;
|
|
1368
|
-
return composition.some((m) => {
|
|
1369
|
-
if (!isReference(m)) return constrains(m);
|
|
1370
|
-
if (seen.has(m.$ref)) return false;
|
|
1371
|
-
seen.add(m.$ref);
|
|
1372
|
-
const r = resolveRefSilent(m.$ref);
|
|
1373
|
-
return r ? constrains(r) : false;
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
return constrains(variant) ? null : value;
|
|
1377
|
-
}
|
|
1205
|
+
function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse, refs }) {
|
|
1206
|
+
const ctx = {
|
|
1207
|
+
schema,
|
|
1208
|
+
name,
|
|
1209
|
+
nullable,
|
|
1210
|
+
defaultValue
|
|
1211
|
+
};
|
|
1378
1212
|
const unionMembers = [...schema.oneOf ?? [], ...schema.anyOf ?? []];
|
|
1379
1213
|
const strategy = schema.oneOf ? "one" : "any";
|
|
1380
|
-
const
|
|
1381
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1214
|
+
const unionExtras = {
|
|
1382
1215
|
discriminatorPropertyName: isDiscriminator(schema) ? schema.discriminator.propertyName : void 0,
|
|
1383
1216
|
strategy
|
|
1384
1217
|
};
|
|
@@ -1389,41 +1222,29 @@ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1389
1222
|
name
|
|
1390
1223
|
}, rawOptions) : void 0;
|
|
1391
1224
|
if (sharedPropertiesNode || discriminator) {
|
|
1392
|
-
const members =
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
const narrowedDiscriminatorNode = sharedPropertiesNode ? pickDiscriminatorPropertyNode(ast.applyMacros(sharedPropertiesNode, [macroDiscriminatorEnum({
|
|
1401
|
-
propertyName: discriminator.propertyName,
|
|
1402
|
-
values: [discriminatorValue]
|
|
1403
|
-
})], { depth: "shallow" }), discriminator.propertyName) : void 0;
|
|
1404
|
-
return ast.factory.createSchema({
|
|
1405
|
-
type: "intersection",
|
|
1406
|
-
members: [memberNode, narrowedDiscriminatorNode ?? createDiscriminantNode({
|
|
1407
|
-
propertyName: discriminator.propertyName,
|
|
1408
|
-
value: discriminatorValue
|
|
1409
|
-
})]
|
|
1410
|
-
});
|
|
1225
|
+
const members = narrowUnionMembers({
|
|
1226
|
+
unionMembers,
|
|
1227
|
+
discriminator,
|
|
1228
|
+
sharedPropertiesNode,
|
|
1229
|
+
parse,
|
|
1230
|
+
rawOptions,
|
|
1231
|
+
name,
|
|
1232
|
+
refs
|
|
1411
1233
|
});
|
|
1412
|
-
const unionNode =
|
|
1234
|
+
const unionNode = createNode(ctx, {
|
|
1413
1235
|
type: "union",
|
|
1414
|
-
...
|
|
1236
|
+
...unionExtras,
|
|
1415
1237
|
members
|
|
1416
1238
|
});
|
|
1417
1239
|
if (!sharedPropertiesNode) return unionNode;
|
|
1418
|
-
return
|
|
1240
|
+
return createNode(ctx, {
|
|
1419
1241
|
type: "intersection",
|
|
1420
|
-
...buildSchemaNode(schema, name, nullable, defaultValue),
|
|
1421
1242
|
members: [unionNode, sharedPropertiesNode]
|
|
1422
1243
|
});
|
|
1423
1244
|
}
|
|
1424
|
-
const unionNode =
|
|
1245
|
+
const unionNode = createNode(ctx, {
|
|
1425
1246
|
type: "union",
|
|
1426
|
-
...
|
|
1247
|
+
...unionExtras,
|
|
1427
1248
|
members: unionMembers.map((s) => parse({
|
|
1428
1249
|
schema: s,
|
|
1429
1250
|
name
|
|
@@ -1432,60 +1253,125 @@ function convertUnion({ schema, name, nullable, defaultValue, rawOptions, parse,
|
|
|
1432
1253
|
return ast.applyMacros(unionNode, [macroSimplifyUnion], { depth: "shallow" });
|
|
1433
1254
|
}
|
|
1434
1255
|
/**
|
|
1256
|
+
* Converts an OAS 3.1 multi-type array (e.g. `type: ['string', 'number']`) into a `UnionSchemaNode`.
|
|
1257
|
+
* Only called once the multi-type rule's `match` has confirmed more than one non-`null` type
|
|
1258
|
+
* remains; a single remaining type (e.g. `['string', 'null']`) is handled as that type instead,
|
|
1259
|
+
* with nullability already folded in.
|
|
1260
|
+
*/
|
|
1261
|
+
function convertMultiType({ schema, name, nullable, defaultValue, rawOptions, parse }) {
|
|
1262
|
+
const types = schema.type;
|
|
1263
|
+
const nonNullTypes = types.filter((t) => t !== "null");
|
|
1264
|
+
return createNode({
|
|
1265
|
+
schema,
|
|
1266
|
+
name,
|
|
1267
|
+
nullable: types.includes("null") || nullable || void 0,
|
|
1268
|
+
defaultValue
|
|
1269
|
+
}, {
|
|
1270
|
+
type: "union",
|
|
1271
|
+
members: nonNullTypes.map((t) => {
|
|
1272
|
+
return parse({
|
|
1273
|
+
schema: {
|
|
1274
|
+
...schema,
|
|
1275
|
+
type: t
|
|
1276
|
+
},
|
|
1277
|
+
name
|
|
1278
|
+
}, rawOptions);
|
|
1279
|
+
})
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
//#endregion
|
|
1283
|
+
//#region src/emit/converters/scalar.ts
|
|
1284
|
+
/**
|
|
1285
|
+
* Normalizes malformed `{ type: 'array', enum: [...] }` schemas by moving enum values into items.
|
|
1286
|
+
*
|
|
1287
|
+
* This pattern violates the OpenAPI spec but appears in real specs. The fix moves enum values
|
|
1288
|
+
* from the array to its items sub-schema, so they are valid for downstream processing.
|
|
1289
|
+
*
|
|
1290
|
+
* @note A defensive measure for non-compliant specs.
|
|
1291
|
+
*/
|
|
1292
|
+
function normalizeArrayEnum(schema) {
|
|
1293
|
+
const normalizedItems = {
|
|
1294
|
+
...typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : {},
|
|
1295
|
+
enum: schema.enum
|
|
1296
|
+
};
|
|
1297
|
+
const { enum: _enum, ...schemaWithoutEnum } = schema;
|
|
1298
|
+
return {
|
|
1299
|
+
...schemaWithoutEnum,
|
|
1300
|
+
items: normalizedItems
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Builds a `null` scalar node carrying the schema's documentation. Shared by the `const: null`
|
|
1305
|
+
* and the drf-spectacular `NullEnum` (`{ enum: [null] }`) branches, which render identically.
|
|
1306
|
+
*/
|
|
1307
|
+
function createNullNode(schema, name, nullable) {
|
|
1308
|
+
return ast.factory.createSchema({
|
|
1309
|
+
type: "null",
|
|
1310
|
+
primitive: "null",
|
|
1311
|
+
name,
|
|
1312
|
+
title: schema.title,
|
|
1313
|
+
description: schema.description,
|
|
1314
|
+
deprecated: schema.deprecated,
|
|
1315
|
+
nullable,
|
|
1316
|
+
format: schema.format
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1435
1320
|
* Converts an OAS 3.1 `const` schema into a null scalar or a single-value `EnumSchemaNode`.
|
|
1436
1321
|
*/
|
|
1437
1322
|
function convertConst({ schema, name, nullable, defaultValue }) {
|
|
1438
1323
|
const constValue = schema.const;
|
|
1439
1324
|
if (constValue === null) return createNullNode(schema, name);
|
|
1440
1325
|
const constPrimitive = getPrimitiveType(typeof constValue === "number" ? "number" : typeof constValue === "boolean" ? "boolean" : "string");
|
|
1441
|
-
return
|
|
1326
|
+
return createNode({
|
|
1327
|
+
schema,
|
|
1328
|
+
name,
|
|
1329
|
+
nullable,
|
|
1330
|
+
defaultValue
|
|
1331
|
+
}, {
|
|
1442
1332
|
type: "enum",
|
|
1443
1333
|
primitive: constPrimitive,
|
|
1444
|
-
enumValues: [constValue]
|
|
1445
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1334
|
+
enumValues: [constValue]
|
|
1446
1335
|
});
|
|
1447
1336
|
}
|
|
1448
1337
|
/**
|
|
1449
|
-
* Converts a format-annotated schema into a special-type `SchemaNode`.
|
|
1450
|
-
*
|
|
1338
|
+
* Converts a format-annotated schema into a special-type `SchemaNode`. Only called once the
|
|
1339
|
+
* `format` rule's `match` has confirmed the format is handled (see `isHandledFormat`) and, for
|
|
1340
|
+
* a date-ish format, that `dateType` is not `false`.
|
|
1451
1341
|
*/
|
|
1452
1342
|
function convertFormat({ schema, name, nullable, defaultValue, options }) {
|
|
1453
|
-
const
|
|
1454
|
-
|
|
1343
|
+
const ctx = {
|
|
1344
|
+
schema,
|
|
1345
|
+
name,
|
|
1346
|
+
nullable,
|
|
1347
|
+
defaultValue
|
|
1348
|
+
};
|
|
1349
|
+
if (schema.format === "int64") return createNode(ctx, {
|
|
1455
1350
|
type: options.integerType === "bigint" ? "bigint" : "integer",
|
|
1456
1351
|
primitive: "integer",
|
|
1457
|
-
...base,
|
|
1458
1352
|
min: schema.minimum,
|
|
1459
1353
|
max: schema.maximum,
|
|
1460
|
-
|
|
1461
|
-
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0
|
|
1354
|
+
...getExclusiveBounds(schema)
|
|
1462
1355
|
});
|
|
1463
1356
|
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") {
|
|
1464
1357
|
const dateType = getDateType(options, schema.format);
|
|
1465
|
-
if (
|
|
1466
|
-
if (dateType.type === "datetime") return ast.factory.createSchema({
|
|
1467
|
-
...base,
|
|
1358
|
+
if (dateType.type === "datetime") return createNode(ctx, {
|
|
1468
1359
|
primitive: "string",
|
|
1469
1360
|
type: "datetime",
|
|
1470
1361
|
offset: dateType.offset,
|
|
1471
1362
|
local: dateType.local
|
|
1472
1363
|
});
|
|
1473
|
-
return
|
|
1474
|
-
...base,
|
|
1364
|
+
return createNode(ctx, {
|
|
1475
1365
|
primitive: "string",
|
|
1476
1366
|
type: dateType.type,
|
|
1477
1367
|
representation: dateType.representation
|
|
1478
1368
|
});
|
|
1479
1369
|
}
|
|
1480
1370
|
const specialType = getSchemaType(schema.format);
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
const hasLength = specialType === "url" || specialType === "uuid" || specialType === "email";
|
|
1484
|
-
return ast.factory.createSchema({
|
|
1485
|
-
...base,
|
|
1486
|
-
primitive: specialPrimitive,
|
|
1371
|
+
return createNode(ctx, {
|
|
1372
|
+
primitive: specialType === "number" || specialType === "integer" || specialType === "bigint" ? specialType : "string",
|
|
1487
1373
|
type: specialType,
|
|
1488
|
-
...
|
|
1374
|
+
...specialType === "url" || specialType === "uuid" || specialType === "email" ? {
|
|
1489
1375
|
min: schema.minLength,
|
|
1490
1376
|
max: schema.maxLength
|
|
1491
1377
|
} : {}
|
|
@@ -1505,21 +1391,28 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
|
|
|
1505
1391
|
const enumNullable = nullable || nullInEnum || void 0;
|
|
1506
1392
|
const enumDefault = schema.default === null && enumNullable ? void 0 : schema.default;
|
|
1507
1393
|
const enumPrimitive = getPrimitiveType(type);
|
|
1508
|
-
const
|
|
1394
|
+
const ctx = {
|
|
1395
|
+
schema,
|
|
1396
|
+
name,
|
|
1397
|
+
nullable: enumNullable,
|
|
1398
|
+
defaultValue: enumDefault
|
|
1399
|
+
};
|
|
1400
|
+
const enumExtras = {
|
|
1509
1401
|
type: "enum",
|
|
1510
|
-
primitive: enumPrimitive
|
|
1511
|
-
...buildSchemaNode(schema, name, enumNullable, enumDefault)
|
|
1402
|
+
primitive: enumPrimitive
|
|
1512
1403
|
};
|
|
1513
1404
|
const extensionKey = enumExtensionKeys.find((key) => key in schema);
|
|
1514
1405
|
const descriptionKey = enumDescriptionKeys.find((key) => key in schema);
|
|
1515
1406
|
if (extensionKey || descriptionKey || enumPrimitive === "number" || enumPrimitive === "integer" || enumPrimitive === "boolean") {
|
|
1516
|
-
|
|
1407
|
+
let enumPrimitiveType = "string";
|
|
1408
|
+
if (enumPrimitive === "number" || enumPrimitive === "integer") enumPrimitiveType = "number";
|
|
1409
|
+
else if (enumPrimitive === "boolean") enumPrimitiveType = "boolean";
|
|
1517
1410
|
const rawEnumNames = extensionKey ? schema[extensionKey] : void 0;
|
|
1518
1411
|
const rawEnumDescriptions = descriptionKey ? schema[descriptionKey] : void 0;
|
|
1519
1412
|
const uniqueValues = [...new Set(filteredValues)];
|
|
1520
1413
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1521
|
-
return
|
|
1522
|
-
...
|
|
1414
|
+
return createNode(ctx, {
|
|
1415
|
+
...enumExtras,
|
|
1523
1416
|
primitive: enumPrimitiveType,
|
|
1524
1417
|
namedEnumValues: uniqueValues.map((value, index) => ({
|
|
1525
1418
|
name: String(rawEnumNames?.[index] ?? value),
|
|
@@ -1533,12 +1426,103 @@ function convertEnum({ schema, name, nullable, type, rawOptions, parse }) {
|
|
|
1533
1426
|
})
|
|
1534
1427
|
});
|
|
1535
1428
|
}
|
|
1536
|
-
return
|
|
1537
|
-
...
|
|
1429
|
+
return createNode(ctx, {
|
|
1430
|
+
...enumExtras,
|
|
1538
1431
|
enumValues: [...new Set(filteredValues)]
|
|
1539
1432
|
});
|
|
1540
1433
|
}
|
|
1541
1434
|
/**
|
|
1435
|
+
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
1436
|
+
*/
|
|
1437
|
+
function convertString({ schema, name, nullable, defaultValue }) {
|
|
1438
|
+
return createNode({
|
|
1439
|
+
schema,
|
|
1440
|
+
name,
|
|
1441
|
+
nullable,
|
|
1442
|
+
defaultValue
|
|
1443
|
+
}, {
|
|
1444
|
+
type: "string",
|
|
1445
|
+
primitive: "string",
|
|
1446
|
+
min: schema.minLength,
|
|
1447
|
+
max: schema.maxLength,
|
|
1448
|
+
pattern: schema.pattern
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
1453
|
+
*/
|
|
1454
|
+
function convertNumeric({ schema, name, nullable, defaultValue }, type) {
|
|
1455
|
+
return createNode({
|
|
1456
|
+
schema,
|
|
1457
|
+
name,
|
|
1458
|
+
nullable,
|
|
1459
|
+
defaultValue
|
|
1460
|
+
}, {
|
|
1461
|
+
type,
|
|
1462
|
+
primitive: type,
|
|
1463
|
+
min: schema.minimum,
|
|
1464
|
+
max: schema.maximum,
|
|
1465
|
+
...getExclusiveBounds(schema),
|
|
1466
|
+
multipleOf: schema.multipleOf
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Converts a `type: 'boolean'` schema.
|
|
1471
|
+
*/
|
|
1472
|
+
function convertBoolean({ schema, name, nullable, defaultValue }) {
|
|
1473
|
+
return createNode({
|
|
1474
|
+
schema,
|
|
1475
|
+
name,
|
|
1476
|
+
nullable,
|
|
1477
|
+
defaultValue
|
|
1478
|
+
}, {
|
|
1479
|
+
type: "boolean",
|
|
1480
|
+
primitive: "boolean"
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Converts a binary string schema (`type: 'string'`, `contentMediaType: 'application/octet-stream'`)
|
|
1485
|
+
* into a `blob` node.
|
|
1486
|
+
*/
|
|
1487
|
+
function convertBinary({ schema, name, nullable, defaultValue }) {
|
|
1488
|
+
return createNode({
|
|
1489
|
+
schema,
|
|
1490
|
+
name,
|
|
1491
|
+
nullable,
|
|
1492
|
+
defaultValue
|
|
1493
|
+
}, {
|
|
1494
|
+
type: "blob",
|
|
1495
|
+
primitive: "string"
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
//#endregion
|
|
1499
|
+
//#region src/emit/converters/structural.ts
|
|
1500
|
+
/**
|
|
1501
|
+
* Resolves a `true` or empty-object map schema (`additionalProperties`/`patternProperties`) to
|
|
1502
|
+
* `options.unknownType`, otherwise parses it as a regular schema.
|
|
1503
|
+
*/
|
|
1504
|
+
function resolveMapSchema(mapSchema, options, parse, rawOptions) {
|
|
1505
|
+
if (mapSchema === true || typeof mapSchema === "object" && Object.keys(mapSchema).length === 0) return ast.factory.createSchema({ type: options.unknownType });
|
|
1506
|
+
return parse({ schema: mapSchema }, rawOptions);
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Names the inline enums on a property's schema, and on each item when the property is a tuple, from
|
|
1510
|
+
* the parent and property name. Wraps `macroEnumName` at the property construction site.
|
|
1511
|
+
*/
|
|
1512
|
+
function nameEnums(node, options) {
|
|
1513
|
+
const macro = macroEnumName(options);
|
|
1514
|
+
const named = ast.applyMacros(node, [macro], { depth: "shallow" });
|
|
1515
|
+
const tupleNode = ast.narrowSchema(named, "tuple");
|
|
1516
|
+
if (tupleNode?.items) {
|
|
1517
|
+
const namedItems = tupleNode.items.map((item) => ast.applyMacros(item, [macro], { depth: "shallow" }));
|
|
1518
|
+
if (namedItems.some((item, i) => item !== tupleNode.items[i])) return {
|
|
1519
|
+
...tupleNode,
|
|
1520
|
+
items: namedItems
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
return named;
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1542
1526
|
* Converts an object-like schema into an `ObjectSchemaNode`.
|
|
1543
1527
|
*/
|
|
1544
1528
|
function convertObject({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
@@ -1564,146 +1548,84 @@ function convertObject({ schema, name, nullable, defaultValue, rawOptions, optio
|
|
|
1564
1548
|
});
|
|
1565
1549
|
}) : [];
|
|
1566
1550
|
const additionalProperties = schema.additionalProperties;
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
if (additionalProperties) return ast.factory.createSchema({ type: options.unknownType });
|
|
1572
|
-
})();
|
|
1551
|
+
let additionalPropertiesNode;
|
|
1552
|
+
if (additionalProperties === true) additionalPropertiesNode = true;
|
|
1553
|
+
else if (additionalProperties) additionalPropertiesNode = resolveMapSchema(additionalProperties, options, parse, rawOptions);
|
|
1554
|
+
else additionalPropertiesNode = additionalProperties;
|
|
1573
1555
|
const rawPatternProperties = "patternProperties" in schema ? schema.patternProperties : void 0;
|
|
1574
|
-
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern,
|
|
1575
|
-
const objectNode =
|
|
1556
|
+
const patternProperties = rawPatternProperties ? Object.fromEntries(Object.entries(rawPatternProperties).map(([pattern, patternSchema]) => [pattern, resolveMapSchema(patternSchema, options, parse, rawOptions)])) : void 0;
|
|
1557
|
+
const objectNode = createNode({
|
|
1558
|
+
schema,
|
|
1559
|
+
name,
|
|
1560
|
+
nullable,
|
|
1561
|
+
defaultValue
|
|
1562
|
+
}, {
|
|
1576
1563
|
type: "object",
|
|
1577
1564
|
primitive: "object",
|
|
1578
1565
|
properties,
|
|
1579
1566
|
additionalProperties: additionalPropertiesNode,
|
|
1580
1567
|
patternProperties,
|
|
1581
1568
|
minProperties: schema.minProperties,
|
|
1582
|
-
maxProperties: schema.maxProperties
|
|
1583
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1569
|
+
maxProperties: schema.maxProperties
|
|
1584
1570
|
});
|
|
1585
1571
|
if (isDiscriminator(schema) && schema.discriminator.mapping) {
|
|
1586
1572
|
const discPropName = schema.discriminator.propertyName;
|
|
1587
1573
|
const values = Object.keys(schema.discriminator.mapping);
|
|
1588
1574
|
const enumName = name ? enumPropName(name, discPropName, options.enumSuffix) : void 0;
|
|
1589
|
-
return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
|
|
1590
|
-
propertyName: discPropName,
|
|
1591
|
-
values,
|
|
1592
|
-
enumName
|
|
1593
|
-
})], { depth: "shallow" });
|
|
1594
|
-
}
|
|
1595
|
-
return objectNode;
|
|
1596
|
-
}
|
|
1597
|
-
/**
|
|
1598
|
-
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
1599
|
-
*/
|
|
1600
|
-
function convertTuple({ schema, name, nullable, defaultValue, rawOptions, parse }) {
|
|
1601
|
-
const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
|
|
1602
|
-
const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? ast.factory.createSchema({ type: "any" }) : parse({ schema: schema.items }, rawOptions);
|
|
1603
|
-
return ast.factory.createSchema({
|
|
1604
|
-
type: "tuple",
|
|
1605
|
-
primitive: "array",
|
|
1606
|
-
items: tupleItems,
|
|
1607
|
-
rest,
|
|
1608
|
-
min: schema.minItems,
|
|
1609
|
-
max: schema.maxItems,
|
|
1610
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
|
-
/**
|
|
1614
|
-
* Converts a `type: 'array'` schema into an `ArraySchemaNode`.
|
|
1615
|
-
*/
|
|
1616
|
-
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1617
|
-
const rawItems = schema.items;
|
|
1618
|
-
const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name;
|
|
1619
|
-
const items = rawItems ? [parse({
|
|
1620
|
-
schema: rawItems,
|
|
1621
|
-
name: itemName
|
|
1622
|
-
}, rawOptions)] : [];
|
|
1623
|
-
return ast.factory.createSchema({
|
|
1624
|
-
type: "array",
|
|
1625
|
-
primitive: "array",
|
|
1626
|
-
items,
|
|
1627
|
-
min: schema.minItems,
|
|
1628
|
-
max: schema.maxItems,
|
|
1629
|
-
unique: schema.uniqueItems ?? void 0,
|
|
1630
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
/**
|
|
1634
|
-
* Converts a `type: 'string'` schema into a `StringSchemaNode`.
|
|
1635
|
-
*/
|
|
1636
|
-
function convertString({ schema, name, nullable, defaultValue }) {
|
|
1637
|
-
return ast.factory.createSchema({
|
|
1638
|
-
type: "string",
|
|
1639
|
-
primitive: "string",
|
|
1640
|
-
min: schema.minLength,
|
|
1641
|
-
max: schema.maxLength,
|
|
1642
|
-
pattern: schema.pattern,
|
|
1643
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1646
|
-
/**
|
|
1647
|
-
* Converts a `type: 'number'` or `type: 'integer'` schema.
|
|
1648
|
-
*/
|
|
1649
|
-
function convertNumeric({ schema, name, nullable, defaultValue }, type) {
|
|
1650
|
-
return ast.factory.createSchema({
|
|
1651
|
-
type,
|
|
1652
|
-
primitive: type,
|
|
1653
|
-
min: schema.minimum,
|
|
1654
|
-
max: schema.maximum,
|
|
1655
|
-
exclusiveMinimum: typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : void 0,
|
|
1656
|
-
exclusiveMaximum: typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : void 0,
|
|
1657
|
-
multipleOf: schema.multipleOf,
|
|
1658
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1659
|
-
});
|
|
1660
|
-
}
|
|
1661
|
-
/**
|
|
1662
|
-
* Converts a `type: 'boolean'` schema.
|
|
1663
|
-
*/
|
|
1664
|
-
function convertBoolean({ schema, name, nullable, defaultValue }) {
|
|
1665
|
-
return ast.factory.createSchema({
|
|
1666
|
-
type: "boolean",
|
|
1667
|
-
primitive: "boolean",
|
|
1668
|
-
...buildSchemaNode(schema, name, nullable, defaultValue)
|
|
1669
|
-
});
|
|
1575
|
+
return ast.applyMacros(objectNode, [macroDiscriminatorEnum({
|
|
1576
|
+
propertyName: discPropName,
|
|
1577
|
+
values,
|
|
1578
|
+
enumName
|
|
1579
|
+
})], { depth: "shallow" });
|
|
1580
|
+
}
|
|
1581
|
+
return objectNode;
|
|
1670
1582
|
}
|
|
1671
1583
|
/**
|
|
1672
|
-
* Converts
|
|
1673
|
-
* into a `blob` node.
|
|
1584
|
+
* Converts an OAS 3.1 `prefixItems` tuple into a `TupleSchemaNode`.
|
|
1674
1585
|
*/
|
|
1675
|
-
function
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1586
|
+
function convertTuple({ schema, name, nullable, defaultValue, rawOptions, parse }) {
|
|
1587
|
+
const tupleItems = (schema.prefixItems ?? []).map((item) => parse({ schema: item }, rawOptions));
|
|
1588
|
+
const rest = schema.items === false ? void 0 : !schema.items || schema.items === true ? ast.factory.createSchema({ type: "any" }) : parse({ schema: schema.items }, rawOptions);
|
|
1589
|
+
return createNode({
|
|
1590
|
+
schema,
|
|
1591
|
+
name,
|
|
1592
|
+
nullable,
|
|
1593
|
+
defaultValue
|
|
1594
|
+
}, {
|
|
1595
|
+
type: "tuple",
|
|
1596
|
+
primitive: "array",
|
|
1597
|
+
items: tupleItems,
|
|
1598
|
+
rest,
|
|
1599
|
+
min: schema.minItems,
|
|
1600
|
+
max: schema.maxItems
|
|
1680
1601
|
});
|
|
1681
1602
|
}
|
|
1682
1603
|
/**
|
|
1683
|
-
* Converts
|
|
1684
|
-
*
|
|
1685
|
-
* Returns `null` when only one non-`null` type remains (e.g. `['string', 'null']`), so `parse`
|
|
1686
|
-
* falls through and handles it as that single type with nullability already folded in.
|
|
1604
|
+
* Converts a `type: 'array'` schema into an `ArraySchemaNode`.
|
|
1687
1605
|
*/
|
|
1688
|
-
function
|
|
1689
|
-
const
|
|
1690
|
-
const
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1606
|
+
function convertArray({ schema, name, nullable, defaultValue, rawOptions, options, parse }) {
|
|
1607
|
+
const rawItems = schema.items;
|
|
1608
|
+
const itemName = rawItems?.enum?.length && name ? enumPropName(null, name, options.enumSuffix) : name;
|
|
1609
|
+
const items = rawItems ? [parse({
|
|
1610
|
+
schema: rawItems,
|
|
1611
|
+
name: itemName
|
|
1612
|
+
}, rawOptions)] : [];
|
|
1613
|
+
return createNode({
|
|
1614
|
+
schema,
|
|
1615
|
+
name,
|
|
1616
|
+
nullable,
|
|
1617
|
+
defaultValue
|
|
1618
|
+
}, {
|
|
1619
|
+
type: "array",
|
|
1620
|
+
primitive: "array",
|
|
1621
|
+
items,
|
|
1622
|
+
min: schema.minItems,
|
|
1623
|
+
max: schema.maxItems,
|
|
1624
|
+
unique: schema.uniqueItems ?? void 0
|
|
1705
1625
|
});
|
|
1706
1626
|
}
|
|
1627
|
+
//#endregion
|
|
1628
|
+
//#region src/emit/parseSchema.ts
|
|
1707
1629
|
/**
|
|
1708
1630
|
* Ordered schema rule table. Order is significant: composition keywords (`$ref`, `allOf`,
|
|
1709
1631
|
* `oneOf`/`anyOf`) take precedence over `const`/`format`, which take precedence over the plain
|
|
@@ -1728,7 +1650,11 @@ const schemaRules = [
|
|
|
1728
1650
|
convert: convertConst
|
|
1729
1651
|
},
|
|
1730
1652
|
{
|
|
1731
|
-
match: ({ schema }) =>
|
|
1653
|
+
match: ({ schema, options }) => {
|
|
1654
|
+
if (!schema.format) return false;
|
|
1655
|
+
if (schema.format === "date-time" || schema.format === "date" || schema.format === "time") return options.dateType !== false;
|
|
1656
|
+
return isHandledFormat(schema.format);
|
|
1657
|
+
},
|
|
1732
1658
|
convert: convertFormat
|
|
1733
1659
|
},
|
|
1734
1660
|
{
|
|
@@ -1736,7 +1662,7 @@ const schemaRules = [
|
|
|
1736
1662
|
convert: convertBinary
|
|
1737
1663
|
},
|
|
1738
1664
|
{
|
|
1739
|
-
match: ({ schema }) => Array.isArray(schema.type) && schema.type.length > 1,
|
|
1665
|
+
match: ({ schema }) => Array.isArray(schema.type) && schema.type.filter((t) => t !== "null").length > 1,
|
|
1740
1666
|
convert: convertMultiType
|
|
1741
1667
|
},
|
|
1742
1668
|
{
|
|
@@ -1785,72 +1711,306 @@ const schemaRules = [
|
|
|
1785
1711
|
}
|
|
1786
1712
|
];
|
|
1787
1713
|
//#endregion
|
|
1788
|
-
//#region src/
|
|
1714
|
+
//#region src/refs.ts
|
|
1715
|
+
const _refCache = /* @__PURE__ */ new WeakMap();
|
|
1789
1716
|
/**
|
|
1790
|
-
*
|
|
1717
|
+
* Walks a local `#/...` JSON pointer against `document`, memoized per document. `applicable` is
|
|
1718
|
+
* `false` for an empty or non-local ref (the caller should not treat that as a failed lookup).
|
|
1719
|
+
* Shared by `resolveRef`'s reporting walk and `createRefs().resolve`'s silent walk, so both use
|
|
1720
|
+
* the same trimming and caching instead of two separate implementations.
|
|
1721
|
+
*/
|
|
1722
|
+
function walkPointer(document, $ref) {
|
|
1723
|
+
const trimmed = $ref.trim();
|
|
1724
|
+
if (trimmed === "" || !trimmed.startsWith("#")) return {
|
|
1725
|
+
applicable: false,
|
|
1726
|
+
value: null
|
|
1727
|
+
};
|
|
1728
|
+
const pointer = globalThis.decodeURIComponent(trimmed.substring(1));
|
|
1729
|
+
let docCache = _refCache.get(document);
|
|
1730
|
+
if (!docCache) {
|
|
1731
|
+
docCache = /* @__PURE__ */ new Map();
|
|
1732
|
+
_refCache.set(document, docCache);
|
|
1733
|
+
}
|
|
1734
|
+
if (docCache.has(pointer)) return {
|
|
1735
|
+
applicable: true,
|
|
1736
|
+
value: docCache.get(pointer)
|
|
1737
|
+
};
|
|
1738
|
+
const current = pointer.split("/").filter(Boolean).reduce((obj, key) => obj?.[key], document);
|
|
1739
|
+
if (current) docCache.set(pointer, current);
|
|
1740
|
+
return {
|
|
1741
|
+
applicable: true,
|
|
1742
|
+
value: current ?? null
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Resolves a local JSON pointer reference from a document.
|
|
1791
1747
|
*
|
|
1792
|
-
*
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
* `parse` function passed to it, so this file only wires state to the converters.
|
|
1748
|
+
* Accepts `#/...` refs. Returns `null` for an empty or non-local ref. When the pointer cannot be
|
|
1749
|
+
* resolved, reports a `refNotFound` diagnostic into the active build and returns `null`. Outside a
|
|
1750
|
+
* build there is no sink to collect it, so it throws instead.
|
|
1796
1751
|
*
|
|
1797
|
-
* @
|
|
1752
|
+
* @example
|
|
1753
|
+
* ```ts
|
|
1754
|
+
* resolveRef<SchemaObject>(document, '#/components/schemas/Pet')
|
|
1755
|
+
* ```
|
|
1798
1756
|
*/
|
|
1799
|
-
function
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1757
|
+
function resolveRef(document, $ref) {
|
|
1758
|
+
const { applicable, value } = walkPointer(document, $ref);
|
|
1759
|
+
if (!applicable) return null;
|
|
1760
|
+
if (value) return value;
|
|
1761
|
+
const diagnostic = {
|
|
1762
|
+
code: Diagnostics.code.refNotFound,
|
|
1763
|
+
severity: "error",
|
|
1764
|
+
message: `Could not find a definition for ${$ref}.`,
|
|
1765
|
+
help: "Add the schema under `components.schemas`, or fix the `$ref`. Run `kubb validate` to check the spec.",
|
|
1766
|
+
location: {
|
|
1767
|
+
kind: "schema",
|
|
1768
|
+
pointer: $ref,
|
|
1769
|
+
ref: $ref
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
if (!Diagnostics.report(diagnostic)) throw new Diagnostics.Error(diagnostic);
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Resolves a `$ref` object while preserving the original `$ref` field on the result.
|
|
1777
|
+
*
|
|
1778
|
+
* Useful for parser flows that need both dereferenced fields and pointer
|
|
1779
|
+
* identity (for naming/import purposes). Non-reference values are returned as-is.
|
|
1780
|
+
*
|
|
1781
|
+
* @example
|
|
1782
|
+
* ```ts
|
|
1783
|
+
* dereferenceWithRef(document, { $ref: '#/components/schemas/Pet' })
|
|
1784
|
+
* // { $ref: '#/components/schemas/Pet', type: 'object', properties: { ... } }
|
|
1785
|
+
* ```
|
|
1786
|
+
*/
|
|
1787
|
+
function dereferenceWithRef(document, schema) {
|
|
1788
|
+
if (isReference(schema)) return {
|
|
1789
|
+
...schema,
|
|
1790
|
+
...resolveRef(document, schema.$ref),
|
|
1791
|
+
$ref: schema.$ref
|
|
1792
|
+
};
|
|
1793
|
+
return schema;
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Creates the `$ref` resolution service for one document.
|
|
1797
|
+
*
|
|
1798
|
+
* Replaces what used to be six overlapping resolvers (a reporting walk, a silent walk, an
|
|
1799
|
+
* existence check, and a resolve-then-parse-into-a-node step, each with its own cache) with one
|
|
1800
|
+
* pointer walk and one explicit `report` contract for a missing ref: `report: true` (the default)
|
|
1801
|
+
* reports a `refNotFound` diagnostic (or throws outside a build), `report: false` resolves to
|
|
1802
|
+
* `null` silently for a speculative lookup.
|
|
1803
|
+
*
|
|
1804
|
+
* @example
|
|
1805
|
+
* ```ts
|
|
1806
|
+
* const refs = createRefs(document)
|
|
1807
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet')
|
|
1808
|
+
* refs.resolve<SchemaObject>('#/components/schemas/Pet', { report: false })
|
|
1809
|
+
* refs.exists('#/components/schemas/Pet')
|
|
1810
|
+
* refs.resolveNode('#/components/schemas/Pet', parseSchema)
|
|
1811
|
+
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
1812
|
+
* ```
|
|
1813
|
+
*/
|
|
1814
|
+
function createRefs(document) {
|
|
1815
|
+
const resolvedNodeCache = /* @__PURE__ */ new Map();
|
|
1816
|
+
const existenceCache = /* @__PURE__ */ new Map();
|
|
1805
1817
|
const resolvingRefs = /* @__PURE__ */ new Set();
|
|
1806
1818
|
/**
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
*
|
|
1810
|
-
*
|
|
1811
|
-
* since one schema can be referenced from dozens of parents, each re-walking its whole subtree.
|
|
1812
|
-
* Memoizing by ref path drops the work from O(2^depth) to O(N) unique schema names.
|
|
1819
|
+
* Resolves a local `#/...` JSON pointer. Returns `null` for an empty or non-local ref.
|
|
1820
|
+
* `report: true` (default) reports a `refNotFound` diagnostic into the active build (or throws
|
|
1821
|
+
* outside one) when the pointer cannot be resolved. `report: false` resolves to `null` silently,
|
|
1822
|
+
* for a speculative lookup where a missing ref is not an error.
|
|
1813
1823
|
*/
|
|
1814
|
-
|
|
1824
|
+
function resolve(refPath, options) {
|
|
1825
|
+
if (options?.report === false) {
|
|
1826
|
+
const { applicable, value } = walkPointer(document, refPath);
|
|
1827
|
+
return applicable ? value : null;
|
|
1828
|
+
}
|
|
1829
|
+
return resolveRef(document, refPath);
|
|
1830
|
+
}
|
|
1815
1831
|
/**
|
|
1816
|
-
*
|
|
1832
|
+
* Returns `true` when a `$ref` path resolves to a component the document actually defines.
|
|
1817
1833
|
* A circular ref still resolves to an existing target, so this stays `true` for cycles and only
|
|
1818
|
-
* goes `false` for a `$ref` that points at a component the spec never declares.
|
|
1834
|
+
* goes `false` for a `$ref` that points at a component the spec never declares. Memoized.
|
|
1819
1835
|
*/
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
let exists = false;
|
|
1824
|
-
try {
|
|
1825
|
-
exists = !!resolveRef(document, refPath);
|
|
1826
|
-
} catch {
|
|
1827
|
-
exists = false;
|
|
1828
|
-
}
|
|
1829
|
-
refExistence.set(refPath, exists);
|
|
1830
|
-
}
|
|
1831
|
-
return refExistence.get(refPath) ?? false;
|
|
1836
|
+
function exists(refPath) {
|
|
1837
|
+
if (!existenceCache.has(refPath)) existenceCache.set(refPath, !!resolve(refPath, { report: false }));
|
|
1838
|
+
return existenceCache.get(refPath) ?? false;
|
|
1832
1839
|
}
|
|
1833
1840
|
/**
|
|
1834
|
-
* Resolves a `$ref` to its parsed node
|
|
1835
|
-
* Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
1836
|
-
* (e.g. a minimal document in a unit test).
|
|
1841
|
+
* Resolves a `$ref` to its parsed node via `parse`, guarding against cycles and memoizing per
|
|
1842
|
+
* instance. Returns `null` when the ref is currently being resolved (a cycle) or cannot be
|
|
1843
|
+
* resolved (e.g. a minimal document in a unit test).
|
|
1837
1844
|
*/
|
|
1838
|
-
function
|
|
1845
|
+
function resolveNode(refPath, parse, rawOptions) {
|
|
1839
1846
|
if (resolvingRefs.has(refPath)) return null;
|
|
1840
|
-
if (!
|
|
1847
|
+
if (!resolvedNodeCache.has(refPath)) {
|
|
1841
1848
|
let resolved = null;
|
|
1842
1849
|
try {
|
|
1843
|
-
const referenced =
|
|
1850
|
+
const referenced = resolve(refPath);
|
|
1844
1851
|
if (referenced) {
|
|
1845
1852
|
resolvingRefs.add(refPath);
|
|
1846
|
-
resolved =
|
|
1853
|
+
resolved = parse({ schema: referenced }, rawOptions);
|
|
1847
1854
|
resolvingRefs.delete(refPath);
|
|
1848
1855
|
}
|
|
1849
1856
|
} catch {}
|
|
1850
|
-
|
|
1857
|
+
resolvedNodeCache.set(refPath, resolved);
|
|
1851
1858
|
}
|
|
1852
|
-
return
|
|
1859
|
+
return resolvedNodeCache.get(refPath) ?? null;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Resolves a `$ref` value without mutating anything: when `value` holds a `$ref`, returns the
|
|
1863
|
+
* resolved target. Returns `null` when the value is empty, cannot be resolved, or is still a
|
|
1864
|
+
* `$ref` after resolving (e.g. a document with no component registry). A non-`$ref` value is
|
|
1865
|
+
* returned as-is.
|
|
1866
|
+
*
|
|
1867
|
+
* @example
|
|
1868
|
+
* ```ts
|
|
1869
|
+
* refs.deref<ResponseObject>(operation.schema.responses?.['200'])
|
|
1870
|
+
* ```
|
|
1871
|
+
*/
|
|
1872
|
+
function deref(value) {
|
|
1873
|
+
if (!isReference(value)) return value ? value : null;
|
|
1874
|
+
const resolved = resolve(value.$ref);
|
|
1875
|
+
return resolved && !isReference(resolved) ? resolved : null;
|
|
1853
1876
|
}
|
|
1877
|
+
return {
|
|
1878
|
+
resolve,
|
|
1879
|
+
exists,
|
|
1880
|
+
resolveNode,
|
|
1881
|
+
deref
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
//#endregion
|
|
1885
|
+
//#region src/model/operations.ts
|
|
1886
|
+
/**
|
|
1887
|
+
* Returns all parameters for an operation, merging path-level and operation-level entries.
|
|
1888
|
+
* Operation-level parameters override path-level ones with the same `in:name` key.
|
|
1889
|
+
* Each `$ref` parameter is dereferenced via `dereferenceWithRef` before merging.
|
|
1890
|
+
*
|
|
1891
|
+
* @example
|
|
1892
|
+
* ```ts
|
|
1893
|
+
* getParameters({ document, operation })
|
|
1894
|
+
* // [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }]
|
|
1895
|
+
* ```
|
|
1896
|
+
*/
|
|
1897
|
+
function getParameters({ document, operation }) {
|
|
1898
|
+
const resolveParams = (params) => params.map((p) => dereferenceWithRef(document, p)).filter((p) => !!p && typeof p === "object" && "in" in p && "name" in p);
|
|
1899
|
+
const operationParams = resolveParams(operation.schema?.parameters || []);
|
|
1900
|
+
const pathLevelParams = resolveParams(operation.pathItem.parameters ?? []);
|
|
1901
|
+
const paramMap = /* @__PURE__ */ new Map();
|
|
1902
|
+
for (const p of pathLevelParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
1903
|
+
for (const p of operationParams) if (p.name && p.in) paramMap.set(`${p.in}:${p.name}`, p);
|
|
1904
|
+
return Array.from(paramMap.values());
|
|
1905
|
+
}
|
|
1906
|
+
function getResponseBody(responseBody, contentType) {
|
|
1907
|
+
if (!responseBody) return false;
|
|
1908
|
+
if (isReference(responseBody)) return false;
|
|
1909
|
+
const body = responseBody;
|
|
1910
|
+
if (!body.content) return false;
|
|
1911
|
+
if (contentType) return contentType in body.content ? body.content[contentType] : false;
|
|
1912
|
+
const picked = pickContentEntry(body.content);
|
|
1913
|
+
return picked ? picked[1] : false;
|
|
1914
|
+
}
|
|
1915
|
+
/**
|
|
1916
|
+
* Returns the response schema for a given operation and HTTP status code.
|
|
1917
|
+
*
|
|
1918
|
+
* Returns an empty object `{}` when no response body schema is available.
|
|
1919
|
+
*
|
|
1920
|
+
* @example
|
|
1921
|
+
* ```ts
|
|
1922
|
+
* getResponseSchema({ document, operation, refs, statusCode: 200 }) // SchemaObject
|
|
1923
|
+
* getResponseSchema({ document, operation, refs, statusCode: '4XX' }) // {}
|
|
1924
|
+
* ```
|
|
1925
|
+
*/
|
|
1926
|
+
function getResponseSchema({ document, operation, refs, statusCode, options = {} }) {
|
|
1927
|
+
const responseBody = getResponseBody(getResponseByStatusCode({
|
|
1928
|
+
operation,
|
|
1929
|
+
refs,
|
|
1930
|
+
statusCode
|
|
1931
|
+
}), options.contentType);
|
|
1932
|
+
if (responseBody === false) return {};
|
|
1933
|
+
const schema = responseBody.schema;
|
|
1934
|
+
if (!schema) return {};
|
|
1935
|
+
return dereferenceWithRef(document, schema);
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Returns the request body schema for an operation, or `null` when absent.
|
|
1939
|
+
*
|
|
1940
|
+
* @example
|
|
1941
|
+
* ```ts
|
|
1942
|
+
* getRequestSchema({ document, operation, refs }) // SchemaObject | null
|
|
1943
|
+
* ```
|
|
1944
|
+
*/
|
|
1945
|
+
function getRequestSchema({ document, operation, refs, options = {} }) {
|
|
1946
|
+
const requestBody = getRequestContent({
|
|
1947
|
+
operation,
|
|
1948
|
+
refs,
|
|
1949
|
+
mediaType: options.contentType
|
|
1950
|
+
});
|
|
1951
|
+
if (requestBody === false) return null;
|
|
1952
|
+
const mediaType = Array.isArray(requestBody) ? requestBody[0] : options.contentType;
|
|
1953
|
+
const schema = Array.isArray(requestBody) ? requestBody[1].schema : requestBody.schema;
|
|
1954
|
+
if (mediaType === "application/octet-stream" && (!schema || Object.keys(schema).length === 0)) return {
|
|
1955
|
+
type: "string",
|
|
1956
|
+
contentMediaType: "application/octet-stream"
|
|
1957
|
+
};
|
|
1958
|
+
if (!schema) return null;
|
|
1959
|
+
return dereferenceWithRef(document, schema);
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Returns all request body content type keys for an operation, resolving a `$ref` requestBody
|
|
1963
|
+
* through `refs`.
|
|
1964
|
+
*
|
|
1965
|
+
* @example
|
|
1966
|
+
* ```ts
|
|
1967
|
+
* getRequestBodyContentTypes(operation, refs)
|
|
1968
|
+
* // ['application/json', 'multipart/form-data']
|
|
1969
|
+
* ```
|
|
1970
|
+
*/
|
|
1971
|
+
function getRequestBodyContentTypes(operation, refs) {
|
|
1972
|
+
const body = getRequestBody({
|
|
1973
|
+
operation,
|
|
1974
|
+
refs
|
|
1975
|
+
});
|
|
1976
|
+
return body?.content ? Object.keys(body.content) : [];
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Returns all response content type keys for an operation at a given status code, resolving the
|
|
1980
|
+
* response `$ref` through `refs`.
|
|
1981
|
+
*
|
|
1982
|
+
* @example
|
|
1983
|
+
* ```ts
|
|
1984
|
+
* getResponseBodyContentTypes(operation, refs, 200)
|
|
1985
|
+
* // ['application/json', 'application/xml']
|
|
1986
|
+
* ```
|
|
1987
|
+
*/
|
|
1988
|
+
function getResponseBodyContentTypes(operation, refs, statusCode) {
|
|
1989
|
+
const responseObj = getResponseByStatusCode({
|
|
1990
|
+
operation,
|
|
1991
|
+
refs,
|
|
1992
|
+
statusCode
|
|
1993
|
+
});
|
|
1994
|
+
if (!responseObj || typeof responseObj !== "object" || isReference(responseObj)) return [];
|
|
1995
|
+
const body = responseObj;
|
|
1996
|
+
return body.content ? Object.keys(body.content) : [];
|
|
1997
|
+
}
|
|
1998
|
+
//#endregion
|
|
1999
|
+
//#region src/parser.ts
|
|
2000
|
+
/**
|
|
2001
|
+
* Creates the schema and operation converters bound to one OpenAPI document.
|
|
2002
|
+
*
|
|
2003
|
+
* Takes the `$ref` service for this document (shared with the rest of the pipeline, see
|
|
2004
|
+
* `adapter.ts`) and owns the `parseSchema` recursion seam, then dispatches each schema through
|
|
2005
|
+
* the ordered `schemaRules` table from `emit/parseSchema.ts`. Every converter is a standalone
|
|
2006
|
+
* function that recurses through the `parse` function passed to it, so this file only wires
|
|
2007
|
+
* state to the converters.
|
|
2008
|
+
*
|
|
2009
|
+
* @internal
|
|
2010
|
+
*/
|
|
2011
|
+
function createSchemaParser(ctx) {
|
|
2012
|
+
const document = ctx.document;
|
|
2013
|
+
const refs = ctx.refs;
|
|
1854
2014
|
/**
|
|
1855
2015
|
* Converts an OAS `SchemaObject` into a `SchemaNode`.
|
|
1856
2016
|
*
|
|
@@ -1879,15 +2039,10 @@ function createSchemaParser(ctx) {
|
|
|
1879
2039
|
options,
|
|
1880
2040
|
parse: parseSchema,
|
|
1881
2041
|
document,
|
|
1882
|
-
|
|
1883
|
-
refExists,
|
|
2042
|
+
refs,
|
|
1884
2043
|
renames: ctx.renames
|
|
1885
2044
|
};
|
|
1886
|
-
for (const rule of schemaRules)
|
|
1887
|
-
if (!rule.match(context)) continue;
|
|
1888
|
-
const node = rule.convert(context);
|
|
1889
|
-
if (node) return node;
|
|
1890
|
-
}
|
|
2045
|
+
for (const rule of schemaRules) if (rule.match(context)) return rule.convert(context);
|
|
1891
2046
|
const emptyType = options.emptySchemaType;
|
|
1892
2047
|
return ast.factory.createSchema({
|
|
1893
2048
|
type: emptyType,
|
|
@@ -1924,10 +2079,14 @@ function createSchemaParser(ctx) {
|
|
|
1924
2079
|
}
|
|
1925
2080
|
/**
|
|
1926
2081
|
* Reads the inline `requestBody` metadata (description / required) that OAS exposes
|
|
1927
|
-
* outside the schema itself
|
|
2082
|
+
* outside the schema itself, resolving a `$ref` requestBody through `refs`. Returns an
|
|
2083
|
+
* empty object when the request body is missing or cannot be resolved.
|
|
1928
2084
|
*/
|
|
1929
2085
|
function getRequestBodyMeta(operation) {
|
|
1930
|
-
const body =
|
|
2086
|
+
const body = getRequestBody({
|
|
2087
|
+
operation,
|
|
2088
|
+
refs
|
|
2089
|
+
});
|
|
1931
2090
|
if (!body) return { required: false };
|
|
1932
2091
|
return {
|
|
1933
2092
|
description: body.description,
|
|
@@ -1953,12 +2112,20 @@ function createSchemaParser(ctx) {
|
|
|
1953
2112
|
function parseOperation(options, operation) {
|
|
1954
2113
|
const operationId = getOperationId(operation);
|
|
1955
2114
|
const operationName = operationId ? pascalCase(operationId) : void 0;
|
|
1956
|
-
const parameters = getParameters(
|
|
1957
|
-
|
|
2115
|
+
const parameters = getParameters({
|
|
2116
|
+
document,
|
|
2117
|
+
operation
|
|
2118
|
+
}).map((param) => parseParameter(options, param, operationName));
|
|
2119
|
+
const allContentTypes = ctx.contentType ? [ctx.contentType] : getRequestBodyContentTypes(operation, refs);
|
|
1958
2120
|
const requestBodyMeta = getRequestBodyMeta(operation);
|
|
1959
2121
|
const requestBodyName = operationName ? `${operationName}Request` : void 0;
|
|
1960
2122
|
const content = allContentTypes.flatMap((ct) => {
|
|
1961
|
-
const schema = getRequestSchema(
|
|
2123
|
+
const schema = getRequestSchema({
|
|
2124
|
+
document,
|
|
2125
|
+
operation,
|
|
2126
|
+
refs,
|
|
2127
|
+
options: { contentType: ct }
|
|
2128
|
+
});
|
|
1962
2129
|
if (!schema) return [];
|
|
1963
2130
|
return [ast.factory.createContent({
|
|
1964
2131
|
contentType: ct,
|
|
@@ -1976,14 +2143,20 @@ function createSchemaParser(ctx) {
|
|
|
1976
2143
|
} : void 0;
|
|
1977
2144
|
const responses = getResponseStatusCodes(operation).map((statusCode) => {
|
|
1978
2145
|
const responseObj = getResponseByStatusCode({
|
|
1979
|
-
document,
|
|
1980
2146
|
operation,
|
|
2147
|
+
refs,
|
|
1981
2148
|
statusCode
|
|
1982
2149
|
});
|
|
1983
2150
|
const responseName = operationName ? `${operationName}Status${statusCode}` : void 0;
|
|
1984
2151
|
const description = typeof responseObj === "object" && responseObj !== null ? responseObj.description : void 0;
|
|
1985
2152
|
const parseEntrySchema = (contentType) => {
|
|
1986
|
-
const raw = getResponseSchema(
|
|
2153
|
+
const raw = getResponseSchema({
|
|
2154
|
+
document,
|
|
2155
|
+
operation,
|
|
2156
|
+
refs,
|
|
2157
|
+
statusCode,
|
|
2158
|
+
options: { contentType }
|
|
2159
|
+
});
|
|
1987
2160
|
return {
|
|
1988
2161
|
schema: raw && Object.keys(raw).length > 0 ? parseSchema({
|
|
1989
2162
|
schema: raw,
|
|
@@ -1992,14 +2165,14 @@ function createSchemaParser(ctx) {
|
|
|
1992
2165
|
keysToOmit: collectPropertyKeysByFlag(raw, "writeOnly")
|
|
1993
2166
|
};
|
|
1994
2167
|
};
|
|
1995
|
-
const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(
|
|
2168
|
+
const content = (ctx.contentType ? [ctx.contentType] : getResponseBodyContentTypes(operation, refs, statusCode)).map((contentType) => ast.factory.createContent({
|
|
1996
2169
|
contentType,
|
|
1997
2170
|
...parseEntrySchema(contentType)
|
|
1998
2171
|
}));
|
|
1999
2172
|
if (content.length === 0) content.push(ast.factory.createContent({
|
|
2000
2173
|
contentType: getRequestContentType({
|
|
2001
|
-
|
|
2002
|
-
|
|
2174
|
+
operation,
|
|
2175
|
+
refs
|
|
2003
2176
|
}) || "application/json",
|
|
2004
2177
|
...parseEntrySchema(ctx.contentType)
|
|
2005
2178
|
}));
|
|
@@ -2009,12 +2182,10 @@ function createSchemaParser(ctx) {
|
|
|
2009
2182
|
content
|
|
2010
2183
|
});
|
|
2011
2184
|
});
|
|
2012
|
-
const pathItem = document.paths?.[operation.path];
|
|
2013
|
-
const pathItemDoc = pathItem && !isReference(pathItem) ? pathItem : void 0;
|
|
2014
2185
|
const pickDoc = (key) => {
|
|
2015
2186
|
const own = operation.schema[key];
|
|
2016
2187
|
if (typeof own === "string") return own;
|
|
2017
|
-
const fallback =
|
|
2188
|
+
const fallback = operation.pathItem[key];
|
|
2018
2189
|
return typeof fallback === "string" ? fallback : void 0;
|
|
2019
2190
|
};
|
|
2020
2191
|
return ast.factory.createOperation({
|
|
@@ -2183,40 +2354,8 @@ const adapterOas = createAdapter((options) => {
|
|
|
2183
2354
|
enumSuffix
|
|
2184
2355
|
};
|
|
2185
2356
|
let parsedDocument = null;
|
|
2186
|
-
const
|
|
2187
|
-
|
|
2188
|
-
const schemaParserCache = /* @__PURE__ */ new WeakMap();
|
|
2189
|
-
function ensureDocument(source) {
|
|
2190
|
-
const cached = documentCache.get(source);
|
|
2191
|
-
if (cached) return cached;
|
|
2192
|
-
const promise = (async () => {
|
|
2193
|
-
const fresh = await parseFromConfig(source);
|
|
2194
|
-
if (validate) await validateDocument(fresh);
|
|
2195
|
-
parsedDocument = fresh;
|
|
2196
|
-
return fresh;
|
|
2197
|
-
})();
|
|
2198
|
-
documentCache.set(source, promise);
|
|
2199
|
-
return promise;
|
|
2200
|
-
}
|
|
2201
|
-
function ensureSchemas(document) {
|
|
2202
|
-
const cached = schemasCache.get(document);
|
|
2203
|
-
if (cached) return cached;
|
|
2204
|
-
const result = getSchemas(document, { contentType });
|
|
2205
|
-
schemasCache.set(document, result);
|
|
2206
|
-
return result;
|
|
2207
|
-
}
|
|
2208
|
-
function ensureSchemaParser({ document, renames }) {
|
|
2209
|
-
const cached = schemaParserCache.get(document);
|
|
2210
|
-
if (cached) return cached;
|
|
2211
|
-
const parser = createSchemaParser({
|
|
2212
|
-
document,
|
|
2213
|
-
contentType,
|
|
2214
|
-
renames
|
|
2215
|
-
});
|
|
2216
|
-
schemaParserCache.set(document, parser);
|
|
2217
|
-
return parser;
|
|
2218
|
-
}
|
|
2219
|
-
function parseInput({ document, schemas, parser }) {
|
|
2357
|
+
const inputCache = /* @__PURE__ */ new WeakMap();
|
|
2358
|
+
function parseInput({ document, refs, schemas, parser }) {
|
|
2220
2359
|
const { parseSchema, parseOperation } = parser;
|
|
2221
2360
|
const parsedByName = /* @__PURE__ */ new Map();
|
|
2222
2361
|
const refAliasMap = /* @__PURE__ */ new Map();
|
|
@@ -2239,7 +2378,7 @@ const adapterOas = createAdapter((options) => {
|
|
|
2239
2378
|
const circularNames = [...findCircularSchemas([...parsedByName.values()])];
|
|
2240
2379
|
const discriminatorChildMap = discriminatorParentNodes.length > 0 ? buildDiscriminatorChildMap(discriminatorParentNodes) : null;
|
|
2241
2380
|
const operationNodes = [];
|
|
2242
|
-
for (const operation of getOperations(document)) {
|
|
2381
|
+
for (const operation of getOperations(document, refs)) {
|
|
2243
2382
|
const operationNode = parseOperation(parserOptions, operation);
|
|
2244
2383
|
if (operationNode) operationNodes.push(operationNode);
|
|
2245
2384
|
}
|
|
@@ -2304,16 +2443,28 @@ const adapterOas = createAdapter((options) => {
|
|
|
2304
2443
|
await validateDocument(await parseDocument(input), options);
|
|
2305
2444
|
},
|
|
2306
2445
|
async parse(source) {
|
|
2307
|
-
const
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
document
|
|
2311
|
-
|
|
2312
|
-
|
|
2446
|
+
const cached = inputCache.get(source);
|
|
2447
|
+
if (cached) return cached;
|
|
2448
|
+
const promise = (async () => {
|
|
2449
|
+
const document = await parseFromConfig(source);
|
|
2450
|
+
if (validate) await validateDocument(document);
|
|
2451
|
+
parsedDocument = document;
|
|
2452
|
+
const refs = createRefs(document);
|
|
2453
|
+
const { schemas, renames } = getSchemas(document, { contentType }, refs);
|
|
2454
|
+
return parseInput({
|
|
2313
2455
|
document,
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2456
|
+
refs,
|
|
2457
|
+
schemas,
|
|
2458
|
+
parser: createSchemaParser({
|
|
2459
|
+
document,
|
|
2460
|
+
refs,
|
|
2461
|
+
contentType,
|
|
2462
|
+
renames
|
|
2463
|
+
})
|
|
2464
|
+
});
|
|
2465
|
+
})();
|
|
2466
|
+
inputCache.set(source, promise);
|
|
2467
|
+
return promise;
|
|
2317
2468
|
}
|
|
2318
2469
|
};
|
|
2319
2470
|
});
|