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