@workos/oagen 0.30.2 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -12
- package/dist/cli/index.d.mts +1 -1
- package/dist/cli/index.mjs +2885 -651
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +85 -36
- package/dist/compat/index.d.mts.map +1 -1
- package/dist/compat/index.mjs +33 -23
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-BO5FieX5.mjs → diff-CAsypxXV.mjs} +107 -89
- package/dist/{diff-BO5FieX5.mjs.map → diff-CAsypxXV.mjs.map} +1 -1
- package/dist/differ-B0DSuKUZ.d.mts +72 -0
- package/dist/differ-B0DSuKUZ.d.mts.map +1 -0
- package/dist/index.d.mts +129 -61
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2877 -546
- package/dist/index.mjs.map +1 -1
- package/dist/{overlay-DWAHPofk.mjs → overlay-Dj3WWlNP.mjs} +44 -38
- package/dist/overlay-Dj3WWlNP.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.d.mts +1 -1
- package/dist/scripts/smoke/baseline.mjs +2740 -527
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/scripts/smoke/sdk-test.d.mts +1 -1
- package/dist/scripts/smoke/sdk-test.mjs +4 -2
- package/dist/scripts/smoke/sdk-test.mjs.map +1 -1
- package/dist/smoke.d.mts +251 -36
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +2740 -527
- package/dist/smoke.mjs.map +1 -1
- package/dist/{tree-sitter-CDKm3n64.mjs → tree-sitter-C7Hk-NDu.mjs} +35 -7
- package/dist/tree-sitter-C7Hk-NDu.mjs.map +1 -0
- package/dist/{types-BcbGLNkh.d.mts → types-I7W7CNl1.d.mts} +219 -9
- package/dist/types-I7W7CNl1.d.mts.map +1 -0
- package/dist/verify/index.d.mts +7 -8
- package/dist/verify/index.d.mts.map +1 -1
- package/dist/verify/index.mjs +24 -14
- package/dist/verify/index.mjs.map +1 -1
- package/docs/architecture/emitter-contract.md +1 -0
- package/docs/architecture/ir-types.md +170 -17
- package/docs/cli.md +13 -13
- package/package.json +3 -3
- package/skills/check-emitter-parity/SKILL.md +15 -1
- package/dist/differ-nZSxhCfC.d.mts +0 -123
- package/dist/differ-nZSxhCfC.d.mts.map +0 -1
- package/dist/overlay-DWAHPofk.mjs.map +0 -1
- package/dist/tree-sitter-CDKm3n64.mjs.map +0 -1
- package/dist/types-BcbGLNkh.d.mts.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -7,11 +7,11 @@ import * as path$1 from "node:path";
|
|
|
7
7
|
import path, { relative, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { BaseResolver, Oas3_1Types, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
10
11
|
import Parser from "tree-sitter";
|
|
11
12
|
import { execFile, execFileSync } from "node:child_process";
|
|
12
13
|
import { promisify } from "node:util";
|
|
13
14
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { createHash } from "node:crypto";
|
|
15
15
|
//#region src/ir/sdk-behavior.ts
|
|
16
16
|
/**
|
|
17
17
|
* Canonical SDK behavior defaults. Per-language or per-project overrides can
|
|
@@ -182,11 +182,15 @@ var RegistryError = class extends OagenError {
|
|
|
182
182
|
//#region src/parser/refs.ts
|
|
183
183
|
async function loadAndBundleSpec(specPath) {
|
|
184
184
|
const absolutePath = resolve(specPath);
|
|
185
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
186
|
+
const document = makeDocumentFromString(content, pathToFileURL(absolutePath).href);
|
|
187
|
+
const config = await createConfig({});
|
|
188
|
+
const resolver = new BaseResolver();
|
|
185
189
|
const result = await bundleDocument({
|
|
186
|
-
document
|
|
187
|
-
config
|
|
190
|
+
document,
|
|
191
|
+
config,
|
|
188
192
|
types: Oas3_1Types,
|
|
189
|
-
externalRefResolver:
|
|
193
|
+
externalRefResolver: resolver,
|
|
190
194
|
dereference: false
|
|
191
195
|
});
|
|
192
196
|
if (result.problems.some((p) => p.severity === "error")) throw new SpecParseError(`Failed to parse spec: ${result.problems.filter((p) => p.severity === "error").map((p) => p.message).join("\n")}`, `Check the OpenAPI spec at "${absolutePath}" for syntax errors. Run a linter such as \`npx @redocly/cli lint ${specPath}\` to identify issues.`);
|
|
@@ -253,6 +257,14 @@ function walkTypeRef(ref, visitor) {
|
|
|
253
257
|
}
|
|
254
258
|
}
|
|
255
259
|
/**
|
|
260
|
+
* Collect all model names referenced (directly or transitively) by a TypeRef.
|
|
261
|
+
*/
|
|
262
|
+
function collectModelRefs(ref) {
|
|
263
|
+
const names = [];
|
|
264
|
+
walkTypeRef(ref, { model: (r) => names.push(r.name) });
|
|
265
|
+
return names;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
256
268
|
* Validate model-placement hints against the FULL spec. Run once at the start of
|
|
257
269
|
* generation — where the complete model/service universe is known — so typos in
|
|
258
270
|
* `modelHints` fail loud even though {@link assignModelsToServices} is
|
|
@@ -274,6 +286,30 @@ function validateModelHints(models, services, hints) {
|
|
|
274
286
|
}
|
|
275
287
|
}
|
|
276
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* Every response header an operation declares: the primary response's, each
|
|
291
|
+
* alternate success entry's, and each error entry's. The primary list is the
|
|
292
|
+
* same array as its `successResponses` entry when both exist, so it is yielded
|
|
293
|
+
* once. Every pass that walks an operation's type refs (reachability, model
|
|
294
|
+
* assignment, ref rewriting and validation, direction, compat filtering, enum
|
|
295
|
+
* collection) goes through this so a type reached only through a header is
|
|
296
|
+
* treated the way a body or parameter type is.
|
|
297
|
+
*/
|
|
298
|
+
function collectResponseHeaders(op) {
|
|
299
|
+
const result = [];
|
|
300
|
+
const seen = /* @__PURE__ */ new Set();
|
|
301
|
+
const lists = [
|
|
302
|
+
op.responseHeaders,
|
|
303
|
+
...(op.successResponses ?? []).map((r) => r.headers),
|
|
304
|
+
...op.errors.map((e) => e.headers)
|
|
305
|
+
];
|
|
306
|
+
for (const list of lists) {
|
|
307
|
+
if (!list || seen.has(list)) continue;
|
|
308
|
+
seen.add(list);
|
|
309
|
+
result.push(...list);
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
277
313
|
//#endregion
|
|
278
314
|
//#region src/utils/naming.ts
|
|
279
315
|
/**
|
|
@@ -321,7 +357,7 @@ function splitWords(s) {
|
|
|
321
357
|
if (!s) return [];
|
|
322
358
|
return recombineCompounds(s.replace(/[^a-zA-Z0-9_\-\s.]/g, "_").replace(/([a-z])([A-Z])/g, "$1\0$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1\0$2").replace(/([a-zA-Z])(\d)/g, "$1\0$2").replace(/(\d)([a-zA-Z])/g, "$1\0$2").split(/[\0_\-\s.]+/).filter((w) => w.length > 0));
|
|
323
359
|
}
|
|
324
|
-
const ACRONYM_SET = new Set([
|
|
360
|
+
const ACRONYM_SET = /* @__PURE__ */ new Set([
|
|
325
361
|
"SSO",
|
|
326
362
|
"FGA",
|
|
327
363
|
"SAML",
|
|
@@ -331,7 +367,7 @@ const ACRONYM_SET = new Set([
|
|
|
331
367
|
"M2M"
|
|
332
368
|
]);
|
|
333
369
|
function toPascalCase(s, acronyms) {
|
|
334
|
-
const merged = acronyms ? new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
|
|
370
|
+
const merged = acronyms ? /* @__PURE__ */ new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
|
|
335
371
|
return splitWords(s).map((w) => {
|
|
336
372
|
const upper = w.toUpperCase();
|
|
337
373
|
if (merged.has(upper)) return upper;
|
|
@@ -340,7 +376,7 @@ function toPascalCase(s, acronyms) {
|
|
|
340
376
|
}).join("");
|
|
341
377
|
}
|
|
342
378
|
function toCamelCase(s, acronyms) {
|
|
343
|
-
const merged = acronyms ? new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
|
|
379
|
+
const merged = acronyms ? /* @__PURE__ */ new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
|
|
344
380
|
const words = splitWords(s);
|
|
345
381
|
if (words.length === 0) return "";
|
|
346
382
|
return words.map((w, i) => {
|
|
@@ -373,7 +409,7 @@ function stripListItemMarkers(name) {
|
|
|
373
409
|
return name.replace(/ListItem/g, "").replace(/ByExternalId/g, "").replace(/ByResourceId/g, "").replace(/ForResource/g, "");
|
|
374
410
|
}
|
|
375
411
|
/** Words that look plural but must NOT be singularized. */
|
|
376
|
-
const SINGULAR_SAFE_LIST = new Set([
|
|
412
|
+
const SINGULAR_SAFE_LIST = /* @__PURE__ */ new Set([
|
|
377
413
|
"Status",
|
|
378
414
|
"Address",
|
|
379
415
|
"Access",
|
|
@@ -446,138 +482,598 @@ function qualifyInlineModelName(baseName, parentName) {
|
|
|
446
482
|
return `${cleanParent}${baseName}`;
|
|
447
483
|
}
|
|
448
484
|
/**
|
|
485
|
+
* The inline model or enum names `schemaToTypeRef` would mint for `schema`
|
|
486
|
+
* typed under `contextName` with `parentModelName`; empty when it mints none
|
|
487
|
+
* (a `$ref`, a primitive, an array or map of those). Mirrors the naming
|
|
488
|
+
* branches of `schemaToTypeRef`: an inline object in any form it recognises
|
|
489
|
+
* (`properties`, an `allOf` with object members) takes
|
|
490
|
+
* `qualifyInlineModelName`; a `oneOf`/`anyOf` mints one model per inline
|
|
491
|
+
* object variant, named exactly as the union references them
|
|
492
|
+
* (`nameInlineVariants`: `DrawingLayer`, `DrawingLayerWithOpacity`, or
|
|
493
|
+
* `CircleDrawingLayer` when every variant is titled); an inline enum, or a
|
|
494
|
+
* `oneOf` of string consts that collapses into one, takes the parent-prefixed
|
|
495
|
+
* enum name, which is never singularized; an array or map claims whatever its
|
|
496
|
+
* items or values would.
|
|
497
|
+
*/
|
|
498
|
+
function mintedInlineNames(contextName, schema, parentModelName) {
|
|
499
|
+
return mintedInlineNameEntries(contextName, schema, parentModelName).map((entry) => entry.name);
|
|
500
|
+
}
|
|
501
|
+
/** `mintedInlineNames`, also telling for each name whether it is context-free. */
|
|
502
|
+
function mintedInlineNameEntries(contextName, schema, parentModelName) {
|
|
503
|
+
const s = unwrapNullableSchema(schema);
|
|
504
|
+
if (s.$ref) return [];
|
|
505
|
+
const base = toPascalCase(contextName);
|
|
506
|
+
const modelName = () => [{
|
|
507
|
+
name: qualifyInlineModelName(base, parentModelName),
|
|
508
|
+
contextFree: false
|
|
509
|
+
}];
|
|
510
|
+
const enumName = () => {
|
|
511
|
+
const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
|
|
512
|
+
return [{
|
|
513
|
+
name: cleanParent && !base.startsWith(cleanParent) ? `${cleanParent}${base}` : base,
|
|
514
|
+
contextFree: false
|
|
515
|
+
}];
|
|
516
|
+
};
|
|
517
|
+
if (s.allOf) {
|
|
518
|
+
const only = s.allOf.length === 1 ? s.allOf[0] : void 0;
|
|
519
|
+
if (only) return mintedInlineNameEntries(contextName, only, parentModelName);
|
|
520
|
+
return s.allOf.some((m) => !m.$ref && (m.properties || m.type === "object")) ? modelName() : [];
|
|
521
|
+
}
|
|
522
|
+
const variants = s.oneOf ?? s.anyOf;
|
|
523
|
+
if (variants) {
|
|
524
|
+
const nonNull = variants.filter((v) => !isNullSchema$1(v));
|
|
525
|
+
if (nonNull.length >= 2 && nonNull.every((v) => typeof v.const === "string")) return enumName();
|
|
526
|
+
return [...nameInlineVariantEntries(s, modelName()[0].name)].map(([variant, entry]) => ({
|
|
527
|
+
...entry,
|
|
528
|
+
schema: variant
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
if (s.const !== void 0) return [];
|
|
532
|
+
if ((s.enum ?? []).filter((v) => v !== null).length >= 2) return enumName();
|
|
533
|
+
if (isInlineObjectSchema(s)) return modelName();
|
|
534
|
+
if (s.items && (s.type === "array" || !s.type)) return mintedInlineNameEntries(contextName, s.items, parentModelName);
|
|
535
|
+
if (s.type === "object" && !s.properties) {
|
|
536
|
+
const value = mapValueSchema(s);
|
|
537
|
+
return value ? mintedInlineNameEntries(contextName, value, parentModelName) : [];
|
|
538
|
+
}
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* The inline names the other properties of an object mint
|
|
543
|
+
* (`mintedInlineNameEntries`) which a `oneOf`/`anyOf` under `fieldName` must
|
|
544
|
+
* not give one of its variants (`nameInlineVariants`): beside a
|
|
545
|
+
* `shape_with_side` object, the `{ side }` variant of `shape` cannot become
|
|
546
|
+
* `DrawingShapeWithSide`. Every context-dependent sibling name is reserved.
|
|
547
|
+
* A context-free name — a title-mapped variant's title — names one shape
|
|
548
|
+
* wherever it appears, so it is shared, not reserved, when this field gives
|
|
549
|
+
* the same title a variant of the identical shape (`schemaShapeKey`:
|
|
550
|
+
* documentation keywords ignored, property names verbatim — the rule array
|
|
551
|
+
* items already use to share a singular sibling's model); a sibling giving
|
|
552
|
+
* the same title a different shape, or one this field does not use, reserves
|
|
553
|
+
* it, so this union's variant falls back to its context-qualified name. Each
|
|
554
|
+
* sibling's names are read in isolation, without the reservations that
|
|
555
|
+
* sibling would itself apply, so the set depends on sibling names and content
|
|
556
|
+
* only, two unions beside each other cannot chase each other's names, and
|
|
557
|
+
* the shape test is symmetric: when `Circle` means `{ radius }` on one side
|
|
558
|
+
* and `{ diameter }` on the other, both fall back (`CircleDrawingShape`,
|
|
559
|
+
* `CircleDrawingMarker`) and neither keeps the bare `Circle`. Empty without a
|
|
560
|
+
* sibling set, as for a request body or response that is itself a union.
|
|
561
|
+
*/
|
|
562
|
+
function reservedInlineNames(fieldName, siblings, parentModelName) {
|
|
563
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
564
|
+
if (!siblings) return reserved;
|
|
565
|
+
const ownShapes = /* @__PURE__ */ new Map();
|
|
566
|
+
const fieldSchema = siblings.get(fieldName);
|
|
567
|
+
if (fieldSchema) for (const own of mintedInlineNameEntries(fieldName, fieldSchema, parentModelName)) {
|
|
568
|
+
if (!own.contextFree || !own.schema) continue;
|
|
569
|
+
ownShapes.set(own.name, [...ownShapes.get(own.name) ?? [], schemaShapeKey(own.schema)]);
|
|
570
|
+
}
|
|
571
|
+
for (const [siblingName, siblingSchema] of siblings) {
|
|
572
|
+
if (siblingName === fieldName) continue;
|
|
573
|
+
for (const minted of mintedInlineNameEntries(siblingName, siblingSchema, parentModelName)) {
|
|
574
|
+
if (!minted.contextFree) {
|
|
575
|
+
reserved.add(minted.name);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const shapes = ownShapes.get(minted.name);
|
|
579
|
+
const siblingShape = minted.schema ? schemaShapeKey(minted.schema) : void 0;
|
|
580
|
+
if (!(shapes !== void 0 && siblingShape !== void 0 && shapes.every((s) => s === siblingShape))) reserved.add(minted.name);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return reserved;
|
|
584
|
+
}
|
|
585
|
+
/** Documentation keywords that do not shape a model; ignored by `schemaShapeKey` at a schema's own level. */
|
|
586
|
+
const DOC_KEYWORDS = /* @__PURE__ */ new Set([
|
|
587
|
+
"description",
|
|
588
|
+
"title",
|
|
589
|
+
"example",
|
|
590
|
+
"examples",
|
|
591
|
+
"summary",
|
|
592
|
+
"externalDocs",
|
|
593
|
+
"deprecated"
|
|
594
|
+
]);
|
|
595
|
+
/** Keywords whose value maps NAMES (not keywords) to schemas: names are kept verbatim, values recurse as schemas. */
|
|
596
|
+
const NAMED_SCHEMA_MAPS = /* @__PURE__ */ new Set([
|
|
597
|
+
"properties",
|
|
598
|
+
"patternProperties",
|
|
599
|
+
"dependentSchemas",
|
|
600
|
+
"$defs",
|
|
601
|
+
"definitions"
|
|
602
|
+
]);
|
|
603
|
+
/** Keywords whose value is a single schema. */
|
|
604
|
+
const SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
605
|
+
"items",
|
|
606
|
+
"additionalProperties",
|
|
607
|
+
"additionalItems",
|
|
608
|
+
"contains",
|
|
609
|
+
"propertyNames",
|
|
610
|
+
"not",
|
|
611
|
+
"if",
|
|
612
|
+
"then",
|
|
613
|
+
"else",
|
|
614
|
+
"unevaluatedProperties",
|
|
615
|
+
"unevaluatedItems"
|
|
616
|
+
]);
|
|
617
|
+
/** Keywords whose value is an ordered list of schemas. */
|
|
618
|
+
const SCHEMA_LIST_KEYWORDS = /* @__PURE__ */ new Set([
|
|
619
|
+
"allOf",
|
|
620
|
+
"oneOf",
|
|
621
|
+
"anyOf",
|
|
622
|
+
"prefixItems"
|
|
623
|
+
]);
|
|
624
|
+
/** Keywords whose list value is a set: compared order-insensitively. */
|
|
625
|
+
const UNORDERED_LIST_KEYWORDS = /* @__PURE__ */ new Set(["required", "type"]);
|
|
626
|
+
/**
|
|
627
|
+
* A canonical rendering of a schema's shape, for deciding whether two inline
|
|
628
|
+
* schemas describe the same model: every structural keyword (`type`,
|
|
629
|
+
* `properties`, `required`, `items`, `additionalProperties`,
|
|
630
|
+
* `patternProperties`, `allOf`/`oneOf`/`anyOf`, `enum`, `const`, `format`,
|
|
631
|
+
* `nullable`, ...) recursively, with keywords sorted and the `required` and
|
|
632
|
+
* `type` lists order-insensitive. Documentation keywords (`description`,
|
|
633
|
+
* `title`, `example`, `examples`, `summary`, `externalDocs`, `deprecated`)
|
|
634
|
+
* and vendor extensions (`x-*`) are ignored, and so is `nullable: false`,
|
|
635
|
+
* which the nullable unwrap leaves behind — but only where they are a
|
|
636
|
+
* schema's own keywords. Inside `properties`, `patternProperties`,
|
|
637
|
+
* `dependentSchemas`, `dependentRequired` and `$defs` the keys are property
|
|
638
|
+
* names, kept verbatim: a field named `description` or `title` is part of the
|
|
639
|
+
* shape. Enum, const and default values are compared as plain data, in
|
|
640
|
+
* order, as the IR keeps them.
|
|
641
|
+
*/
|
|
642
|
+
function schemaShapeKey(schema) {
|
|
643
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
644
|
+
const dataKey = (value) => {
|
|
645
|
+
if (Array.isArray(value)) return value.map(dataKey);
|
|
646
|
+
if (!isRecord(value)) return value;
|
|
647
|
+
const out = {};
|
|
648
|
+
for (const key of Object.keys(value).sort()) out[key] = dataKey(value[key]);
|
|
649
|
+
return out;
|
|
650
|
+
};
|
|
651
|
+
const schemaKey = (value) => {
|
|
652
|
+
if (!isRecord(value)) return value;
|
|
653
|
+
const out = {};
|
|
654
|
+
for (const keyword of Object.keys(value).sort()) {
|
|
655
|
+
const v = value[keyword];
|
|
656
|
+
if (DOC_KEYWORDS.has(keyword) || keyword.startsWith("x-")) continue;
|
|
657
|
+
if (keyword === "nullable" && v === false) continue;
|
|
658
|
+
if (NAMED_SCHEMA_MAPS.has(keyword) && isRecord(v)) {
|
|
659
|
+
const named = {};
|
|
660
|
+
for (const name of Object.keys(v).sort()) named[name] = schemaKey(v[name]);
|
|
661
|
+
out[keyword] = named;
|
|
662
|
+
} else if (keyword === "dependentRequired" && isRecord(v)) {
|
|
663
|
+
const named = {};
|
|
664
|
+
for (const name of Object.keys(v).sort()) {
|
|
665
|
+
const names = v[name];
|
|
666
|
+
named[name] = Array.isArray(names) ? names.map(String).sort() : dataKey(names);
|
|
667
|
+
}
|
|
668
|
+
out[keyword] = named;
|
|
669
|
+
} else if (SCHEMA_KEYWORDS.has(keyword)) out[keyword] = schemaKey(v);
|
|
670
|
+
else if (SCHEMA_LIST_KEYWORDS.has(keyword) && Array.isArray(v)) out[keyword] = v.map(schemaKey);
|
|
671
|
+
else if (UNORDERED_LIST_KEYWORDS.has(keyword) && Array.isArray(v)) out[keyword] = v.map(String).sort();
|
|
672
|
+
else out[keyword] = dataKey(v);
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
};
|
|
676
|
+
return JSON.stringify(schemaKey(schema));
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* The context name an array field's items (`kind: 'item'`) or a map field's
|
|
680
|
+
* values (`kind: 'value'`) are typed and extracted under. By default it is
|
|
681
|
+
* the field itself, so the inline model is the parent plus the singularized
|
|
682
|
+
* field (`layers` on `Drawing` → `DrawingLayer`).
|
|
683
|
+
*
|
|
684
|
+
* When a sibling property of the same object would mint that very name —
|
|
685
|
+
* `layer` as an inline object, an inline enum, an inline union with object
|
|
686
|
+
* variants (any of whose variant names counts), or an array or map of such
|
|
687
|
+
* (`mintedInlineNames`); a primitive `email` beside `emails` claims nothing
|
|
688
|
+
* — the elements step aside to
|
|
689
|
+
* `<field>_item` / `<field>_value`, which qualify to `<Parent><Field>Item` /
|
|
690
|
+
* `<Parent><Field>Value` (`DrawingLayersItem`, `DrawingLayersValue`),
|
|
691
|
+
* matching the `<Variant>Item` convention. Should a sibling mint that name
|
|
692
|
+
* too (a property `layers_item`), the fallback keeps going the way
|
|
693
|
+
* `variantMemberKeys` does for variants: `<field>_array_item`, then
|
|
694
|
+
* `<field>_array_item2`, `<field>_array_item3`, ... for items
|
|
695
|
+
* (`DrawingLayersArrayItem`, ...) and `<field>_member_value`,
|
|
696
|
+
* `<field>_member_value2`, ... for values, taking the first candidate whose
|
|
697
|
+
* minted name no sibling claims.
|
|
698
|
+
*
|
|
699
|
+
* Identical-shape sharing: when every sibling claiming the singular name is
|
|
700
|
+
* an inline object whose shape is structurally identical to the items or
|
|
701
|
+
* values (`schemaShapeKey`: same properties, required, types, nested inline
|
|
702
|
+
* schemas and enum values; description, title and example ignored), the
|
|
703
|
+
* fields share one model under the singular name. They are the same shape,
|
|
704
|
+
* so one model is the right SDK type and a duplicate would only add noise;
|
|
705
|
+
* should a later spec version make the shapes diverge, the split happens
|
|
706
|
+
* then, which is the moment the item type genuinely changes. This
|
|
707
|
+
* deliberately departs from a literal one-model-per-field reading for the
|
|
708
|
+
* identical case.
|
|
709
|
+
*
|
|
710
|
+
* Only context-dependent names take part: a title-mapped variant's title
|
|
711
|
+
* (`MintedInlineName.contextFree`) is the same under every context, so no
|
|
712
|
+
* candidate could step away from it, and such a name is shared by design —
|
|
713
|
+
* two sibling arrays whose items carry the same titled variants both
|
|
714
|
+
* reference `Circle` and `Square` (`nameInlineVariants`). The search always
|
|
715
|
+
* terminates: it stops as soon as a candidate changes nothing about the names
|
|
716
|
+
* (`firstFreeElementContext`'s no-progress rule), and
|
|
717
|
+
* `MAX_ELEMENT_CONTEXT_ATTEMPTS` caps it as a backstop; both return the last
|
|
718
|
+
* candidate tried, deterministically.
|
|
719
|
+
*
|
|
720
|
+
* Decided from sibling names and schema content alone, so document order
|
|
721
|
+
* plays no part. `schemaToTypeRef` and every extractor call this with the
|
|
722
|
+
* same sibling set (`collectProperties`); otherwise the reference and the
|
|
723
|
+
* model disagree.
|
|
724
|
+
*/
|
|
725
|
+
function elementContextName(fieldName, kind, siblings, parentModelName, elementSchema) {
|
|
726
|
+
return elementContextDecision(fieldName, kind, siblings, parentModelName, elementSchema).context;
|
|
727
|
+
}
|
|
728
|
+
/** `elementContextName`, with the reason it decided as it did (see `ElementContextReason`). */
|
|
729
|
+
function elementContextDecision(fieldName, kind, siblings, parentModelName, elementSchema) {
|
|
730
|
+
if (!siblings) return {
|
|
731
|
+
context: fieldName,
|
|
732
|
+
reason: "no-siblings"
|
|
733
|
+
};
|
|
734
|
+
const dependentNames = (candidate) => mintedInlineNameEntries(candidate, elementSchema, parentModelName).filter((entry) => !entry.contextFree).map((entry) => entry.name);
|
|
735
|
+
const own = dependentNames(fieldName);
|
|
736
|
+
if (own.length === 0) return {
|
|
737
|
+
context: fieldName,
|
|
738
|
+
reason: "nothing-minted"
|
|
739
|
+
};
|
|
740
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
741
|
+
for (const [siblingName, siblingSchema] of siblings) {
|
|
742
|
+
if (siblingName === fieldName) continue;
|
|
743
|
+
for (const minted of mintedInlineNames(siblingName, siblingSchema, parentModelName)) claimed.set(minted, [...claimed.get(minted) ?? [], siblingSchema]);
|
|
744
|
+
}
|
|
745
|
+
const rivals = [...new Set(own.flatMap((name) => claimed.get(name) ?? []))];
|
|
746
|
+
if (rivals.length === 0) return {
|
|
747
|
+
context: fieldName,
|
|
748
|
+
reason: "unclaimed"
|
|
749
|
+
};
|
|
750
|
+
const element = unwrapNullableSchema(elementSchema);
|
|
751
|
+
const identical = (rival) => {
|
|
752
|
+
const sibling = unwrapNullableSchema(rival);
|
|
753
|
+
return isInlineObjectSchema(sibling) && isInlineObjectSchema(element) && schemaShapeKey(sibling) === schemaShapeKey(element);
|
|
754
|
+
};
|
|
755
|
+
if (rivals.every(identical)) return {
|
|
756
|
+
context: fieldName,
|
|
757
|
+
reason: "shared-shape"
|
|
758
|
+
};
|
|
759
|
+
return firstFreeElementContext(fieldName, kind, new Set(claimed.keys()), dependentNames, own);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* The first fallback context under which the elements mint no claimed name:
|
|
763
|
+
* `<field>_item` / `<field>_value`, then `<field>_array_item` /
|
|
764
|
+
* `<field>_member_value`, then `<field>_array_item2`, `…3`, ... (the way
|
|
765
|
+
* `variantMemberKeys` numbers a variant's members). `mint` gives the names
|
|
766
|
+
* under a candidate and `previous` those under the context tried before the
|
|
767
|
+
* first candidate. The search stops with `no-progress` as soon as a
|
|
768
|
+
* candidate mints the same names as the one before it — the names do not
|
|
769
|
+
* depend on the context, so no candidate can avoid the claim — and with
|
|
770
|
+
* `cap` after `maxAttempts` candidates; both keep the last candidate tried.
|
|
771
|
+
*/
|
|
772
|
+
function firstFreeElementContext(fieldName, kind, claimed, mint, previous, maxAttempts = 256) {
|
|
773
|
+
const fallback = kind === "item" ? "array_item" : "member_value";
|
|
774
|
+
let last = previous;
|
|
775
|
+
let candidate = fieldName;
|
|
776
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
777
|
+
candidate = i === 0 ? `${fieldName}_${kind}` : i === 1 ? `${fieldName}_${fallback}` : `${fieldName}_${fallback}${i}`;
|
|
778
|
+
const minted = mint(candidate);
|
|
779
|
+
if (minted.every((name) => !claimed.has(name))) return {
|
|
780
|
+
context: candidate,
|
|
781
|
+
reason: "free"
|
|
782
|
+
};
|
|
783
|
+
if (sameNames(minted, last)) return {
|
|
784
|
+
context: candidate,
|
|
785
|
+
reason: "no-progress"
|
|
786
|
+
};
|
|
787
|
+
last = minted;
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
context: candidate,
|
|
791
|
+
reason: "cap"
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/** The same names, in any order. */
|
|
795
|
+
function sameNames(a, b) {
|
|
796
|
+
if (a.length !== b.length) return false;
|
|
797
|
+
const sortedB = [...b].sort();
|
|
798
|
+
return [...a].sort().every((name, i) => name === sortedB[i]);
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Every property the object described by `schema` exposes, by name: its own
|
|
802
|
+
* `properties`, those of its `allOf` members (following a member's `$ref`
|
|
803
|
+
* into `schemas` when given, as `extractAllOfModel` does), and those of
|
|
804
|
+
* inline `oneOf`/`anyOf` variants, whose fields the model extractors flatten
|
|
805
|
+
* in; the first definition of a name wins. This is the sibling set
|
|
806
|
+
* `elementContextName` decides from; the TypeRef builder and every extractor
|
|
807
|
+
* derive it through this one function so they agree.
|
|
808
|
+
*/
|
|
809
|
+
function collectProperties(schema, schemas) {
|
|
810
|
+
const properties = /* @__PURE__ */ new Map();
|
|
811
|
+
const seen = /* @__PURE__ */ new Set();
|
|
812
|
+
const visit = (s, followRef) => {
|
|
813
|
+
if (!s || seen.has(s)) return;
|
|
814
|
+
seen.add(s);
|
|
815
|
+
if (s.$ref) {
|
|
816
|
+
if (followRef && schemas) visit(schemas[s.$ref.split("/").pop() ?? ""], true);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
for (const [key, value] of Object.entries(s.properties ?? {})) if (value && !properties.has(key)) properties.set(key, value);
|
|
820
|
+
for (const sub of s.allOf ?? []) visit(sub, true);
|
|
821
|
+
for (const variant of [...s.oneOf ?? [], ...s.anyOf ?? []]) visit(variant, false);
|
|
822
|
+
};
|
|
823
|
+
visit(schema, false);
|
|
824
|
+
return properties;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
449
827
|
* Walk all component schemas and extract inline Model definitions for fields
|
|
450
|
-
* that are objects with properties
|
|
828
|
+
* that are objects with properties, including objects reached through array
|
|
829
|
+
* items, `oneOf` variants, and map (`additionalProperties` /
|
|
830
|
+
* `patternProperties`) values.
|
|
451
831
|
* These correspond to the ModelRef entries created by schemaToTypeRef.
|
|
452
832
|
*/
|
|
453
833
|
function extractInlineModelsFromSchemas(schemas) {
|
|
454
834
|
if (!schemas) return [];
|
|
455
835
|
const inlineModels = [];
|
|
456
|
-
for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, resolveSchemaName(schemaName));
|
|
836
|
+
for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, resolveSchemaName(schemaName), schemas, collectProperties(schema, schemas));
|
|
457
837
|
return inlineModels;
|
|
458
838
|
}
|
|
459
|
-
|
|
839
|
+
/**
|
|
840
|
+
* Walk a schema's properties and materialise every inline object as a model.
|
|
841
|
+
* `componentSchemas` is optional and only feeds `buildFieldFromSchema`, so a
|
|
842
|
+
* field that `$ref`s a component can take its `example`/`examples` from it;
|
|
843
|
+
* callers without the component map at hand simply omit it.
|
|
844
|
+
* `siblingProperties` is the parent object's full property set, for callers
|
|
845
|
+
* that see more of it than this schema shows (see `elementContextName`).
|
|
846
|
+
* `reservedNames` is set only on the re-entry for a field's array items or
|
|
847
|
+
* map values: the inline names that field's siblings mint, which a union
|
|
848
|
+
* among the items or values must step around exactly as `schemaToTypeRef`
|
|
849
|
+
* does when it hands the same set down (`reservedNamesFor`).
|
|
850
|
+
*
|
|
851
|
+
* The recursion follows the document's own inline structure only: a `$ref`
|
|
852
|
+
* is resolved solely to read a component's property list for typing (an
|
|
853
|
+
* augmentation's inherited fields, a sibling set), never descended into, so
|
|
854
|
+
* the depth is bounded by the document's inline nesting and no component
|
|
855
|
+
* graph — self-extending, mutually extending, or reused at several levels —
|
|
856
|
+
* can make it run away.
|
|
857
|
+
*/
|
|
858
|
+
function extractInlineModelsFromProperties(schema, results, parentName, componentSchemas, siblingProperties, reservedNames) {
|
|
460
859
|
const properties = schema.properties ?? {};
|
|
461
860
|
const allOfSchemas = schema.allOf ?? [];
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
861
|
+
const siblings = siblingProperties ?? collectProperties(schema);
|
|
862
|
+
const reservedFor = (fieldName) => reservedNames ?? reservedInlineNames(fieldName, siblings, parentName);
|
|
863
|
+
for (const sub of allOfSchemas) if (sub.properties) extractInlineModelsFromProperties(sub, results, parentName, componentSchemas, siblings, reservedNames);
|
|
864
|
+
for (const [fieldName, rawFieldSchema] of Object.entries(properties)) {
|
|
865
|
+
if (!rawFieldSchema) continue;
|
|
866
|
+
const fieldSchema = unwrapNullableField(rawFieldSchema);
|
|
867
|
+
if (isInlineObjectSchema(fieldSchema)) {
|
|
466
868
|
const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
|
|
467
|
-
results.push(buildInlineModel(modelName, fieldSchema));
|
|
468
|
-
extractInlineModelsFromProperties(fieldSchema, results, modelName);
|
|
869
|
+
results.push(buildInlineModel(modelName, fieldSchema, componentSchemas));
|
|
870
|
+
extractInlineModelsFromProperties(fieldSchema, results, modelName, componentSchemas);
|
|
469
871
|
}
|
|
470
|
-
if (fieldSchema.type === "array" && fieldSchema.items) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
results.push(buildInlineModel(modelName, items));
|
|
475
|
-
extractInlineModelsFromProperties(items, results, modelName);
|
|
476
|
-
}
|
|
872
|
+
if (fieldSchema.type === "array" && fieldSchema.items) extractInlineModelsFromProperties({ properties: { [elementContextName(fieldName, "item", siblings, parentName, fieldSchema.items)]: fieldSchema.items } }, results, parentName, componentSchemas, void 0, reservedFor(fieldName));
|
|
873
|
+
if (fieldSchema.type === "object" && !fieldSchema.properties) {
|
|
874
|
+
const valueSchema = mapValueSchema(fieldSchema);
|
|
875
|
+
if (valueSchema) extractInlineModelsFromProperties({ properties: { [elementContextName(fieldName, "value", siblings, parentName, valueSchema)]: valueSchema } }, results, parentName, componentSchemas, void 0, reservedFor(fieldName));
|
|
477
876
|
}
|
|
478
|
-
if (fieldSchema.allOf
|
|
479
|
-
const
|
|
480
|
-
|
|
877
|
+
if (fieldSchema.allOf) {
|
|
878
|
+
const members = fieldSchema.allOf;
|
|
879
|
+
const emitMerged = () => {
|
|
880
|
+
const merged = mergeAugmentation(fieldSchema, componentSchemas);
|
|
881
|
+
if (merged.properties.length === 0) return;
|
|
481
882
|
const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
|
|
482
|
-
results.push(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
883
|
+
results.push(buildMergedModel(modelName, merged, componentSchemas));
|
|
884
|
+
const own = {};
|
|
885
|
+
for (const p of merged.properties) if (p.origin === null) own[p.name] = p.schema;
|
|
886
|
+
const mergedSiblings = new Map(merged.properties.map((p) => [p.name, p.schema]));
|
|
887
|
+
extractInlineModelsFromProperties({ properties: own }, results, modelName, componentSchemas, mergedSiblings);
|
|
888
|
+
};
|
|
889
|
+
const only = members.length === 1 ? members[0] : void 0;
|
|
890
|
+
if (members.some((m) => m.$ref)) {
|
|
891
|
+
if (members.some((m) => !m.$ref && (m.properties || m.type === "object"))) emitMerged();
|
|
892
|
+
} else if (only) extractInlineModelsFromProperties({ properties: { [fieldName]: {
|
|
893
|
+
...only,
|
|
894
|
+
...extractConstraints(fieldSchema)
|
|
895
|
+
} } }, results, parentName, componentSchemas, siblings, reservedNames);
|
|
896
|
+
else if (members.some((m) => m.properties)) emitMerged();
|
|
897
|
+
}
|
|
898
|
+
const unionVariants = fieldSchema.oneOf ?? fieldSchema.anyOf;
|
|
899
|
+
if (unionVariants) {
|
|
900
|
+
const inlineObjectVariants = unionVariants.filter((v) => isInlineObjectSchema(v));
|
|
488
901
|
if (inlineObjectVariants.length > 0) {
|
|
489
902
|
const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
|
|
490
903
|
const existingNames = new Set(results.map((r) => r.name));
|
|
491
|
-
const
|
|
492
|
-
const emittedNames = [];
|
|
904
|
+
const variantNames = nameInlineVariants(fieldSchema, modelName, reservedFor(fieldName));
|
|
493
905
|
for (const variant of inlineObjectVariants) {
|
|
494
|
-
const variantName =
|
|
495
|
-
|
|
906
|
+
const variantName = variantNames.get(variant);
|
|
907
|
+
if (variantName === void 0) continue;
|
|
496
908
|
if (!existingNames.has(variantName)) {
|
|
497
909
|
existingNames.add(variantName);
|
|
498
|
-
results.push(buildInlineModel(variantName, variant));
|
|
499
|
-
extractInlineModelsFromProperties(variant, results, variantName);
|
|
910
|
+
results.push(buildInlineModel(variantName, variant, componentSchemas));
|
|
911
|
+
extractInlineModelsFromProperties(variant, results, variantName, componentSchemas);
|
|
912
|
+
extractInlineModelsFromProperties({ properties: variantMemberSchemas(variant, variantName) }, results, variantName, componentSchemas);
|
|
500
913
|
}
|
|
501
914
|
}
|
|
502
915
|
}
|
|
503
916
|
}
|
|
504
917
|
}
|
|
505
918
|
}
|
|
506
|
-
/** Find a single string-const-valued property shared by every variant whose
|
|
507
|
-
* values are all distinct — the implicit discriminator. Returns null when no
|
|
508
|
-
* such property exists. Mirrors `deriveConstNamingDiscriminator` in schemas.ts. */
|
|
509
|
-
function deriveOneOfNamingDiscriminator(variants) {
|
|
510
|
-
if (variants.length < 2) return null;
|
|
511
|
-
const candidates = Object.keys(variants[0]?.properties ?? {});
|
|
512
|
-
for (const propName of candidates) {
|
|
513
|
-
const values = variants.map((v) => readConstString(v.properties?.[propName]));
|
|
514
|
-
if (values.some((v) => v === null)) continue;
|
|
515
|
-
if (new Set(values).size !== values.length) continue;
|
|
516
|
-
return propName;
|
|
517
|
-
}
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
function readConstString(prop) {
|
|
521
|
-
if (!prop) return null;
|
|
522
|
-
if (typeof prop.const === "string") return prop.const;
|
|
523
|
-
if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
/** Produce a per-variant model name. Variant 0 keeps the bare parent name;
|
|
527
|
-
* later variants are prefixed by the const-derived label. Mirrors
|
|
528
|
-
* `nameVariantModel` in schemas.ts. Falls back to a numeric suffix when no
|
|
529
|
-
* discriminator is available, the const value PascalCases to nothing, or
|
|
530
|
-
* the derived candidate collides with the parent or an already-emitted name. */
|
|
531
|
-
function nameOneOfVariant(variant, parentName, alreadyEmitted, discriminatorProperty) {
|
|
532
|
-
if (alreadyEmitted.length === 0) return parentName;
|
|
533
|
-
if (discriminatorProperty) {
|
|
534
|
-
const constValue = readConstString(variant.properties?.[discriminatorProperty]);
|
|
535
|
-
if (constValue) {
|
|
536
|
-
const prefix = toPascalCase(constValue);
|
|
537
|
-
if (prefix) {
|
|
538
|
-
const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
|
|
539
|
-
if (!(candidate === parentName || alreadyEmitted.includes(candidate))) return candidate;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
return `${parentName}${alreadyEmitted.length + 1}`;
|
|
544
|
-
}
|
|
545
919
|
/**
|
|
546
|
-
* Merge
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
920
|
+
* Merge an `allOf` field into one object shape, remembering where each
|
|
921
|
+
* property comes from. A `$ref` member is resolved against `componentSchemas`
|
|
922
|
+
* by its component name, as `extractAllOfModel` does, and contributes its
|
|
923
|
+
* properties — its own nested `allOf` chain included — tagged with that
|
|
924
|
+
* component's model name, so the merged model can type them exactly as the
|
|
925
|
+
* component's own model does; inline members contribute properties tagged as
|
|
926
|
+
* the merged model's own. `required` is collected from every member. Earlier
|
|
927
|
+
* members win on key conflicts, matching the merge order used elsewhere in
|
|
928
|
+
* the parser; an unresolvable ref contributes nothing. The walk reads the
|
|
929
|
+
* components' property lists only — it never descends into a property — so
|
|
930
|
+
* it is finite for any component graph (the `seen` set only short-circuits a
|
|
931
|
+
* component reached twice within one merge).
|
|
551
932
|
*/
|
|
552
|
-
function
|
|
553
|
-
const properties =
|
|
933
|
+
function mergeAugmentation(schema, componentSchemas) {
|
|
934
|
+
const properties = [];
|
|
935
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
554
936
|
const required = [];
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
937
|
+
const seen = /* @__PURE__ */ new Set();
|
|
938
|
+
const visit = (s, origin) => {
|
|
939
|
+
if (seen.has(s)) return;
|
|
940
|
+
seen.add(s);
|
|
941
|
+
if (s.$ref) {
|
|
942
|
+
const rawName = s.$ref.split("/").pop() ?? "";
|
|
943
|
+
const target = componentSchemas?.[rawName];
|
|
944
|
+
if (!target) return;
|
|
945
|
+
visit(target, origin ?? {
|
|
946
|
+
name: resolveSchemaName(rawName),
|
|
947
|
+
schema: target
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
for (const [key, value] of Object.entries(s.properties ?? {})) {
|
|
952
|
+
if (!value || seenNames.has(key)) continue;
|
|
953
|
+
seenNames.add(key);
|
|
954
|
+
properties.push({
|
|
955
|
+
name: key,
|
|
956
|
+
schema: value,
|
|
957
|
+
origin
|
|
958
|
+
});
|
|
558
959
|
}
|
|
559
960
|
if (s.required) required.push(...s.required);
|
|
560
|
-
|
|
961
|
+
for (const sub of s.allOf ?? []) visit(sub, origin);
|
|
561
962
|
};
|
|
562
|
-
visit(schema);
|
|
963
|
+
visit(schema, null);
|
|
563
964
|
return {
|
|
564
|
-
type: "object",
|
|
565
965
|
properties,
|
|
566
966
|
required,
|
|
567
|
-
description: schema.description
|
|
967
|
+
description: schema.description,
|
|
968
|
+
constraints: extractConstraints(schema)
|
|
568
969
|
};
|
|
569
970
|
}
|
|
570
|
-
|
|
971
|
+
/**
|
|
972
|
+
* Build the model for a merged `allOf` field. An inherited property is typed
|
|
973
|
+
* with its component as naming context and the component's own sibling set,
|
|
974
|
+
* so it carries the same TypeRef as in the component's model (`BaseMeta`,
|
|
975
|
+
* `BaseChild`, ...); the merged model's own properties are typed under its
|
|
976
|
+
* name with the full merged property set as siblings.
|
|
977
|
+
*/
|
|
978
|
+
function buildMergedModel(name, merged, componentSchemas) {
|
|
979
|
+
const requiredSet = new Set(merged.required);
|
|
980
|
+
const mergedSiblings = new Map(merged.properties.map((p) => [p.name, p.schema]));
|
|
981
|
+
const componentSiblings = /* @__PURE__ */ new Map();
|
|
982
|
+
const fields = merged.properties.map((p) => {
|
|
983
|
+
if (p.origin === null) return buildFieldFromSchema(p.name, p.schema, name, requiredSet, componentSchemas, mergedSiblings);
|
|
984
|
+
let siblings = componentSiblings.get(p.origin.name);
|
|
985
|
+
if (!siblings) {
|
|
986
|
+
siblings = collectProperties(p.origin.schema, componentSchemas);
|
|
987
|
+
componentSiblings.set(p.origin.name, siblings);
|
|
988
|
+
}
|
|
989
|
+
return buildFieldFromSchema(p.name, p.schema, p.origin.name, requiredSet, componentSchemas, siblings);
|
|
990
|
+
});
|
|
991
|
+
return {
|
|
992
|
+
name,
|
|
993
|
+
description: merged.description,
|
|
994
|
+
fields,
|
|
995
|
+
...merged.constraints ? { constraints: merged.constraints } : {}
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Strip the nullable wrappers `schemaToTypeRef` unwraps before it names
|
|
1000
|
+
* anything — the OAS 3.1 `type: [X, 'null']` array, the OAS 3.0
|
|
1001
|
+
* `nullable: true` flag, and a `oneOf`/`anyOf` with a single non-null
|
|
1002
|
+
* variant — so extraction dispatches on the same shape the TypeRef was built
|
|
1003
|
+
* from. The unwrap keeps `contextName`/`parentModelName` semantics intact:
|
|
1004
|
+
* `schemaToTypeRef` passes both through each of these wrappers unchanged.
|
|
1005
|
+
*/
|
|
1006
|
+
function unwrapNullableSchema(schema) {
|
|
1007
|
+
if (schema.$ref) return schema;
|
|
1008
|
+
if (Array.isArray(schema.type)) {
|
|
1009
|
+
const nonNullTypes = schema.type.filter((t) => t !== "null");
|
|
1010
|
+
if (nonNullTypes.length !== 1) return schema;
|
|
1011
|
+
return unwrapNullableSchema({
|
|
1012
|
+
...schema,
|
|
1013
|
+
type: nonNullTypes[0],
|
|
1014
|
+
nullable: false
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
if (schema.nullable && schema.type) return unwrapNullableSchema({
|
|
1018
|
+
...schema,
|
|
1019
|
+
nullable: false
|
|
1020
|
+
});
|
|
1021
|
+
const variants = schema.allOf ? void 0 : schema.oneOf ?? schema.anyOf;
|
|
1022
|
+
if (variants) {
|
|
1023
|
+
const nullVariant = variants.find(isNullSchema$1);
|
|
1024
|
+
const nonNullVariants = variants.filter((v) => v !== nullVariant);
|
|
1025
|
+
const only = nonNullVariants[0];
|
|
1026
|
+
if (nullVariant && nonNullVariants.length === 1 && only) return unwrapNullableSchema(only);
|
|
1027
|
+
}
|
|
1028
|
+
return schema;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* `unwrapNullableSchema` for a field, carrying the wrapper's limits onto the
|
|
1032
|
+
* branch it unwraps to. The type-array and `nullable: true` forms unwrap by
|
|
1033
|
+
* spreading, so every keyword survives; the union form swaps in the bare
|
|
1034
|
+
* branch, which would drop a `minProperties` written beside
|
|
1035
|
+
* `anyOf: [{ type: 'object', … }, { type: 'null' }]`. `extractConstraints`
|
|
1036
|
+
* conjoins that wrapper's own keywords with its branch's as the field's
|
|
1037
|
+
* limits, so the model built from the branch must carry the same ones or
|
|
1038
|
+
* `Model.constraints` would omit what the field declares.
|
|
1039
|
+
*/
|
|
1040
|
+
function unwrapNullableField(schema) {
|
|
1041
|
+
const inner = unwrapNullableSchema(schema);
|
|
1042
|
+
if (inner === schema || schema.allOf || !(schema.oneOf ?? schema.anyOf)) return inner;
|
|
1043
|
+
return {
|
|
1044
|
+
...inner,
|
|
1045
|
+
...extractConstraints(schema)
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
/** The `{ type: 'null' }` member that marks a `oneOf`/`anyOf` as nullable — same test as schemaToTypeRef. */
|
|
1049
|
+
function isNullSchema$1(schema) {
|
|
1050
|
+
return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* The schema that types a map's values, in the precedence `schemaToTypeRef`
|
|
1054
|
+
* uses: a non-empty `additionalProperties` object wins, otherwise the first
|
|
1055
|
+
* `patternProperties` entry. `additionalProperties: {}` means "any value"
|
|
1056
|
+
* (typed `unknown`, nothing to extract) and does not fall through to
|
|
1057
|
+
* `patternProperties`, matching the type-ref branch.
|
|
1058
|
+
*/
|
|
1059
|
+
function mapValueSchema(schema) {
|
|
1060
|
+
const additional = schema.additionalProperties;
|
|
1061
|
+
if (additional && typeof additional === "object") return Object.keys(additional).length > 0 ? additional : null;
|
|
1062
|
+
return (schema.patternProperties ? Object.values(schema.patternProperties) : [])[0] ?? null;
|
|
1063
|
+
}
|
|
1064
|
+
function buildInlineModel(name, schema, componentSchemas) {
|
|
571
1065
|
const requiredSet = new Set(schema.required ?? []);
|
|
572
1066
|
const fields = [];
|
|
1067
|
+
const siblings = collectProperties(schema);
|
|
573
1068
|
for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
|
|
574
1069
|
if (!fieldSchema) continue;
|
|
575
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
1070
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, componentSchemas, siblings));
|
|
576
1071
|
}
|
|
577
1072
|
return {
|
|
578
1073
|
name,
|
|
579
1074
|
description: schema.description,
|
|
580
|
-
fields
|
|
1075
|
+
fields,
|
|
1076
|
+
...constraintsEntry(schema)
|
|
581
1077
|
};
|
|
582
1078
|
}
|
|
583
1079
|
//#endregion
|
|
@@ -585,9 +1081,21 @@ function buildInlineModel(name, schema) {
|
|
|
585
1081
|
/**
|
|
586
1082
|
* Module-level transform set during extractSchemas(). Used by schemaToTypeRef()
|
|
587
1083
|
* to apply the same name transform to $ref model/enum references.
|
|
1084
|
+
*
|
|
1085
|
+
* Receives the cleaned PascalCase name and, when the name is a
|
|
1086
|
+
* `components.schemas` key, the raw key exactly as spelled in the document.
|
|
1087
|
+
* Names oagen synthesises (inline enums, request-body variant models,
|
|
1088
|
+
* title-derived names) pass no second argument.
|
|
588
1089
|
*/
|
|
589
1090
|
let activeSchemaNameTransform = null;
|
|
590
1091
|
/**
|
|
1092
|
+
* Module-level set of raw `components.schemas` keys for the spec being
|
|
1093
|
+
* extracted. resolveSchemaName() consults it to decide whether the name it was
|
|
1094
|
+
* handed is a real component key (forwarded to the transform as
|
|
1095
|
+
* `originalName`) or a name oagen synthesised (no `originalName`).
|
|
1096
|
+
*/
|
|
1097
|
+
let activeComponentSchemaKeys = /* @__PURE__ */ new Set();
|
|
1098
|
+
/**
|
|
591
1099
|
* Module-level set of resolved schema names whose components/schemas entry is
|
|
592
1100
|
* an enum (i.e. has `enum:`). Populated by extractSchemas() so schemaToTypeRef()
|
|
593
1101
|
* can classify a `$ref` to a top-level enum schema as `kind: 'enum'` instead of
|
|
@@ -595,33 +1103,308 @@ let activeSchemaNameTransform = null;
|
|
|
595
1103
|
* ref, even when the target was a shared enum like `PaginationOrder`.
|
|
596
1104
|
*/
|
|
597
1105
|
let activeEnumSchemaNames = /* @__PURE__ */ new Set();
|
|
598
|
-
/**
|
|
1106
|
+
/**
|
|
1107
|
+
* Subset of `activeEnumSchemaNames` whose component schema also admits
|
|
1108
|
+
* `null` — via an OAS 3.1 `type: [..., 'null']` array, the OAS 3.0
|
|
1109
|
+
* `nullable: true` flag, or a literal `null` member in `enum:`. A `$ref` to
|
|
1110
|
+
* one of these resolves to `nullable(enum)` so the component's own
|
|
1111
|
+
* nullability survives the reference, matching the shape an inline
|
|
1112
|
+
* `oneOf: [{ $ref }, { type: 'null' }]` already produces.
|
|
1113
|
+
*/
|
|
1114
|
+
let activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
|
|
1115
|
+
/** Drop `null` from an `enum:` list — it marks nullability, it is not a member. */
|
|
1116
|
+
function nonNullEnumValues(values) {
|
|
1117
|
+
return (values ?? []).filter((v) => v !== null);
|
|
1118
|
+
}
|
|
1119
|
+
/** True when an enum schema admits `null` by any OAS 3.0 / 3.1 spelling. */
|
|
1120
|
+
function isNullableEnumSchema(schema) {
|
|
1121
|
+
if (schema.nullable === true) return true;
|
|
1122
|
+
if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
|
|
1123
|
+
return Array.isArray(schema.enum) && schema.enum.includes(null);
|
|
1124
|
+
}
|
|
1125
|
+
/** Wrap `ref` in `nullable` unless it already is one, so a nullable enum `$ref` is never double-wrapped. */
|
|
1126
|
+
function asNullable(ref) {
|
|
1127
|
+
return ref.kind === "nullable" ? ref : {
|
|
1128
|
+
kind: "nullable",
|
|
1129
|
+
inner: ref
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Apply cleanSchemaName + the active transform (if any) to a raw schema name.
|
|
1134
|
+
* When `rawName` is a `components.schemas` key the transform also receives it
|
|
1135
|
+
* as `originalName`, so a consumer can opt out of normalization by returning
|
|
1136
|
+
* the original spelling. Names oagen synthesises (request-body variant
|
|
1137
|
+
* models, title-derived names, ...) are not component keys and get no
|
|
1138
|
+
* `originalName`.
|
|
1139
|
+
*/
|
|
599
1140
|
function resolveSchemaName(rawName) {
|
|
600
1141
|
let name = cleanSchemaName(toPascalCase(rawName));
|
|
601
|
-
if (activeSchemaNameTransform)
|
|
1142
|
+
if (activeSchemaNameTransform) {
|
|
1143
|
+
const originalName = activeComponentSchemaKeys.has(rawName) ? rawName : void 0;
|
|
1144
|
+
name = activeSchemaNameTransform(name, originalName);
|
|
1145
|
+
}
|
|
602
1146
|
return name;
|
|
603
1147
|
}
|
|
1148
|
+
/** Validation keywords copied through unchanged when they hold a number. */
|
|
1149
|
+
const NUMERIC_CONSTRAINT_KEYS = [
|
|
1150
|
+
"multipleOf",
|
|
1151
|
+
"minLength",
|
|
1152
|
+
"maxLength",
|
|
1153
|
+
"minItems",
|
|
1154
|
+
"maxItems",
|
|
1155
|
+
"minContains",
|
|
1156
|
+
"maxContains",
|
|
1157
|
+
"minProperties",
|
|
1158
|
+
"maxProperties"
|
|
1159
|
+
];
|
|
1160
|
+
/**
|
|
1161
|
+
* Collect the JSON Schema validation keywords declared directly on `schema`
|
|
1162
|
+
* into a `Constraints` object, or `undefined` when it declares none — so a
|
|
1163
|
+
* schema without limits yields no `constraints` key on its IR node at all.
|
|
1164
|
+
*
|
|
1165
|
+
* Numeric bounds are normalized to the OAS 3.1 shape. OAS 3.0 spells an
|
|
1166
|
+
* exclusive bound as `exclusiveMinimum: true` beside `minimum: n`; that pair
|
|
1167
|
+
* becomes `exclusiveMinimum: n` with no `minimum`. A boolean `false`, or a
|
|
1168
|
+
* `true` with no bound to apply to, is dropped. The 3.1 numeric form passes
|
|
1169
|
+
* through, including the legal-but-unusual case of both an inclusive and an
|
|
1170
|
+
* exclusive bound on the same side. `uniqueItems: false` restates the default
|
|
1171
|
+
* and is dropped too.
|
|
1172
|
+
*
|
|
1173
|
+
* Only the schema's own keywords are read: limits nested inside array `items`
|
|
1174
|
+
* or map values are not collected. The one exception is a nullable wrapper —
|
|
1175
|
+
* a `oneOf`/`anyOf` with a single non-null, non-`$ref` branch — that declares
|
|
1176
|
+
* none itself: it takes that branch's keywords, since the IR collapses it to
|
|
1177
|
+
* `nullable<T>`. Values of the wrong type (a string `maxLength`, say) are
|
|
1178
|
+
* ignored rather than carried.
|
|
1179
|
+
*/
|
|
1180
|
+
function extractConstraints(schema) {
|
|
1181
|
+
let out = ownConstraints(schema);
|
|
1182
|
+
for (const member of schema.allOf ?? []) out = combineConstraints(out, extractConstraints(member));
|
|
1183
|
+
const branch = singleNonNullBranch(schema);
|
|
1184
|
+
return branch ? combineConstraints(out, extractConstraints(branch)) : out;
|
|
1185
|
+
}
|
|
1186
|
+
/** The keywords declared strictly on `schema`, normalized — without the nullable-branch fallback. */
|
|
1187
|
+
function ownConstraints(schema) {
|
|
1188
|
+
const out = {};
|
|
1189
|
+
const lower = normalizeBound(schema.minimum, schema.exclusiveMinimum);
|
|
1190
|
+
if (lower.inclusive !== void 0) out.minimum = lower.inclusive;
|
|
1191
|
+
if (lower.exclusive !== void 0) out.exclusiveMinimum = lower.exclusive;
|
|
1192
|
+
const upper = normalizeBound(schema.maximum, schema.exclusiveMaximum);
|
|
1193
|
+
if (upper.inclusive !== void 0) out.maximum = upper.inclusive;
|
|
1194
|
+
if (upper.exclusive !== void 0) out.exclusiveMaximum = upper.exclusive;
|
|
1195
|
+
for (const key of NUMERIC_CONSTRAINT_KEYS) {
|
|
1196
|
+
const value = schema[key];
|
|
1197
|
+
if (typeof value === "number") out[key] = value;
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof schema.pattern === "string") out.pattern = schema.pattern;
|
|
1200
|
+
if (schema.uniqueItems === true) out.uniqueItems = true;
|
|
1201
|
+
const dependentRequired = readDependentRequired(schema.dependentRequired);
|
|
1202
|
+
if (dependentRequired) out.dependentRequired = dependentRequired;
|
|
1203
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1204
|
+
}
|
|
1205
|
+
/** The one non-null, non-`$ref` branch of a `oneOf`/`anyOf`, or `undefined` for anything else. */
|
|
1206
|
+
function singleNonNullBranch(schema) {
|
|
1207
|
+
const variants = schema.oneOf ?? schema.anyOf;
|
|
1208
|
+
if (!variants) return void 0;
|
|
1209
|
+
const nonNull = variants.filter((v) => !isNullSchema(v));
|
|
1210
|
+
return nonNull.length === 1 && !nonNull[0].$ref ? nonNull[0] : void 0;
|
|
1211
|
+
}
|
|
1212
|
+
/** True for the `{ type: 'null' }` / `{ type: ['null'] }` branch of a nullable wrapper. */
|
|
1213
|
+
function isNullSchema(schema) {
|
|
1214
|
+
return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* `{ constraints }` when `schema` declares any validation keyword, `{}`
|
|
1218
|
+
* otherwise — spread into an IR node so the key stays absent rather than
|
|
1219
|
+
* present-but-undefined.
|
|
1220
|
+
*/
|
|
1221
|
+
function constraintsEntry(schema) {
|
|
1222
|
+
const constraints = extractConstraints(schema);
|
|
1223
|
+
return constraints ? { constraints } : {};
|
|
1224
|
+
}
|
|
1225
|
+
/** Keywords where the stricter of two declarations is the larger value. */
|
|
1226
|
+
const MIN_KEYS = [
|
|
1227
|
+
"minimum",
|
|
1228
|
+
"exclusiveMinimum",
|
|
1229
|
+
"minLength",
|
|
1230
|
+
"minItems",
|
|
1231
|
+
"minContains",
|
|
1232
|
+
"minProperties"
|
|
1233
|
+
];
|
|
1234
|
+
/** Keywords where the stricter of two declarations is the smaller value. */
|
|
1235
|
+
const MAX_KEYS = [
|
|
1236
|
+
"maximum",
|
|
1237
|
+
"exclusiveMaximum",
|
|
1238
|
+
"maxLength",
|
|
1239
|
+
"maxItems",
|
|
1240
|
+
"maxContains",
|
|
1241
|
+
"maxProperties"
|
|
1242
|
+
];
|
|
1243
|
+
/**
|
|
1244
|
+
* Conjoin two constraint sets the way `allOf` conjoins schemas: an instance
|
|
1245
|
+
* must satisfy both, so every `min*` takes the larger value, every `max*` the
|
|
1246
|
+
* smaller, `uniqueItems` holds if either side demands it, and
|
|
1247
|
+
* `dependentRequired` merges per property with the union of its names
|
|
1248
|
+
* (deduplicated, `a`'s order first). A keyword declared on one side only is
|
|
1249
|
+
* kept as is, and an absent side yields the other unchanged.
|
|
1250
|
+
*
|
|
1251
|
+
* `multipleOf` and `pattern` keep `a`'s value when the two differ: a true
|
|
1252
|
+
* conjunction would need the least common multiple, or a lookahead regex
|
|
1253
|
+
* joining both patterns, and neither is worth carrying into the IR.
|
|
1254
|
+
*
|
|
1255
|
+
* Not for the parameter `$ref` fallback in extractParams: keywords written
|
|
1256
|
+
* beside a `$ref` override the target's there, which is a different operation.
|
|
1257
|
+
*/
|
|
1258
|
+
function combineConstraints(a, b) {
|
|
1259
|
+
if (!a) return b;
|
|
1260
|
+
if (!b) return a;
|
|
1261
|
+
const out = {
|
|
1262
|
+
...a,
|
|
1263
|
+
...b
|
|
1264
|
+
};
|
|
1265
|
+
for (const key of MIN_KEYS) {
|
|
1266
|
+
const [x, y] = [a[key], b[key]];
|
|
1267
|
+
if (x !== void 0 && y !== void 0) out[key] = Math.max(x, y);
|
|
1268
|
+
}
|
|
1269
|
+
for (const key of MAX_KEYS) {
|
|
1270
|
+
const [x, y] = [a[key], b[key]];
|
|
1271
|
+
if (x !== void 0 && y !== void 0) out[key] = Math.min(x, y);
|
|
1272
|
+
}
|
|
1273
|
+
if (a.uniqueItems || b.uniqueItems) out.uniqueItems = true;
|
|
1274
|
+
if (a.multipleOf !== void 0 && b.multipleOf !== void 0) out.multipleOf = a.multipleOf;
|
|
1275
|
+
if (a.pattern !== void 0 && b.pattern !== void 0) out.pattern = a.pattern;
|
|
1276
|
+
if (a.dependentRequired && b.dependentRequired) out.dependentRequired = unionDependentRequired(a.dependentRequired, b.dependentRequired);
|
|
1277
|
+
return out;
|
|
1278
|
+
}
|
|
1279
|
+
/** Per-property union of two `dependentRequired` maps, deduplicated and keeping `a`'s order first. */
|
|
1280
|
+
function unionDependentRequired(a, b) {
|
|
1281
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1282
|
+
for (const [property, names] of Object.entries(a)) merged.set(property, [...names]);
|
|
1283
|
+
for (const [property, names] of Object.entries(b)) {
|
|
1284
|
+
const existing = merged.get(property);
|
|
1285
|
+
if (!existing) {
|
|
1286
|
+
merged.set(property, [...names]);
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
for (const name of names) if (!existing.includes(name)) existing.push(name);
|
|
1290
|
+
}
|
|
1291
|
+
return Object.fromEntries(merged);
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Resolve one side's bound from `minimum` / `maximum` plus its `exclusive*`
|
|
1295
|
+
* companion into the OAS 3.1 shape: a numeric companion is the exclusive
|
|
1296
|
+
* bound itself, `true` turns the inclusive bound into the exclusive one, and
|
|
1297
|
+
* `false` or absent leaves the inclusive bound as is.
|
|
1298
|
+
*/
|
|
1299
|
+
function normalizeBound(inclusive, exclusive) {
|
|
1300
|
+
const bound = typeof inclusive === "number" ? inclusive : void 0;
|
|
1301
|
+
if (typeof exclusive === "number") return {
|
|
1302
|
+
inclusive: bound,
|
|
1303
|
+
exclusive
|
|
1304
|
+
};
|
|
1305
|
+
if (exclusive === true) return { exclusive: bound };
|
|
1306
|
+
return { inclusive: bound };
|
|
1307
|
+
}
|
|
1308
|
+
/** Copy `dependentRequired` when it is an object whose values are string arrays; anything else is ignored. */
|
|
1309
|
+
function readDependentRequired(value) {
|
|
1310
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1311
|
+
const entries = [];
|
|
1312
|
+
for (const [property, deps] of Object.entries(value)) if (Array.isArray(deps) && deps.every((d) => typeof d === "string")) entries.push([property, [...deps]]);
|
|
1313
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Narrow a raw `examples` value to a non-empty list, or undefined. Guards
|
|
1317
|
+
* against documents that put an OAS Example Object map (or anything else)
|
|
1318
|
+
* where JSON Schema expects an array, and against an empty list, so the IR
|
|
1319
|
+
* carries `examples` only when there is at least one value.
|
|
1320
|
+
*/
|
|
1321
|
+
function nonEmptyExamples(value) {
|
|
1322
|
+
return Array.isArray(value) && value.length > 0 ? value : void 0;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Pick the single `example` the IR carries: the document's own `example`
|
|
1326
|
+
* when it is set (including an explicit `null`), otherwise the first entry
|
|
1327
|
+
* of the resolved `examples` list.
|
|
1328
|
+
*/
|
|
1329
|
+
function pickExample(example, examples) {
|
|
1330
|
+
return example !== void 0 ? example : examples?.[0];
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Resolve `example` and `examples` across ordered precedence levels: for a
|
|
1334
|
+
* parameter its own declaration, its inline schema, then the component its
|
|
1335
|
+
* `$ref` points at; for a field its own schema, then its `$ref` target.
|
|
1336
|
+
*
|
|
1337
|
+
* Each value walks the levels independently. `examples` is the first
|
|
1338
|
+
* non-empty list. `example` comes from the first level that has either an
|
|
1339
|
+
* `example` or a list, preferring that level's own `example` over its first
|
|
1340
|
+
* list entry — so a schema-level `example` can never mask the parameter's
|
|
1341
|
+
* own `examples` map. A level with neither falls through; an explicit
|
|
1342
|
+
* `null` is a value and stops the walk.
|
|
1343
|
+
*/
|
|
1344
|
+
function resolveExamples(levels) {
|
|
1345
|
+
let example;
|
|
1346
|
+
let examples;
|
|
1347
|
+
for (const level of levels) {
|
|
1348
|
+
if (examples === void 0) examples = level.examples;
|
|
1349
|
+
if (example === void 0) example = pickExample(level.example, level.examples);
|
|
1350
|
+
}
|
|
1351
|
+
return {
|
|
1352
|
+
example,
|
|
1353
|
+
examples
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* If a schema is a `$ref` to `#/components/schemas/X`, return the referenced
|
|
1358
|
+
* component schema. Used to surface schema-level metadata (default, example,
|
|
1359
|
+
* examples, deprecated, description) that would otherwise be invisible at the
|
|
1360
|
+
* field or parameter level.
|
|
1361
|
+
*
|
|
1362
|
+
* Refs are not pre-resolved by the bundler (`dereference: false` in refs.ts),
|
|
1363
|
+
* so without this, a param like `schema: { $ref: '.../PaginationOrder' }`
|
|
1364
|
+
* silently loses the target's `default: desc` and the SDK stops emitting the
|
|
1365
|
+
* spec-mandated default value in generated method signatures.
|
|
1366
|
+
*/
|
|
1367
|
+
function resolveComponentSchemaRef(schema, componentSchemas) {
|
|
1368
|
+
if (!schema || !componentSchemas) return void 0;
|
|
1369
|
+
const ref = schema.$ref;
|
|
1370
|
+
if (!ref) return void 0;
|
|
1371
|
+
const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
|
|
1372
|
+
if (!match) return void 0;
|
|
1373
|
+
return componentSchemas[match[1]];
|
|
1374
|
+
}
|
|
604
1375
|
function extractSchemas(schemas, options) {
|
|
605
1376
|
const enums = [];
|
|
606
1377
|
if (!schemas) return {
|
|
607
1378
|
models: [],
|
|
608
1379
|
enums
|
|
609
1380
|
};
|
|
1381
|
+
activeComponentSchemaKeys = new Set(Object.keys(schemas));
|
|
610
1382
|
if (options?.schemaNameTransform) {
|
|
611
1383
|
const transform = options.schemaNameTransform;
|
|
612
|
-
const
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
for (const [,
|
|
621
|
-
|
|
1384
|
+
const transformedToRawKeys = /* @__PURE__ */ new Map();
|
|
1385
|
+
for (const raw of Object.keys(schemas)) {
|
|
1386
|
+
const transformed = transform(cleanSchemaName(toPascalCase(raw)), raw);
|
|
1387
|
+
if (!transformedToRawKeys.has(transformed)) transformedToRawKeys.set(transformed, []);
|
|
1388
|
+
transformedToRawKeys.get(transformed).push(raw);
|
|
1389
|
+
}
|
|
1390
|
+
const unsafeRawKeys = /* @__PURE__ */ new Set();
|
|
1391
|
+
const unsafeCleanedNames = /* @__PURE__ */ new Set();
|
|
1392
|
+
for (const [, rawKeys] of transformedToRawKeys) if (rawKeys.length > 1) for (const raw of rawKeys) {
|
|
1393
|
+
unsafeRawKeys.add(raw);
|
|
1394
|
+
unsafeCleanedNames.add(cleanSchemaName(toPascalCase(raw)));
|
|
1395
|
+
}
|
|
1396
|
+
activeSchemaNameTransform = (name, originalName) => {
|
|
1397
|
+
return (originalName !== void 0 ? unsafeRawKeys.has(originalName) : unsafeCleanedNames.has(name)) ? name : transform(name, originalName);
|
|
1398
|
+
};
|
|
622
1399
|
} else activeSchemaNameTransform = null;
|
|
623
1400
|
activeEnumSchemaNames = /* @__PURE__ */ new Set();
|
|
624
|
-
|
|
1401
|
+
activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
|
|
1402
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
1403
|
+
if (!schema.enum) continue;
|
|
1404
|
+
const resolved = resolveSchemaName(name);
|
|
1405
|
+
activeEnumSchemaNames.add(resolved);
|
|
1406
|
+
if (isNullableEnumSchema(schema)) activeNullableEnumSchemaNames.add(resolved);
|
|
1407
|
+
}
|
|
625
1408
|
const modelsByCleanName = /* @__PURE__ */ new Map();
|
|
626
1409
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
627
1410
|
const pascalName = resolveSchemaName(name);
|
|
@@ -632,7 +1415,7 @@ function extractSchemas(schemas, options) {
|
|
|
632
1415
|
if (existing) {
|
|
633
1416
|
if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
|
|
634
1417
|
} else modelsByCleanName.set(pascalName, model);
|
|
635
|
-
for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName)) {
|
|
1418
|
+
for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName, schemas)) {
|
|
636
1419
|
const existingInline = modelsByCleanName.get(inlineModel.name);
|
|
637
1420
|
if (existingInline) {
|
|
638
1421
|
if (inlineModel.fields.length > existingInline.fields.length) modelsByCleanName.set(inlineModel.name, inlineModel);
|
|
@@ -657,6 +1440,8 @@ function extractSchemas(schemas, options) {
|
|
|
657
1440
|
function clearSchemaNameTransform() {
|
|
658
1441
|
activeSchemaNameTransform = null;
|
|
659
1442
|
activeEnumSchemaNames = /* @__PURE__ */ new Set();
|
|
1443
|
+
activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
|
|
1444
|
+
activeComponentSchemaKeys = /* @__PURE__ */ new Set();
|
|
660
1445
|
}
|
|
661
1446
|
/**
|
|
662
1447
|
* Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
|
|
@@ -692,14 +1477,17 @@ function collectInlineEnums(fields, enums) {
|
|
|
692
1477
|
* that schemaToTypeRef turns into model refs but aren't extracted from components.
|
|
693
1478
|
*/
|
|
694
1479
|
function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
|
|
1480
|
+
const directRefs = directModelRefNames(ref);
|
|
695
1481
|
walkTypeRef(ref, { model: (modelRef) => {
|
|
696
1482
|
if (modelNames.has(modelRef.name)) return;
|
|
1483
|
+
if (!directRefs.has(modelRef.name)) return;
|
|
697
1484
|
if (Object.keys(schemas).some((k) => resolveSchemaName(k) === modelRef.name)) return;
|
|
698
1485
|
const parentSchema = findSchemaByName(parentModelName, schemas);
|
|
699
1486
|
if (!parentSchema) return;
|
|
700
1487
|
const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
|
|
701
1488
|
if (!fieldSchema) return;
|
|
702
|
-
const
|
|
1489
|
+
const reserved = reservedInlineNames(fieldName, collectProperties(parentSchema, schemas), parentModelName);
|
|
1490
|
+
const extracted = extractNestedSchema(unionBaseName(modelRef.name, fieldName, fieldSchema, parentModelName, reserved), fieldSchema, schemas, reserved);
|
|
703
1491
|
for (const m of extracted) if (!modelNames.has(m.name)) {
|
|
704
1492
|
models.push(m);
|
|
705
1493
|
modelNames.add(m.name);
|
|
@@ -707,6 +1495,35 @@ function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, mod
|
|
|
707
1495
|
}
|
|
708
1496
|
} });
|
|
709
1497
|
}
|
|
1498
|
+
/**
|
|
1499
|
+
* The base name `extractNestedSchema` should name a field's `oneOf` variants
|
|
1500
|
+
* from: the field's qualified inline name when naming the variants from it
|
|
1501
|
+
* yields `refName` (the ref the walk is resolving), else `refName` itself,
|
|
1502
|
+
* which is right for every non-union field and for a union whose variant 0 is
|
|
1503
|
+
* bare.
|
|
1504
|
+
*/
|
|
1505
|
+
function unionBaseName(refName, fieldName, fieldSchema, parentModelName, reserved) {
|
|
1506
|
+
if (!fieldSchema.oneOf) return refName;
|
|
1507
|
+
const fromField = qualifyInlineModelName(toPascalCase(fieldName), parentModelName);
|
|
1508
|
+
return [...nameInlineVariants(fieldSchema, fromField, reserved).values()].includes(refName) ? fromField : refName;
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* The model refs a field's TypeRef produces directly: the ref itself, the
|
|
1512
|
+
* items of a (possibly nullable) array, or each (possibly nullable) variant of
|
|
1513
|
+
* a union — the shapes `extractNestedSchema` can name from a ref. Refs nested
|
|
1514
|
+
* any deeper are excluded.
|
|
1515
|
+
*/
|
|
1516
|
+
function directModelRefNames(ref) {
|
|
1517
|
+
const names = /* @__PURE__ */ new Set();
|
|
1518
|
+
let root = ref;
|
|
1519
|
+
while (root.kind === "nullable" || root.kind === "array") root = root.kind === "nullable" ? root.inner : root.items;
|
|
1520
|
+
const leaves = root.kind === "union" ? root.variants : [root];
|
|
1521
|
+
for (const leaf of leaves) {
|
|
1522
|
+
const inner = leaf.kind === "nullable" ? leaf.inner : leaf;
|
|
1523
|
+
if (inner.kind === "model") names.add(inner.name);
|
|
1524
|
+
}
|
|
1525
|
+
return names;
|
|
1526
|
+
}
|
|
710
1527
|
/** Find a component schema by PascalCase name. */
|
|
711
1528
|
function findSchemaByName(pascalName, schemas) {
|
|
712
1529
|
for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
|
|
@@ -735,104 +1552,278 @@ function findNestedFieldSchema(fieldName, parentSchema, schemas) {
|
|
|
735
1552
|
}
|
|
736
1553
|
return null;
|
|
737
1554
|
}
|
|
738
|
-
/**
|
|
739
|
-
|
|
1555
|
+
/**
|
|
1556
|
+
* Extract a model (and nested models) from an inline field schema. `reserved`
|
|
1557
|
+
* holds the inline names the field's sibling properties mint
|
|
1558
|
+
* (`reservedInlineNames`), which a `oneOf` variant must not take.
|
|
1559
|
+
*/
|
|
1560
|
+
function extractNestedSchema(name, schema, schemas, reserved = NO_RESERVED) {
|
|
740
1561
|
if (schema.oneOf) {
|
|
741
1562
|
const models = [];
|
|
742
|
-
const
|
|
1563
|
+
const variantNames = nameInlineVariants(schema, name, reserved);
|
|
743
1564
|
for (const variant of schema.oneOf) {
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const variantName = nameVariantModel(variant, name, models, constNamingDiscriminator);
|
|
753
|
-
models.push({
|
|
754
|
-
name: variantName,
|
|
755
|
-
description: variant.description,
|
|
756
|
-
fields
|
|
757
|
-
});
|
|
1565
|
+
const variantName = variantNames.get(variant);
|
|
1566
|
+
if (variantName === void 0) continue;
|
|
1567
|
+
const requiredSet = new Set(variant.required ?? []);
|
|
1568
|
+
const siblings = collectProperties(variant);
|
|
1569
|
+
const fields = [];
|
|
1570
|
+
for (const [fn, fs] of Object.entries(variant.properties ?? {})) {
|
|
1571
|
+
if (!fs) continue;
|
|
1572
|
+
fields.push(buildFieldFromSchema(fn, fs, variantName, requiredSet, schemas, siblings));
|
|
758
1573
|
}
|
|
1574
|
+
models.push({
|
|
1575
|
+
name: variantName,
|
|
1576
|
+
description: variant.description,
|
|
1577
|
+
fields,
|
|
1578
|
+
...constraintsEntry(variant)
|
|
1579
|
+
});
|
|
759
1580
|
}
|
|
760
1581
|
return models;
|
|
761
1582
|
}
|
|
762
1583
|
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
763
1584
|
const requiredSet = new Set(schema.required ?? []);
|
|
1585
|
+
const siblings = collectProperties(schema);
|
|
764
1586
|
const fields = [];
|
|
765
1587
|
for (const [fn, fs] of Object.entries(schema.properties)) {
|
|
766
1588
|
if (!fs) continue;
|
|
767
|
-
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
1589
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet, schemas, siblings));
|
|
768
1590
|
}
|
|
769
1591
|
return [{
|
|
770
1592
|
name,
|
|
771
1593
|
description: schema.description,
|
|
772
|
-
fields
|
|
1594
|
+
fields,
|
|
1595
|
+
...constraintsEntry(schema)
|
|
773
1596
|
}];
|
|
774
1597
|
}
|
|
775
1598
|
if (schema.type === "array" && schema.items?.properties) {
|
|
776
1599
|
const requiredSet = new Set(schema.items.required ?? []);
|
|
1600
|
+
const siblings = collectProperties(schema.items);
|
|
777
1601
|
const fields = [];
|
|
778
1602
|
for (const [fn, fs] of Object.entries(schema.items.properties)) {
|
|
779
1603
|
if (!fs) continue;
|
|
780
|
-
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
1604
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet, schemas, siblings));
|
|
781
1605
|
}
|
|
782
1606
|
return [{
|
|
783
1607
|
name,
|
|
784
1608
|
description: schema.items.description,
|
|
785
|
-
fields
|
|
1609
|
+
fields,
|
|
1610
|
+
...constraintsEntry(schema.items)
|
|
786
1611
|
}];
|
|
787
1612
|
}
|
|
788
1613
|
return [];
|
|
789
1614
|
}
|
|
790
1615
|
/**
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
* The
|
|
805
|
-
*
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1616
|
+
* The prefix a variant's `title` contributes to its name: the title trimmed
|
|
1617
|
+
* and PascalCased, or null when there is no title or it PascalCases to
|
|
1618
|
+
* nothing.
|
|
1619
|
+
*/
|
|
1620
|
+
function variantTitlePrefix(variant) {
|
|
1621
|
+
const title = typeof variant["title"] === "string" ? variant["title"].trim() : "";
|
|
1622
|
+
return title ? toPascalCase(title) || null : null;
|
|
1623
|
+
}
|
|
1624
|
+
/** `<Prefix><Base>`, or `base` unchanged when it already starts with the prefix (no doubling). */
|
|
1625
|
+
function prefixedVariantName(prefix, base) {
|
|
1626
|
+
return base.startsWith(prefix) ? base : `${prefix}${base}`;
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* The properties `variant` declares that no other inline object variant of
|
|
1630
|
+
* the same union declares, in the variant's own `properties` order.
|
|
1631
|
+
*/
|
|
1632
|
+
function distinguishingProperties(variant, siblings) {
|
|
1633
|
+
const declaredElsewhere = /* @__PURE__ */ new Set();
|
|
1634
|
+
for (const sibling of siblings) {
|
|
1635
|
+
if (sibling === variant) continue;
|
|
1636
|
+
for (const property of Object.keys(sibling.properties ?? {})) declaredElsewhere.add(property);
|
|
1637
|
+
}
|
|
1638
|
+
return Object.keys(variant.properties ?? {}).filter((property) => !declaredElsewhere.has(property));
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Name an inline object variant of a `oneOf`/`anyOf` that has no better
|
|
1642
|
+
* signal: it is not variant 0 (which keeps the union's base name) and no
|
|
1643
|
+
* const discriminator gave it a prefix. The name is derived from the
|
|
1644
|
+
* variant's own content, never from where it sits in the document (beyond
|
|
1645
|
+
* its position among the variants, which only the final counter reads), so
|
|
1646
|
+
* an SDK user can tell the variants apart and reordering them changes nothing
|
|
1647
|
+
* but which one is variant 0. In order, taking the first candidate that is
|
|
1648
|
+
* neither the base name nor a name already given to an earlier variant of
|
|
1649
|
+
* the same union:
|
|
1650
|
+
*
|
|
1651
|
+
* 1. the variant's `title` (trimmed and PascalCased): `<Title><Base>`
|
|
1652
|
+
* (`CircleDrawingShape`), the same prefix form a const discriminator
|
|
1653
|
+
* produces, including the rule that a base already starting with the
|
|
1654
|
+
* prefix is not doubled (that candidate equals the base, so it falls
|
|
1655
|
+
* through); a title an earlier variant already took falls through too;
|
|
1656
|
+
* 2. a distinguishing property — the first property, in the variant's own
|
|
1657
|
+
* order, that it declares and no other inline object variant declares:
|
|
1658
|
+
* `<Base>With<Property>` (`DrawingShapeWithSide`); a property that
|
|
1659
|
+
* PascalCases to nothing is skipped;
|
|
1660
|
+
* 3. the position counter, `<Base><position + 1>` (`DrawingShape2`), which
|
|
1661
|
+
* now remains only for variants whose property sets are identical, and
|
|
1662
|
+
* which keeps counting past a taken name (`DrawingShape3`).
|
|
1663
|
+
*
|
|
1664
|
+
* Every candidate is also checked against `reserved`: the inline names the
|
|
1665
|
+
* parent's other properties mint (`reservedInlineNames`), so a variant can
|
|
1666
|
+
* never take a name another field's model already has — beside a
|
|
1667
|
+
* `shape_with_side` object, the `{ side }` variant of `shape` is not
|
|
1668
|
+
* `DrawingShapeWithSide` but the next candidate. Read from sibling names and
|
|
1669
|
+
* content only, so document order plays no part.
|
|
1670
|
+
*
|
|
1671
|
+
* `siblings` are the inline object variants of the union, this one included;
|
|
1672
|
+
* `emitted` are the names already given to the earlier ones, in order. Every
|
|
1673
|
+
* site that names a variant — the union TypeRef, the component-field,
|
|
1674
|
+
* request-body and response extractors, and `mintedInlineNames` — goes
|
|
1675
|
+
* through this function (via `nameInlineVariant`), so they cannot drift.
|
|
1676
|
+
*/
|
|
1677
|
+
function nameUndiscriminatedVariant(variant, siblings, base, emitted, reserved = NO_RESERVED) {
|
|
1678
|
+
const taken = (candidate) => candidate === base || emitted.includes(candidate) || reserved.has(candidate);
|
|
1679
|
+
const titlePrefix = variantTitlePrefix(variant);
|
|
1680
|
+
if (titlePrefix !== null) {
|
|
1681
|
+
const candidate = prefixedVariantName(titlePrefix, base);
|
|
1682
|
+
if (!taken(candidate)) return candidate;
|
|
1683
|
+
}
|
|
1684
|
+
for (const property of distinguishingProperties(variant, siblings)) {
|
|
1685
|
+
const word = toPascalCase(property);
|
|
1686
|
+
if (!word) continue;
|
|
1687
|
+
const candidate = `${base}With${word}`;
|
|
1688
|
+
if (!taken(candidate)) return candidate;
|
|
1689
|
+
}
|
|
1690
|
+
for (let position = emitted.length + 1;; position++) {
|
|
1691
|
+
const candidate = `${base}${position}`;
|
|
1692
|
+
if (!taken(candidate)) return candidate;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
/** No reserved names: for a union with no sibling properties (a request body or response that is itself a union). */
|
|
1696
|
+
const NO_RESERVED = /* @__PURE__ */ new Set();
|
|
1697
|
+
/**
|
|
1698
|
+
* True when the union's inline object variants (two or more) each carry a
|
|
1699
|
+
* title and no two titles PascalCase to the same prefix: the one case in
|
|
1700
|
+
* which variant 0 is named from its title rather than keeping the base name.
|
|
1701
|
+
*/
|
|
1702
|
+
function allDistinctlyTitled(siblings) {
|
|
1703
|
+
if (siblings.length < 2) return false;
|
|
1704
|
+
const prefixes = siblings.map(variantTitlePrefix);
|
|
1705
|
+
return prefixes.every((prefix) => prefix !== null) && new Set(prefixes).size === prefixes.length;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* The name of one inline object variant of a `oneOf`/`anyOf`, given the
|
|
1709
|
+
* union's base name (the field's qualified inline name, e.g. `ApiKeyOwner`),
|
|
1710
|
+
* the inline object variants of the union (`siblings`, this one included),
|
|
1711
|
+
* the names already given to the earlier variants (`emitted`, in order), and
|
|
1712
|
+
* the const-valued property that distinguishes the variants when there is
|
|
1713
|
+
* one (`deriveConstNamingDiscriminator`).
|
|
1714
|
+
*
|
|
1715
|
+
* Variant 0 keeps the bare base name: the union TypeRef and every extractor
|
|
1716
|
+
* reference it under that name, and a consumer's typed field keeps the name
|
|
1717
|
+
* it always had. The one exception: when the union has no const discriminator
|
|
1718
|
+
* and every variant carries a distinct title, variant 0 is named from its
|
|
1719
|
+
* title like the others (`CircleDrawingShape` beside `SquareDrawingShape`)
|
|
1720
|
+
* instead of being the only one left bare. Whether the union is discriminated
|
|
1721
|
+
* is read from the variants themselves here, not from `discriminator`, so a
|
|
1722
|
+
* caller forcing a property (an explicit `discriminator:` without `mapping:`)
|
|
1723
|
+
* still agrees with the extractors on variant 0.
|
|
1724
|
+
*
|
|
1725
|
+
* A later variant takes `<ConstValue><Base>` from the discriminator
|
|
1726
|
+
* (`UserApiKeyOwner`) when the value PascalCases to something and the name is
|
|
1727
|
+
* neither the base, nor already emitted, nor minted by one of the parent's
|
|
1728
|
+
* other properties (`reserved`), and otherwise falls to
|
|
1729
|
+
* `nameUndiscriminatedVariant`: its title, a distinguishing property, then
|
|
1730
|
+
* the position counter, each checked the same way.
|
|
1731
|
+
*/
|
|
1732
|
+
function nameInlineVariant(variant, siblings, base, emitted, discriminator, reserved = NO_RESERVED) {
|
|
1733
|
+
if (emitted.length === 0) {
|
|
1734
|
+
const titlePrefix = deriveConstNamingDiscriminator([...siblings]) === null && allDistinctlyTitled(siblings) ? variantTitlePrefix(variant) : null;
|
|
1735
|
+
return titlePrefix === null ? base : prefixedVariantName(titlePrefix, base);
|
|
1736
|
+
}
|
|
819
1737
|
if (discriminator) {
|
|
820
1738
|
const constValue = getConstPropertyValue(variant, discriminator.property);
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1739
|
+
const prefix = constValue ? toPascalCase(constValue) : "";
|
|
1740
|
+
if (prefix) {
|
|
1741
|
+
const candidate = prefixedVariantName(prefix, base);
|
|
1742
|
+
if (candidate !== base && !emitted.includes(candidate) && !reserved.has(candidate)) return candidate;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return nameUndiscriminatedVariant(variant, siblings, base, emitted, reserved);
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Name every inline object variant of a `oneOf`/`anyOf` schema from the
|
|
1749
|
+
* union's base name, keyed by variant schema identity; null, non-object and
|
|
1750
|
+
* `$ref` variants get no entry. Positions are local to this variant list, so
|
|
1751
|
+
* the union's TypeRef entries and the extracted models agree no matter what
|
|
1752
|
+
* else has been extracted. `reserved` holds the inline names the parent's
|
|
1753
|
+
* other properties mint (`reservedInlineNames`); every site that has a
|
|
1754
|
+
* sibling set passes the same one, so the reference and the model step
|
|
1755
|
+
* around them identically.
|
|
1756
|
+
*
|
|
1757
|
+
* One union shape is named differently: a `oneOf` with no explicit
|
|
1758
|
+
* `discriminator:` whose every variant carries a `$ref` or a `title` and pins
|
|
1759
|
+
* the same const property (`titleMappedDiscriminatorProperty`). Its
|
|
1760
|
+
* discriminator mapping has always named an inline variant by its title
|
|
1761
|
+
* (`resolveSchemaName(title)`: `Circle`, `Square`, run through the project's
|
|
1762
|
+
* schema-name transform like a component), and `schemaToTypeRef` references
|
|
1763
|
+
* the variant by that mapped name, so the title is the model name here too —
|
|
1764
|
+
* unless it is the base, already emitted, or reserved by a sibling, in which
|
|
1765
|
+
* case the variant takes a context-qualified name (its title as a prefix on
|
|
1766
|
+
* the base, `CircleDrawingMarker`, else a distinguishing property, else the
|
|
1767
|
+
* counter) and the mapping, which is read from this map, follows it. A kept
|
|
1768
|
+
* title is context-free: it is the same whatever field, array or map the
|
|
1769
|
+
* union sits under, so it names one shape wherever it appears, as a
|
|
1770
|
+
* component name would. A second union beside it using the same title for a
|
|
1771
|
+
* variant of the identical shape (`schemaShapeKey`) therefore shares the
|
|
1772
|
+
* model instead of stepping around it; a sibling giving the same title a
|
|
1773
|
+
* different shape reserves it (`reservedInlineNames`), and since both sides
|
|
1774
|
+
* read their siblings in isolation the decision is symmetric: when the
|
|
1775
|
+
* shapes differ, both unions fall back to their context-qualified names
|
|
1776
|
+
* (`CircleDrawingShape`, `CircleDrawingMarker`) and neither keeps the bare
|
|
1777
|
+
* `Circle`; when they are identical, both keep `Circle` and one model is
|
|
1778
|
+
* emitted. `elementContextName` never lets a context-free name drive an
|
|
1779
|
+
* items/values step-aside, which no context could satisfy.
|
|
1780
|
+
* `nameInlineVariantEntries` reports which names are context-free; a
|
|
1781
|
+
* fallen-back title name is context-dependent.
|
|
1782
|
+
*
|
|
1783
|
+
* This is the list-level entry point every site uses: `schemaToTypeRef` (via
|
|
1784
|
+
* `nameInlineObjectVariants`), `extractNestedSchema`,
|
|
1785
|
+
* `extractInlineModelDeep`, `extractInlineModelsFromProperties` and
|
|
1786
|
+
* `mintedInlineNames`.
|
|
1787
|
+
*/
|
|
1788
|
+
function nameInlineVariants(union, base, reserved = NO_RESERVED) {
|
|
1789
|
+
const names = /* @__PURE__ */ new Map();
|
|
1790
|
+
for (const [variant, entry] of nameInlineVariantEntries(union, base, reserved)) names.set(variant, entry.name);
|
|
1791
|
+
return names;
|
|
1792
|
+
}
|
|
1793
|
+
/** `nameInlineVariants`, also telling for each name whether it is context-free. */
|
|
1794
|
+
function nameInlineVariantEntries(union, base, reserved = NO_RESERVED) {
|
|
1795
|
+
const nonNullVariants = (union.oneOf ?? union.anyOf ?? []).filter((v) => !isNullVariantSchema(v));
|
|
1796
|
+
const inlineObjectVariants = nonNullVariants.filter((v) => isInlineObjectSchema(v));
|
|
1797
|
+
const discriminator = deriveConstNamingDiscriminator(inlineObjectVariants);
|
|
1798
|
+
const titleMapped = union.oneOf !== void 0 && !union.discriminator && titleMappedDiscriminatorProperty(nonNullVariants) !== null;
|
|
1799
|
+
const emitted = [];
|
|
1800
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1801
|
+
for (const variant of inlineObjectVariants) {
|
|
1802
|
+
let name = null;
|
|
1803
|
+
let contextFree = false;
|
|
1804
|
+
if (titleMapped) {
|
|
1805
|
+
const titled = resolveVariantModelName(variant);
|
|
1806
|
+
if (titled !== null) {
|
|
1807
|
+
if (titled !== base && !emitted.includes(titled) && !reserved.has(titled)) {
|
|
1808
|
+
name = titled;
|
|
1809
|
+
contextFree = true;
|
|
1810
|
+
} else name = nameUndiscriminatedVariant(variant, inlineObjectVariants, base, emitted, reserved);
|
|
826
1811
|
}
|
|
827
1812
|
}
|
|
1813
|
+
name ??= nameInlineVariant(variant, inlineObjectVariants, base, emitted, discriminator, reserved);
|
|
1814
|
+
emitted.push(name);
|
|
1815
|
+
entries.set(variant, {
|
|
1816
|
+
name,
|
|
1817
|
+
contextFree
|
|
1818
|
+
});
|
|
828
1819
|
}
|
|
829
|
-
return
|
|
1820
|
+
return entries;
|
|
830
1821
|
}
|
|
831
1822
|
/**
|
|
832
1823
|
* Returns a discriminator descriptor when every inline object variant in a
|
|
833
1824
|
* oneOf has a string-const value on the same property — the signature of a
|
|
834
1825
|
* discriminated union without an explicit `discriminator:` keyword. Used by
|
|
835
|
-
* `
|
|
1826
|
+
* `nameInlineVariant` to derive variant names from the const values.
|
|
836
1827
|
*
|
|
837
1828
|
* Distinct from `detectConstPropertyDiscriminator` (which requires every
|
|
838
1829
|
* variant to carry a `$ref` or `title` so a name mapping can be built):
|
|
@@ -850,18 +1841,96 @@ function deriveConstNamingDiscriminator(variants) {
|
|
|
850
1841
|
}
|
|
851
1842
|
return null;
|
|
852
1843
|
}
|
|
853
|
-
/**
|
|
854
|
-
|
|
1844
|
+
/**
|
|
1845
|
+
* True for a schema that `schemaToTypeRef` turns into an inline model reference:
|
|
1846
|
+
* it declares `properties` directly (no `$ref`) and is an object — whether by
|
|
1847
|
+
* `type: 'object'`, by omitting `type`, or by an OAS 3.1 nullable array such as
|
|
1848
|
+
* `type: ['object', 'null']`. Every inline-model extractor must use the same
|
|
1849
|
+
* test, or a field's reference points at a model nobody materializes.
|
|
1850
|
+
*/
|
|
1851
|
+
function isInlineObjectSchema(schema) {
|
|
1852
|
+
if (!schema || schema.$ref || !schema.properties) return false;
|
|
1853
|
+
const { type } = schema;
|
|
1854
|
+
return type === void 0 || type === "object" || Array.isArray(type) && type.includes("object");
|
|
1855
|
+
}
|
|
1856
|
+
/** The `{ type: 'null' }` member that marks a `oneOf`/`anyOf` as nullable; never a variant to name. */
|
|
1857
|
+
function isNullVariantSchema(schema) {
|
|
1858
|
+
return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* The synthetic context names a multi-type inline variant's non-object
|
|
1862
|
+
* members are typed and extracted under, chosen so the models they name can
|
|
1863
|
+
* never collide with a model named from one of the variant's own properties.
|
|
1864
|
+
* An `array` member's items go under `item` (`<Variant>Item`), falling back
|
|
1865
|
+
* to `array_item` (`<Variant>ArrayItem`) and then `array_item2`,
|
|
1866
|
+
* `array_item3`, ... when a property already claims that name; any other
|
|
1867
|
+
* member goes under `value` (`<Variant>Value`), then `member_value`,
|
|
1868
|
+
* `member_value2`, .... A candidate is skipped when the model name
|
|
1869
|
+
* `qualifyInlineModelName` derives from it equals one derived from a
|
|
1870
|
+
* property — compared after qualification, so a property `items`, whose
|
|
1871
|
+
* name singularizes to `<Variant>Item`, blocks `item` as well.
|
|
1872
|
+
* This is the single source of these names for the TypeRef builder
|
|
1873
|
+
* (`inlineVariantTypeRef`) and the extractors (`variantMemberSchemas`).
|
|
1874
|
+
*/
|
|
1875
|
+
function variantMemberKeys(variant, variantName) {
|
|
1876
|
+
const modelNameFor = (key) => qualifyInlineModelName(toPascalCase(key), variantName);
|
|
1877
|
+
const claimed = new Set(Object.keys(variant.properties ?? {}).map(modelNameFor));
|
|
1878
|
+
const pick = (preferred, fallback) => {
|
|
1879
|
+
for (let i = 0;; i++) {
|
|
1880
|
+
const candidate = i === 0 ? preferred : i === 1 ? fallback : `${fallback}${i}`;
|
|
1881
|
+
if (!claimed.has(modelNameFor(candidate))) return candidate;
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
return {
|
|
1885
|
+
item: pick("item", "array_item"),
|
|
1886
|
+
value: pick("value", "member_value")
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* The schemas carried by the non-object members of a multi-type inline
|
|
1891
|
+
* variant (`type: ['object', 'array']` with `items`), keyed by the synthetic
|
|
1892
|
+
* context name from `variantMemberKeys` that `schemaToTypeRef` types them
|
|
1893
|
+
* under, so an inline object item is `<Variant>Item` (or `<Variant>ArrayItem`
|
|
1894
|
+
* when `item` is taken) and its own nested objects `<Variant>ItemChild`.
|
|
1895
|
+
* Every extractor re-enters with these under the variant's name so the
|
|
1896
|
+
* models line up with the refs by construction. A map member cannot occur
|
|
1897
|
+
* here: a variant with `properties` is typed as a model, never as a map.
|
|
1898
|
+
*/
|
|
1899
|
+
function variantMemberSchemas(variant, variantName) {
|
|
1900
|
+
const members = {};
|
|
1901
|
+
if (Array.isArray(variant.type) && variant.type.includes("array") && variant.items) members[variantMemberKeys(variant, variantName).item] = variant.items;
|
|
1902
|
+
return members;
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Build a single Field from a schema property entry. Shared across all
|
|
1906
|
+
* extraction sites. Pass `componentSchemas` where they are at hand so a
|
|
1907
|
+
* field whose schema is a bare `$ref` can take `example`/`examples` from the
|
|
1908
|
+
* component it points at, as a parameter does. `siblingProperties` are the
|
|
1909
|
+
* other properties of the object the field belongs to (`collectProperties`);
|
|
1910
|
+
* array items and map values use them to step aside from a sibling that
|
|
1911
|
+
* would take their singular name (`elementContextName`).
|
|
1912
|
+
*/
|
|
1913
|
+
function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet, componentSchemas, siblingProperties) {
|
|
1914
|
+
const refTarget = resolveComponentSchemaRef(fieldSchema, componentSchemas);
|
|
1915
|
+
const { example, examples } = resolveExamples([{
|
|
1916
|
+
example: fieldSchema.example,
|
|
1917
|
+
examples: nonEmptyExamples(fieldSchema.examples)
|
|
1918
|
+
}, {
|
|
1919
|
+
example: refTarget?.example,
|
|
1920
|
+
examples: nonEmptyExamples(refTarget?.examples)
|
|
1921
|
+
}]);
|
|
855
1922
|
return {
|
|
856
1923
|
name: fieldName,
|
|
857
|
-
type: schemaToTypeRef(fieldSchema, fieldName, contextName),
|
|
1924
|
+
type: schemaToTypeRef(fieldSchema, fieldName, contextName, siblingProperties),
|
|
858
1925
|
required: requiredSet.has(fieldName),
|
|
859
1926
|
description: fieldSchema.description,
|
|
860
1927
|
readOnly: fieldSchema.readOnly || void 0,
|
|
861
1928
|
writeOnly: fieldSchema.writeOnly || void 0,
|
|
862
1929
|
deprecated: fieldSchema.deprecated || void 0,
|
|
863
1930
|
default: fieldSchema.default,
|
|
864
|
-
example
|
|
1931
|
+
example,
|
|
1932
|
+
...examples ? { examples } : {},
|
|
1933
|
+
...constraintsEntry(fieldSchema)
|
|
865
1934
|
};
|
|
866
1935
|
}
|
|
867
1936
|
/**
|
|
@@ -898,39 +1967,27 @@ function buildSyntheticEnumRef(values, contextName, parentModelName) {
|
|
|
898
1967
|
};
|
|
899
1968
|
}
|
|
900
1969
|
/**
|
|
901
|
-
* Detect an implicit discriminator on
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
1970
|
+
* Detect an implicit discriminator on a `oneOf` whose every variant pins the
|
|
1971
|
+
* same property to a const value AND is nameable on its own — by a `$ref` to
|
|
1972
|
+
* a component or by a `title` — so the IR can represent the union as
|
|
1973
|
+
* discriminated, with a mapping to concrete names, without an explicit
|
|
1974
|
+
* `discriminator:` keyword. Returns the property, or null when any variant
|
|
1975
|
+
* doesn't fit the pattern or when the variants disagree on which property
|
|
1976
|
+
* carries the discriminator. The mapping itself is built by `schemaToTypeRef`
|
|
1977
|
+
* from the names `nameInlineVariants` gives the inline variants (their titles
|
|
1978
|
+
* unless taken) and the component names of `$ref` variants, so it can only
|
|
1979
|
+
* ever point at a model that exists.
|
|
905
1980
|
*
|
|
906
|
-
* Both shapes are supported:
|
|
1981
|
+
* Both const shapes are supported:
|
|
907
1982
|
* { properties: { event: { const: "x" } }, required: ["event"] }
|
|
908
1983
|
* { properties: { event: { type: "string", enum: ["x"] } } }
|
|
909
|
-
*
|
|
910
|
-
* Returns null when any variant doesn't fit the pattern or when variants
|
|
911
|
-
* disagree on which property carries the discriminator.
|
|
912
1984
|
*/
|
|
913
|
-
function
|
|
1985
|
+
function titleMappedDiscriminatorProperty(variants) {
|
|
914
1986
|
if (variants.length < 2) return null;
|
|
915
|
-
|
|
1987
|
+
if (!variants.every((v) => resolveVariantModelName(v) !== null)) return null;
|
|
916
1988
|
const candidates = Object.keys(variants[0]?.properties ?? {});
|
|
917
|
-
for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null))
|
|
918
|
-
|
|
919
|
-
break;
|
|
920
|
-
}
|
|
921
|
-
if (!candidateProperty) return null;
|
|
922
|
-
const mapping = {};
|
|
923
|
-
for (const v of variants) {
|
|
924
|
-
const value = getConstPropertyValue(v, candidateProperty);
|
|
925
|
-
if (value === null) return null;
|
|
926
|
-
const variantName = resolveVariantModelName(v);
|
|
927
|
-
if (!variantName) return null;
|
|
928
|
-
mapping[value] = variantName;
|
|
929
|
-
}
|
|
930
|
-
return {
|
|
931
|
-
property: candidateProperty,
|
|
932
|
-
mapping
|
|
933
|
-
};
|
|
1989
|
+
for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) return propName;
|
|
1990
|
+
return null;
|
|
934
1991
|
}
|
|
935
1992
|
function getConstPropertyValue(schema, property) {
|
|
936
1993
|
const prop = schema.properties?.[property];
|
|
@@ -956,42 +2013,96 @@ function mapVariantToDiscriminatorEntry(variant, discriminator) {
|
|
|
956
2013
|
}
|
|
957
2014
|
/**
|
|
958
2015
|
* Derive a discriminator value → variant model name mapping for an inline-
|
|
959
|
-
* variant `oneOf`
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
*
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
* built from `contextName` (the field name) plus `parentModelName` (the
|
|
970
|
-
* containing model). Returns null when any non-object variant is present
|
|
971
|
-
* or when a variant lacks a const value on the discriminator property.
|
|
972
|
-
*/
|
|
973
|
-
function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, parentModelName) {
|
|
2016
|
+
* variant `oneOf` from the names `nameInlineVariants` gave its variants
|
|
2017
|
+
* (`inlineVariantNames`): the explicit `discriminator:` without a `mapping:`
|
|
2018
|
+
* clause, and the implicit const discriminator of anonymous inline variants
|
|
2019
|
+
* (`ApiKeyCreatedData.owner`: `ApiKeyCreatedDataOwner`,
|
|
2020
|
+
* `UserApiKeyCreatedDataOwner`). Reading the names rather than deriving them
|
|
2021
|
+
* again is what keeps the mapping pointing at models the extractors create.
|
|
2022
|
+
* Returns null without a context name, when any variant is not an inline
|
|
2023
|
+
* object, or when a variant lacks a const value on the property.
|
|
2024
|
+
*/
|
|
2025
|
+
function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, inlineVariantNames) {
|
|
974
2026
|
if (variants.length === 0 || !contextName) return null;
|
|
975
|
-
const inlineObjectVariants = variants.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
|
|
976
|
-
if (inlineObjectVariants.length !== variants.length) return null;
|
|
977
|
-
const baseInlineName = qualifyInlineModelName(toPascalCase(contextName), parentModelName);
|
|
978
|
-
const namingDiscriminator = { property: discriminatorProperty };
|
|
979
2027
|
const mapping = {};
|
|
980
|
-
const
|
|
981
|
-
|
|
2028
|
+
for (const variant of variants) {
|
|
2029
|
+
const variantName = inlineVariantNames.get(variant);
|
|
2030
|
+
if (variantName === void 0) return null;
|
|
982
2031
|
const constValue = getConstPropertyValue(variant, discriminatorProperty);
|
|
983
2032
|
if (constValue === null) return null;
|
|
984
|
-
const variantName = nameVariantModel(variant, baseInlineName, emittedSoFar, namingDiscriminator);
|
|
985
2033
|
mapping[constValue] = variantName;
|
|
986
|
-
emittedSoFar.push({
|
|
987
|
-
name: variantName,
|
|
988
|
-
fields: []
|
|
989
|
-
});
|
|
990
2034
|
}
|
|
991
2035
|
return mapping;
|
|
992
2036
|
}
|
|
2037
|
+
/**
|
|
2038
|
+
* The names reserved for a union typed under `contextName`: the set an
|
|
2039
|
+
* enclosing field handed down (`reservedNames`, for a union among a field's
|
|
2040
|
+
* array items or map values), else the inline names the field's own sibling
|
|
2041
|
+
* properties mint (`reservedInlineNames`), else nothing. The extractor
|
|
2042
|
+
* derives the same set at each of those levels, so the reference and the
|
|
2043
|
+
* model step around a sibling's name identically.
|
|
2044
|
+
*/
|
|
2045
|
+
function reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames) {
|
|
2046
|
+
if (reservedNames) return reservedNames;
|
|
2047
|
+
if (contextName === void 0) return NO_RESERVED;
|
|
2048
|
+
return reservedInlineNames(contextName, siblingProperties, parentModelName);
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Name the inline object variants of a field's `oneOf`/`anyOf` exactly as the
|
|
2052
|
+
* inline-model extractors do (`extractInlineModelsFromProperties` in
|
|
2053
|
+
* inline-models.ts, and `extractNestedSchema` / `extractInlineModelDeep`
|
|
2054
|
+
* here): the union's base name is the field's qualified inline name, the
|
|
2055
|
+
* names the field's sibling properties mint — or the set handed down from an
|
|
2056
|
+
* enclosing field — are reserved (`reservedNamesFor`), and
|
|
2057
|
+
* `nameInlineVariants` hands out the variant names from there. Returned keyed
|
|
2058
|
+
* by variant schema identity.
|
|
2059
|
+
*/
|
|
2060
|
+
function nameInlineObjectVariants(union, contextName, parentModelName, siblingProperties, reservedNames) {
|
|
2061
|
+
return nameInlineVariants(union, qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName), reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames));
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* The TypeRef for an inline object variant referenced by the name its
|
|
2065
|
+
* extracted model carries, keeping every other type the variant allows.
|
|
2066
|
+
* Mirrors what `schemaToTypeRef` builds for the same schema — one member per
|
|
2067
|
+
* non-null entry of a `type` array, `union`-wrapped when there is more than
|
|
2068
|
+
* one, `nullable`-wrapped when `null` or OAS 3.0 `nullable: true` is present
|
|
2069
|
+
* — with the `object` member swapped for the named ref:
|
|
2070
|
+
* `type: ['object', 'string']` → `union[model, string]`,
|
|
2071
|
+
* `['object', 'integer', 'null']` → `nullable<union[model, integer]>`,
|
|
2072
|
+
* `['object', 'null']` / `nullable: true` → `nullable<model>`.
|
|
2073
|
+
* A `oneOf: [X, { type: 'null' }]` variant has no `properties` of its own
|
|
2074
|
+
* and never reaches this path; `schemaToTypeRef` types it directly.
|
|
2075
|
+
*
|
|
2076
|
+
* Anything inline under a non-object member is named from the variant, never
|
|
2077
|
+
* from the union's field: an `array` member types its items under the
|
|
2078
|
+
* variant as parent with the context `variantMemberKeys` picks (`item` →
|
|
2079
|
+
* `<Variant>Item`, or `array_item` → `<Variant>ArrayItem` when a property
|
|
2080
|
+
* claims `item`), and any other member under its `value` key
|
|
2081
|
+
* (`<Variant>Value` for an inline enum, else `<Variant>MemberValue`). Naming
|
|
2082
|
+
* them from the field would hand variant 0's model name to every variant's
|
|
2083
|
+
* items.
|
|
2084
|
+
*/
|
|
2085
|
+
function inlineVariantTypeRef(variant, name) {
|
|
2086
|
+
const modelRef = {
|
|
2087
|
+
kind: "model",
|
|
2088
|
+
name
|
|
2089
|
+
};
|
|
2090
|
+
const typeList = Array.isArray(variant.type) ? variant.type : [];
|
|
2091
|
+
const nullable = variant.nullable === true || typeList.includes("null");
|
|
2092
|
+
const keys = variantMemberKeys(variant, name);
|
|
2093
|
+
const members = typeList.filter((t) => t !== "null").map((t) => t === "object" ? modelRef : schemaToTypeRef({
|
|
2094
|
+
...variant,
|
|
2095
|
+
type: t,
|
|
2096
|
+
nullable: false
|
|
2097
|
+
}, t === "array" ? keys.item : keys.value, name));
|
|
2098
|
+
const ref = members.length > 1 ? {
|
|
2099
|
+
kind: "union",
|
|
2100
|
+
variants: members
|
|
2101
|
+
} : modelRef;
|
|
2102
|
+
return nullable ? asNullable(ref) : ref;
|
|
2103
|
+
}
|
|
993
2104
|
function extractEnum(name, schema) {
|
|
994
|
-
const values = (schema.enum
|
|
2105
|
+
const values = nonNullEnumValues(schema.enum).map((v) => ({
|
|
995
2106
|
name: toUpperSnakeCase(String(v)),
|
|
996
2107
|
value: typeof v === "number" ? v : String(v),
|
|
997
2108
|
description: void 0
|
|
@@ -1007,9 +2118,10 @@ function extractModel(name, schema, schemas) {
|
|
|
1007
2118
|
if (schema.allOf) return extractAllOfModel(name, schema, schemas);
|
|
1008
2119
|
const requiredSet = new Set(schema.required ?? []);
|
|
1009
2120
|
const fields = [];
|
|
2121
|
+
const siblings = collectProperties(schema, schemas);
|
|
1010
2122
|
for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
|
|
1011
2123
|
if (!fieldSchema) continue;
|
|
1012
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
2124
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, schemas, siblings));
|
|
1013
2125
|
}
|
|
1014
2126
|
if (fields.length === 0 && (schema.oneOf || schema.anyOf)) {
|
|
1015
2127
|
const variants = schema.oneOf ?? schema.anyOf ?? [];
|
|
@@ -1020,7 +2132,7 @@ function extractModel(name, schema, schemas) {
|
|
|
1020
2132
|
for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
|
|
1021
2133
|
if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
|
|
1022
2134
|
seenFieldNames.add(fieldName);
|
|
1023
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
|
|
2135
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
|
|
1024
2136
|
}
|
|
1025
2137
|
}
|
|
1026
2138
|
}
|
|
@@ -1041,12 +2153,14 @@ function extractModel(name, schema, schemas) {
|
|
|
1041
2153
|
return {
|
|
1042
2154
|
name,
|
|
1043
2155
|
description: schema.description,
|
|
1044
|
-
fields
|
|
2156
|
+
fields,
|
|
2157
|
+
...constraintsEntry(schema)
|
|
1045
2158
|
};
|
|
1046
2159
|
}
|
|
1047
|
-
function extractAllOfModel(name, schema, schemas) {
|
|
2160
|
+
function extractAllOfModel(name, schema, schemas, siblingProperties) {
|
|
1048
2161
|
const fields = [];
|
|
1049
2162
|
const requiredSet = /* @__PURE__ */ new Set();
|
|
2163
|
+
const siblings = siblingProperties ?? collectProperties(schema, schemas);
|
|
1050
2164
|
let resultDiscriminator;
|
|
1051
2165
|
for (const subSchema of schema.allOf ?? []) {
|
|
1052
2166
|
let resolved = subSchema;
|
|
@@ -1056,7 +2170,7 @@ function extractAllOfModel(name, schema, schemas) {
|
|
|
1056
2170
|
if (refName && schemas[refName]) resolved = schemas[refName];
|
|
1057
2171
|
}
|
|
1058
2172
|
if (resolved.allOf) {
|
|
1059
|
-
const nested = extractAllOfModel(name, resolved, schemas);
|
|
2173
|
+
const nested = extractAllOfModel(name, resolved, schemas, siblings);
|
|
1060
2174
|
for (const f of nested.fields) {
|
|
1061
2175
|
fields.push({
|
|
1062
2176
|
...f,
|
|
@@ -1076,7 +2190,7 @@ function extractAllOfModel(name, schema, schemas) {
|
|
|
1076
2190
|
for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
|
|
1077
2191
|
if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
|
|
1078
2192
|
seenFieldNames.add(fieldName);
|
|
1079
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
|
|
2193
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
|
|
1080
2194
|
}
|
|
1081
2195
|
}
|
|
1082
2196
|
}
|
|
@@ -1086,7 +2200,7 @@ function extractAllOfModel(name, schema, schemas) {
|
|
|
1086
2200
|
const emptyRequired = /* @__PURE__ */ new Set();
|
|
1087
2201
|
for (const [fieldName, fieldSchema] of Object.entries(resolved.properties)) {
|
|
1088
2202
|
if (!fieldSchema) continue;
|
|
1089
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
|
|
2203
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
|
|
1090
2204
|
}
|
|
1091
2205
|
}
|
|
1092
2206
|
}
|
|
@@ -1097,7 +2211,8 @@ function extractAllOfModel(name, schema, schemas) {
|
|
|
1097
2211
|
name,
|
|
1098
2212
|
description: schema.description,
|
|
1099
2213
|
fields,
|
|
1100
|
-
...resultDiscriminator ? { discriminator: resultDiscriminator } : {}
|
|
2214
|
+
...resultDiscriminator ? { discriminator: resultDiscriminator } : {},
|
|
2215
|
+
...constraintsEntry(schema)
|
|
1101
2216
|
};
|
|
1102
2217
|
}
|
|
1103
2218
|
/**
|
|
@@ -1168,18 +2283,23 @@ function detectAllOfVariantDiscriminator(variants, fallbackName) {
|
|
|
1168
2283
|
mapping
|
|
1169
2284
|
};
|
|
1170
2285
|
}
|
|
1171
|
-
function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
|
|
2286
|
+
function extractDiscriminatedAllOfVariantModels(schema, fallbackName, schemas) {
|
|
1172
2287
|
const models = [];
|
|
1173
2288
|
const seenNames = /* @__PURE__ */ new Set();
|
|
1174
2289
|
const baseProperties = {};
|
|
1175
2290
|
const baseRequired = /* @__PURE__ */ new Set();
|
|
2291
|
+
let baseConstraints = ownConstraints(schema);
|
|
1176
2292
|
for (const subSchema of schema.allOf ?? []) {
|
|
1177
|
-
if (subSchema.oneOf)
|
|
2293
|
+
if (subSchema.oneOf) {
|
|
2294
|
+
baseConstraints = combineConstraints(baseConstraints, ownConstraints(subSchema));
|
|
2295
|
+
continue;
|
|
2296
|
+
}
|
|
1178
2297
|
const flat = subSchema.allOf ? flattenVariantForExtraction(subSchema) : subSchema;
|
|
1179
2298
|
if (flat.properties) {
|
|
1180
2299
|
for (const [k, v] of Object.entries(flat.properties)) if (!(k in baseProperties) && v) baseProperties[k] = v;
|
|
1181
2300
|
}
|
|
1182
2301
|
if (flat.required) for (const r of flat.required) baseRequired.add(r);
|
|
2302
|
+
baseConstraints = combineConstraints(baseConstraints, extractConstraints(subSchema));
|
|
1183
2303
|
}
|
|
1184
2304
|
for (const subSchema of schema.allOf ?? []) for (const variant of subSchema.oneOf ?? []) {
|
|
1185
2305
|
const flat = flattenVariantForExtraction(variant);
|
|
@@ -1189,10 +2309,11 @@ function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
|
|
|
1189
2309
|
const merged = {
|
|
1190
2310
|
type: "object",
|
|
1191
2311
|
properties: mergedProperties,
|
|
1192
|
-
required: [
|
|
2312
|
+
required: [.../* @__PURE__ */ new Set([...baseRequired, ...flat.required ?? []])],
|
|
2313
|
+
...combineConstraints(baseConstraints, extractConstraints(variant))
|
|
1193
2314
|
};
|
|
1194
2315
|
const variantName = deriveDiscriminatedVariantName(variant, fallbackName);
|
|
1195
|
-
for (const model of extractInlineModelDeep(variantName, merged)) {
|
|
2316
|
+
for (const model of extractInlineModelDeep(variantName, merged, schemas)) {
|
|
1196
2317
|
if (seenNames.has(model.name)) continue;
|
|
1197
2318
|
seenNames.add(model.name);
|
|
1198
2319
|
models.push(model);
|
|
@@ -1298,16 +2419,27 @@ function deriveDiscriminatedVariantName(schema, fallbackName) {
|
|
|
1298
2419
|
if (pascal.endsWith(fallbackName)) return pascal;
|
|
1299
2420
|
return `${fallbackName}${pascal}`;
|
|
1300
2421
|
}
|
|
1301
|
-
function extractInlineModelDeep(name, schema) {
|
|
2422
|
+
function extractInlineModelDeep(name, schema, schemas) {
|
|
1302
2423
|
const requiredSet = new Set(schema.required ?? []);
|
|
1303
2424
|
const fields = [];
|
|
1304
2425
|
const properties = schema.properties ?? {};
|
|
1305
2426
|
const nestedModels = [];
|
|
2427
|
+
const siblings = collectProperties(schema);
|
|
1306
2428
|
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
|
1307
2429
|
if (!fieldSchema) continue;
|
|
1308
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
1309
|
-
if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema));
|
|
1310
|
-
if (fieldSchema.type === "array" && fieldSchema.items?.properties)
|
|
2430
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, schemas, siblings));
|
|
2431
|
+
if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema, schemas));
|
|
2432
|
+
if (fieldSchema.type === "array" && fieldSchema.items?.properties) {
|
|
2433
|
+
const itemContext = elementContextName(fieldName, "item", siblings, name, fieldSchema.items);
|
|
2434
|
+
nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, itemContext), fieldSchema.items, schemas));
|
|
2435
|
+
}
|
|
2436
|
+
if (fieldSchema.type === "object" && !fieldSchema.properties) {
|
|
2437
|
+
const valueSchema = mapValueSchema(fieldSchema);
|
|
2438
|
+
if (valueSchema && !valueSchema.$ref && valueSchema.properties) {
|
|
2439
|
+
const valueContext = elementContextName(fieldName, "value", siblings, name, valueSchema);
|
|
2440
|
+
nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, valueContext), valueSchema, schemas));
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
1311
2443
|
if (fieldSchema.allOf) {
|
|
1312
2444
|
const mergedProperties = {};
|
|
1313
2445
|
const mergedRequired = [];
|
|
@@ -1318,22 +2450,20 @@ function extractInlineModelDeep(name, schema) {
|
|
|
1318
2450
|
if (Object.keys(mergedProperties).length > 0) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), {
|
|
1319
2451
|
type: "object",
|
|
1320
2452
|
properties: mergedProperties,
|
|
1321
|
-
required: mergedRequired
|
|
1322
|
-
|
|
2453
|
+
required: mergedRequired,
|
|
2454
|
+
...extractConstraints(fieldSchema)
|
|
2455
|
+
}, schemas));
|
|
1323
2456
|
}
|
|
1324
2457
|
if (fieldSchema.oneOf) {
|
|
1325
|
-
const inlineObjectVariants = fieldSchema.oneOf.filter((v) =>
|
|
2458
|
+
const inlineObjectVariants = fieldSchema.oneOf.filter((v) => isInlineObjectSchema(v));
|
|
1326
2459
|
if (inlineObjectVariants.length > 0) {
|
|
1327
|
-
const
|
|
1328
|
-
const namingDisc = deriveConstNamingDiscriminator(inlineObjectVariants);
|
|
1329
|
-
const emitted = [];
|
|
2460
|
+
const variantNames = nameInlineVariants(fieldSchema, qualifyNestedInlineName(name, fieldName), reservedInlineNames(fieldName, siblings, name));
|
|
1330
2461
|
for (const variant of inlineObjectVariants) {
|
|
1331
|
-
const variantName =
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
});
|
|
1336
|
-
nestedModels.push(...extractInlineModelDeep(variantName, variant));
|
|
2462
|
+
const variantName = variantNames.get(variant);
|
|
2463
|
+
if (variantName === void 0) continue;
|
|
2464
|
+
nestedModels.push(...extractInlineModelDeep(variantName, variant, schemas));
|
|
2465
|
+
const memberSchemas = variantMemberSchemas(variant, variantName);
|
|
2466
|
+
if (Object.keys(memberSchemas).length > 0) nestedModels.push(...extractInlineModelDeep(variantName, { properties: memberSchemas }, schemas).slice(1));
|
|
1337
2467
|
}
|
|
1338
2468
|
}
|
|
1339
2469
|
}
|
|
@@ -1341,7 +2471,8 @@ function extractInlineModelDeep(name, schema) {
|
|
|
1341
2471
|
return [{
|
|
1342
2472
|
name,
|
|
1343
2473
|
description: schema.description,
|
|
1344
|
-
fields
|
|
2474
|
+
fields,
|
|
2475
|
+
...constraintsEntry(schema)
|
|
1345
2476
|
}, ...nestedModels];
|
|
1346
2477
|
}
|
|
1347
2478
|
function qualifyNestedInlineName(parentName, fieldName) {
|
|
@@ -1352,15 +2483,28 @@ function qualifyNestedInlineName(parentName, fieldName) {
|
|
|
1352
2483
|
if (match && match[2]) return match[1] + singularize$1(match[2]);
|
|
1353
2484
|
return qualified;
|
|
1354
2485
|
}
|
|
1355
|
-
|
|
2486
|
+
/**
|
|
2487
|
+
* `elementContextName` for an optional context: with no field name there is
|
|
2488
|
+
* nothing to decide, and the items / values keep an undefined context.
|
|
2489
|
+
*/
|
|
2490
|
+
function elementContext(contextName, kind, siblingProperties, parentModelName, elementSchema) {
|
|
2491
|
+
return contextName === void 0 ? void 0 : elementContextName(contextName, kind, siblingProperties, parentModelName, elementSchema);
|
|
2492
|
+
}
|
|
2493
|
+
function schemaToTypeRef(schema, contextName, parentModelName, siblingProperties, reservedNames) {
|
|
1356
2494
|
if (schema.$ref) {
|
|
1357
2495
|
const segments = schema.$ref.split("/");
|
|
1358
2496
|
const rawName = segments[segments.length - 1];
|
|
1359
2497
|
const resolved = resolveSchemaName(rawName);
|
|
1360
|
-
if (activeEnumSchemaNames.has(resolved))
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
2498
|
+
if (activeEnumSchemaNames.has(resolved)) {
|
|
2499
|
+
const enumRef = {
|
|
2500
|
+
kind: "enum",
|
|
2501
|
+
name: resolved
|
|
2502
|
+
};
|
|
2503
|
+
return activeNullableEnumSchemaNames.has(resolved) ? {
|
|
2504
|
+
kind: "nullable",
|
|
2505
|
+
inner: enumRef
|
|
2506
|
+
} : enumRef;
|
|
2507
|
+
}
|
|
1364
2508
|
return {
|
|
1365
2509
|
kind: "model",
|
|
1366
2510
|
name: resolved
|
|
@@ -1375,21 +2519,18 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1375
2519
|
type: "unknown"
|
|
1376
2520
|
}
|
|
1377
2521
|
};
|
|
1378
|
-
if (schema.type.includes("null") && nonNullTypes.length === 1) return {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
nullable: false
|
|
1384
|
-
}, contextName)
|
|
1385
|
-
};
|
|
2522
|
+
if (schema.type.includes("null") && nonNullTypes.length === 1) return asNullable(schemaToTypeRef({
|
|
2523
|
+
...schema,
|
|
2524
|
+
type: nonNullTypes[0],
|
|
2525
|
+
nullable: false
|
|
2526
|
+
}, contextName, parentModelName, siblingProperties, reservedNames));
|
|
1386
2527
|
if (nonNullTypes.length > 1) {
|
|
1387
2528
|
const ref = {
|
|
1388
2529
|
kind: "union",
|
|
1389
2530
|
variants: nonNullTypes.map((t) => schemaToTypeRef({
|
|
1390
2531
|
...schema,
|
|
1391
2532
|
type: t
|
|
1392
|
-
}, contextName))
|
|
2533
|
+
}, contextName, parentModelName, siblingProperties, reservedNames))
|
|
1393
2534
|
};
|
|
1394
2535
|
if (schema.type.includes("null")) return {
|
|
1395
2536
|
kind: "nullable",
|
|
@@ -1398,13 +2539,10 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1398
2539
|
return ref;
|
|
1399
2540
|
}
|
|
1400
2541
|
}
|
|
1401
|
-
if (schema.nullable && schema.type) return {
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
nullable: false
|
|
1406
|
-
}, contextName)
|
|
1407
|
-
};
|
|
2542
|
+
if (schema.nullable && schema.type) return asNullable(schemaToTypeRef({
|
|
2543
|
+
...schema,
|
|
2544
|
+
nullable: false
|
|
2545
|
+
}, contextName, parentModelName, siblingProperties, reservedNames));
|
|
1408
2546
|
if (schema.allOf) {
|
|
1409
2547
|
const refItem = schema.allOf.find((s) => s.$ref);
|
|
1410
2548
|
if (refItem) {
|
|
@@ -1414,7 +2552,7 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1414
2552
|
};
|
|
1415
2553
|
return schemaToTypeRef(refItem, contextName, parentModelName);
|
|
1416
2554
|
}
|
|
1417
|
-
if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName);
|
|
2555
|
+
if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName, siblingProperties, reservedNames);
|
|
1418
2556
|
if (schema.allOf.some((s) => s.properties)) return {
|
|
1419
2557
|
kind: "model",
|
|
1420
2558
|
name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
|
|
@@ -1423,12 +2561,9 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1423
2561
|
if (schema.oneOf || schema.anyOf) {
|
|
1424
2562
|
const compositionKind = schema.oneOf ? "oneOf" : "anyOf";
|
|
1425
2563
|
const rawVariants = schema.oneOf ?? schema.anyOf ?? [];
|
|
1426
|
-
const nullVariant = rawVariants.find(
|
|
2564
|
+
const nullVariant = rawVariants.find(isNullVariantSchema);
|
|
1427
2565
|
const nonNullVariants = rawVariants.filter((v) => v !== nullVariant);
|
|
1428
|
-
if (nullVariant && nonNullVariants.length === 1) return
|
|
1429
|
-
kind: "nullable",
|
|
1430
|
-
inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
|
|
1431
|
-
};
|
|
2566
|
+
if (nullVariant && nonNullVariants.length === 1) return asNullable(schemaToTypeRef(nonNullVariants[0], contextName, parentModelName, siblingProperties, reservedNames));
|
|
1432
2567
|
const literalStrings = collectLiteralStringConsts(nonNullVariants);
|
|
1433
2568
|
if (literalStrings !== null && literalStrings.length >= 2) {
|
|
1434
2569
|
const enumRef = buildSyntheticEnumRef(literalStrings, contextName, parentModelName);
|
|
@@ -1437,13 +2572,14 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1437
2572
|
inner: enumRef
|
|
1438
2573
|
} : enumRef;
|
|
1439
2574
|
}
|
|
2575
|
+
const inlineVariantNames = nameInlineObjectVariants(schema, contextName, parentModelName, siblingProperties, reservedNames);
|
|
1440
2576
|
let resolvedDiscriminator;
|
|
1441
2577
|
if (schema.discriminator) {
|
|
1442
2578
|
const property = schema.discriminator.propertyName;
|
|
1443
2579
|
const explicitMapping = schema.discriminator.mapping ?? {};
|
|
1444
2580
|
let mapping = Object.fromEntries(Object.entries(explicitMapping).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]));
|
|
1445
2581
|
if (Object.keys(mapping).length === 0) {
|
|
1446
|
-
const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName,
|
|
2582
|
+
const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName, inlineVariantNames);
|
|
1447
2583
|
if (inlineMapping) mapping = inlineMapping;
|
|
1448
2584
|
}
|
|
1449
2585
|
resolvedDiscriminator = {
|
|
@@ -1451,12 +2587,22 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1451
2587
|
mapping
|
|
1452
2588
|
};
|
|
1453
2589
|
} else if (compositionKind === "oneOf") {
|
|
1454
|
-
const
|
|
1455
|
-
if (
|
|
1456
|
-
|
|
2590
|
+
const titleProperty = titleMappedDiscriminatorProperty(nonNullVariants);
|
|
2591
|
+
if (titleProperty) {
|
|
2592
|
+
const mapping = {};
|
|
2593
|
+
for (const v of nonNullVariants) {
|
|
2594
|
+
const value = getConstPropertyValue(v, titleProperty);
|
|
2595
|
+
const mappedName = inlineVariantNames.get(v) ?? resolveVariantModelName(v);
|
|
2596
|
+
if (value !== null && mappedName !== null) mapping[value] = mappedName;
|
|
2597
|
+
}
|
|
2598
|
+
resolvedDiscriminator = {
|
|
2599
|
+
property: titleProperty,
|
|
2600
|
+
mapping
|
|
2601
|
+
};
|
|
2602
|
+
} else {
|
|
1457
2603
|
const namingDisc = deriveConstNamingDiscriminator(nonNullVariants);
|
|
1458
2604
|
if (namingDisc) {
|
|
1459
|
-
const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName,
|
|
2605
|
+
const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName, inlineVariantNames);
|
|
1460
2606
|
if (inlineMapping) resolvedDiscriminator = {
|
|
1461
2607
|
property: namingDisc.property,
|
|
1462
2608
|
mapping: inlineMapping
|
|
@@ -1467,12 +2613,11 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1467
2613
|
const variants = [];
|
|
1468
2614
|
for (const v of rawVariants) {
|
|
1469
2615
|
if (v === nullVariant) continue;
|
|
1470
|
-
const
|
|
1471
|
-
if (
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
});
|
|
1475
|
-
else variants.push(schemaToTypeRef(v, contextName, parentModelName));
|
|
2616
|
+
const inlineName = inlineVariantNames.get(v);
|
|
2617
|
+
if (inlineName) {
|
|
2618
|
+
const mappedName = resolvedDiscriminator ? mapVariantToDiscriminatorEntry(v, resolvedDiscriminator) : null;
|
|
2619
|
+
variants.push(inlineVariantTypeRef(v, mappedName ?? inlineName));
|
|
2620
|
+
} else variants.push(schemaToTypeRef(v, contextName, parentModelName));
|
|
1476
2621
|
}
|
|
1477
2622
|
const hasNull = !!nullVariant;
|
|
1478
2623
|
const union = {
|
|
@@ -1515,30 +2660,30 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1515
2660
|
};
|
|
1516
2661
|
}
|
|
1517
2662
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
2663
|
+
const enumValues = nonNullEnumValues(schema.enum);
|
|
2664
|
+
const enumAdmitsNull = schema.enum !== void 0 && enumValues.length !== schema.enum.length;
|
|
2665
|
+
if (schema.enum && enumValues.length === 1) {
|
|
2666
|
+
const v = enumValues[0];
|
|
2667
|
+
const literal = {
|
|
1521
2668
|
kind: "literal",
|
|
1522
2669
|
value: typeof v === "number" ? v : String(v)
|
|
1523
2670
|
};
|
|
2671
|
+
return enumAdmitsNull ? asNullable(literal) : literal;
|
|
1524
2672
|
}
|
|
1525
2673
|
if (schema.enum) {
|
|
1526
2674
|
const baseName = toPascalCase(contextName ?? "UnknownEnum");
|
|
1527
2675
|
const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
|
|
1528
2676
|
const qualifiedName = cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName;
|
|
1529
|
-
|
|
2677
|
+
const enumRef = {
|
|
1530
2678
|
kind: "enum",
|
|
1531
2679
|
name: activeSchemaNameTransform ? activeSchemaNameTransform(qualifiedName) : qualifiedName,
|
|
1532
|
-
values:
|
|
2680
|
+
values: enumValues.map((v) => typeof v === "number" ? v : String(v))
|
|
1533
2681
|
};
|
|
2682
|
+
return enumAdmitsNull ? asNullable(enumRef) : enumRef;
|
|
1534
2683
|
}
|
|
1535
|
-
if (schema.type === "array" && schema.items) return {
|
|
2684
|
+
if ((schema.type === "array" || !schema.type) && schema.items) return {
|
|
1536
2685
|
kind: "array",
|
|
1537
|
-
items: schemaToTypeRef(schema.items, contextName, parentModelName)
|
|
1538
|
-
};
|
|
1539
|
-
if (!schema.type && schema.items) return {
|
|
1540
|
-
kind: "array",
|
|
1541
|
-
items: schemaToTypeRef(schema.items, contextName, parentModelName)
|
|
2686
|
+
items: schemaToTypeRef(schema.items, elementContext(contextName, "item", siblingProperties, parentModelName, schema.items), parentModelName, void 0, reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames))
|
|
1542
2687
|
};
|
|
1543
2688
|
if (schema.type === "object" && schema.properties) return {
|
|
1544
2689
|
kind: "model",
|
|
@@ -1549,12 +2694,8 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
|
1549
2694
|
kind: "primitive",
|
|
1550
2695
|
type: "unknown"
|
|
1551
2696
|
};
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
} else if (schema.patternProperties) {
|
|
1555
|
-
const patterns = Object.values(schema.patternProperties);
|
|
1556
|
-
if (patterns.length > 0) valueType = schemaToTypeRef(patterns[0], contextName);
|
|
1557
|
-
}
|
|
2697
|
+
const valueSchema = mapValueSchema(schema);
|
|
2698
|
+
if (valueSchema) valueType = schemaToTypeRef(valueSchema, elementContext(contextName, "value", siblingProperties, parentModelName, valueSchema), parentModelName, void 0, reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames));
|
|
1558
2699
|
return {
|
|
1559
2700
|
kind: "map",
|
|
1560
2701
|
valueType
|
|
@@ -1654,11 +2795,11 @@ function classifyAndExtractResponse(schema, contextName, componentSchemas) {
|
|
|
1654
2795
|
isPaginated: false
|
|
1655
2796
|
};
|
|
1656
2797
|
const envelope = detectListEnvelope(schema);
|
|
1657
|
-
if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath);
|
|
1658
|
-
if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName);
|
|
2798
|
+
if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath, componentSchemas);
|
|
2799
|
+
if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName, componentSchemas);
|
|
1659
2800
|
return extractDirectResource(schema, contextName, componentSchemas);
|
|
1660
2801
|
}
|
|
1661
|
-
const KNOWN_DATA_PATHS = new Set([
|
|
2802
|
+
const KNOWN_DATA_PATHS = /* @__PURE__ */ new Set([
|
|
1662
2803
|
"data",
|
|
1663
2804
|
"items",
|
|
1664
2805
|
"results",
|
|
@@ -1716,33 +2857,27 @@ function detectListEnvelope(schema) {
|
|
|
1716
2857
|
dataPath: null
|
|
1717
2858
|
};
|
|
1718
2859
|
}
|
|
1719
|
-
function extractListResponse(schema, contextName, dataPath) {
|
|
2860
|
+
function extractListResponse(schema, contextName, dataPath, componentSchemas) {
|
|
1720
2861
|
let itemTypeRef = {
|
|
1721
2862
|
kind: "primitive",
|
|
1722
2863
|
type: "string"
|
|
1723
2864
|
};
|
|
1724
2865
|
const inlineModels = [];
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
const itemName = contextName.replace(/Response$/, "") + "Item";
|
|
1738
|
-
itemTypeRef = {
|
|
1739
|
-
kind: "model",
|
|
1740
|
-
name: itemName
|
|
1741
|
-
};
|
|
1742
|
-
inlineModels.push(...extractInlineModel(itemName, items));
|
|
1743
|
-
}
|
|
2866
|
+
const { properties, required } = collectEnvelopeProperties(schema, componentSchemas);
|
|
2867
|
+
const dataProp = properties[dataPath];
|
|
2868
|
+
if (dataProp?.type === "array" && dataProp.items) {
|
|
2869
|
+
const items = dataProp.items;
|
|
2870
|
+
if (items.$ref) itemTypeRef = schemaToTypeRef(items, contextName);
|
|
2871
|
+
else if (items.type === "object" && items.properties) {
|
|
2872
|
+
const itemName = contextName.replace(/Response$/, "") + "Item";
|
|
2873
|
+
itemTypeRef = {
|
|
2874
|
+
kind: "model",
|
|
2875
|
+
name: itemName
|
|
2876
|
+
};
|
|
2877
|
+
inlineModels.push(...extractInlineModel(itemName, items, componentSchemas));
|
|
1744
2878
|
}
|
|
1745
2879
|
}
|
|
2880
|
+
const { envelopeFields, envelopeCompanions } = extractEnvelopeCompanions(properties, required, dataPath, contextName.replace(/Response$/, ""), componentSchemas);
|
|
1746
2881
|
return {
|
|
1747
2882
|
response: {
|
|
1748
2883
|
kind: "array",
|
|
@@ -1751,7 +2886,117 @@ function extractListResponse(schema, contextName, dataPath) {
|
|
|
1751
2886
|
inlineModels,
|
|
1752
2887
|
isPaginated: true,
|
|
1753
2888
|
dataPath,
|
|
1754
|
-
itemType: itemTypeRef
|
|
2889
|
+
itemType: itemTypeRef,
|
|
2890
|
+
envelopeFields,
|
|
2891
|
+
envelopeCompanions: envelopeCompanions.length > 0 ? envelopeCompanions : void 0
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
/**
|
|
2895
|
+
* Every property the envelope declares, merged across its `allOf` branches,
|
|
2896
|
+
* with the `required` lists that go with them.
|
|
2897
|
+
*
|
|
2898
|
+
* A `$ref` branch is resolved against the component schemas so fields an
|
|
2899
|
+
* envelope inherits from a shared base (`trace_id` on every response, say)
|
|
2900
|
+
* are not silently dropped. A property declared by more than one branch keeps
|
|
2901
|
+
* both declarations' shape when both are inline objects — `pagination.next`
|
|
2902
|
+
* from one branch and `pagination.total` from another both belong to the
|
|
2903
|
+
* response — and otherwise takes the later declaration, as `allOf` merging
|
|
2904
|
+
* does elsewhere in the parser.
|
|
2905
|
+
*/
|
|
2906
|
+
function collectEnvelopeProperties(schema, componentSchemas) {
|
|
2907
|
+
const properties = {};
|
|
2908
|
+
const required = [];
|
|
2909
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
2910
|
+
const visit = (s) => {
|
|
2911
|
+
if (s.$ref) {
|
|
2912
|
+
if (seenRefs.has(s.$ref)) return;
|
|
2913
|
+
seenRefs.add(s.$ref);
|
|
2914
|
+
const refName = s.$ref.split("/").pop();
|
|
2915
|
+
const resolved = refName ? componentSchemas?.[refName] : void 0;
|
|
2916
|
+
if (resolved) visit(resolved);
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
for (const sub of s.allOf ?? []) visit(sub);
|
|
2920
|
+
for (const [key, prop] of Object.entries(s.properties ?? {})) {
|
|
2921
|
+
if (!prop) continue;
|
|
2922
|
+
const existing = properties[key];
|
|
2923
|
+
properties[key] = existing ? mergePropertyDeclarations(existing, prop) : prop;
|
|
2924
|
+
}
|
|
2925
|
+
if (s.required) required.push(...s.required);
|
|
2926
|
+
};
|
|
2927
|
+
visit(schema);
|
|
2928
|
+
return {
|
|
2929
|
+
properties,
|
|
2930
|
+
required
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Combine two `allOf` declarations of one property. Inline objects merge their
|
|
2935
|
+
* fields; an array keeps the earlier `items` when the later declaration only
|
|
2936
|
+
* constrains it (`minItems: 1`), so `data: User[]` is not downgraded to
|
|
2937
|
+
* `string[]`; anything else takes the later declaration.
|
|
2938
|
+
*/
|
|
2939
|
+
function mergePropertyDeclarations(earlier, later) {
|
|
2940
|
+
if (isInlineObjectSchema(earlier) && isInlineObjectSchema(later)) return {
|
|
2941
|
+
...earlier,
|
|
2942
|
+
...later,
|
|
2943
|
+
properties: {
|
|
2944
|
+
...earlier.properties,
|
|
2945
|
+
...later.properties
|
|
2946
|
+
},
|
|
2947
|
+
required: [...earlier.required ?? [], ...later.required ?? []]
|
|
2948
|
+
};
|
|
2949
|
+
if (earlier.type === "array" && later.type === "array") return {
|
|
2950
|
+
...earlier,
|
|
2951
|
+
...later,
|
|
2952
|
+
items: later.items ?? earlier.items
|
|
2953
|
+
};
|
|
2954
|
+
return later;
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* Type a list envelope's companion properties as fields, lifting any inline
|
|
2958
|
+
* object among them (and anything nested inside it) to a model.
|
|
2959
|
+
*
|
|
2960
|
+
* Models come from `extractInlineModelsFromProperties`, the extractor that
|
|
2961
|
+
* mirrors `schemaToTypeRef` form for form — plain, nullable and `allOf`
|
|
2962
|
+
* objects, arrays of objects, and every `oneOf`/`anyOf` object variant — so a
|
|
2963
|
+
* field's reference always has a model behind it.
|
|
2964
|
+
*
|
|
2965
|
+
* Each companion is extracted twice: once under the operation's name, which
|
|
2966
|
+
* is how its models enter the IR (`ListUsersListMetadata`), and once under no
|
|
2967
|
+
* parent, which yields the name each model would carry if named from its
|
|
2968
|
+
* property alone (`ListMetadata`). Both runs walk the same code path, so the
|
|
2969
|
+
* two model lists line up index for index. `resolveEnvelopeCompanionNames`
|
|
2970
|
+
* later picks, per companion, which of the two names survives — the bare one
|
|
2971
|
+
* only where every copy of that companion across the API agrees structurally.
|
|
2972
|
+
*/
|
|
2973
|
+
function extractEnvelopeCompanions(properties, required, dataPath, operationName, componentSchemas) {
|
|
2974
|
+
const envelopeFields = [];
|
|
2975
|
+
const envelopeCompanions = [];
|
|
2976
|
+
const requiredSet = new Set(required);
|
|
2977
|
+
const siblings = collectProperties({ properties });
|
|
2978
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
2979
|
+
if (key === dataPath) continue;
|
|
2980
|
+
const field = buildFieldFromSchema(key, propSchema, operationName, requiredSet, componentSchemas, siblings);
|
|
2981
|
+
envelopeFields.push(field);
|
|
2982
|
+
const single = {
|
|
2983
|
+
type: "object",
|
|
2984
|
+
properties: { [key]: propSchema }
|
|
2985
|
+
};
|
|
2986
|
+
const models = [];
|
|
2987
|
+
extractInlineModelsFromProperties(single, models, operationName, componentSchemas, siblings);
|
|
2988
|
+
if (models.length === 0) continue;
|
|
2989
|
+
const bare = [];
|
|
2990
|
+
extractInlineModelsFromProperties(single, bare, void 0, componentSchemas, siblings);
|
|
2991
|
+
envelopeCompanions.push({
|
|
2992
|
+
field,
|
|
2993
|
+
models,
|
|
2994
|
+
bareNames: bare.map((m) => m.name)
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
return {
|
|
2998
|
+
envelopeFields,
|
|
2999
|
+
envelopeCompanions
|
|
1755
3000
|
};
|
|
1756
3001
|
}
|
|
1757
3002
|
function hasDiscriminantConstField(schema) {
|
|
@@ -1797,7 +3042,7 @@ function isSingleResourceWrapper(schema) {
|
|
|
1797
3042
|
if (propSchema.oneOf) return propSchema.oneOf.some((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
|
|
1798
3043
|
return false;
|
|
1799
3044
|
}
|
|
1800
|
-
function extractWrappedResource(schema, contextName) {
|
|
3045
|
+
function extractWrappedResource(schema, contextName, componentSchemas) {
|
|
1801
3046
|
const props = schema.properties;
|
|
1802
3047
|
const propSchema = props[schema.required && schema.required.length === 1 ? schema.required[0] : Object.keys(props)[0]];
|
|
1803
3048
|
const inlineModels = [];
|
|
@@ -1806,7 +3051,7 @@ function extractWrappedResource(schema, contextName) {
|
|
|
1806
3051
|
const objectVariant = propSchema.oneOf.find((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
|
|
1807
3052
|
if (objectVariant) {
|
|
1808
3053
|
const modelName = deriveModelName(objectVariant, resourceName);
|
|
1809
|
-
inlineModels.push(...extractInlineModel(modelName, objectVariant));
|
|
3054
|
+
inlineModels.push(...extractInlineModel(modelName, objectVariant, componentSchemas));
|
|
1810
3055
|
const hasNullVariant = propSchema.oneOf.some((v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null"));
|
|
1811
3056
|
const modelRef = {
|
|
1812
3057
|
kind: "model",
|
|
@@ -1823,7 +3068,7 @@ function extractWrappedResource(schema, contextName) {
|
|
|
1823
3068
|
}
|
|
1824
3069
|
}
|
|
1825
3070
|
const modelName = deriveModelName(propSchema, resourceName);
|
|
1826
|
-
inlineModels.push(...extractInlineModel(modelName, propSchema));
|
|
3071
|
+
inlineModels.push(...extractInlineModel(modelName, propSchema, componentSchemas));
|
|
1827
3072
|
return {
|
|
1828
3073
|
response: {
|
|
1829
3074
|
kind: "model",
|
|
@@ -1837,7 +3082,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
|
|
|
1837
3082
|
const inlineModels = [];
|
|
1838
3083
|
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
1839
3084
|
const modelName = deriveModelName(schema, contextName);
|
|
1840
|
-
inlineModels.push(...extractInlineModel(modelName, schema));
|
|
3085
|
+
inlineModels.push(...extractInlineModel(modelName, schema, componentSchemas));
|
|
1841
3086
|
return {
|
|
1842
3087
|
response: {
|
|
1843
3088
|
kind: "model",
|
|
@@ -1884,10 +3129,11 @@ function extractDirectResource(schema, contextName, componentSchemas) {
|
|
|
1884
3129
|
const merged = {
|
|
1885
3130
|
type: "object",
|
|
1886
3131
|
properties: mergedProperties,
|
|
1887
|
-
required: mergedRequired
|
|
3132
|
+
required: mergedRequired,
|
|
3133
|
+
...extractConstraints(schema)
|
|
1888
3134
|
};
|
|
1889
3135
|
const modelName = mergedRefFields ? toPascalCase(contextName) : deriveModelName(merged, contextName);
|
|
1890
|
-
inlineModels.push(...extractInlineModel(modelName, merged));
|
|
3136
|
+
inlineModels.push(...extractInlineModel(modelName, merged, componentSchemas));
|
|
1891
3137
|
return {
|
|
1892
3138
|
response: {
|
|
1893
3139
|
kind: "model",
|
|
@@ -1898,12 +3144,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
|
|
|
1898
3144
|
};
|
|
1899
3145
|
}
|
|
1900
3146
|
}
|
|
1901
|
-
if (schema.oneOf) {
|
|
1902
|
-
for (const variant of schema.oneOf) if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
1903
|
-
const modelName = deriveModelName(variant, contextName);
|
|
1904
|
-
if (!new Set(inlineModels.map((m) => m.name)).has(modelName)) inlineModels.push(...extractInlineModel(modelName, variant));
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
3147
|
+
if (schema.oneOf) extractInlineModelsFromProperties({ properties: { [contextName]: schema } }, inlineModels, void 0, componentSchemas);
|
|
1907
3148
|
if (schema.type === "array" && schema.items) {
|
|
1908
3149
|
const items = schema.items;
|
|
1909
3150
|
if (items.$ref) return {
|
|
@@ -1916,7 +3157,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
|
|
|
1916
3157
|
};
|
|
1917
3158
|
if (items.properties && (items.type === "object" || !items.type)) {
|
|
1918
3159
|
const itemName = contextName.replace(/Response$/, "") + "Item";
|
|
1919
|
-
inlineModels.push(...extractInlineModel(itemName, items));
|
|
3160
|
+
inlineModels.push(...extractInlineModel(itemName, items, componentSchemas));
|
|
1920
3161
|
return {
|
|
1921
3162
|
response: {
|
|
1922
3163
|
kind: "array",
|
|
@@ -1946,70 +3187,288 @@ function deriveModelName(schema, fallback) {
|
|
|
1946
3187
|
return toPascalCase(fallback);
|
|
1947
3188
|
}
|
|
1948
3189
|
/**
|
|
1949
|
-
*
|
|
1950
|
-
*
|
|
1951
|
-
*
|
|
3190
|
+
* Materialise an inline response object (and every nested inline object
|
|
3191
|
+
* under it) as models. `componentSchemas` only feeds `buildFieldFromSchema`,
|
|
3192
|
+
* so a response field that `$ref`s a component takes the component's
|
|
3193
|
+
* `example`/`examples` exactly as a request-body or component-model field
|
|
3194
|
+
* does; nested models get the same map so the fallback reaches every level.
|
|
1952
3195
|
*/
|
|
1953
|
-
function
|
|
1954
|
-
const pascalField = toPascalCase(fieldName);
|
|
1955
|
-
if (pascalField.startsWith(parentName)) return pascalField;
|
|
1956
|
-
const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
|
|
1957
|
-
const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
|
|
1958
|
-
if (match && match[2]) {
|
|
1959
|
-
const trailing = singularize$1(match[2]);
|
|
1960
|
-
return match[1] + trailing;
|
|
1961
|
-
}
|
|
1962
|
-
return qualified;
|
|
1963
|
-
}
|
|
1964
|
-
function extractInlineModel(name, schema) {
|
|
3196
|
+
function extractInlineModel(name, schema, componentSchemas) {
|
|
1965
3197
|
const requiredSet = new Set(schema.required ?? []);
|
|
1966
3198
|
const fields = [];
|
|
1967
3199
|
const properties = schema.properties ?? {};
|
|
1968
3200
|
const nestedModels = [];
|
|
3201
|
+
const siblings = collectProperties(schema);
|
|
1969
3202
|
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
|
1970
3203
|
if (!fieldSchema) continue;
|
|
1971
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
3204
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, componentSchemas, siblings));
|
|
3205
|
+
}
|
|
3206
|
+
extractInlineModelsFromProperties(schema, nestedModels, name, componentSchemas);
|
|
3207
|
+
return [{
|
|
3208
|
+
name,
|
|
3209
|
+
description: schema.description,
|
|
3210
|
+
fields,
|
|
3211
|
+
...constraintsEntry(schema)
|
|
3212
|
+
}, ...nestedModels];
|
|
3213
|
+
}
|
|
3214
|
+
//#endregion
|
|
3215
|
+
//#region src/parser/security.ts
|
|
3216
|
+
/**
|
|
3217
|
+
* Convert a document- or operation-level `security` list into the IR shape,
|
|
3218
|
+
* preserving what the list says rather than flattening it:
|
|
3219
|
+
*
|
|
3220
|
+
* - absent (or not a list) → `undefined`: nothing declared here.
|
|
3221
|
+
* - `[]` → `[]`: no authentication required.
|
|
3222
|
+
* - `[{ a: [] }, { b: [] }]` → `[[a], [b]]`: either scheme suffices.
|
|
3223
|
+
* - `[{ a: [], b: [] }]` → `[[a, b]]`: both schemes are required together.
|
|
3224
|
+
* - `[{ a: [] }, {}]` → `[[a], []]`: the empty set is an anonymous alternative,
|
|
3225
|
+
* which is how a document marks authentication optional.
|
|
3226
|
+
*
|
|
3227
|
+
* An entry that is not an object (`null`, say) is not a requirement object.
|
|
3228
|
+
* Reading it as `{}` would declare an anonymous alternative and make the
|
|
3229
|
+
* operation look open, so it is dropped with an `[oagen] Warning:` naming
|
|
3230
|
+
* `context` (the operation, or the document). A non-empty list left with no
|
|
3231
|
+
* valid entry is treated as undeclared (`undefined`) rather than as `[]`:
|
|
3232
|
+
* the document never said "no authentication", so the operation keeps
|
|
3233
|
+
* inheriting the default.
|
|
3234
|
+
*
|
|
3235
|
+
* The previous flattening collapsed the alternatives onto one list and turned
|
|
3236
|
+
* `[]` into `undefined`, so "no auth" read the same as "use the default" and
|
|
3237
|
+
* "either of" the same as "all of" (#182).
|
|
3238
|
+
*/
|
|
3239
|
+
function extractSecurityRequirements(security, context) {
|
|
3240
|
+
if (!Array.isArray(security)) return void 0;
|
|
3241
|
+
const sets = [];
|
|
3242
|
+
for (const entry of security) {
|
|
3243
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
3244
|
+
console.warn(`[oagen] Warning: Ignoring security requirement ${JSON.stringify(entry)} on ${context} — not a requirement object`);
|
|
3245
|
+
continue;
|
|
1982
3246
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
3247
|
+
sets.push(Object.entries(entry).map(([schemeName, scopes]) => ({
|
|
3248
|
+
schemeName,
|
|
3249
|
+
scopes: Array.isArray(scopes) ? scopes : []
|
|
3250
|
+
})));
|
|
3251
|
+
}
|
|
3252
|
+
if (security.length > 0 && sets.length === 0) {
|
|
3253
|
+
console.warn(`[oagen] Warning: No valid security requirement on ${context}; treating security as undeclared`);
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
return sets;
|
|
3257
|
+
}
|
|
3258
|
+
//#endregion
|
|
3259
|
+
//#region src/parser/envelope-companion-names.ts
|
|
3260
|
+
/**
|
|
3261
|
+
* Decide the final name of every lifted companion model, across all list
|
|
3262
|
+
* operations at once, and return the models to merge into the IR.
|
|
3263
|
+
*
|
|
3264
|
+
* The same envelope recurs on every list endpoint of an API, so naming each
|
|
3265
|
+
* copy after its operation would mint one identical `ListMetadata` per
|
|
3266
|
+
* endpoint. But naming by property alone is only safe while the copies agree:
|
|
3267
|
+
* `mergeInlineResponseModels` keeps the largest model of a name, so a
|
|
3268
|
+
* `pagination` with three fields would silently replace an unrelated
|
|
3269
|
+
* `Pagination` component, or absorb another endpoint's two-field `pagination`.
|
|
3270
|
+
*
|
|
3271
|
+
* So a companion takes its bare name only when that cannot change any other
|
|
3272
|
+
* type's shape:
|
|
3273
|
+
*
|
|
3274
|
+
* Copies are compared structurally — field names, requiredness and each
|
|
3275
|
+
* field's shape, with nested models resolved and inline enums compared by
|
|
3276
|
+
* value — never by name alone. A copy collapses onto a target shape only when
|
|
3277
|
+
* the target *admits* every value the copy can hold: the same fields with the
|
|
3278
|
+
* same types, where the target may be looser (a field the copy requires may be
|
|
3279
|
+
* optional on the target, a non-null copy field may be nullable there) but
|
|
3280
|
+
* never stricter. Widening a response type loses no data a reader can see;
|
|
3281
|
+
* narrowing it would promise fields the wire may omit.
|
|
3282
|
+
*
|
|
3283
|
+
* - When a component model of that name exists — compared after the parser's
|
|
3284
|
+
* own renaming, so `PaginationDto` → `Pagination` is caught — the copies it
|
|
3285
|
+
* admits collapse onto it (the component stays as written); the others keep
|
|
3286
|
+
* their operation-qualified name.
|
|
3287
|
+
* - Otherwise the most common exact shape (first declared, on a tie) becomes
|
|
3288
|
+
* the target: it and every copy it admits take the bare name; any other
|
|
3289
|
+
* copy keeps its operation-qualified name.
|
|
3290
|
+
*
|
|
3291
|
+
* The comparison covers the companion's whole field type, so a `oneOf`
|
|
3292
|
+
* companion is judged on every variant, not just the first, and a copy folds
|
|
3293
|
+
* among copies only when its lifted models line up name for name with the
|
|
3294
|
+
* target's (same variants, same order); onto a component, only a companion
|
|
3295
|
+
* whose field references exactly its own lifted model can fold.
|
|
3296
|
+
*
|
|
3297
|
+
* Only the target's definition survives a collapse. The admitted copies'
|
|
3298
|
+
* models are dropped rather than handed to the merge pass, which keeps the
|
|
3299
|
+
* first model of a name and would let a stricter copy declared earlier
|
|
3300
|
+
* narrow the shared type for everyone; their field references are repointed
|
|
3301
|
+
* at the target's models instead.
|
|
3302
|
+
*
|
|
3303
|
+
* Enums declared inline in a companion stay operation-qualified, exactly like
|
|
3304
|
+
* enums declared inline in a parameter.
|
|
3305
|
+
*/
|
|
3306
|
+
function resolveEnvelopeCompanionNames(companions, componentModels) {
|
|
3307
|
+
const byBareName = /* @__PURE__ */ new Map();
|
|
3308
|
+
/** Copies folded onto another definition; their own models are not emitted. */
|
|
3309
|
+
const folded = /* @__PURE__ */ new Set();
|
|
3310
|
+
for (const companion of companions) {
|
|
3311
|
+
if (companion.models.length === 0) continue;
|
|
3312
|
+
const bare = companion.bareNames[0];
|
|
3313
|
+
const group = byBareName.get(bare) ?? [];
|
|
3314
|
+
group.push(companion);
|
|
3315
|
+
byBareName.set(bare, group);
|
|
3316
|
+
}
|
|
3317
|
+
const componentMap = new Map((componentModels ?? []).map((m) => [m.name, m]));
|
|
3318
|
+
const localMap = (c) => new Map(c.models.map((m) => [m.name, m]));
|
|
3319
|
+
const unwrap = (t) => t.kind === "nullable" ? t.inner : t;
|
|
3320
|
+
for (const [bare, group] of byBareName) {
|
|
3321
|
+
let folds;
|
|
3322
|
+
let finalNames;
|
|
3323
|
+
let owner;
|
|
3324
|
+
const component = componentMap.get(bare);
|
|
3325
|
+
if (component) {
|
|
3326
|
+
const targetType = {
|
|
3327
|
+
kind: "model",
|
|
3328
|
+
name: component.name
|
|
3329
|
+
};
|
|
3330
|
+
finalNames = [component.name];
|
|
3331
|
+
folds = (c) => {
|
|
3332
|
+
const refs = collectModelRefs(c.field.type);
|
|
3333
|
+
if (refs.length !== 1 || refs[0] !== c.models[0].name) return false;
|
|
3334
|
+
return typeAdmits(targetType, componentMap, unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set());
|
|
3335
|
+
};
|
|
3336
|
+
} else {
|
|
3337
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3338
|
+
for (const c of group) {
|
|
3339
|
+
const key = `${fingerprintType$1(unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set())}|${c.bareNames.join(",")}`;
|
|
3340
|
+
const seen = counts.get(key);
|
|
3341
|
+
if (seen) seen.n += 1;
|
|
3342
|
+
else counts.set(key, {
|
|
3343
|
+
n: 1,
|
|
3344
|
+
first: c
|
|
3345
|
+
});
|
|
1998
3346
|
}
|
|
3347
|
+
let best = {
|
|
3348
|
+
n: 0,
|
|
3349
|
+
first: group[0]
|
|
3350
|
+
};
|
|
3351
|
+
for (const entry of counts.values()) if (entry.n > best.n) best = entry;
|
|
3352
|
+
owner = best.first;
|
|
3353
|
+
finalNames = owner.bareNames;
|
|
3354
|
+
const ownerType = unwrap(owner.field.type);
|
|
3355
|
+
const ownerMap = localMap(owner);
|
|
3356
|
+
const ownerNames = owner.bareNames;
|
|
3357
|
+
folds = (c) => c.bareNames.length === ownerNames.length && c.bareNames.every((name, i) => name === ownerNames[i]) && typeAdmits(ownerType, ownerMap, unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set());
|
|
1999
3358
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
3359
|
+
const collapsing = group.filter(folds);
|
|
3360
|
+
for (const companion of collapsing) {
|
|
3361
|
+
renameCompanion(companion, finalNames);
|
|
3362
|
+
if (companion !== owner) folded.add(companion);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
return companions.filter((c) => !folded.has(c)).flatMap((c) => c.models);
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* Rename a companion's models to `names` (index-aligned with `models`) and
|
|
3369
|
+
* repoint every reference, in its field and inside its models.
|
|
3370
|
+
*/
|
|
3371
|
+
function renameCompanion(companion, names) {
|
|
3372
|
+
const renames = /* @__PURE__ */ new Map();
|
|
3373
|
+
companion.models.forEach((model, i) => {
|
|
3374
|
+
const name = names[i];
|
|
3375
|
+
if (name !== void 0 && model.name !== name) renames.set(model.name, name);
|
|
3376
|
+
});
|
|
3377
|
+
if (renames.size === 0) return;
|
|
3378
|
+
const repoint = (ref) => {
|
|
3379
|
+
walkTypeRef(ref, { model: (r) => {
|
|
3380
|
+
const renamed = renames.get(r.name);
|
|
3381
|
+
if (renamed !== void 0) r.name = renamed;
|
|
3382
|
+
} });
|
|
3383
|
+
};
|
|
3384
|
+
repoint(companion.field.type);
|
|
3385
|
+
for (const model of companion.models) {
|
|
3386
|
+
model.name = renames.get(model.name) ?? model.name;
|
|
3387
|
+
for (const field of model.fields) repoint(field.type);
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
/**
|
|
3391
|
+
* Whether `target` admits every value `copy` can hold: the same field names,
|
|
3392
|
+
* each field's type admitted (see `typeAdmits`), and no field the target
|
|
3393
|
+
* requires that the copy leaves optional. Nested models resolve through their
|
|
3394
|
+
* respective maps and are compared the same way; a model neither map resolves
|
|
3395
|
+
* is compared by name.
|
|
3396
|
+
*/
|
|
3397
|
+
function admits(target, targetMap, copy, copyMap, visiting) {
|
|
3398
|
+
const key = `${target.name}|${copy.name}`;
|
|
3399
|
+
if (visiting.has(key)) return true;
|
|
3400
|
+
visiting.add(key);
|
|
3401
|
+
try {
|
|
3402
|
+
if (target.fields.length !== copy.fields.length) return false;
|
|
3403
|
+
const targetFields = new Map(target.fields.map((f) => [f.name, f]));
|
|
3404
|
+
for (const field of copy.fields) {
|
|
3405
|
+
const expected = targetFields.get(field.name);
|
|
3406
|
+
if (!expected) return false;
|
|
3407
|
+
if (expected.required && !field.required) return false;
|
|
3408
|
+
if (!typeAdmits(expected.type, targetMap, field.type, copyMap, visiting)) return false;
|
|
3409
|
+
}
|
|
3410
|
+
return true;
|
|
3411
|
+
} finally {
|
|
3412
|
+
visiting.delete(key);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
/**
|
|
3416
|
+
* Whether `target` admits every value of `copy`'s type. A nullable target
|
|
3417
|
+
* admits a nullable or a non-null copy; a non-null target admits only a
|
|
3418
|
+
* non-null copy. An enum with known values admits a copy whose values are a
|
|
3419
|
+
* subset of its own. Everything else must match exactly, by fingerprint.
|
|
3420
|
+
*/
|
|
3421
|
+
function typeAdmits(target, targetMap, copy, copyMap, visiting) {
|
|
3422
|
+
if (target.kind === "nullable") return typeAdmits(target.inner, targetMap, copy.kind === "nullable" ? copy.inner : copy, copyMap, visiting);
|
|
3423
|
+
if (copy.kind === "nullable") return false;
|
|
3424
|
+
if (target.kind !== copy.kind) return false;
|
|
3425
|
+
switch (target.kind) {
|
|
3426
|
+
case "model": {
|
|
3427
|
+
const t = targetMap.get(target.name);
|
|
3428
|
+
const c = copyMap.get(copy.name);
|
|
3429
|
+
if (t && c) return admits(t, targetMap, c, copyMap, visiting);
|
|
3430
|
+
return target.name === copy.name;
|
|
3431
|
+
}
|
|
3432
|
+
case "array": return typeAdmits(target.items, targetMap, copy.items, copyMap, visiting);
|
|
3433
|
+
case "enum": {
|
|
3434
|
+
const c = copy;
|
|
3435
|
+
if (target.values && c.values) {
|
|
3436
|
+
const allowed = new Set(target.values.map((v) => `${typeof v}:${String(v)}`));
|
|
3437
|
+
return c.values.every((v) => allowed.has(`${typeof v}:${String(v)}`));
|
|
2005
3438
|
}
|
|
3439
|
+
return target.name === c.name;
|
|
2006
3440
|
}
|
|
3441
|
+
default: return fingerprintType$1(target, targetMap, /* @__PURE__ */ new Set()) === fingerprintType$1(copy, copyMap, /* @__PURE__ */ new Set());
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* Structural fingerprint of a companion model: field names, requiredness, and
|
|
3446
|
+
* each field's shape, with models nested inside the companion compared by
|
|
3447
|
+
* shape rather than by (operation-qualified) name, and inline enums compared
|
|
3448
|
+
* by their values. Models outside the companion are compared by name.
|
|
3449
|
+
*/
|
|
3450
|
+
function fingerprintModel(model, local, visiting) {
|
|
3451
|
+
if (visiting.has(model.name)) return `rec:${model.name}`;
|
|
3452
|
+
visiting.add(model.name);
|
|
3453
|
+
const fields = [...model.fields].sort((a, b) => a.name.localeCompare(b.name)).map((f) => `${f.name}${f.required ? "!" : "?"}:${fingerprintType$1(f.type, local, visiting)}`);
|
|
3454
|
+
visiting.delete(model.name);
|
|
3455
|
+
return `{${fields.join(",")}}`;
|
|
3456
|
+
}
|
|
3457
|
+
function fingerprintType$1(ref, local, visiting) {
|
|
3458
|
+
switch (ref.kind) {
|
|
3459
|
+
case "primitive": return `p:${ref.type}${ref.format ? `:${ref.format}` : ""}`;
|
|
3460
|
+
case "literal": return `l:${typeof ref.value}:${String(ref.value)}`;
|
|
3461
|
+
case "nullable": return `n<${fingerprintType$1(ref.inner, local, visiting)}>`;
|
|
3462
|
+
case "array": return `a<${fingerprintType$1(ref.items, local, visiting)}>`;
|
|
3463
|
+
case "map": return `m<${ref.keyType ? `${fingerprintType$1(ref.keyType, local, visiting)},` : ""}${fingerprintType$1(ref.valueType, local, visiting)}>`;
|
|
3464
|
+
case "union": return `u:${ref.compositionKind ?? ""}<${ref.variants.map((v) => fingerprintType$1(v, local, visiting)).sort().join(",")}>`;
|
|
3465
|
+
case "enum": return ref.values ? `e<${[...ref.values].map((v) => `${typeof v}:${String(v)}`).sort().join("|")}>` : `e:${ref.name}`;
|
|
3466
|
+
case "model": {
|
|
3467
|
+
const nested = local.get(ref.name);
|
|
3468
|
+
return nested ? fingerprintModel(nested, local, visiting) : `ref:${ref.name}`;
|
|
3469
|
+
}
|
|
3470
|
+
default: return assertNever(ref);
|
|
2007
3471
|
}
|
|
2008
|
-
return [{
|
|
2009
|
-
name,
|
|
2010
|
-
description: schema.description,
|
|
2011
|
-
fields
|
|
2012
|
-
}, ...nestedModels];
|
|
2013
3472
|
}
|
|
2014
3473
|
//#endregion
|
|
2015
3474
|
//#region src/parser/operations.ts
|
|
@@ -2058,6 +3517,39 @@ function resolveRequestBodyRef(raw, componentRequestBodies, operationContext) {
|
|
|
2058
3517
|
}
|
|
2059
3518
|
return current;
|
|
2060
3519
|
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Resolve a raw response header to a concrete Header Object, following `$ref`
|
|
3522
|
+
* chains into `components/headers` with a cycle guard.
|
|
3523
|
+
*
|
|
3524
|
+
* Unlike parameters and request bodies, a header that cannot be resolved is
|
|
3525
|
+
* warned about and dropped rather than failing the parse: a response header
|
|
3526
|
+
* is informational, so losing one costs an emitter a typed accessor, not a
|
|
3527
|
+
* wire contract, and should not block generating the rest of the SDK.
|
|
3528
|
+
*/
|
|
3529
|
+
function resolveHeaderRef(raw, componentHeaders, headerName, operationContext) {
|
|
3530
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
3531
|
+
let current = raw;
|
|
3532
|
+
let description;
|
|
3533
|
+
while ("$ref" in current) {
|
|
3534
|
+
if (description === void 0 && typeof current.description === "string") description = current.description;
|
|
3535
|
+
if (seenRefs.has(current.$ref)) {
|
|
3536
|
+
console.warn(`[oagen] Warning: Circular header $ref chain starting at "${current.$ref}" for response header "${headerName}" in ${operationContext}. Skipping the header.`);
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
seenRefs.add(current.$ref);
|
|
3540
|
+
const refName = /^#\/components\/headers\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
|
|
3541
|
+
const resolved = refName ? componentHeaders?.[refName] : void 0;
|
|
3542
|
+
if (!resolved) {
|
|
3543
|
+
console.warn(`[oagen] Warning: Unresolved header $ref "${current.$ref}" for response header "${headerName}" in ${operationContext}: no matching entry under components/headers. Skipping the header.`);
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
current = resolved;
|
|
3547
|
+
}
|
|
3548
|
+
return description === void 0 ? current : {
|
|
3549
|
+
...current,
|
|
3550
|
+
description
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
2061
3553
|
const HTTP_METHODS = [
|
|
2062
3554
|
"get",
|
|
2063
3555
|
"post",
|
|
@@ -2068,26 +3560,34 @@ const HTTP_METHODS = [
|
|
|
2068
3560
|
"options",
|
|
2069
3561
|
"trace"
|
|
2070
3562
|
];
|
|
2071
|
-
function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters, componentRequestBodies) {
|
|
3563
|
+
function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters, componentRequestBodies, componentModels, documentTags, componentExamples, componentHeaders) {
|
|
2072
3564
|
if (!paths) return {
|
|
2073
3565
|
services: [],
|
|
2074
3566
|
inlineModels: []
|
|
2075
3567
|
};
|
|
3568
|
+
const pathModelBaseNames = resolvePathModelBaseNames(paths, componentModels, componentRequestBodies);
|
|
2076
3569
|
const serviceMap = /* @__PURE__ */ new Map();
|
|
2077
3570
|
const inlineModels = [];
|
|
3571
|
+
const envelopeCompanions = [];
|
|
3572
|
+
const tagServiceByOperation = /* @__PURE__ */ new Map();
|
|
3573
|
+
const headerModelRegistry = new Map((componentModels ?? []).map((model) => [model.name, { fingerprint: modelFingerprint(model) }]));
|
|
2078
3574
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
2079
3575
|
const pathLevelParams = pathItem.parameters ?? [];
|
|
2080
3576
|
for (const method of HTTP_METHODS) {
|
|
2081
3577
|
const op = pathItem[method];
|
|
2082
3578
|
if (!op) continue;
|
|
2083
|
-
const
|
|
2084
|
-
const
|
|
3579
|
+
const tag = op.tags?.[0];
|
|
3580
|
+
const serviceName = inferServiceName(path, tag);
|
|
3581
|
+
const { operation, inlineModels: opModels, envelopeCompanions: opCompanions } = buildOperation(method, path, op, pathLevelParams, pathModelBaseNames.get(`${method} ${path}`) ?? derivePathModelBaseName(path, method), operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies, componentExamples, componentHeaders, headerModelRegistry);
|
|
2085
3582
|
inlineModels.push(...opModels);
|
|
3583
|
+
if (opCompanions) envelopeCompanions.push(...opCompanions);
|
|
3584
|
+
if (tag) tagServiceByOperation.set(operation, serviceName);
|
|
2086
3585
|
const ops = serviceMap.get(serviceName) ?? [];
|
|
2087
3586
|
ops.push(operation);
|
|
2088
3587
|
serviceMap.set(serviceName, ops);
|
|
2089
3588
|
}
|
|
2090
3589
|
}
|
|
3590
|
+
inlineModels.push(...resolveEnvelopeCompanionNames(envelopeCompanions, componentModels));
|
|
2091
3591
|
for (const [serviceName, operations] of serviceMap) {
|
|
2092
3592
|
const pathGroups = /* @__PURE__ */ new Map();
|
|
2093
3593
|
for (const op of operations) {
|
|
@@ -2108,14 +3608,73 @@ function extractOperations(paths, operationIdTransform, componentSchemas, compon
|
|
|
2108
3608
|
}
|
|
2109
3609
|
}
|
|
2110
3610
|
}
|
|
2111
|
-
for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
operations
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
3611
|
+
for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
|
|
3612
|
+
const declaredTags = indexDocumentTags(documentTags);
|
|
3613
|
+
return {
|
|
3614
|
+
services: Array.from(serviceMap.entries()).map(([name, operations]) => {
|
|
3615
|
+
const description = resolveServiceDescription(name, operations, tagServiceByOperation, declaredTags);
|
|
3616
|
+
return description === void 0 ? {
|
|
3617
|
+
name,
|
|
3618
|
+
operations
|
|
3619
|
+
} : {
|
|
3620
|
+
name,
|
|
3621
|
+
description,
|
|
3622
|
+
operations
|
|
3623
|
+
};
|
|
3624
|
+
}),
|
|
3625
|
+
inlineModels
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
/**
|
|
3629
|
+
* Index the document's top-level `tags[]` by the service name each tag
|
|
3630
|
+
* produces, so a tag spelled `user_management`, `User Management`, or
|
|
3631
|
+
* `user-management` all map to the `UserManagement` service. Every named tag
|
|
3632
|
+
* is indexed — with its description when it declares a non-blank one, and
|
|
3633
|
+
* `undefined` otherwise — because a declared-but-undescribed tag still marks
|
|
3634
|
+
* its service as one that must not borrow another tag's wording. When several
|
|
3635
|
+
* tags normalize to the same service name, the first described one wins.
|
|
3636
|
+
*
|
|
3637
|
+
* `parseSpec` ignored `tags` entirely before descriptions were read, so a
|
|
3638
|
+
* document with a malformed value — `tags` written as an object, a `null`
|
|
3639
|
+
* entry, a numeric `name` — used to parse and must keep parsing. Anything
|
|
3640
|
+
* that cannot be matched is skipped rather than thrown on.
|
|
3641
|
+
*/
|
|
3642
|
+
function indexDocumentTags(documentTags) {
|
|
3643
|
+
const index = /* @__PURE__ */ new Map();
|
|
3644
|
+
if (!Array.isArray(documentTags)) return index;
|
|
3645
|
+
for (const tag of documentTags) {
|
|
3646
|
+
if (typeof tag !== "object" || tag === null) continue;
|
|
3647
|
+
const { name, description } = tag;
|
|
3648
|
+
if (typeof name !== "string" || name === "") continue;
|
|
3649
|
+
const serviceName = toPascalCase(name);
|
|
3650
|
+
if (index.get(serviceName) !== void 0) continue;
|
|
3651
|
+
const described = typeof description === "string" && description.trim() !== "";
|
|
3652
|
+
index.set(serviceName, described ? description : void 0);
|
|
3653
|
+
}
|
|
3654
|
+
return index;
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Pick a service's description from the document's tags.
|
|
3658
|
+
*
|
|
3659
|
+
* A service whose own name is a declared tag is described by that tag alone:
|
|
3660
|
+
* its description when it has one, nothing otherwise. Another tag's wording is
|
|
3661
|
+
* never borrowed for it — operations tagged `api_keys` that the path-prefix
|
|
3662
|
+
* split merges into `UserManagement` must not label that service "API keys".
|
|
3663
|
+
*
|
|
3664
|
+
* A service whose own name is not declared is one the split carved out of a
|
|
3665
|
+
* tag (e.g. `OrganizationDomains` from operations tagged `organizations`); it
|
|
3666
|
+
* inherits the description of the first described tag its operations carry.
|
|
3667
|
+
* Path-named services whose operations have no tag carry nothing and stay
|
|
3668
|
+
* undescribed.
|
|
3669
|
+
*/
|
|
3670
|
+
function resolveServiceDescription(serviceName, operations, tagServiceByOperation, declaredTags) {
|
|
3671
|
+
if (declaredTags.has(serviceName)) return declaredTags.get(serviceName);
|
|
3672
|
+
for (const op of operations) {
|
|
3673
|
+
const tagService = tagServiceByOperation.get(op);
|
|
3674
|
+
if (tagService === void 0) continue;
|
|
3675
|
+
const description = declaredTags.get(tagService);
|
|
3676
|
+
if (description !== void 0) return description;
|
|
3677
|
+
}
|
|
2119
3678
|
}
|
|
2120
3679
|
function inferServiceName(path, tag) {
|
|
2121
3680
|
if (tag) return toPascalCase(tag);
|
|
@@ -2135,6 +3694,24 @@ function inferServiceName(path, tag) {
|
|
|
2135
3694
|
function normalizeOperationIdForNaming(operationId) {
|
|
2136
3695
|
return operationId.replace(/\[\d+\]\s*$/, "");
|
|
2137
3696
|
}
|
|
3697
|
+
/**
|
|
3698
|
+
* The inline request body or response model name of an operation that has an
|
|
3699
|
+
* `operationId`. The single source of that name: extractRequestBody and
|
|
3700
|
+
* deriveResponseName use it to name, resolvePathModelBaseNames to reserve.
|
|
3701
|
+
*/
|
|
3702
|
+
function operationIdModelName(operationId, suffix) {
|
|
3703
|
+
return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(operationId)) + suffix);
|
|
3704
|
+
}
|
|
3705
|
+
/**
|
|
3706
|
+
* The stem every model derived from an `operationId` starts with: its
|
|
3707
|
+
* `Request` and `Response` models, a list's `Item`, nested inline models under
|
|
3708
|
+
* those (`…ResponseOwner`) and envelope companions (`…ListMetadata`). Every
|
|
3709
|
+
* step of cleanSchemaName is a global replace or acts on the leading word, so
|
|
3710
|
+
* this is a true prefix of each of those names. Used to reserve them all.
|
|
3711
|
+
*/
|
|
3712
|
+
function operationIdModelBase(operationId) {
|
|
3713
|
+
return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(operationId)));
|
|
3714
|
+
}
|
|
2138
3715
|
function inferOperationName(method, path, operationId, operationIdTransform) {
|
|
2139
3716
|
if (operationId) {
|
|
2140
3717
|
const normalized = normalizeOperationIdForNaming(operationId);
|
|
@@ -2322,8 +3899,9 @@ function extractBodyParameterGroups(op, requestBody, operationContext, component
|
|
|
2322
3899
|
}
|
|
2323
3900
|
return groups.length > 0 ? groups : void 0;
|
|
2324
3901
|
}
|
|
2325
|
-
function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies) {
|
|
2326
|
-
const opLabel = op
|
|
3902
|
+
function buildOperation(method, path, op, pathLevelParams, pathModelBaseName, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies, componentExamples, componentHeaders, headerModelRegistry = /* @__PURE__ */ new Map()) {
|
|
3903
|
+
const opLabel = operationLabel(op, path, method);
|
|
3904
|
+
const operationName = inferOperationName(method, path, op.operationId, operationIdTransform);
|
|
2327
3905
|
const requestBodyObject = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
|
|
2328
3906
|
const paramsByIdentity = /* @__PURE__ */ new Map();
|
|
2329
3907
|
for (const raw of [...pathLevelParams, ...op.parameters ?? []]) {
|
|
@@ -2340,14 +3918,20 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
2340
3918
|
}
|
|
2341
3919
|
const wireParams = bodyOwnedNames.size > 0 ? allParams.filter((p) => !(p.in === "query" && bodyOwnedNames.has(p.name))) : allParams;
|
|
2342
3920
|
const opContext = serviceName;
|
|
2343
|
-
const pathParams = extractParams(wireParams, "path", opContext, componentSchemas);
|
|
2344
|
-
const queryParams = extractParams(wireParams, "query", opContext, componentSchemas);
|
|
2345
|
-
const headerParams = extractParams(wireParams, "header", opContext, componentSchemas).filter((p) => p.name.toLowerCase() !== "idempotency-key");
|
|
2346
|
-
const cookieParams = extractParams(wireParams, "cookie", opContext, componentSchemas);
|
|
2347
|
-
const bodyOwnedQueryParams = bodyOwnedNames.size > 0 ? extractParams(allParams.filter((p) => p.in === "query" && bodyOwnedNames.has(p.name)), "query", opContext, componentSchemas) : [];
|
|
3921
|
+
const pathParams = extractParams(wireParams, "path", opContext, componentSchemas, componentExamples, opLabel);
|
|
3922
|
+
const queryParams = extractParams(wireParams, "query", opContext, componentSchemas, componentExamples, opLabel);
|
|
3923
|
+
const headerParams = extractParams(wireParams, "header", opContext, componentSchemas, componentExamples, opLabel).filter((p) => p.name.toLowerCase() !== "idempotency-key");
|
|
3924
|
+
const cookieParams = extractParams(wireParams, "cookie", opContext, componentSchemas, componentExamples, opLabel);
|
|
3925
|
+
const bodyOwnedQueryParams = bodyOwnedNames.size > 0 ? extractParams(allParams.filter((p) => p.in === "query" && bodyOwnedNames.has(p.name)), "query", opContext, componentSchemas, componentExamples, opLabel) : [];
|
|
2348
3926
|
const reqBodyModels = [];
|
|
2349
|
-
const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(requestBodyObject, op, reqBodyModels);
|
|
2350
|
-
const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method, componentSchemas
|
|
3927
|
+
const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(requestBodyObject, op, pathModelBaseName, reqBodyModels, componentSchemas);
|
|
3928
|
+
const { response, responseHeaders, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType, envelopeFields, envelopeCompanions } = extractResponses(op.responses, op, pathModelBaseName, path, method, componentSchemas, {
|
|
3929
|
+
componentHeaders,
|
|
3930
|
+
componentSchemas,
|
|
3931
|
+
serviceName: opContext,
|
|
3932
|
+
operationQualifier: toPascalCase(operationName),
|
|
3933
|
+
registry: headerModelRegistry
|
|
3934
|
+
});
|
|
2351
3935
|
inlineModels.push(...reqBodyModels);
|
|
2352
3936
|
const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
|
|
2353
3937
|
let pagination;
|
|
@@ -2358,10 +3942,11 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
2358
3942
|
param: paginationFromParams?.param ?? "after",
|
|
2359
3943
|
limitParam: paginationFromParams?.limitParam,
|
|
2360
3944
|
dataPath: responseDataPath ?? paginationFromParams?.dataPath,
|
|
2361
|
-
itemType
|
|
3945
|
+
itemType,
|
|
3946
|
+
envelopeFields
|
|
2362
3947
|
};
|
|
2363
3948
|
} else if (paginationFromParams) pagination = paginationFromParams;
|
|
2364
|
-
const security =
|
|
3949
|
+
const security = extractSecurityRequirements(op.security, opLabel);
|
|
2365
3950
|
const queryParamGroups = extractParameterGroups(op, [
|
|
2366
3951
|
...pathParams,
|
|
2367
3952
|
...queryParams,
|
|
@@ -2370,10 +3955,12 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
2370
3955
|
], opLabel);
|
|
2371
3956
|
const bodyParamGroups = extractBodyParameterGroups(op, requestBodyObject, opLabel, componentSchemas);
|
|
2372
3957
|
const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
|
|
3958
|
+
const summary = presentText(op.summary);
|
|
2373
3959
|
return {
|
|
2374
3960
|
operation: {
|
|
2375
|
-
name:
|
|
2376
|
-
|
|
3961
|
+
name: operationName,
|
|
3962
|
+
...summary !== void 0 ? { summary } : {},
|
|
3963
|
+
description: buildDescription(summary, op.description),
|
|
2377
3964
|
httpMethod: method,
|
|
2378
3965
|
path,
|
|
2379
3966
|
pathParams,
|
|
@@ -2384,6 +3971,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
2384
3971
|
requestBody,
|
|
2385
3972
|
requestBodyEncoding,
|
|
2386
3973
|
response,
|
|
3974
|
+
...responseHeaders && { responseHeaders },
|
|
2387
3975
|
successResponses,
|
|
2388
3976
|
errors,
|
|
2389
3977
|
pagination,
|
|
@@ -2393,31 +3981,49 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
2393
3981
|
security,
|
|
2394
3982
|
parameterGroups
|
|
2395
3983
|
},
|
|
2396
|
-
inlineModels
|
|
3984
|
+
inlineModels,
|
|
3985
|
+
envelopeCompanions
|
|
2397
3986
|
};
|
|
2398
3987
|
}
|
|
2399
3988
|
/**
|
|
2400
|
-
*
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2403
|
-
*
|
|
3989
|
+
* Combine an operation's `summary` and `description` into one doc text.
|
|
3990
|
+
* Empty or whitespace-only text counts as absent, so `summary: "List pets"`
|
|
3991
|
+
* with `description: ""` yields `"List pets"` rather than `""`. Text that has
|
|
3992
|
+
* content is kept verbatim, surrounding whitespace included.
|
|
2404
3993
|
*/
|
|
2405
|
-
function extractOperationSecurity(security) {
|
|
2406
|
-
if (!security || security.length === 0) return void 0;
|
|
2407
|
-
const requirements = [];
|
|
2408
|
-
for (const entry of security) for (const [schemeName, scopes] of Object.entries(entry)) requirements.push({
|
|
2409
|
-
schemeName,
|
|
2410
|
-
scopes: scopes ?? []
|
|
2411
|
-
});
|
|
2412
|
-
return requirements.length > 0 ? requirements : void 0;
|
|
2413
|
-
}
|
|
2414
3994
|
function buildDescription(summary, description) {
|
|
2415
|
-
|
|
2416
|
-
|
|
3995
|
+
const presentSummary = presentText(summary);
|
|
3996
|
+
const presentDescription = presentText(description);
|
|
3997
|
+
if (presentSummary && presentDescription && presentSummary !== presentDescription) return `${presentSummary}\n\n${presentDescription}`;
|
|
3998
|
+
return presentDescription ?? presentSummary;
|
|
3999
|
+
}
|
|
4000
|
+
/** The text itself when it has non-whitespace content, otherwise `undefined`. */
|
|
4001
|
+
function presentText(text) {
|
|
4002
|
+
return typeof text === "string" && text.trim() !== "" ? text : void 0;
|
|
2417
4003
|
}
|
|
2418
|
-
function extractParams(params, location, operationContext, componentSchemas) {
|
|
4004
|
+
function extractParams(params, location, operationContext, componentSchemas, componentExamples, opLabel) {
|
|
2419
4005
|
return params.filter((p) => p.in === location).map((p) => {
|
|
2420
|
-
const refTarget =
|
|
4006
|
+
const refTarget = resolveComponentSchemaRef(p.schema, componentSchemas);
|
|
4007
|
+
const { example, examples } = resolveExamples([
|
|
4008
|
+
{
|
|
4009
|
+
example: p.example,
|
|
4010
|
+
examples: exampleObjectValues(p.examples, componentExamples, `parameter "${p.name}" in ${opLabel ?? location}`)
|
|
4011
|
+
},
|
|
4012
|
+
{
|
|
4013
|
+
example: p.schema?.example,
|
|
4014
|
+
examples: nonEmptyExamples(p.schema?.examples)
|
|
4015
|
+
},
|
|
4016
|
+
{
|
|
4017
|
+
example: refTarget?.example,
|
|
4018
|
+
examples: nonEmptyExamples(refTarget?.examples)
|
|
4019
|
+
}
|
|
4020
|
+
]);
|
|
4021
|
+
const own = p.schema ? extractConstraints(p.schema) : void 0;
|
|
4022
|
+
const inherited = refTarget ? extractConstraints(refTarget) : void 0;
|
|
4023
|
+
const constraints = own && inherited ? {
|
|
4024
|
+
...inherited,
|
|
4025
|
+
...own
|
|
4026
|
+
} : own ?? inherited;
|
|
2421
4027
|
return {
|
|
2422
4028
|
name: p.name,
|
|
2423
4029
|
type: p.schema ? schemaToTypeRef(p.schema, p.name, operationContext ? toPascalCase(operationContext) : void 0) : {
|
|
@@ -2425,32 +4031,74 @@ function extractParams(params, location, operationContext, componentSchemas) {
|
|
|
2425
4031
|
type: "string"
|
|
2426
4032
|
},
|
|
2427
4033
|
required: p.required ?? false,
|
|
2428
|
-
description: p.description,
|
|
4034
|
+
description: firstDescription(p.description, p.schema?.description, refTarget?.description),
|
|
2429
4035
|
deprecated: p.deprecated || p.schema?.deprecated || refTarget?.deprecated || void 0,
|
|
2430
4036
|
default: p.schema?.default ?? refTarget?.default,
|
|
2431
|
-
example
|
|
4037
|
+
example,
|
|
4038
|
+
...examples ? { examples } : {},
|
|
2432
4039
|
style: p.style,
|
|
2433
|
-
explode: p.explode
|
|
4040
|
+
explode: p.explode,
|
|
4041
|
+
...constraints ? { constraints } : {}
|
|
2434
4042
|
};
|
|
2435
4043
|
});
|
|
2436
4044
|
}
|
|
2437
4045
|
/**
|
|
2438
|
-
*
|
|
2439
|
-
*
|
|
2440
|
-
*
|
|
2441
|
-
*
|
|
2442
|
-
* Refs are not pre-resolved by the bundler (`dereference: false` in refs.ts),
|
|
2443
|
-
* so without this, a param like `schema: { $ref: '.../PaginationOrder' }`
|
|
2444
|
-
* silently loses the target's `default: desc` and the SDK stops emitting the
|
|
2445
|
-
* spec-mandated default value in generated method signatures.
|
|
4046
|
+
* First non-empty description in precedence order. A parameter that declares
|
|
4047
|
+
* `description: ""` is treated as undocumented so its schema's text still
|
|
4048
|
+
* surfaces, and the result is `undefined` rather than `""` when nothing in
|
|
4049
|
+
* the chain is documented.
|
|
2446
4050
|
*/
|
|
2447
|
-
function
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
4051
|
+
function firstDescription(...candidates) {
|
|
4052
|
+
return candidates.find((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
4053
|
+
}
|
|
4054
|
+
/**
|
|
4055
|
+
* Flatten a parameter's `examples` map of Example Objects to the inline
|
|
4056
|
+
* values it carries, in document order. A `$ref` entry is resolved against
|
|
4057
|
+
* `components.examples` first (see `resolveExampleRef`). Entries that point
|
|
4058
|
+
* elsewhere (`externalValue`) or have no `value` are skipped; `null` is a
|
|
4059
|
+
* legitimate value and kept. Returns undefined when nothing remains so the
|
|
4060
|
+
* caller can fall through to the schema's `examples`.
|
|
4061
|
+
*/
|
|
4062
|
+
function exampleObjectValues(examples, componentExamples, context) {
|
|
4063
|
+
if (!examples || typeof examples !== "object" || Array.isArray(examples)) return void 0;
|
|
4064
|
+
const values = [];
|
|
4065
|
+
for (const raw of Object.values(examples)) {
|
|
4066
|
+
const entry = resolveExampleRef(raw, componentExamples, context);
|
|
4067
|
+
if (!entry || typeof entry !== "object") continue;
|
|
4068
|
+
if (entry.externalValue !== void 0 || entry.value === void 0) continue;
|
|
4069
|
+
values.push(entry.value);
|
|
4070
|
+
}
|
|
4071
|
+
return values.length > 0 ? values : void 0;
|
|
4072
|
+
}
|
|
4073
|
+
/**
|
|
4074
|
+
* Follow an Example Object's `$ref` into `components.examples`, including a
|
|
4075
|
+
* component that is itself a `$ref`, with a cycle guard. The bundler keeps
|
|
4076
|
+
* refs (`dereference: false`), so without this every `$ref` entry would be
|
|
4077
|
+
* dropped and a map made only of refs would silently yield nothing — the
|
|
4078
|
+
* parameter would then take its `examples` from the schema instead of the
|
|
4079
|
+
* values the document points at. A ref that can't be resolved is skipped
|
|
4080
|
+
* with a warning rather than failing the parse: a missing example is a doc
|
|
4081
|
+
* gap, not a broken API surface.
|
|
4082
|
+
*/
|
|
4083
|
+
function resolveExampleRef(raw, componentExamples, context) {
|
|
4084
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
4085
|
+
let current = raw;
|
|
4086
|
+
while (current && typeof current === "object" && current.$ref !== void 0) {
|
|
4087
|
+
const ref = current.$ref;
|
|
4088
|
+
if (seenRefs.has(ref)) {
|
|
4089
|
+
console.warn(`[oagen] Warning: Circular example $ref chain starting at "${ref}" on ${context}; skipping`);
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
seenRefs.add(ref);
|
|
4093
|
+
const refName = /^#\/components\/examples\/(.+)$/.exec(ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
|
|
4094
|
+
const resolved = refName ? componentExamples?.[refName] : void 0;
|
|
4095
|
+
if (!resolved) {
|
|
4096
|
+
console.warn(`[oagen] Warning: Unresolved example $ref "${ref}" on ${context}; skipping`);
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
4099
|
+
current = resolved;
|
|
4100
|
+
}
|
|
4101
|
+
return current;
|
|
2454
4102
|
}
|
|
2455
4103
|
/**
|
|
2456
4104
|
* Collect the property schemas of every `oneOf` branch reachable from a request
|
|
@@ -2502,7 +4150,7 @@ function resolveSchemaRefObject(schema, componentSchemas) {
|
|
|
2502
4150
|
return componentSchemas[match[1]];
|
|
2503
4151
|
}
|
|
2504
4152
|
/** HTTP methods whose request body is the primary parameter carrier. */
|
|
2505
|
-
const BODY_CARRYING_METHODS = new Set([
|
|
4153
|
+
const BODY_CARRYING_METHODS = /* @__PURE__ */ new Set([
|
|
2506
4154
|
"post",
|
|
2507
4155
|
"put",
|
|
2508
4156
|
"patch"
|
|
@@ -2565,7 +4213,7 @@ function collectBodyPropertyNames(schema, componentSchemas, seen = /* @__PURE__
|
|
|
2565
4213
|
const ref = schema.$ref;
|
|
2566
4214
|
if (ref) {
|
|
2567
4215
|
if (seen.has(ref)) return /* @__PURE__ */ new Set();
|
|
2568
|
-
return collectBodyPropertyNames(resolveSchemaRefObject(schema, componentSchemas), componentSchemas, new Set([...seen, ref]));
|
|
4216
|
+
return collectBodyPropertyNames(resolveSchemaRefObject(schema, componentSchemas), componentSchemas, /* @__PURE__ */ new Set([...seen, ref]));
|
|
2569
4217
|
}
|
|
2570
4218
|
const names = new Set(Object.keys(schema.properties ?? {}));
|
|
2571
4219
|
for (const sub of schema.allOf ?? []) for (const name of collectBodyPropertyNames(sub, componentSchemas, seen)) names.add(name);
|
|
@@ -2581,7 +4229,7 @@ function collectBodyPropertyNames(schema, componentSchemas, seen = /* @__PURE__
|
|
|
2581
4229
|
}
|
|
2582
4230
|
return names;
|
|
2583
4231
|
}
|
|
2584
|
-
function extractRequestBody(body, op, inlineModels) {
|
|
4232
|
+
function extractRequestBody(body, op, pathModelBaseName, inlineModels, componentSchemas) {
|
|
2585
4233
|
if (!body?.content) return {};
|
|
2586
4234
|
const selected = selectRequestBodyContent(body);
|
|
2587
4235
|
const encoding = selected.encoding;
|
|
@@ -2604,24 +4252,26 @@ function extractRequestBody(body, op, inlineModels) {
|
|
|
2604
4252
|
};
|
|
2605
4253
|
return {};
|
|
2606
4254
|
}
|
|
2607
|
-
const contextName =
|
|
4255
|
+
const contextName = op?.operationId ? operationIdModelName(op.operationId, "Request") : pathModelBaseName + "Request";
|
|
2608
4256
|
if (schema.oneOf && schema.discriminator) {
|
|
2609
4257
|
const merged = flattenDiscriminatedUnionBody(schema);
|
|
2610
4258
|
if (merged) schema = merged;
|
|
2611
4259
|
}
|
|
2612
4260
|
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
2613
4261
|
const requiredSet = new Set(schema.required ?? []);
|
|
4262
|
+
const siblings = collectProperties(schema);
|
|
2614
4263
|
const fields = [];
|
|
2615
4264
|
for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
|
|
2616
4265
|
if (!fieldSchema) continue;
|
|
2617
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet));
|
|
4266
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet, componentSchemas, siblings));
|
|
2618
4267
|
}
|
|
2619
4268
|
inlineModels.push({
|
|
2620
4269
|
name: contextName,
|
|
2621
4270
|
description: void 0,
|
|
2622
|
-
fields
|
|
4271
|
+
fields,
|
|
4272
|
+
...constraintsEntry(schema)
|
|
2623
4273
|
});
|
|
2624
|
-
extractInlineModelsFromProperties(schema, inlineModels, contextName);
|
|
4274
|
+
extractInlineModelsFromProperties(schema, inlineModels, contextName, componentSchemas);
|
|
2625
4275
|
return {
|
|
2626
4276
|
body: {
|
|
2627
4277
|
kind: "model",
|
|
@@ -2632,23 +4282,29 @@ function extractRequestBody(body, op, inlineModels) {
|
|
|
2632
4282
|
}
|
|
2633
4283
|
if (schema.oneOf) {
|
|
2634
4284
|
const variants = [];
|
|
4285
|
+
const inlineObjectVariants = schema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
|
|
4286
|
+
const contentNamed = [];
|
|
2635
4287
|
for (const variant of schema.oneOf) if (variant.$ref) {
|
|
2636
4288
|
const ref = schemaToTypeRef(variant, contextName);
|
|
2637
4289
|
variants.push(ref);
|
|
2638
4290
|
} else if (variant.type === "null") {} else if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
2639
|
-
const
|
|
4291
|
+
const named = deriveOneOfVariantName(variant, contextName, inlineModels, inlineObjectVariants, contentNamed, Boolean(op?.operationId));
|
|
4292
|
+
const variantName = named.name;
|
|
4293
|
+
if (named.fromContent) contentNamed.push(variantName);
|
|
2640
4294
|
const requiredSet = new Set(variant.required ?? []);
|
|
4295
|
+
const siblings = collectProperties(variant);
|
|
2641
4296
|
const fields = [];
|
|
2642
4297
|
for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
|
|
2643
4298
|
if (!fieldSchema) continue;
|
|
2644
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet));
|
|
4299
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet, componentSchemas, siblings));
|
|
2645
4300
|
}
|
|
2646
4301
|
inlineModels.push({
|
|
2647
4302
|
name: variantName,
|
|
2648
4303
|
description: variant.description,
|
|
2649
|
-
fields
|
|
4304
|
+
fields,
|
|
4305
|
+
...constraintsEntry(variant)
|
|
2650
4306
|
});
|
|
2651
|
-
extractInlineModelsFromProperties(variant, inlineModels, variantName);
|
|
4307
|
+
extractInlineModelsFromProperties(variant, inlineModels, variantName, componentSchemas);
|
|
2652
4308
|
variants.push({
|
|
2653
4309
|
kind: "model",
|
|
2654
4310
|
name: variantName
|
|
@@ -2734,38 +4390,451 @@ function flattenDiscriminatedUnionBody(schema) {
|
|
|
2734
4390
|
type: "object",
|
|
2735
4391
|
properties,
|
|
2736
4392
|
required: order.filter((name) => name === discProp || requiredCounts[name] === variantCount),
|
|
2737
|
-
...schema.description ? { description: schema.description } : {}
|
|
4393
|
+
...schema.description ? { description: schema.description } : {},
|
|
4394
|
+
...extractConstraints(schema)
|
|
2738
4395
|
};
|
|
2739
4396
|
}
|
|
2740
|
-
/**
|
|
2741
|
-
|
|
4397
|
+
/**
|
|
4398
|
+
* Derive a unique variant name for a request-body `oneOf` inline model.
|
|
4399
|
+
*
|
|
4400
|
+
* A variant with a string-const property is named after it
|
|
4401
|
+
* (`RadarEmailChallengeCodeSessionAuthenticateRequest`). Any other variant is
|
|
4402
|
+
* named from its content by the same rule every inline union variant follows
|
|
4403
|
+
* (`nameInlineVariant`): the first const-less variant keeps the body's name
|
|
4404
|
+
* (or its title, when every inline variant of the union is titled), and each
|
|
4405
|
+
* later one takes its title, a distinguishing property
|
|
4406
|
+
* (`CreateThingRequestWithB`) or, when its property set repeats an earlier
|
|
4407
|
+
* variant's, the position counter (`CreateThingRequest2`). `siblings` are the
|
|
4408
|
+
* union's inline object variants and `contentNamed` the names its const-less
|
|
4409
|
+
* variants have taken so far; `fromContent` tells the caller to record this
|
|
4410
|
+
* one there. A clash with a model minted elsewhere still bumps the counter:
|
|
4411
|
+
* on the base for a content-named variant (`CreateThingRequest2`), on the
|
|
4412
|
+
* const-derived name for a const-named one (`FooCreateThingRequest2`) — so a
|
|
4413
|
+
* variant titled `Foo` and a later one pinning `kind: foo` in the same body
|
|
4414
|
+
* never share a model.
|
|
4415
|
+
*
|
|
4416
|
+
* `fromOperationId` says where `baseName` came from. An operationId-derived
|
|
4417
|
+
* base is already in cleanSchemaName form and its variants go through
|
|
4418
|
+
* resolveSchemaName, so they follow the same project-level naming policy as
|
|
4419
|
+
* component schemas. A path-derived base is used verbatim: it is deliberately
|
|
4420
|
+
* not cleaned (see derivePathModelBaseName) and resolvePathModelBaseNames
|
|
4421
|
+
* reserves names by that exact prefix, so cleaning a variant here would let it
|
|
4422
|
+
* slip past the reservation — `NotesPostRequest` became `NotePostRequest` and
|
|
4423
|
+
* collided with a component of that name.
|
|
4424
|
+
*/
|
|
4425
|
+
function deriveOneOfVariantName(variant, baseName, existingModels, siblings, contentNamed, fromOperationId) {
|
|
4426
|
+
const finalize = (name) => fromOperationId ? resolveSchemaName(name) : name;
|
|
4427
|
+
const existingNames = new Set(existingModels.map((m) => m.name));
|
|
4428
|
+
const unique = (name, stem) => {
|
|
4429
|
+
let candidate = name;
|
|
4430
|
+
for (let suffix = 2; existingNames.has(candidate); suffix++) candidate = `${stem}${suffix}`;
|
|
4431
|
+
return candidate;
|
|
4432
|
+
};
|
|
2742
4433
|
if (variant.properties) {
|
|
2743
|
-
for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string")
|
|
4434
|
+
for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string") {
|
|
4435
|
+
const derivedName = finalize(toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request");
|
|
4436
|
+
return {
|
|
4437
|
+
name: unique(derivedName, derivedName),
|
|
4438
|
+
fromContent: false
|
|
4439
|
+
};
|
|
4440
|
+
}
|
|
2744
4441
|
}
|
|
2745
|
-
const
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
4442
|
+
const finalBaseName = finalize(baseName);
|
|
4443
|
+
return {
|
|
4444
|
+
name: unique(nameInlineVariant(variant, siblings, finalBaseName, contentNamed, null), finalBaseName),
|
|
4445
|
+
fromContent: true
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
/** Human-readable label for an operation in diagnostics: its operationId, else `METHOD /path`. */
|
|
4449
|
+
function operationLabel(op, path, method) {
|
|
4450
|
+
return op?.operationId ?? `${method.toUpperCase()} ${path}`;
|
|
4451
|
+
}
|
|
4452
|
+
/** Shape of a model, independent of field order: name, requiredness, and type of every field. */
|
|
4453
|
+
function modelFingerprint(model) {
|
|
4454
|
+
return model.fields.map((f) => `${f.name}${f.required ? "!" : "?"}:${stableStringify(f.type)}`).sort().join("|");
|
|
4455
|
+
}
|
|
4456
|
+
/** `JSON.stringify` with object keys sorted, so equal shapes serialize equally. */
|
|
4457
|
+
function stableStringify(value) {
|
|
4458
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
4459
|
+
if (value !== null && typeof value === "object") {
|
|
4460
|
+
const record = value;
|
|
4461
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
|
|
2752
4462
|
}
|
|
2753
|
-
return
|
|
4463
|
+
return JSON.stringify(value) ?? "undefined";
|
|
4464
|
+
}
|
|
4465
|
+
/**
|
|
4466
|
+
* Type a header's schema and materialize the inline models it names, under a
|
|
4467
|
+
* property name that does not collide with a different shape anywhere in the
|
|
4468
|
+
* spec.
|
|
4469
|
+
*
|
|
4470
|
+
* Header models are named from the service and header name alone, so an
|
|
4471
|
+
* inline `X-Meta` on a `Documents` operation is `DocumentsXMeta` — the same
|
|
4472
|
+
* name a component schema, or the same header on a 429 or on another
|
|
4473
|
+
* `Documents` operation, may already own with a different shape. Left alone,
|
|
4474
|
+
* the later merge keeps one definition for the name and every other
|
|
4475
|
+
* reference silently gets the wrong fields (a request body using the
|
|
4476
|
+
* component would even change type without any emitter reading headers).
|
|
4477
|
+
*
|
|
4478
|
+
* The first owner of a name keeps it: component models are seeded before any
|
|
4479
|
+
* operation, operations are visited in path order, and a single operation's
|
|
4480
|
+
* responses in ascending status-code order (integer-like keys iterate that
|
|
4481
|
+
* way), so the lowest code wins regardless of where it appears in the
|
|
4482
|
+
* document. A later header with a different shape — at any depth, a nested
|
|
4483
|
+
* object that differs counts — is re-derived under the first free candidate:
|
|
4484
|
+
*
|
|
4485
|
+
* 1. the bare header name `DocumentsXMeta`
|
|
4486
|
+
* 2. when the name is taken inside this operation, the status code
|
|
4487
|
+
* (`DocumentsXMeta429`); when taken by a component or another
|
|
4488
|
+
* operation, the operation (`DocumentsCreateDocumentXMeta`)
|
|
4489
|
+
* 3. the other of those two
|
|
4490
|
+
* 4. operation and status code `DocumentsCreateDocumentXMeta429`
|
|
4491
|
+
* 5. a numeric suffix `DocumentsXMeta2`, `DocumentsXMeta3`, ...
|
|
4492
|
+
*
|
|
4493
|
+
* Both the type and the models come from the same property name, so nested
|
|
4494
|
+
* models (`DocumentsXMeta429Origin`) line up by construction. A shape
|
|
4495
|
+
* identical to the name's current owner shares it and materializes nothing
|
|
4496
|
+
* new, which is also how a header reuses a matching component model.
|
|
4497
|
+
*
|
|
4498
|
+
* Treating the header as a one-property object reuses the field-level
|
|
4499
|
+
* extractor, whose names come from the same `qualifyInlineModelName(name,
|
|
4500
|
+
* parent)` rule `schemaToTypeRef` applies — the trick the inline request-body
|
|
4501
|
+
* path uses.
|
|
4502
|
+
*/
|
|
4503
|
+
function typeHeaderSchema(schema, name, statusCode, ctx, models) {
|
|
4504
|
+
const parentName = ctx.serviceName ? toPascalCase(ctx.serviceName) : void 0;
|
|
4505
|
+
const byStatus = `${name}${statusCode}`;
|
|
4506
|
+
const byOperation = `${ctx.operationQualifier}${name}`;
|
|
4507
|
+
const byBoth = `${ctx.operationQualifier}${name}${statusCode}`;
|
|
4508
|
+
let candidates = [
|
|
4509
|
+
name,
|
|
4510
|
+
byStatus,
|
|
4511
|
+
byOperation,
|
|
4512
|
+
byBoth
|
|
4513
|
+
];
|
|
4514
|
+
for (let attempt = 0;; attempt++) {
|
|
4515
|
+
const propertyName = attempt < candidates.length ? candidates[attempt] : `${name}${attempt - candidates.length + 2}`;
|
|
4516
|
+
const trial = [];
|
|
4517
|
+
extractInlineModelsFromProperties({ properties: { [propertyName]: schema } }, trial, parentName, ctx.componentSchemas);
|
|
4518
|
+
const fingerprints = trial.map((model) => [model, modelFingerprint(model)]);
|
|
4519
|
+
const collisions = fingerprints.flatMap(([model, fingerprint]) => {
|
|
4520
|
+
const registered = ctx.registry.get(model.name);
|
|
4521
|
+
return registered !== void 0 && registered.fingerprint !== fingerprint ? [registered] : [];
|
|
4522
|
+
});
|
|
4523
|
+
if (collisions.length > 0) {
|
|
4524
|
+
if (attempt === 0 && !collisions.every((c) => c.operation === ctx.operationQualifier)) candidates = [
|
|
4525
|
+
name,
|
|
4526
|
+
byOperation,
|
|
4527
|
+
byStatus,
|
|
4528
|
+
byBoth
|
|
4529
|
+
];
|
|
4530
|
+
continue;
|
|
4531
|
+
}
|
|
4532
|
+
for (const [model, fingerprint] of fingerprints) {
|
|
4533
|
+
if (ctx.registry.has(model.name)) continue;
|
|
4534
|
+
ctx.registry.set(model.name, {
|
|
4535
|
+
fingerprint,
|
|
4536
|
+
operation: ctx.operationQualifier
|
|
4537
|
+
});
|
|
4538
|
+
models.push(model);
|
|
4539
|
+
}
|
|
4540
|
+
return schemaToTypeRef(schema, propertyName, parentName);
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
/**
|
|
4544
|
+
* Read a Response Object's `headers` map into IR response headers, resolving
|
|
4545
|
+
* `components/headers` refs. Returns `undefined` rather than `[]` when nothing
|
|
4546
|
+
* survives so callers can leave the `headers` key off entirely.
|
|
4547
|
+
*
|
|
4548
|
+
* An object-shaped header schema (directly, inside an array, or as a oneOf
|
|
4549
|
+
* variant) becomes a `ModelRef`, so the model it names is appended to
|
|
4550
|
+
* `models` — otherwise the ref would dangle with nothing to emit.
|
|
4551
|
+
*/
|
|
4552
|
+
function extractResponseHeaders(headers, operationLabel, statusCode, ctx, models) {
|
|
4553
|
+
if (!headers) return void 0;
|
|
4554
|
+
const result = [];
|
|
4555
|
+
for (const [name, raw] of Object.entries(headers)) {
|
|
4556
|
+
if (name.toLowerCase() === "content-type") continue;
|
|
4557
|
+
const header = resolveHeaderRef(raw, ctx.componentHeaders, name, operationLabel);
|
|
4558
|
+
if (!header) continue;
|
|
4559
|
+
const contentSchema = header.content ? Object.values(header.content)[0]?.schema : void 0;
|
|
4560
|
+
const schema = header.schema ?? contentSchema;
|
|
4561
|
+
const type = schema ? typeHeaderSchema(schema, name, statusCode, ctx, models) : {
|
|
4562
|
+
kind: "primitive",
|
|
4563
|
+
type: "string"
|
|
4564
|
+
};
|
|
4565
|
+
result.push({
|
|
4566
|
+
name,
|
|
4567
|
+
type,
|
|
4568
|
+
required: header.required ?? false,
|
|
4569
|
+
description: header.description,
|
|
4570
|
+
deprecated: header.deprecated || void 0
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
return result.length > 0 ? result : void 0;
|
|
4574
|
+
}
|
|
4575
|
+
/**
|
|
4576
|
+
* Build the prefix (`NotesByIdPinPost`) that names both inline models — the
|
|
4577
|
+
* request body (`…Request`) and the response (`…Response`) — of an operation
|
|
4578
|
+
* that has no `operationId`, from the only thing that identifies it: its path
|
|
4579
|
+
* and method.
|
|
4580
|
+
*
|
|
4581
|
+
* Every non-parameter segment is PascalCased in order and every parameter
|
|
4582
|
+
* segment becomes `By` + PascalCase(param name); then the optional
|
|
4583
|
+
* disambiguator and the method are appended. The whole is normalized with
|
|
4584
|
+
* toPascalCase so it is a fixed point of that function: responses.ts re-cases
|
|
4585
|
+
* the response name in `deriveModelName`, and without this step `POST /a/b`
|
|
4586
|
+
* named its body `ABPostRequest` but its response `AbPostResponse`.
|
|
4587
|
+
*
|
|
4588
|
+
* GET /widgets → WidgetsGet
|
|
4589
|
+
* POST /notes → NotesPost
|
|
4590
|
+
* POST /notes/{id}/pin → NotesByIdPinPost
|
|
4591
|
+
* GET /users/{user_id}/sessions → UsersByUserIdSessionsGet
|
|
4592
|
+
*
|
|
4593
|
+
* This tells the common path shapes apart (list vs. item, sibling and nested
|
|
4594
|
+
* sub-resources) but is not unique by construction: `/foo-bar` and `/foo/bar`,
|
|
4595
|
+
* `/notes/by-id` and `/notes/{id}`, `/notes` and `/notes/` all flatten to the
|
|
4596
|
+
* same words. `resolvePathModelBaseNames` detects such collisions: the path
|
|
4597
|
+
* spelled most plainly keeps the plain name and every other one gets a
|
|
4598
|
+
* `disambiguator` derived from its raw path.
|
|
4599
|
+
*
|
|
4600
|
+
* A single-segment path keeps the name the old first-segment rule produced.
|
|
4601
|
+
* The result is deliberately not passed through cleanSchemaName (the old
|
|
4602
|
+
* fallback wasn't either): the segments are the resource names as the API
|
|
4603
|
+
* spells them.
|
|
4604
|
+
*/
|
|
4605
|
+
function derivePathModelBaseName(path, method, disambiguator = "") {
|
|
4606
|
+
const parts = path.split("/").filter(Boolean).map((segment) => segment.startsWith("{") ? "By" + toPascalCase(segment.replace(/[{}]/g, "")) : toPascalCase(segment));
|
|
4607
|
+
return toPascalCase([
|
|
4608
|
+
parts.length > 0 ? parts.join("") : "Unknown",
|
|
4609
|
+
disambiguator,
|
|
4610
|
+
method
|
|
4611
|
+
].filter(Boolean).join("_"));
|
|
4612
|
+
}
|
|
4613
|
+
/**
|
|
4614
|
+
* Decide the path-derived model prefix of every operation that has no
|
|
4615
|
+
* `operationId`, keyed by `${method} ${path}`.
|
|
4616
|
+
*
|
|
4617
|
+
* A prefix collides when two distinct raw paths with the same method flatten
|
|
4618
|
+
* to it (see derivePathModelBaseName), or when a reserved name starts with
|
|
4619
|
+
* it. The reserved names are every resolved component model name and, for
|
|
4620
|
+
* every operation with an `operationId`, its model stem
|
|
4621
|
+
* (operationIdModelBase). A prefix is the stem of several emitted names —
|
|
4622
|
+
* `…Request`, `…Response`, a list's `…Item`, nested inline models such as
|
|
4623
|
+
* `…ResponseOwner`, envelope companions such as `…ListMetadata` — so the
|
|
4624
|
+
* check is on the stem, not on an enumeration of suffixes: an unnamed
|
|
4625
|
+
* `GET /audit/events` must not overwrite `components/schemas/AuditEventsGetItem`
|
|
4626
|
+
* any more than an unnamed `POST /audit/events` may overwrite the models of
|
|
4627
|
+
* a `POST /legacy-audit` whose operationId is `auditEventsPost`. Reserved
|
|
4628
|
+
* names never change.
|
|
4629
|
+
*
|
|
4630
|
+
* This is deliberately a superset: a component named `AuditEventsGetter`, or
|
|
4631
|
+
* an operationId `auditEventsGetSummary`, also pushes `GET /audit/events`
|
|
4632
|
+
* onto a hashed name although no emitted name would coincide. Determinism
|
|
4633
|
+
* and never sharing a name wrongly matter more than the rare extra hash, and
|
|
4634
|
+
* the reservation is unconditional — an inline body is never merged into a
|
|
4635
|
+
* same-named component even when the shapes agree — so the rule stays
|
|
4636
|
+
* simple and order-free. Every name derived from a path-based prefix uses it
|
|
4637
|
+
* verbatim (body `oneOf` variants included; see deriveOneOfVariantName), and
|
|
4638
|
+
* as a second line of defence the check also runs on the cleaned spellings
|
|
4639
|
+
* of the prefix (see prefixSpellings), so `GET /notes` yields to a component
|
|
4640
|
+
* `NoteGetItem` although its own item model is `NotesGetItem`. Names that
|
|
4641
|
+
* embed the prefix behind a const value — a body `oneOf` variant pinning
|
|
4642
|
+
* `kind: email` is `EmailTagsPostRequest`, a nested variant
|
|
4643
|
+
* `<Const><Parent><Field>` — are checked too, with each const the operation
|
|
4644
|
+
* can mint prepended to every spelling (see collectOperationConstPrefixes);
|
|
4645
|
+
* the hash then lands inside those names as well (`EmailTags151Dd5PostRequest`).
|
|
4646
|
+
*
|
|
4647
|
+
* These names are types SDK users read and import, so a collision costs the
|
|
4648
|
+
* plain name only where it must. Within a group of paths that flatten to one
|
|
4649
|
+
* prefix, the path spelled most plainly (see pathDeviation: no trailing
|
|
4650
|
+
* slash, `{id}` rather than a literal `by-id`) is canonical and keeps the
|
|
4651
|
+
* plain prefix; every other path gets a six-character disambiguator, the
|
|
4652
|
+
* start of the SHA-1 of its raw path, inserted before the method
|
|
4653
|
+
* (`POST /foo-bar` next to `POST /foo/bar` → `FooBar546F12Post`). When two
|
|
4654
|
+
* paths tie for plainest, or the prefix is reserved, nobody is canonical and
|
|
4655
|
+
* every path in the group is disambiguated. The outcome depends only on the
|
|
4656
|
+
* document's paths, operationIds and component names, never on their order,
|
|
4657
|
+
* and a path that stops colliding returns to its plain name.
|
|
4658
|
+
*/
|
|
4659
|
+
function resolvePathModelBaseNames(paths, componentModels, componentRequestBodies) {
|
|
4660
|
+
const candidates = [];
|
|
4661
|
+
const rawPathsByPlainName = /* @__PURE__ */ new Map();
|
|
4662
|
+
const constPrefixesByPlainName = /* @__PURE__ */ new Map();
|
|
4663
|
+
const reservedNames = /* @__PURE__ */ new Set();
|
|
4664
|
+
for (const model of componentModels ?? []) reservedNames.add(model.name);
|
|
4665
|
+
for (const [path, pathItem] of Object.entries(paths)) for (const method of HTTP_METHODS) {
|
|
4666
|
+
const op = pathItem[method];
|
|
4667
|
+
if (!op) continue;
|
|
4668
|
+
if (op.operationId) {
|
|
4669
|
+
reservedNames.add(operationIdModelBase(op.operationId));
|
|
4670
|
+
continue;
|
|
4671
|
+
}
|
|
4672
|
+
const plain = derivePathModelBaseName(path, method);
|
|
4673
|
+
candidates.push({
|
|
4674
|
+
key: `${method} ${path}`,
|
|
4675
|
+
path,
|
|
4676
|
+
method,
|
|
4677
|
+
plain
|
|
4678
|
+
});
|
|
4679
|
+
const rawPaths = rawPathsByPlainName.get(plain) ?? /* @__PURE__ */ new Set();
|
|
4680
|
+
rawPaths.add(path);
|
|
4681
|
+
rawPathsByPlainName.set(plain, rawPaths);
|
|
4682
|
+
const constPrefixes = constPrefixesByPlainName.get(plain) ?? /* @__PURE__ */ new Set();
|
|
4683
|
+
for (const prefix of collectOperationConstPrefixes(op, componentRequestBodies, operationLabel(op, path, method))) constPrefixes.add(prefix);
|
|
4684
|
+
constPrefixesByPlainName.set(plain, constPrefixes);
|
|
4685
|
+
}
|
|
4686
|
+
const reservedList = [...reservedNames];
|
|
4687
|
+
const canonicalByPlainName = /* @__PURE__ */ new Map();
|
|
4688
|
+
for (const [plain, rawPaths] of rawPathsByPlainName) {
|
|
4689
|
+
const spellings = prefixSpellings(plain);
|
|
4690
|
+
const reserved = ["", ...constPrefixesByPlainName.get(plain) ?? []].flatMap((constPrefix) => spellings.map((spelling) => constPrefix + spelling)).some((stem) => reservedList.some((name) => name.startsWith(stem)));
|
|
4691
|
+
canonicalByPlainName.set(plain, reserved ? void 0 : canonicalPath(rawPaths));
|
|
4692
|
+
}
|
|
4693
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
4694
|
+
for (const { key, path, method, plain } of candidates) {
|
|
4695
|
+
const keepsPlainName = canonicalByPlainName.get(plain) === path;
|
|
4696
|
+
resolved.set(key, keepsPlainName ? plain : derivePathModelBaseName(path, method, pathDisambiguator(path)));
|
|
4697
|
+
}
|
|
4698
|
+
return resolved;
|
|
4699
|
+
}
|
|
4700
|
+
/**
|
|
4701
|
+
* The one path in a group that is spelled most plainly by pathDeviation, or
|
|
4702
|
+
* undefined when two or more tie for it, so the choice never depends on the
|
|
4703
|
+
* order the paths were written in.
|
|
4704
|
+
*/
|
|
4705
|
+
function canonicalPath(rawPaths) {
|
|
4706
|
+
let best;
|
|
4707
|
+
let bestScore;
|
|
4708
|
+
let tied = false;
|
|
4709
|
+
for (const path of rawPaths) {
|
|
4710
|
+
const score = pathDeviation(path);
|
|
4711
|
+
const cmp = bestScore ? compareDeviation(score, bestScore) : -1;
|
|
4712
|
+
if (cmp < 0) {
|
|
4713
|
+
best = path;
|
|
4714
|
+
bestScore = score;
|
|
4715
|
+
tied = false;
|
|
4716
|
+
} else if (cmp === 0) tied = true;
|
|
4717
|
+
}
|
|
4718
|
+
return tied ? void 0 : best;
|
|
4719
|
+
}
|
|
4720
|
+
/**
|
|
4721
|
+
* How far a raw path's spelling is from the plain prefix it flattens to:
|
|
4722
|
+
* `[trailing slash, literal segments with any character outside a-z0-9]`,
|
|
4723
|
+
* compared lexicographically. `/notes` scores [0, 0] and `/notes/` [1, 0];
|
|
4724
|
+
* `/notes/{id}` scores [0, 0] and `/notes/by-id` [0, 1].
|
|
4725
|
+
*/
|
|
4726
|
+
function pathDeviation(rawPath) {
|
|
4727
|
+
return [rawPath.length > 1 && rawPath.endsWith("/") ? 1 : 0, rawPath.split("/").filter(Boolean).filter((segment) => !segment.startsWith("{") && /[^a-z0-9]/.test(segment)).length];
|
|
4728
|
+
}
|
|
4729
|
+
function compareDeviation(a, b) {
|
|
4730
|
+
return a[0] - b[0] || a[1] - b[1];
|
|
4731
|
+
}
|
|
4732
|
+
/** The first six hex characters of the SHA-1 of a raw path. */
|
|
4733
|
+
function pathDisambiguator(rawPath) {
|
|
4734
|
+
return createHash("sha1").update(rawPath).digest("hex").slice(0, 6);
|
|
4735
|
+
}
|
|
4736
|
+
/**
|
|
4737
|
+
* The spellings a path-based prefix could take if a name derived from it were
|
|
4738
|
+
* run through one of the parser's cleaning steps: the prefix itself (every
|
|
4739
|
+
* path-derived name uses it verbatim), cleanSchemaName's form (lead-word
|
|
4740
|
+
* singularization plus backend and ListItem-marker stripping),
|
|
4741
|
+
* resolveSchemaName's form (the same plus the project's schemaNameTransform),
|
|
4742
|
+
* and stripListItemMarkers alone, which nested-model and inline-enum
|
|
4743
|
+
* qualification apply to a parent name. Reserving by every spelling is the
|
|
4744
|
+
* second line of defence behind "use the base verbatim": a cleaning step added
|
|
4745
|
+
* later cannot let a derived name slip past the reservation. Empty spellings
|
|
4746
|
+
* are dropped so a degenerate prefix cannot match every reserved name.
|
|
4747
|
+
*/
|
|
4748
|
+
function prefixSpellings(prefix) {
|
|
4749
|
+
const spellings = [
|
|
4750
|
+
prefix,
|
|
4751
|
+
cleanSchemaName(prefix),
|
|
4752
|
+
resolveSchemaName(prefix),
|
|
4753
|
+
stripListItemMarkers(prefix)
|
|
4754
|
+
];
|
|
4755
|
+
return [...new Set(spellings)].filter((spelling) => spelling.length > 0);
|
|
4756
|
+
}
|
|
4757
|
+
/**
|
|
4758
|
+
* The PascalCase const values an operationId-less operation can prepend to a
|
|
4759
|
+
* name minted from its prefix, read from the schemas its request body and 2xx
|
|
4760
|
+
* responses declare inline. A body `oneOf` variant pinning `kind: email`
|
|
4761
|
+
* yields `EmailTagsPostRequest` (deriveOneOfVariantName); a nested
|
|
4762
|
+
* `oneOf`/`anyOf` object variant yields `<Const><Parent><Field>`
|
|
4763
|
+
* (nameOneOfVariant, nameVariantModel). Every string const or single-value
|
|
4764
|
+
* enum of every inline object variant is collected, top level and nested — a
|
|
4765
|
+
* superset of whichever property each of those picks as the discriminator, so
|
|
4766
|
+
* the reservation can never be narrower than the naming. `$ref` targets are
|
|
4767
|
+
* components with names of their own and are not followed. A request body
|
|
4768
|
+
* `$ref` that cannot be resolved is left for buildOperation to report.
|
|
4769
|
+
*
|
|
4770
|
+
* Names that do not embed the prefix at all — a response `oneOf` variant
|
|
4771
|
+
* named from its bare const (`Email`, see deriveModelName in responses.ts)
|
|
4772
|
+
* or a `title`-mapped variant — are outside what a disambiguator can
|
|
4773
|
+
* separate and are not reserved here.
|
|
4774
|
+
*/
|
|
4775
|
+
function collectOperationConstPrefixes(op, componentRequestBodies, opLabel) {
|
|
4776
|
+
const prefixes = /* @__PURE__ */ new Set();
|
|
4777
|
+
let body;
|
|
4778
|
+
try {
|
|
4779
|
+
body = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
|
|
4780
|
+
} catch {
|
|
4781
|
+
body = void 0;
|
|
4782
|
+
}
|
|
4783
|
+
collectConstPrefixes(selectRequestBodyContent(body).schema, prefixes, 0);
|
|
4784
|
+
for (const [statusCode, response] of Object.entries(op.responses ?? {})) {
|
|
4785
|
+
const code = parseInt(statusCode, 10);
|
|
4786
|
+
if (code < 200 || code >= 300) continue;
|
|
4787
|
+
for (const media of Object.values(response.content ?? {})) collectConstPrefixes(media?.schema, prefixes, 0);
|
|
4788
|
+
}
|
|
4789
|
+
return prefixes;
|
|
4790
|
+
}
|
|
4791
|
+
function collectConstPrefixes(schema, out, depth) {
|
|
4792
|
+
if (!schema || schema.$ref || depth > 16) return;
|
|
4793
|
+
for (const variants of [schema.oneOf, schema.anyOf]) for (const variant of variants ?? []) {
|
|
4794
|
+
if (variant.$ref || !variant.properties) continue;
|
|
4795
|
+
for (const property of Object.values(variant.properties)) {
|
|
4796
|
+
const value = constStringOf(property);
|
|
4797
|
+
const prefix = value === null ? "" : toPascalCase(value);
|
|
4798
|
+
if (prefix) out.add(prefix);
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
const children = [
|
|
4802
|
+
...schema.oneOf ?? [],
|
|
4803
|
+
...schema.anyOf ?? [],
|
|
4804
|
+
...schema.allOf ?? [],
|
|
4805
|
+
schema.items,
|
|
4806
|
+
...Object.values(schema.properties ?? {}),
|
|
4807
|
+
typeof schema.additionalProperties === "object" ? schema.additionalProperties : void 0
|
|
4808
|
+
];
|
|
4809
|
+
for (const child of children) collectConstPrefixes(child, out, depth + 1);
|
|
2754
4810
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
4811
|
+
/** A property's pinned string value: a string `const`, or a single-value string `enum`. */
|
|
4812
|
+
function constStringOf(property) {
|
|
4813
|
+
if (!property) return null;
|
|
4814
|
+
if (typeof property.const === "string") return property.const;
|
|
4815
|
+
if (Array.isArray(property.enum) && property.enum.length === 1 && typeof property.enum[0] === "string") return property.enum[0];
|
|
4816
|
+
return null;
|
|
4817
|
+
}
|
|
4818
|
+
function deriveResponseName(op, pathModelBaseName) {
|
|
4819
|
+
if (op?.operationId) return operationIdModelName(op.operationId, "Response");
|
|
4820
|
+
return pathModelBaseName + "Response";
|
|
2758
4821
|
}
|
|
2759
|
-
function extractResponses(responses, op, path, method, componentSchemas
|
|
4822
|
+
function extractResponses(responses, op, pathModelBaseName, path, method, componentSchemas, headerContext = {
|
|
4823
|
+
operationQualifier: "",
|
|
4824
|
+
registry: /* @__PURE__ */ new Map()
|
|
4825
|
+
}) {
|
|
2760
4826
|
const errors = [];
|
|
2761
4827
|
let response = {
|
|
2762
4828
|
kind: "primitive",
|
|
2763
4829
|
type: "string"
|
|
2764
4830
|
};
|
|
4831
|
+
let responseHeaders;
|
|
2765
4832
|
let inlineModels = [];
|
|
2766
4833
|
let isPaginated = false;
|
|
2767
4834
|
let dataPath;
|
|
2768
4835
|
let itemType;
|
|
4836
|
+
let envelopeFields;
|
|
4837
|
+
let envelopeCompanions;
|
|
2769
4838
|
if (!responses) return {
|
|
2770
4839
|
response,
|
|
2771
4840
|
errors,
|
|
@@ -2773,8 +4842,11 @@ function extractResponses(responses, op, path, method, componentSchemas) {
|
|
|
2773
4842
|
isPaginated
|
|
2774
4843
|
};
|
|
2775
4844
|
const allSuccessResponses = [];
|
|
4845
|
+
const opLabel = operationLabel(op, path ?? "/", method ?? "get");
|
|
4846
|
+
const headerModels = [];
|
|
2776
4847
|
for (const [statusCode, resp] of Object.entries(responses)) {
|
|
2777
4848
|
const code = parseInt(statusCode, 10);
|
|
4849
|
+
const headers = code >= 200 ? extractResponseHeaders(resp.headers, opLabel, code, headerContext, headerModels) : void 0;
|
|
2778
4850
|
if (code >= 200 && code < 300) {
|
|
2779
4851
|
let extractedType = {
|
|
2780
4852
|
kind: "primitive",
|
|
@@ -2782,62 +4854,75 @@ function extractResponses(responses, op, path, method, componentSchemas) {
|
|
|
2782
4854
|
};
|
|
2783
4855
|
const jsonContent = resp.content?.["application/json"];
|
|
2784
4856
|
if (jsonContent?.schema) {
|
|
2785
|
-
const contextName = deriveResponseName(op,
|
|
4857
|
+
const contextName = deriveResponseName(op, pathModelBaseName);
|
|
2786
4858
|
const result = classifyAndExtractResponse(jsonContent.schema, contextName, componentSchemas);
|
|
2787
4859
|
extractedType = result.response;
|
|
2788
4860
|
inlineModels = result.inlineModels;
|
|
2789
4861
|
isPaginated = result.isPaginated;
|
|
2790
4862
|
dataPath = result.dataPath;
|
|
2791
4863
|
itemType = result.itemType;
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
4864
|
+
envelopeFields = result.envelopeFields;
|
|
4865
|
+
envelopeCompanions = result.envelopeCompanions;
|
|
4866
|
+
} else if (resp.content) {
|
|
4867
|
+
if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) extractedType = {
|
|
4868
|
+
kind: "primitive",
|
|
4869
|
+
type: "string",
|
|
4870
|
+
format: "binary"
|
|
4871
|
+
};
|
|
4872
|
+
else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) extractedType = {
|
|
4873
|
+
kind: "primitive",
|
|
4874
|
+
type: "string"
|
|
4875
|
+
};
|
|
4876
|
+
else {
|
|
4877
|
+
const firstKey = Object.keys(resp.content)[0];
|
|
4878
|
+
if (firstKey) {
|
|
4879
|
+
const firstContent = resp.content[firstKey];
|
|
4880
|
+
if (firstContent?.schema) extractedType = schemaToTypeRef(firstContent.schema, deriveResponseName(op, pathModelBaseName));
|
|
4881
|
+
}
|
|
2806
4882
|
}
|
|
2807
4883
|
}
|
|
2808
4884
|
allSuccessResponses.push({
|
|
2809
4885
|
statusCode: code,
|
|
2810
|
-
type: extractedType
|
|
4886
|
+
type: extractedType,
|
|
4887
|
+
...headers && { headers }
|
|
2811
4888
|
});
|
|
2812
4889
|
} else if (code >= 300 && code < 400) allSuccessResponses.push({
|
|
2813
4890
|
statusCode: code,
|
|
2814
4891
|
type: {
|
|
2815
4892
|
kind: "primitive",
|
|
2816
4893
|
type: "unknown"
|
|
2817
|
-
}
|
|
4894
|
+
},
|
|
4895
|
+
...headers && { headers }
|
|
2818
4896
|
});
|
|
2819
4897
|
else if (code >= 400) {
|
|
2820
4898
|
const jsonContent = resp.content?.["application/json"];
|
|
2821
4899
|
const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
|
|
2822
4900
|
errors.push({
|
|
2823
4901
|
statusCode: code,
|
|
2824
|
-
type
|
|
4902
|
+
type,
|
|
4903
|
+
...headers && { headers }
|
|
2825
4904
|
});
|
|
2826
4905
|
}
|
|
2827
4906
|
}
|
|
2828
4907
|
if (allSuccessResponses.length > 0) {
|
|
2829
4908
|
const sorted = [...allSuccessResponses].sort((a, b) => a.statusCode - b.statusCode);
|
|
2830
|
-
|
|
4909
|
+
const primary = sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0];
|
|
4910
|
+
response = primary.type;
|
|
4911
|
+
responseHeaders = primary.headers;
|
|
2831
4912
|
}
|
|
2832
4913
|
const successResponses = allSuccessResponses.length > 1 ? allSuccessResponses : void 0;
|
|
4914
|
+
if (headerModels.length > 0) inlineModels = [...inlineModels, ...headerModels];
|
|
2833
4915
|
return {
|
|
2834
4916
|
response,
|
|
4917
|
+
responseHeaders,
|
|
2835
4918
|
successResponses,
|
|
2836
4919
|
errors,
|
|
2837
4920
|
inlineModels,
|
|
2838
4921
|
isPaginated,
|
|
2839
4922
|
dataPath,
|
|
2840
|
-
itemType
|
|
4923
|
+
itemType,
|
|
4924
|
+
envelopeFields,
|
|
4925
|
+
envelopeCompanions
|
|
2841
4926
|
};
|
|
2842
4927
|
}
|
|
2843
4928
|
//#endregion
|
|
@@ -2846,10 +4931,25 @@ function keepLargestByName(models) {
|
|
|
2846
4931
|
const byName = /* @__PURE__ */ new Map();
|
|
2847
4932
|
for (const model of models) {
|
|
2848
4933
|
const existing = byName.get(model.name);
|
|
4934
|
+
if (existing && !haveSameFields(existing, model)) {
|
|
4935
|
+
const kept = model.fields.length === existing.fields.length ? "keeping the first definition" : "keeping the one with more fields";
|
|
4936
|
+
console.warn(`[oagen] Warning: Inline model "${model.name}" has more than one definition with different fields or field types (${model.fields.length} vs ${existing.fields.length}) — ${kept}`);
|
|
4937
|
+
}
|
|
2849
4938
|
if (!existing || model.fields.length > existing.fields.length) byName.set(model.name, model);
|
|
2850
4939
|
}
|
|
2851
4940
|
return [...byName.values()];
|
|
2852
4941
|
}
|
|
4942
|
+
/**
|
|
4943
|
+
* Same field names with the same types, in any order. Types are compared by
|
|
4944
|
+
* their JSON serialization: the parser builds a given shape's TypeRef with the
|
|
4945
|
+
* same key order every time, so equal shapes serialize identically.
|
|
4946
|
+
*/
|
|
4947
|
+
function haveSameFields(a, b) {
|
|
4948
|
+
const signatures = (m) => new Set(m.fields.map((f) => `${f.name}:${JSON.stringify(f.type)}`));
|
|
4949
|
+
const aSignatures = signatures(a);
|
|
4950
|
+
const bSignatures = signatures(b);
|
|
4951
|
+
return aSignatures.size === bSignatures.size && [...aSignatures].every((s) => bSignatures.has(s));
|
|
4952
|
+
}
|
|
2853
4953
|
function rewriteModelRef(ref, oldName, newName) {
|
|
2854
4954
|
walkTypeRef(ref, { model: (r) => {
|
|
2855
4955
|
if (r.name === oldName) r.name = newName;
|
|
@@ -2860,6 +4960,8 @@ function rewriteModelRefs(models, services, oldName, newName) {
|
|
|
2860
4960
|
for (const service of services) for (const op of service.operations) {
|
|
2861
4961
|
rewriteModelRef(op.response, oldName, newName);
|
|
2862
4962
|
if (op.requestBody) rewriteModelRef(op.requestBody, oldName, newName);
|
|
4963
|
+
for (const field of op.pagination?.envelopeFields ?? []) rewriteModelRef(field.type, oldName, newName);
|
|
4964
|
+
for (const header of collectResponseHeaders(op)) rewriteModelRef(header.type, oldName, newName);
|
|
2863
4965
|
}
|
|
2864
4966
|
}
|
|
2865
4967
|
function mergeInlineResponseModels(schemaModels, inlineModels) {
|
|
@@ -2882,15 +4984,25 @@ function mergeInlineResponseModels(schemaModels, inlineModels) {
|
|
|
2882
4984
|
});
|
|
2883
4985
|
return [...mergedSchemaModels, ...keepLargestByName(deduplicatedInlineModels)];
|
|
2884
4986
|
}
|
|
2885
|
-
|
|
4987
|
+
/**
|
|
4988
|
+
* Fold the field extractor's inline models into the models gathered so far,
|
|
4989
|
+
* keeping the earlier definition on a name clash. `componentNames` tells a
|
|
4990
|
+
* clash with a real component schema apart from one between two inline
|
|
4991
|
+
* models, so the warning can say which happened and name both shapes.
|
|
4992
|
+
*/
|
|
4993
|
+
function mergeFieldInlineModels(existingModels, fieldInlineModels, componentNames) {
|
|
2886
4994
|
const modelNames = new Set(existingModels.map((m) => m.name));
|
|
2887
4995
|
const modelsByName = new Map(existingModels.map((m) => [m.name, m]));
|
|
4996
|
+
const fieldList = (m) => m.fields.map((f) => f.name).join(", ") || "(no fields)";
|
|
2888
4997
|
const deduplicatedFieldModels = fieldInlineModels.filter((model) => {
|
|
2889
4998
|
if (!modelNames.has(model.name)) return true;
|
|
2890
4999
|
const existing = modelsByName.get(model.name);
|
|
2891
5000
|
const existingFieldNames = new Set(existing.fields.map((f) => f.name));
|
|
2892
5001
|
const inlineFieldNames = new Set(model.fields.map((f) => f.name));
|
|
2893
|
-
if (model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name)))
|
|
5002
|
+
if (model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name))) {
|
|
5003
|
+
if (componentNames && !componentNames.has(model.name)) console.warn(`[oagen] Warning: Two inline models are named "${model.name}" (fields: ${fieldList(existing)} | ${fieldList(model)}) — keeping the first, the second is dropped`);
|
|
5004
|
+
else console.warn(`[oagen] Warning: Inline model "${model.name}" has different fields than component schema — using component schema`);
|
|
5005
|
+
}
|
|
2894
5006
|
return false;
|
|
2895
5007
|
});
|
|
2896
5008
|
return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
|
|
@@ -2922,7 +5034,7 @@ function collapseJsonSuffixModels(models, services) {
|
|
|
2922
5034
|
if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
|
|
2923
5035
|
if (!baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name))) continue;
|
|
2924
5036
|
if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
|
|
2925
|
-
if (isModelReferencedByOthers(model.name, normalizedModels, new Set([model.name, baseName]))) continue;
|
|
5037
|
+
if (isModelReferencedByOthers(model.name, normalizedModels, /* @__PURE__ */ new Set([model.name, baseName]))) continue;
|
|
2926
5038
|
mergeCandidates.push({
|
|
2927
5039
|
jsonModel: model,
|
|
2928
5040
|
baseModel
|
|
@@ -2931,6 +5043,10 @@ function collapseJsonSuffixModels(models, services) {
|
|
|
2931
5043
|
const toRemove = /* @__PURE__ */ new Set();
|
|
2932
5044
|
for (const { jsonModel, baseModel } of mergeCandidates) {
|
|
2933
5045
|
baseModel.fields = jsonModel.fields;
|
|
5046
|
+
if (jsonModel.constraints || baseModel.constraints) baseModel.constraints = {
|
|
5047
|
+
...baseModel.constraints,
|
|
5048
|
+
...jsonModel.constraints
|
|
5049
|
+
};
|
|
2934
5050
|
toRemove.add(jsonModel.name);
|
|
2935
5051
|
rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
|
|
2936
5052
|
console.warn(`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`);
|
|
@@ -2966,17 +5082,82 @@ function collectInlineEnumsFromModels(models, enums) {
|
|
|
2966
5082
|
}
|
|
2967
5083
|
/**
|
|
2968
5084
|
* Collect inline enum definitions from operation parameters (query, path, header)
|
|
2969
|
-
*
|
|
5085
|
+
* list-envelope companion fields, and response headers, and promote them to
|
|
5086
|
+
* top-level enums so emitters can generate typed enum files.
|
|
2970
5087
|
*/
|
|
2971
5088
|
function collectInlineEnumsFromOperations(services, enums) {
|
|
2972
5089
|
const enumNames = new Set(enums.map((e) => e.name));
|
|
2973
|
-
for (const service of services) for (const op of service.operations)
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
5090
|
+
for (const service of services) for (const op of service.operations) {
|
|
5091
|
+
for (const param of [
|
|
5092
|
+
...op.pathParams,
|
|
5093
|
+
...op.queryParams,
|
|
5094
|
+
...op.bodyOwnedQueryParams ?? [],
|
|
5095
|
+
...op.headerParams,
|
|
5096
|
+
...op.cookieParams ?? []
|
|
5097
|
+
]) collectInlineEnumFromRef(param.type, enums, enumNames);
|
|
5098
|
+
for (const field of op.pagination?.envelopeFields ?? []) collectInlineEnumFromRef(field.type, enums, enumNames);
|
|
5099
|
+
for (const { headers, qualifier } of responseHeaderGroups(op)) for (const header of headers) collectHeaderEnums(header, qualifier, enums, enumNames);
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
/**
|
|
5103
|
+
* Header lists to walk, each with the status-code qualifier that keeps a
|
|
5104
|
+
* colliding enum name apart. When `successResponses` exists the primary list
|
|
5105
|
+
* is the same array as its entry there, so the entries are walked with their
|
|
5106
|
+
* own codes instead of `responseHeaders`; a lone 2xx carries no status code
|
|
5107
|
+
* in the IR, so its headers get no qualifier.
|
|
5108
|
+
*/
|
|
5109
|
+
function responseHeaderGroups(op) {
|
|
5110
|
+
const groups = [];
|
|
5111
|
+
if (op.successResponses) {
|
|
5112
|
+
for (const entry of op.successResponses) if (entry.headers) groups.push({
|
|
5113
|
+
headers: entry.headers,
|
|
5114
|
+
qualifier: String(entry.statusCode)
|
|
5115
|
+
});
|
|
5116
|
+
} else if (op.responseHeaders) groups.push({ headers: op.responseHeaders });
|
|
5117
|
+
for (const error of op.errors) if (error.headers) groups.push({
|
|
5118
|
+
headers: error.headers,
|
|
5119
|
+
qualifier: String(error.statusCode)
|
|
5120
|
+
});
|
|
5121
|
+
return groups;
|
|
5122
|
+
}
|
|
5123
|
+
/**
|
|
5124
|
+
* Promote a header's inline enums. A header enum is named from the service
|
|
5125
|
+
* and header name alone, so `X-Policy` allowing `fast`/`slow` on a 200 and
|
|
5126
|
+
* `retry`/`stop` on a 429 would share one name and whichever was collected
|
|
5127
|
+
* first would silently win; a request parameter named like the header
|
|
5128
|
+
* collides the same way. When the name is already taken by a different value
|
|
5129
|
+
* set, the header's ref is renamed to a distinct name before it is promoted.
|
|
5130
|
+
* A taken name with the same values is shared, as before.
|
|
5131
|
+
*/
|
|
5132
|
+
function collectHeaderEnums(header, qualifier, enums, enumNames) {
|
|
5133
|
+
walkTypeRef(header.type, { enum: (ref) => {
|
|
5134
|
+
if (!ref.values) return;
|
|
5135
|
+
const existing = enums.find((e) => e.name === ref.name);
|
|
5136
|
+
if (existing && !sameEnumValues(existing, ref.values)) ref.name = distinctEnumName(ref.name, ref.values, qualifier, enums);
|
|
5137
|
+
collectInlineEnumFromRef(ref, enums, enumNames);
|
|
5138
|
+
} });
|
|
5139
|
+
}
|
|
5140
|
+
/** Order-insensitive: a reordering of the same members is the same type. */
|
|
5141
|
+
function sameEnumValues(existing, values) {
|
|
5142
|
+
if (existing.values.length !== values.length) return false;
|
|
5143
|
+
const have = new Set(existing.values.map((v) => v.value));
|
|
5144
|
+
return values.every((v) => have.has(v));
|
|
5145
|
+
}
|
|
5146
|
+
/**
|
|
5147
|
+
* The first name not taken by a different value set: the base qualified by
|
|
5148
|
+
* the status code (`DocumentsXPolicy429`), then numeric suffixes
|
|
5149
|
+
* (`DocumentsXPolicy2`, `DocumentsXPolicy3`, ...). Deterministic for a given
|
|
5150
|
+
* spec, since it depends only on declaration order.
|
|
5151
|
+
*/
|
|
5152
|
+
function distinctEnumName(base, values, qualifier, enums) {
|
|
5153
|
+
const isUsable = (name) => {
|
|
5154
|
+
const taken = enums.find((e) => e.name === name);
|
|
5155
|
+
return !taken || sameEnumValues(taken, values);
|
|
5156
|
+
};
|
|
5157
|
+
if (qualifier && isUsable(`${base}${qualifier}`)) return `${base}${qualifier}`;
|
|
5158
|
+
let suffix = 2;
|
|
5159
|
+
while (!isUsable(`${base}${suffix}`)) suffix++;
|
|
5160
|
+
return `${base}${suffix}`;
|
|
2980
5161
|
}
|
|
2981
5162
|
//#endregion
|
|
2982
5163
|
//#region src/parser/parameter-group-names.ts
|
|
@@ -3255,6 +5436,8 @@ function validateModelRefs(spec) {
|
|
|
3255
5436
|
]) walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
|
|
3256
5437
|
if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
|
|
3257
5438
|
walkRef(op.response, `${service.name}.${op.name}.response`);
|
|
5439
|
+
for (const field of op.pagination?.envelopeFields ?? []) walkRef(field.type, `${service.name}.${op.name}.pagination.${field.name}`);
|
|
5440
|
+
for (const header of collectResponseHeaders(op)) walkRef(header.type, `${service.name}.${op.name}.headers.${header.name}`);
|
|
3258
5441
|
}
|
|
3259
5442
|
}
|
|
3260
5443
|
//#endregion
|
|
@@ -3265,11 +5448,12 @@ async function parseSpec(specPath, options) {
|
|
|
3265
5448
|
const version = spec.openapi ?? "";
|
|
3266
5449
|
if (!version.startsWith("3.")) throw new SpecParseError(`Unsupported OpenAPI version: ${version}. oagen requires OpenAPI 3.x`, `Update the spec to OpenAPI 3.0 or 3.1. If you are using Swagger 2.x, convert it first with \`npx swagger2openapi ${specPath}\`.`);
|
|
3267
5450
|
const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
|
|
3268
|
-
const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters, spec.components?.requestBodies);
|
|
5451
|
+
const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters, spec.components?.requestBodies, models, spec.tags, spec.components?.examples, spec.components?.headers);
|
|
3269
5452
|
const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
|
|
3270
5453
|
const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
|
|
5454
|
+
const componentNames = new Set(Object.keys(spec.components?.schemas ?? {}).map((schemaName) => resolveSchemaName(schemaName)));
|
|
3271
5455
|
clearSchemaNameTransform();
|
|
3272
|
-
const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels), services);
|
|
5456
|
+
const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels, componentNames), services);
|
|
3273
5457
|
applyFieldHints(finalModels, options?.fieldHints);
|
|
3274
5458
|
collectInlineEnumsFromModels(finalModels, enums);
|
|
3275
5459
|
collectInlineEnumsFromOperations(services, enums);
|
|
@@ -3288,6 +5472,7 @@ async function parseSpec(specPath, options) {
|
|
|
3288
5472
|
models: finalModels,
|
|
3289
5473
|
enums,
|
|
3290
5474
|
auth,
|
|
5475
|
+
security: extractSecurityRequirements(spec.security, "the document"),
|
|
3291
5476
|
sdk: defaultSdkBehavior()
|
|
3292
5477
|
};
|
|
3293
5478
|
assignParameterGroupWrapperNames(result);
|
|
@@ -3310,20 +5495,47 @@ function applyFieldHints(models, fieldHints) {
|
|
|
3310
5495
|
}
|
|
3311
5496
|
}
|
|
3312
5497
|
}
|
|
3313
|
-
/**
|
|
5498
|
+
/**
|
|
5499
|
+
* Extract authentication schemes from OpenAPI securitySchemes.
|
|
5500
|
+
*
|
|
5501
|
+
* Document order is preserved. Each entry carries its `securitySchemes` key as
|
|
5502
|
+
* `schemeName` so emitters can join the requirements under `ApiSpec.security`
|
|
5503
|
+
* and `Operation.security` back to it.
|
|
5504
|
+
* Schemes the IR cannot represent are omitted with a warning rather than silently.
|
|
5505
|
+
*/
|
|
3314
5506
|
function extractAuthSchemes(securitySchemes) {
|
|
3315
5507
|
if (!securitySchemes) return void 0;
|
|
3316
5508
|
const schemes = [];
|
|
3317
|
-
for (const [, scheme] of Object.entries(securitySchemes))
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
5509
|
+
for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
|
|
5510
|
+
const common = scheme.description !== void 0 ? {
|
|
5511
|
+
schemeName,
|
|
5512
|
+
description: scheme.description
|
|
5513
|
+
} : { schemeName };
|
|
5514
|
+
const httpScheme = scheme.type === "http" ? scheme.scheme?.toLowerCase() : void 0;
|
|
5515
|
+
if (httpScheme === "bearer") schemes.push({
|
|
5516
|
+
kind: "bearer",
|
|
5517
|
+
...common
|
|
5518
|
+
});
|
|
5519
|
+
else if (httpScheme === "basic") schemes.push({
|
|
5520
|
+
kind: "basic",
|
|
5521
|
+
...common
|
|
5522
|
+
});
|
|
5523
|
+
else if (scheme.type === "apiKey" && scheme.in && scheme.name) schemes.push({
|
|
5524
|
+
kind: "apiKey",
|
|
5525
|
+
...common,
|
|
5526
|
+
in: scheme.in,
|
|
5527
|
+
name: scheme.name
|
|
5528
|
+
});
|
|
5529
|
+
else if (scheme.type === "oauth2" && scheme.flows) schemes.push({
|
|
5530
|
+
kind: "oauth2",
|
|
5531
|
+
...common,
|
|
5532
|
+
flows: scheme.flows
|
|
5533
|
+
});
|
|
5534
|
+
else {
|
|
5535
|
+
const detail = scheme.type === "http" ? `type: http, scheme: ${scheme.scheme}` : `type: ${scheme.type}`;
|
|
5536
|
+
console.warn(`[oagen] Warning: Unsupported security scheme "${schemeName}" (${detail}) omitted from spec.auth`);
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
3327
5539
|
return schemes.length > 0 ? schemes : void 0;
|
|
3328
5540
|
}
|
|
3329
5541
|
//#endregion
|
|
@@ -3424,7 +5636,7 @@ const DOTNET_URL_FINGERPRINT_CONFIG = {
|
|
|
3424
5636
|
contentNodeTypes: ["string_content", "string_literal_content"],
|
|
3425
5637
|
interpolationNodeTypes: ["interpolation"]
|
|
3426
5638
|
};
|
|
3427
|
-
const DECLARATION_TYPES = new Set([
|
|
5639
|
+
const DECLARATION_TYPES = /* @__PURE__ */ new Set([
|
|
3428
5640
|
"class_declaration",
|
|
3429
5641
|
"struct_declaration",
|
|
3430
5642
|
"interface_declaration",
|
|
@@ -3444,11 +5656,12 @@ function collectPrecedingXmlDocComments(children, index, source) {
|
|
|
3444
5656
|
let firstIdx = -1;
|
|
3445
5657
|
for (let k = index - 1; k >= 0; k--) {
|
|
3446
5658
|
const prev = children[k];
|
|
3447
|
-
if (prev.type === "comment")
|
|
3448
|
-
if (
|
|
3449
|
-
|
|
5659
|
+
if (prev.type === "comment") {
|
|
5660
|
+
if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
|
|
5661
|
+
if (lastIdx === -1) lastIdx = k;
|
|
5662
|
+
firstIdx = k;
|
|
5663
|
+
} else break;
|
|
3450
5664
|
} else break;
|
|
3451
|
-
else break;
|
|
3452
5665
|
}
|
|
3453
5666
|
if (firstIdx === -1) return null;
|
|
3454
5667
|
const first = children[firstIdx];
|
|
@@ -3763,11 +5976,12 @@ function collectPrecedingGoComments(children, index, source) {
|
|
|
3763
5976
|
let firstIdx = -1;
|
|
3764
5977
|
for (let k = index - 1; k >= 0; k--) {
|
|
3765
5978
|
const prev = children[k];
|
|
3766
|
-
if (prev.type === "comment")
|
|
3767
|
-
if (
|
|
3768
|
-
|
|
5979
|
+
if (prev.type === "comment") {
|
|
5980
|
+
if (source.slice(prev.startIndex, prev.endIndex).startsWith("//")) {
|
|
5981
|
+
if (lastIdx === -1) lastIdx = k;
|
|
5982
|
+
firstIdx = k;
|
|
5983
|
+
} else break;
|
|
3769
5984
|
} else break;
|
|
3770
|
-
else break;
|
|
3771
5985
|
}
|
|
3772
5986
|
if (firstIdx === -1) return null;
|
|
3773
5987
|
const first = children[firstIdx];
|
|
@@ -4131,9 +6345,9 @@ function findPrecedingDocstring(children, index, source) {
|
|
|
4131
6345
|
}
|
|
4132
6346
|
return null;
|
|
4133
6347
|
}
|
|
4134
|
-
const CLASS_MEMBER_TYPES = new Set(["method_definition", "public_field_definition"]);
|
|
4135
|
-
const INTERFACE_MEMBER_TYPES = new Set(["property_signature", "method_signature"]);
|
|
4136
|
-
const ENUM_MEMBER_TYPES = new Set(["enum_assignment", "property_identifier"]);
|
|
6348
|
+
const CLASS_MEMBER_TYPES = /* @__PURE__ */ new Set(["method_definition", "public_field_definition"]);
|
|
6349
|
+
const INTERFACE_MEMBER_TYPES = /* @__PURE__ */ new Set(["property_signature", "method_signature"]);
|
|
6350
|
+
const ENUM_MEMBER_TYPES = /* @__PURE__ */ new Set(["enum_assignment", "property_identifier"]);
|
|
4137
6351
|
const NODE_URL_FINGERPRINT_CONFIG = {
|
|
4138
6352
|
stringNodeTypes: ["string", "template_string"],
|
|
4139
6353
|
contentNodeTypes: ["string_fragment"],
|
|
@@ -4545,11 +6759,12 @@ function collectPrecedingRubyComments(children, index, source) {
|
|
|
4545
6759
|
let firstIdx = -1;
|
|
4546
6760
|
for (let k = index - 1; k >= 0; k--) {
|
|
4547
6761
|
const prev = children[k];
|
|
4548
|
-
if ((prev.grammarType ?? prev.type) === "comment")
|
|
4549
|
-
if (
|
|
4550
|
-
|
|
6762
|
+
if ((prev.grammarType ?? prev.type) === "comment") {
|
|
6763
|
+
if (source.slice(prev.startIndex, prev.endIndex).startsWith("#")) {
|
|
6764
|
+
if (lastIdx === -1) lastIdx = k;
|
|
6765
|
+
firstIdx = k;
|
|
6766
|
+
} else break;
|
|
4551
6767
|
} else break;
|
|
4552
|
-
else break;
|
|
4553
6768
|
}
|
|
4554
6769
|
if (firstIdx === -1) return null;
|
|
4555
6770
|
const first = children[firstIdx];
|
|
@@ -4677,11 +6892,12 @@ function collectPrecedingRustDocComments(children, index, source) {
|
|
|
4677
6892
|
let firstIdx = -1;
|
|
4678
6893
|
for (let k = index - 1; k >= 0; k--) {
|
|
4679
6894
|
const prev = children[k];
|
|
4680
|
-
if (prev.type === "line_comment")
|
|
4681
|
-
if (
|
|
4682
|
-
|
|
6895
|
+
if (prev.type === "line_comment") {
|
|
6896
|
+
if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
|
|
6897
|
+
if (lastIdx === -1) lastIdx = k;
|
|
6898
|
+
firstIdx = k;
|
|
6899
|
+
} else break;
|
|
4683
6900
|
} else break;
|
|
4684
|
-
else break;
|
|
4685
6901
|
}
|
|
4686
6902
|
if (firstIdx === -1) return null;
|
|
4687
6903
|
const first = children[firstIdx];
|
|
@@ -4694,7 +6910,7 @@ function collectPrecedingRustDocComments(children, index, source) {
|
|
|
4694
6910
|
}
|
|
4695
6911
|
//#endregion
|
|
4696
6912
|
//#region src/engine/merge-adapters/index.ts
|
|
4697
|
-
const adapters = new Map([
|
|
6913
|
+
const adapters = /* @__PURE__ */ new Map([
|
|
4698
6914
|
["dotnet", dotnetMergeAdapter],
|
|
4699
6915
|
["elixir", elixirMergeAdapter],
|
|
4700
6916
|
["go", goMergeAdapter],
|
|
@@ -5275,7 +7491,7 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
5275
7491
|
if (names.length === 0) continue;
|
|
5276
7492
|
if (!names.some((n) => referencesIdentifier(prunedText, n))) continue;
|
|
5277
7493
|
const line = imp.text.trim();
|
|
5278
|
-
const withoutImport = removeLines(codeOnly, new Set([line]));
|
|
7494
|
+
const withoutImport = removeLines(codeOnly, /* @__PURE__ */ new Set([line]));
|
|
5279
7495
|
if (names.some((n) => referencesIdentifier(withoutImport, n))) continue;
|
|
5280
7496
|
orphaned.add(line);
|
|
5281
7497
|
}
|
|
@@ -5295,26 +7511,28 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
5295
7511
|
if (!existInfo) continue;
|
|
5296
7512
|
if (docIgnoredSymbols.has(symbolName)) continue;
|
|
5297
7513
|
const genDoc = genInfo.docstring && genInfo.docstring.text.trim() !== headerLine ? genInfo.docstring : null;
|
|
5298
|
-
if (genDoc)
|
|
5299
|
-
if (
|
|
5300
|
-
|
|
5301
|
-
|
|
7514
|
+
if (genDoc) {
|
|
7515
|
+
if (existInfo.docstring) {
|
|
7516
|
+
if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
|
|
7517
|
+
const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
|
|
7518
|
+
const range = resolveDocRange(result, existInfo.docstring.text, existInfo.declStartIndex, existInfo.docstring.startIndex, existInfo.docstring.endIndex);
|
|
7519
|
+
edits.push({
|
|
7520
|
+
start: range.start,
|
|
7521
|
+
end: range.end,
|
|
7522
|
+
newText
|
|
7523
|
+
});
|
|
7524
|
+
docstringUpdates++;
|
|
7525
|
+
}
|
|
7526
|
+
} else {
|
|
7527
|
+
const lineStart = declarationLineStart(result, existInfo.declStartIndex);
|
|
7528
|
+
const indent = " ".repeat(existInfo.declColumn);
|
|
5302
7529
|
edits.push({
|
|
5303
|
-
start:
|
|
5304
|
-
end:
|
|
5305
|
-
newText
|
|
7530
|
+
start: lineStart,
|
|
7531
|
+
end: lineStart,
|
|
7532
|
+
newText: indent + genDoc.text + "\n"
|
|
5306
7533
|
});
|
|
5307
7534
|
docstringUpdates++;
|
|
5308
7535
|
}
|
|
5309
|
-
} else {
|
|
5310
|
-
const lineStart = declarationLineStart(result, existInfo.declStartIndex);
|
|
5311
|
-
const indent = " ".repeat(existInfo.declColumn);
|
|
5312
|
-
edits.push({
|
|
5313
|
-
start: lineStart,
|
|
5314
|
-
end: lineStart,
|
|
5315
|
-
newText: indent + genDoc.text + "\n"
|
|
5316
|
-
});
|
|
5317
|
-
docstringUpdates++;
|
|
5318
7536
|
}
|
|
5319
7537
|
const matchedExistMembers = /* @__PURE__ */ new Set();
|
|
5320
7538
|
for (const [memberName, genMember] of genInfo.members) {
|
|
@@ -5654,8 +7872,10 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
5654
7872
|
if (outcome.edited) regionDeclarationEdits++;
|
|
5655
7873
|
if (!alreadyPlaced && !outcome.placed) remaining.push(entry);
|
|
5656
7874
|
}
|
|
5657
|
-
if (!alreadyPlaced)
|
|
5658
|
-
|
|
7875
|
+
if (!alreadyPlaced) {
|
|
7876
|
+
if (remaining.length === 0) blocksByClass.delete("");
|
|
7877
|
+
else blocksByClass.set("", remaining);
|
|
7878
|
+
}
|
|
5659
7879
|
}
|
|
5660
7880
|
}
|
|
5661
7881
|
let effectiveClassEndLines = classEndLines;
|
|
@@ -5674,19 +7894,21 @@ async function overwriteWithPreservedRegions(existingContent, generatedContent,
|
|
|
5674
7894
|
break;
|
|
5675
7895
|
}
|
|
5676
7896
|
}
|
|
5677
|
-
if (insertLine === void 0)
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
7897
|
+
if (insertLine === void 0) {
|
|
7898
|
+
if (info) insertLine = info.bodyEndLine;
|
|
7899
|
+
else {
|
|
7900
|
+
const scopeEndLine = entry.containingScope ? findDescribeScopeEndLine(result, entry.containingScope) : void 0;
|
|
7901
|
+
const blockHasImportsFallback = entry.lines.some((l) => /^\s*(import|from)\s/.test(l));
|
|
7902
|
+
if (scopeEndLine !== void 0) insertLine = scopeEndLine;
|
|
7903
|
+
else if (blockHasImportsFallback || hasTopLevelDeclaration(entry.lines)) {
|
|
7904
|
+
let lastImportLine = 0;
|
|
7905
|
+
for (let li = 0; li < result.length; li++) {
|
|
7906
|
+
const trimmed = result[li].trimStart();
|
|
7907
|
+
if (/^(import|from)\s/.test(trimmed)) lastImportLine = li + 1;
|
|
7908
|
+
}
|
|
7909
|
+
insertLine = lastImportLine;
|
|
7910
|
+
} else insertLine = result.length;
|
|
7911
|
+
}
|
|
5690
7912
|
}
|
|
5691
7913
|
if (entry.lines.some((l) => /^\s*import\s/.test(l))) while (insertLine >= 2 && result[insertLine - 1]?.trim() === "" && /^\s*(import|using|from)\s/.test(result[insertLine - 2] ?? "")) insertLine--;
|
|
5692
7914
|
const prevLine = insertLine > 0 ? result[insertLine - 1]?.trimStart() ?? "" : "";
|
|
@@ -6296,7 +8518,7 @@ function toSnake(s) {
|
|
|
6296
8518
|
* When a path ends with one of these, we use it as the verb instead of
|
|
6297
8519
|
* deriving a CRUD verb from the HTTP method.
|
|
6298
8520
|
*/
|
|
6299
|
-
const ACTION_VERBS = new Set([
|
|
8521
|
+
const ACTION_VERBS = /* @__PURE__ */ new Set([
|
|
6300
8522
|
"accept",
|
|
6301
8523
|
"activate",
|
|
6302
8524
|
"authorize",
|
|
@@ -6611,7 +8833,7 @@ function aggregateSurfaceServices(spec, ctx) {
|
|
|
6611
8833
|
/**
|
|
6612
8834
|
* Collect model and enum names transitively referenced by service operations.
|
|
6613
8835
|
* Walks operation parameters, request bodies, responses, and pagination item
|
|
6614
|
-
* types, then chases model field references until the set stabilizes.
|
|
8836
|
+
* and envelope types, then chases model field references until the set stabilizes.
|
|
6615
8837
|
*/
|
|
6616
8838
|
function collectReferencedNames(services, models, opts = {}) {
|
|
6617
8839
|
const preserveAllDiscriminated = opts.preserveAllDiscriminated !== false;
|
|
@@ -6633,7 +8855,11 @@ function collectReferencedNames(services, models, opts = {}) {
|
|
|
6633
8855
|
]) collectFromTypeRef(p.type);
|
|
6634
8856
|
if (op.requestBody) collectFromTypeRef(op.requestBody);
|
|
6635
8857
|
collectFromTypeRef(op.response);
|
|
6636
|
-
if (op.pagination)
|
|
8858
|
+
if (op.pagination) {
|
|
8859
|
+
collectFromTypeRef(op.pagination.itemType);
|
|
8860
|
+
for (const field of op.pagination.envelopeFields ?? []) collectFromTypeRef(field.type);
|
|
8861
|
+
}
|
|
8862
|
+
for (const header of collectResponseHeaders(op)) collectFromTypeRef(header.type);
|
|
6637
8863
|
for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
|
|
6638
8864
|
if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
|
|
6639
8865
|
}
|
|
@@ -6795,7 +9021,7 @@ async function treeShakeFiles(files, language) {
|
|
|
6795
9021
|
}
|
|
6796
9022
|
deps.set(file.path, fileDeps);
|
|
6797
9023
|
}
|
|
6798
|
-
const codeExtensions = new Set([
|
|
9024
|
+
const codeExtensions = /* @__PURE__ */ new Set([
|
|
6799
9025
|
".ts",
|
|
6800
9026
|
".js",
|
|
6801
9027
|
".tsx",
|
|
@@ -6877,11 +9103,10 @@ async function resolveFileToDirectoryConflicts(files, targetDir) {
|
|
|
6877
9103
|
content: moduleContent,
|
|
6878
9104
|
overwriteExisting: true
|
|
6879
9105
|
});
|
|
6880
|
-
const compatImport = "from ._compat import * # noqa: F401,F403 # pyright: ignore[reportUnusedImport]";
|
|
6881
9106
|
const existingContent = file.content.endsWith("\n") ? file.content : file.content + "\n";
|
|
6882
9107
|
result[i] = {
|
|
6883
9108
|
...file,
|
|
6884
|
-
content: existingContent +
|
|
9109
|
+
content: existingContent + "from ._compat import * # noqa: F401,F403 # pyright: ignore[reportUnusedImport]\n"
|
|
6885
9110
|
};
|
|
6886
9111
|
removals.push(fullModulePath);
|
|
6887
9112
|
}
|
|
@@ -6932,7 +9157,7 @@ async function formatTargetFiles(emitter, targetDir, filePaths) {
|
|
|
6932
9157
|
try {
|
|
6933
9158
|
await execFileAsync(formatCmd.cmd, [...formatCmd.args, ...batch], {
|
|
6934
9159
|
cwd: targetDir,
|
|
6935
|
-
maxBuffer:
|
|
9160
|
+
maxBuffer: 10485760,
|
|
6936
9161
|
timeout: 12e4
|
|
6937
9162
|
});
|
|
6938
9163
|
} catch (err) {
|
|
@@ -7028,7 +9253,7 @@ function mergeScopedManifestRecords(prev, scopedFiles, scopedOperations) {
|
|
|
7028
9253
|
files: scopedFileList,
|
|
7029
9254
|
operations: scopedOperations
|
|
7030
9255
|
};
|
|
7031
|
-
const files = [
|
|
9256
|
+
const files = [.../* @__PURE__ */ new Set([...prev.files, ...scopedFileList])].sort();
|
|
7032
9257
|
const priorOps = prev.operations;
|
|
7033
9258
|
return {
|
|
7034
9259
|
files,
|
|
@@ -7259,7 +9484,7 @@ function getEmitter(language) {
|
|
|
7259
9484
|
//#endregion
|
|
7260
9485
|
//#region src/compat/language-hints.ts
|
|
7261
9486
|
/** Untyped map patterns — hoisted to avoid allocation on every isTypeEquivalent call. */
|
|
7262
|
-
const UNTYPED_MAP_PATTERNS = new Set([
|
|
9487
|
+
const UNTYPED_MAP_PATTERNS = /* @__PURE__ */ new Set([
|
|
7263
9488
|
"Record<string, unknown>",
|
|
7264
9489
|
"Record<string, any>",
|
|
7265
9490
|
"{ [key: string]: any; }",
|
|
@@ -7724,7 +9949,7 @@ function buildModelNameMap(surface, spec, lookup, hints, options) {
|
|
|
7724
9949
|
}
|
|
7725
9950
|
}
|
|
7726
9951
|
/** Categories that can be patched by the overlay retry loop. */
|
|
7727
|
-
const PATCHABLE_CATEGORIES = new Set(["symbol_removed", "symbol_renamed"]);
|
|
9952
|
+
const PATCHABLE_CATEGORIES = /* @__PURE__ */ new Set(["symbol_removed", "symbol_renamed"]);
|
|
7728
9953
|
/** Check if a classified change is patchable by the overlay loop. */
|
|
7729
9954
|
function isPatchableChange(change) {
|
|
7730
9955
|
return PATCHABLE_CATEGORIES.has(change.category);
|
|
@@ -7883,6 +10108,7 @@ function expandDocUrls(spec, docUrl) {
|
|
|
7883
10108
|
description: expand(service.description),
|
|
7884
10109
|
operations: service.operations.map((op) => ({
|
|
7885
10110
|
...op,
|
|
10111
|
+
...op.summary ? { summary: expand(op.summary) } : {},
|
|
7886
10112
|
description: expand(op.description),
|
|
7887
10113
|
pathParams: op.pathParams.map((p) => ({
|
|
7888
10114
|
...p,
|
|
@@ -7994,6 +10220,8 @@ function buildDirectionIndex(...specs) {
|
|
|
7994
10220
|
...op.cookieParams ?? []
|
|
7995
10221
|
]) requestSeeds.push(param.type);
|
|
7996
10222
|
responseSeeds.push(op.response);
|
|
10223
|
+
for (const field of op.pagination?.envelopeFields ?? []) responseSeeds.push(field.type);
|
|
10224
|
+
for (const header of collectResponseHeaders(op)) responseSeeds.push(header.type);
|
|
7997
10225
|
for (const success of op.successResponses ?? []) responseSeeds.push(success.type);
|
|
7998
10226
|
for (const error of op.errors) if (error.type) responseSeeds.push(error.type);
|
|
7999
10227
|
}
|
|
@@ -8041,12 +10269,7 @@ function reachableModels(seeds, models) {
|
|
|
8041
10269
|
case "map":
|
|
8042
10270
|
queue.push(ref.valueType);
|
|
8043
10271
|
break;
|
|
8044
|
-
case "union":
|
|
8045
|
-
queue.push(...ref.variants);
|
|
8046
|
-
break;
|
|
8047
|
-
case "primitive":
|
|
8048
|
-
case "enum":
|
|
8049
|
-
case "literal": break;
|
|
10272
|
+
case "union": queue.push(...ref.variants);
|
|
8050
10273
|
}
|
|
8051
10274
|
}
|
|
8052
10275
|
return found;
|
|
@@ -8175,16 +10398,18 @@ function diffFields(oldModel, newModel, direction = "unknown") {
|
|
|
8175
10398
|
for (const [name, newField] of newByName) {
|
|
8176
10399
|
const oldField = oldByName.get(name);
|
|
8177
10400
|
if (!oldField) continue;
|
|
8178
|
-
if (!typeRefsEqual(oldField.type, newField.type))
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
10401
|
+
if (!typeRefsEqual(oldField.type, newField.type)) {
|
|
10402
|
+
if (primitivesDifferOnlyByFormat(oldField.type, newField.type)) changes.push({
|
|
10403
|
+
...classifyFieldChange("field-format-changed", name),
|
|
10404
|
+
details: formatChangeDetails(oldField.type, newField.type)
|
|
10405
|
+
});
|
|
10406
|
+
else changes.push({
|
|
10407
|
+
kind: "field-type-changed",
|
|
10408
|
+
fieldName: name,
|
|
10409
|
+
classification: "breaking",
|
|
10410
|
+
details: `type changed`
|
|
10411
|
+
});
|
|
10412
|
+
}
|
|
8188
10413
|
if (oldField.required !== newField.required) changes.push(classifyFieldChange("field-required-changed", name, newField.required));
|
|
8189
10414
|
if (!!oldField.readOnly !== !!newField.readOnly || !!oldField.writeOnly !== !!newField.writeOnly) changes.push(classifyFieldChange("field-access-changed", name));
|
|
8190
10415
|
}
|
|
@@ -8329,11 +10554,13 @@ function diffParams(oldOp, newOp) {
|
|
|
8329
10554
|
for (const [name, newEntry] of newByName) {
|
|
8330
10555
|
const oldEntry = oldByName.get(name);
|
|
8331
10556
|
if (!oldEntry) continue;
|
|
8332
|
-
if (!typeRefsEqual(oldEntry.p.type, newEntry.p.type))
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
10557
|
+
if (!typeRefsEqual(oldEntry.p.type, newEntry.p.type)) {
|
|
10558
|
+
if (primitivesDifferOnlyByFormat(oldEntry.p.type, newEntry.p.type)) {
|
|
10559
|
+
const change = classifyParamChange("param-format-changed", name);
|
|
10560
|
+
change.details = formatChangeDetails(oldEntry.p.type, newEntry.p.type);
|
|
10561
|
+
changes.push(change);
|
|
10562
|
+
} else changes.push(classifyParamChange("param-type-changed", name));
|
|
10563
|
+
}
|
|
8337
10564
|
if (oldEntry.p.required !== newEntry.p.required) changes.push(classifyParamChange("param-required-changed", name, newEntry.p.required));
|
|
8338
10565
|
const oldDefault = serializeDefault(oldEntry.p.default);
|
|
8339
10566
|
const newDefault = serializeDefault(newEntry.p.default);
|
|
@@ -8623,6 +10850,7 @@ function mergePolicy(defaults, overrides) {
|
|
|
8623
10850
|
...overrides
|
|
8624
10851
|
};
|
|
8625
10852
|
}
|
|
10853
|
+
Object.keys(LANGUAGE_DEFAULTS);
|
|
8626
10854
|
//#endregion
|
|
8627
10855
|
//#region src/compat/schema.ts
|
|
8628
10856
|
/** Validate the basic structure of a parsed compat snapshot. */
|
|
@@ -8960,7 +11188,7 @@ function enrichWithSpecContext(snapshot, spec) {
|
|
|
8960
11188
|
}
|
|
8961
11189
|
//#endregion
|
|
8962
11190
|
//#region src/compat/config.ts
|
|
8963
|
-
const BREAKING_CATEGORIES = new Set([
|
|
11191
|
+
const BREAKING_CATEGORIES = /* @__PURE__ */ new Set([
|
|
8964
11192
|
"symbol_removed",
|
|
8965
11193
|
"symbol_renamed",
|
|
8966
11194
|
"parameter_removed",
|
|
@@ -8978,7 +11206,7 @@ const BREAKING_CATEGORIES = new Set([
|
|
|
8978
11206
|
"enum_member_value_changed",
|
|
8979
11207
|
"staticness_changed"
|
|
8980
11208
|
]);
|
|
8981
|
-
const SOFT_RISK_CATEGORIES = new Set([
|
|
11209
|
+
const SOFT_RISK_CATEGORIES = /* @__PURE__ */ new Set([
|
|
8982
11210
|
"parameter_added_non_terminal_optional",
|
|
8983
11211
|
"constructor_reordered_named_friendly",
|
|
8984
11212
|
"default_value_changed",
|
|
@@ -9261,37 +11489,38 @@ function classifyParameterChanges(baseline, candidate, policy, specRef, language
|
|
|
9261
11489
|
specRef
|
|
9262
11490
|
}));
|
|
9263
11491
|
if (baseParam.position !== candParam.position) {
|
|
9264
|
-
if (isConstructor)
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
11492
|
+
if (isConstructor) {
|
|
11493
|
+
if (policy.constructorOrderMatters) changes.push(makeChange({
|
|
11494
|
+
category: "constructor_position_changed_order_sensitive",
|
|
11495
|
+
symbol: baseline.fqName,
|
|
11496
|
+
old: {
|
|
11497
|
+
parameter: baseParam.publicName,
|
|
11498
|
+
position: String(baseParam.position)
|
|
11499
|
+
},
|
|
11500
|
+
new: {
|
|
11501
|
+
parameter: candParam.publicName,
|
|
11502
|
+
position: String(candParam.position)
|
|
11503
|
+
},
|
|
11504
|
+
message: `Parameter "${baseParam.publicName}" moved from position ${baseParam.position} to ${candParam.position} on "${baseline.displayName}"`,
|
|
11505
|
+
policy,
|
|
11506
|
+
specRef
|
|
11507
|
+
}));
|
|
11508
|
+
else changes.push(makeChange({
|
|
11509
|
+
category: "constructor_reordered_named_friendly",
|
|
11510
|
+
symbol: baseline.fqName,
|
|
11511
|
+
old: {
|
|
11512
|
+
parameter: baseParam.publicName,
|
|
11513
|
+
position: String(baseParam.position)
|
|
11514
|
+
},
|
|
11515
|
+
new: {
|
|
11516
|
+
parameter: candParam.publicName,
|
|
11517
|
+
position: String(candParam.position)
|
|
11518
|
+
},
|
|
11519
|
+
message: `Parameter "${baseParam.publicName}" reordered on "${baseline.displayName}" (named-friendly language)`,
|
|
11520
|
+
policy,
|
|
11521
|
+
specRef
|
|
11522
|
+
}));
|
|
11523
|
+
} else if (baseParam.passing === "positional") changes.push(makeChange({
|
|
9295
11524
|
category: "parameter_position_changed_order_sensitive",
|
|
9296
11525
|
symbol: baseline.fqName,
|
|
9297
11526
|
old: {
|
|
@@ -9434,6 +11663,8 @@ function specDerivedNames(spec, hints) {
|
|
|
9434
11663
|
for (const op of service.operations) {
|
|
9435
11664
|
collectTypeRefNames(op.response, names, hints, topLevelEnumNames);
|
|
9436
11665
|
if (op.requestBody) collectTypeRefNames(op.requestBody, names, hints, topLevelEnumNames);
|
|
11666
|
+
for (const field of op.pagination?.envelopeFields ?? []) collectTypeRefNames(field.type, names, hints, topLevelEnumNames);
|
|
11667
|
+
for (const header of collectResponseHeaders(op)) collectTypeRefNames(header.type, names, hints, topLevelEnumNames);
|
|
9437
11668
|
for (const p of [
|
|
9438
11669
|
...op.pathParams,
|
|
9439
11670
|
...op.queryParams,
|
|
@@ -10552,11 +12783,12 @@ function resolveScript(scriptRelPath) {
|
|
|
10552
12783
|
}
|
|
10553
12784
|
function runPackagedScript(scriptRelPath, args) {
|
|
10554
12785
|
const { bin, script } = resolveScript(scriptRelPath);
|
|
10555
|
-
|
|
12786
|
+
const fullArgs = bin === "npx" ? [
|
|
10556
12787
|
"tsx",
|
|
10557
12788
|
script,
|
|
10558
12789
|
...args
|
|
10559
|
-
] : [script, ...args]
|
|
12790
|
+
] : [script, ...args];
|
|
12791
|
+
execFileSync(bin, fullArgs, {
|
|
10560
12792
|
stdio: "inherit",
|
|
10561
12793
|
env: process.env
|
|
10562
12794
|
});
|
|
@@ -10588,11 +12820,12 @@ function runSmokeCheck(opts) {
|
|
|
10588
12820
|
try {
|
|
10589
12821
|
if (smokeRunner) {
|
|
10590
12822
|
const bin = smokeRunner.endsWith(".ts") ? "npx" : "node";
|
|
10591
|
-
|
|
12823
|
+
const fullArgs = bin === "npx" ? [
|
|
10592
12824
|
"tsx",
|
|
10593
12825
|
smokeRunner,
|
|
10594
12826
|
...smokeArgs
|
|
10595
|
-
] : [smokeRunner, ...smokeArgs]
|
|
12827
|
+
] : [smokeRunner, ...smokeArgs];
|
|
12828
|
+
execFileSync(bin, fullArgs, {
|
|
10596
12829
|
stdio: "inherit",
|
|
10597
12830
|
env: process.env
|
|
10598
12831
|
});
|
|
@@ -10708,9 +12941,10 @@ async function verifyCommand(opts) {
|
|
|
10708
12941
|
diagData.compatCheck = compatSummary;
|
|
10709
12942
|
if (parsedSpec && maxRetries > 0) setRetryDiagnostics(diagData, compatFlow.attempts, compatFlow.status === "passed", compatSummary.preservationScore, compatFlow.patchedPerIteration);
|
|
10710
12943
|
}
|
|
10711
|
-
if (compatFlow.status === "passed" && compatFlow.compatResult.passed)
|
|
10712
|
-
|
|
10713
|
-
|
|
12944
|
+
if (compatFlow.status === "passed" && compatFlow.compatResult.passed) {
|
|
12945
|
+
if (compatFlow.attempts > 0) console.log(`Compat: converged after ${compatFlow.attempts} retry iteration(s)`);
|
|
12946
|
+
else console.log("Compat: passed");
|
|
12947
|
+
} else {
|
|
10714
12948
|
if (diagnostics) {
|
|
10715
12949
|
writeDiagnostics(diagData);
|
|
10716
12950
|
console.log("Diagnostics written to verify-diagnostics.json");
|