@workos/oagen 0.30.1 → 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.
Files changed (46) hide show
  1. package/README.md +4 -12
  2. package/dist/cli/index.d.mts +1 -1
  3. package/dist/cli/index.mjs +3003 -664
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/compat/index.d.mts +85 -36
  6. package/dist/compat/index.d.mts.map +1 -1
  7. package/dist/compat/index.mjs +33 -23
  8. package/dist/compat/index.mjs.map +1 -1
  9. package/dist/{diff-DMFiDAI6.mjs → diff-CAsypxXV.mjs} +109 -89
  10. package/dist/{diff-DMFiDAI6.mjs.map → diff-CAsypxXV.mjs.map} +1 -1
  11. package/dist/differ-B0DSuKUZ.d.mts +72 -0
  12. package/dist/differ-B0DSuKUZ.d.mts.map +1 -0
  13. package/dist/index.d.mts +129 -61
  14. package/dist/index.d.mts.map +1 -1
  15. package/dist/index.mjs +2992 -563
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/{overlay-BhXruu32.mjs → overlay-Dj3WWlNP.mjs} +45 -38
  18. package/dist/overlay-Dj3WWlNP.mjs.map +1 -0
  19. package/dist/scripts/smoke/baseline.d.mts +1 -1
  20. package/dist/scripts/smoke/baseline.mjs +2848 -537
  21. package/dist/scripts/smoke/baseline.mjs.map +1 -1
  22. package/dist/scripts/smoke/sdk-test.d.mts +1 -1
  23. package/dist/scripts/smoke/sdk-test.mjs +4 -2
  24. package/dist/scripts/smoke/sdk-test.mjs.map +1 -1
  25. package/dist/smoke.d.mts +259 -36
  26. package/dist/smoke.d.mts.map +1 -1
  27. package/dist/smoke.mjs +2848 -537
  28. package/dist/smoke.mjs.map +1 -1
  29. package/dist/{tree-sitter-C6HB4OrC.mjs → tree-sitter-C7Hk-NDu.mjs} +36 -7
  30. package/dist/tree-sitter-C7Hk-NDu.mjs.map +1 -0
  31. package/dist/{types-yrBidOg3.d.mts → types-I7W7CNl1.d.mts} +227 -9
  32. package/dist/types-I7W7CNl1.d.mts.map +1 -0
  33. package/dist/verify/index.d.mts +7 -8
  34. package/dist/verify/index.d.mts.map +1 -1
  35. package/dist/verify/index.mjs +24 -14
  36. package/dist/verify/index.mjs.map +1 -1
  37. package/docs/architecture/emitter-contract.md +1 -0
  38. package/docs/architecture/ir-types.md +175 -17
  39. package/docs/cli.md +13 -13
  40. package/package.json +3 -3
  41. package/skills/check-emitter-parity/SKILL.md +15 -1
  42. package/dist/differ-DHldxZMr.d.mts +0 -123
  43. package/dist/differ-DHldxZMr.d.mts.map +0 -1
  44. package/dist/overlay-BhXruu32.mjs.map +0 -1
  45. package/dist/tree-sitter-C6HB4OrC.mjs.map +0 -1
  46. package/dist/types-yrBidOg3.d.mts.map +0 -1
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { BaseResolver, Oas3_1Types, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
6
+ import { createHash } from "node:crypto";
6
7
  import "dotenv/config";
7
8
  //#region src/ir/sdk-behavior.ts
8
9
  /**
@@ -144,11 +145,15 @@ var SpecParseError = class extends OagenError {
144
145
  //#region src/parser/refs.ts
145
146
  async function loadAndBundleSpec(specPath) {
146
147
  const absolutePath = resolve(specPath);
148
+ const content = await readFile(absolutePath, "utf-8");
149
+ const document = makeDocumentFromString(content, pathToFileURL(absolutePath).href);
150
+ const config = await createConfig({});
151
+ const resolver = new BaseResolver();
147
152
  const result = await bundleDocument({
148
- document: makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href),
149
- config: await createConfig({}),
153
+ document,
154
+ config,
150
155
  types: Oas3_1Types,
151
- externalRefResolver: new BaseResolver(),
156
+ externalRefResolver: resolver,
152
157
  dereference: false
153
158
  });
154
159
  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.`);
@@ -214,6 +219,38 @@ function walkTypeRef(ref, visitor) {
214
219
  default: assertNever(ref);
215
220
  }
216
221
  }
222
+ /**
223
+ * Collect all model names referenced (directly or transitively) by a TypeRef.
224
+ */
225
+ function collectModelRefs(ref) {
226
+ const names = [];
227
+ walkTypeRef(ref, { model: (r) => names.push(r.name) });
228
+ return names;
229
+ }
230
+ /**
231
+ * Every response header an operation declares: the primary response's, each
232
+ * alternate success entry's, and each error entry's. The primary list is the
233
+ * same array as its `successResponses` entry when both exist, so it is yielded
234
+ * once. Every pass that walks an operation's type refs (reachability, model
235
+ * assignment, ref rewriting and validation, direction, compat filtering, enum
236
+ * collection) goes through this so a type reached only through a header is
237
+ * treated the way a body or parameter type is.
238
+ */
239
+ function collectResponseHeaders(op) {
240
+ const result = [];
241
+ const seen = /* @__PURE__ */ new Set();
242
+ const lists = [
243
+ op.responseHeaders,
244
+ ...(op.successResponses ?? []).map((r) => r.headers),
245
+ ...op.errors.map((e) => e.headers)
246
+ ];
247
+ for (const list of lists) {
248
+ if (!list || seen.has(list)) continue;
249
+ seen.add(list);
250
+ result.push(...list);
251
+ }
252
+ return result;
253
+ }
217
254
  //#endregion
218
255
  //#region src/utils/naming.ts
219
256
  /**
@@ -261,7 +298,7 @@ function splitWords(s) {
261
298
  if (!s) return [];
262
299
  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));
263
300
  }
264
- const ACRONYM_SET = new Set([
301
+ const ACRONYM_SET = /* @__PURE__ */ new Set([
265
302
  "SSO",
266
303
  "FGA",
267
304
  "SAML",
@@ -271,7 +308,7 @@ const ACRONYM_SET = new Set([
271
308
  "M2M"
272
309
  ]);
273
310
  function toPascalCase(s, acronyms) {
274
- const merged = acronyms ? new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
311
+ const merged = acronyms ? /* @__PURE__ */ new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
275
312
  return splitWords(s).map((w) => {
276
313
  const upper = w.toUpperCase();
277
314
  if (merged.has(upper)) return upper;
@@ -280,7 +317,7 @@ function toPascalCase(s, acronyms) {
280
317
  }).join("");
281
318
  }
282
319
  function toCamelCase(s, acronyms) {
283
- const merged = acronyms ? new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
320
+ const merged = acronyms ? /* @__PURE__ */ new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
284
321
  const words = splitWords(s);
285
322
  if (words.length === 0) return "";
286
323
  return words.map((w, i) => {
@@ -313,7 +350,7 @@ function stripListItemMarkers(name) {
313
350
  return name.replace(/ListItem/g, "").replace(/ByExternalId/g, "").replace(/ByResourceId/g, "").replace(/ForResource/g, "");
314
351
  }
315
352
  /** Words that look plural but must NOT be singularized. */
316
- const SINGULAR_SAFE_LIST = new Set([
353
+ const SINGULAR_SAFE_LIST = /* @__PURE__ */ new Set([
317
354
  "Status",
318
355
  "Address",
319
356
  "Access",
@@ -373,138 +410,598 @@ function qualifyInlineModelName(baseName, parentName) {
373
410
  return `${cleanParent}${baseName}`;
374
411
  }
375
412
  /**
413
+ * The inline model or enum names `schemaToTypeRef` would mint for `schema`
414
+ * typed under `contextName` with `parentModelName`; empty when it mints none
415
+ * (a `$ref`, a primitive, an array or map of those). Mirrors the naming
416
+ * branches of `schemaToTypeRef`: an inline object in any form it recognises
417
+ * (`properties`, an `allOf` with object members) takes
418
+ * `qualifyInlineModelName`; a `oneOf`/`anyOf` mints one model per inline
419
+ * object variant, named exactly as the union references them
420
+ * (`nameInlineVariants`: `DrawingLayer`, `DrawingLayerWithOpacity`, or
421
+ * `CircleDrawingLayer` when every variant is titled); an inline enum, or a
422
+ * `oneOf` of string consts that collapses into one, takes the parent-prefixed
423
+ * enum name, which is never singularized; an array or map claims whatever its
424
+ * items or values would.
425
+ */
426
+ function mintedInlineNames(contextName, schema, parentModelName) {
427
+ return mintedInlineNameEntries(contextName, schema, parentModelName).map((entry) => entry.name);
428
+ }
429
+ /** `mintedInlineNames`, also telling for each name whether it is context-free. */
430
+ function mintedInlineNameEntries(contextName, schema, parentModelName) {
431
+ const s = unwrapNullableSchema(schema);
432
+ if (s.$ref) return [];
433
+ const base = toPascalCase(contextName);
434
+ const modelName = () => [{
435
+ name: qualifyInlineModelName(base, parentModelName),
436
+ contextFree: false
437
+ }];
438
+ const enumName = () => {
439
+ const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
440
+ return [{
441
+ name: cleanParent && !base.startsWith(cleanParent) ? `${cleanParent}${base}` : base,
442
+ contextFree: false
443
+ }];
444
+ };
445
+ if (s.allOf) {
446
+ const only = s.allOf.length === 1 ? s.allOf[0] : void 0;
447
+ if (only) return mintedInlineNameEntries(contextName, only, parentModelName);
448
+ return s.allOf.some((m) => !m.$ref && (m.properties || m.type === "object")) ? modelName() : [];
449
+ }
450
+ const variants = s.oneOf ?? s.anyOf;
451
+ if (variants) {
452
+ const nonNull = variants.filter((v) => !isNullSchema$1(v));
453
+ if (nonNull.length >= 2 && nonNull.every((v) => typeof v.const === "string")) return enumName();
454
+ return [...nameInlineVariantEntries(s, modelName()[0].name)].map(([variant, entry]) => ({
455
+ ...entry,
456
+ schema: variant
457
+ }));
458
+ }
459
+ if (s.const !== void 0) return [];
460
+ if ((s.enum ?? []).filter((v) => v !== null).length >= 2) return enumName();
461
+ if (isInlineObjectSchema(s)) return modelName();
462
+ if (s.items && (s.type === "array" || !s.type)) return mintedInlineNameEntries(contextName, s.items, parentModelName);
463
+ if (s.type === "object" && !s.properties) {
464
+ const value = mapValueSchema(s);
465
+ return value ? mintedInlineNameEntries(contextName, value, parentModelName) : [];
466
+ }
467
+ return [];
468
+ }
469
+ /**
470
+ * The inline names the other properties of an object mint
471
+ * (`mintedInlineNameEntries`) which a `oneOf`/`anyOf` under `fieldName` must
472
+ * not give one of its variants (`nameInlineVariants`): beside a
473
+ * `shape_with_side` object, the `{ side }` variant of `shape` cannot become
474
+ * `DrawingShapeWithSide`. Every context-dependent sibling name is reserved.
475
+ * A context-free name — a title-mapped variant's title — names one shape
476
+ * wherever it appears, so it is shared, not reserved, when this field gives
477
+ * the same title a variant of the identical shape (`schemaShapeKey`:
478
+ * documentation keywords ignored, property names verbatim — the rule array
479
+ * items already use to share a singular sibling's model); a sibling giving
480
+ * the same title a different shape, or one this field does not use, reserves
481
+ * it, so this union's variant falls back to its context-qualified name. Each
482
+ * sibling's names are read in isolation, without the reservations that
483
+ * sibling would itself apply, so the set depends on sibling names and content
484
+ * only, two unions beside each other cannot chase each other's names, and
485
+ * the shape test is symmetric: when `Circle` means `{ radius }` on one side
486
+ * and `{ diameter }` on the other, both fall back (`CircleDrawingShape`,
487
+ * `CircleDrawingMarker`) and neither keeps the bare `Circle`. Empty without a
488
+ * sibling set, as for a request body or response that is itself a union.
489
+ */
490
+ function reservedInlineNames(fieldName, siblings, parentModelName) {
491
+ const reserved = /* @__PURE__ */ new Set();
492
+ if (!siblings) return reserved;
493
+ const ownShapes = /* @__PURE__ */ new Map();
494
+ const fieldSchema = siblings.get(fieldName);
495
+ if (fieldSchema) for (const own of mintedInlineNameEntries(fieldName, fieldSchema, parentModelName)) {
496
+ if (!own.contextFree || !own.schema) continue;
497
+ ownShapes.set(own.name, [...ownShapes.get(own.name) ?? [], schemaShapeKey(own.schema)]);
498
+ }
499
+ for (const [siblingName, siblingSchema] of siblings) {
500
+ if (siblingName === fieldName) continue;
501
+ for (const minted of mintedInlineNameEntries(siblingName, siblingSchema, parentModelName)) {
502
+ if (!minted.contextFree) {
503
+ reserved.add(minted.name);
504
+ continue;
505
+ }
506
+ const shapes = ownShapes.get(minted.name);
507
+ const siblingShape = minted.schema ? schemaShapeKey(minted.schema) : void 0;
508
+ if (!(shapes !== void 0 && siblingShape !== void 0 && shapes.every((s) => s === siblingShape))) reserved.add(minted.name);
509
+ }
510
+ }
511
+ return reserved;
512
+ }
513
+ /** Documentation keywords that do not shape a model; ignored by `schemaShapeKey` at a schema's own level. */
514
+ const DOC_KEYWORDS = /* @__PURE__ */ new Set([
515
+ "description",
516
+ "title",
517
+ "example",
518
+ "examples",
519
+ "summary",
520
+ "externalDocs",
521
+ "deprecated"
522
+ ]);
523
+ /** Keywords whose value maps NAMES (not keywords) to schemas: names are kept verbatim, values recurse as schemas. */
524
+ const NAMED_SCHEMA_MAPS = /* @__PURE__ */ new Set([
525
+ "properties",
526
+ "patternProperties",
527
+ "dependentSchemas",
528
+ "$defs",
529
+ "definitions"
530
+ ]);
531
+ /** Keywords whose value is a single schema. */
532
+ const SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
533
+ "items",
534
+ "additionalProperties",
535
+ "additionalItems",
536
+ "contains",
537
+ "propertyNames",
538
+ "not",
539
+ "if",
540
+ "then",
541
+ "else",
542
+ "unevaluatedProperties",
543
+ "unevaluatedItems"
544
+ ]);
545
+ /** Keywords whose value is an ordered list of schemas. */
546
+ const SCHEMA_LIST_KEYWORDS = /* @__PURE__ */ new Set([
547
+ "allOf",
548
+ "oneOf",
549
+ "anyOf",
550
+ "prefixItems"
551
+ ]);
552
+ /** Keywords whose list value is a set: compared order-insensitively. */
553
+ const UNORDERED_LIST_KEYWORDS = /* @__PURE__ */ new Set(["required", "type"]);
554
+ /**
555
+ * A canonical rendering of a schema's shape, for deciding whether two inline
556
+ * schemas describe the same model: every structural keyword (`type`,
557
+ * `properties`, `required`, `items`, `additionalProperties`,
558
+ * `patternProperties`, `allOf`/`oneOf`/`anyOf`, `enum`, `const`, `format`,
559
+ * `nullable`, ...) recursively, with keywords sorted and the `required` and
560
+ * `type` lists order-insensitive. Documentation keywords (`description`,
561
+ * `title`, `example`, `examples`, `summary`, `externalDocs`, `deprecated`)
562
+ * and vendor extensions (`x-*`) are ignored, and so is `nullable: false`,
563
+ * which the nullable unwrap leaves behind — but only where they are a
564
+ * schema's own keywords. Inside `properties`, `patternProperties`,
565
+ * `dependentSchemas`, `dependentRequired` and `$defs` the keys are property
566
+ * names, kept verbatim: a field named `description` or `title` is part of the
567
+ * shape. Enum, const and default values are compared as plain data, in
568
+ * order, as the IR keeps them.
569
+ */
570
+ function schemaShapeKey(schema) {
571
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
572
+ const dataKey = (value) => {
573
+ if (Array.isArray(value)) return value.map(dataKey);
574
+ if (!isRecord(value)) return value;
575
+ const out = {};
576
+ for (const key of Object.keys(value).sort()) out[key] = dataKey(value[key]);
577
+ return out;
578
+ };
579
+ const schemaKey = (value) => {
580
+ if (!isRecord(value)) return value;
581
+ const out = {};
582
+ for (const keyword of Object.keys(value).sort()) {
583
+ const v = value[keyword];
584
+ if (DOC_KEYWORDS.has(keyword) || keyword.startsWith("x-")) continue;
585
+ if (keyword === "nullable" && v === false) continue;
586
+ if (NAMED_SCHEMA_MAPS.has(keyword) && isRecord(v)) {
587
+ const named = {};
588
+ for (const name of Object.keys(v).sort()) named[name] = schemaKey(v[name]);
589
+ out[keyword] = named;
590
+ } else if (keyword === "dependentRequired" && isRecord(v)) {
591
+ const named = {};
592
+ for (const name of Object.keys(v).sort()) {
593
+ const names = v[name];
594
+ named[name] = Array.isArray(names) ? names.map(String).sort() : dataKey(names);
595
+ }
596
+ out[keyword] = named;
597
+ } else if (SCHEMA_KEYWORDS.has(keyword)) out[keyword] = schemaKey(v);
598
+ else if (SCHEMA_LIST_KEYWORDS.has(keyword) && Array.isArray(v)) out[keyword] = v.map(schemaKey);
599
+ else if (UNORDERED_LIST_KEYWORDS.has(keyword) && Array.isArray(v)) out[keyword] = v.map(String).sort();
600
+ else out[keyword] = dataKey(v);
601
+ }
602
+ return out;
603
+ };
604
+ return JSON.stringify(schemaKey(schema));
605
+ }
606
+ /**
607
+ * The context name an array field's items (`kind: 'item'`) or a map field's
608
+ * values (`kind: 'value'`) are typed and extracted under. By default it is
609
+ * the field itself, so the inline model is the parent plus the singularized
610
+ * field (`layers` on `Drawing` → `DrawingLayer`).
611
+ *
612
+ * When a sibling property of the same object would mint that very name —
613
+ * `layer` as an inline object, an inline enum, an inline union with object
614
+ * variants (any of whose variant names counts), or an array or map of such
615
+ * (`mintedInlineNames`); a primitive `email` beside `emails` claims nothing
616
+ * — the elements step aside to
617
+ * `<field>_item` / `<field>_value`, which qualify to `<Parent><Field>Item` /
618
+ * `<Parent><Field>Value` (`DrawingLayersItem`, `DrawingLayersValue`),
619
+ * matching the `<Variant>Item` convention. Should a sibling mint that name
620
+ * too (a property `layers_item`), the fallback keeps going the way
621
+ * `variantMemberKeys` does for variants: `<field>_array_item`, then
622
+ * `<field>_array_item2`, `<field>_array_item3`, ... for items
623
+ * (`DrawingLayersArrayItem`, ...) and `<field>_member_value`,
624
+ * `<field>_member_value2`, ... for values, taking the first candidate whose
625
+ * minted name no sibling claims.
626
+ *
627
+ * Identical-shape sharing: when every sibling claiming the singular name is
628
+ * an inline object whose shape is structurally identical to the items or
629
+ * values (`schemaShapeKey`: same properties, required, types, nested inline
630
+ * schemas and enum values; description, title and example ignored), the
631
+ * fields share one model under the singular name. They are the same shape,
632
+ * so one model is the right SDK type and a duplicate would only add noise;
633
+ * should a later spec version make the shapes diverge, the split happens
634
+ * then, which is the moment the item type genuinely changes. This
635
+ * deliberately departs from a literal one-model-per-field reading for the
636
+ * identical case.
637
+ *
638
+ * Only context-dependent names take part: a title-mapped variant's title
639
+ * (`MintedInlineName.contextFree`) is the same under every context, so no
640
+ * candidate could step away from it, and such a name is shared by design —
641
+ * two sibling arrays whose items carry the same titled variants both
642
+ * reference `Circle` and `Square` (`nameInlineVariants`). The search always
643
+ * terminates: it stops as soon as a candidate changes nothing about the names
644
+ * (`firstFreeElementContext`'s no-progress rule), and
645
+ * `MAX_ELEMENT_CONTEXT_ATTEMPTS` caps it as a backstop; both return the last
646
+ * candidate tried, deterministically.
647
+ *
648
+ * Decided from sibling names and schema content alone, so document order
649
+ * plays no part. `schemaToTypeRef` and every extractor call this with the
650
+ * same sibling set (`collectProperties`); otherwise the reference and the
651
+ * model disagree.
652
+ */
653
+ function elementContextName(fieldName, kind, siblings, parentModelName, elementSchema) {
654
+ return elementContextDecision(fieldName, kind, siblings, parentModelName, elementSchema).context;
655
+ }
656
+ /** `elementContextName`, with the reason it decided as it did (see `ElementContextReason`). */
657
+ function elementContextDecision(fieldName, kind, siblings, parentModelName, elementSchema) {
658
+ if (!siblings) return {
659
+ context: fieldName,
660
+ reason: "no-siblings"
661
+ };
662
+ const dependentNames = (candidate) => mintedInlineNameEntries(candidate, elementSchema, parentModelName).filter((entry) => !entry.contextFree).map((entry) => entry.name);
663
+ const own = dependentNames(fieldName);
664
+ if (own.length === 0) return {
665
+ context: fieldName,
666
+ reason: "nothing-minted"
667
+ };
668
+ const claimed = /* @__PURE__ */ new Map();
669
+ for (const [siblingName, siblingSchema] of siblings) {
670
+ if (siblingName === fieldName) continue;
671
+ for (const minted of mintedInlineNames(siblingName, siblingSchema, parentModelName)) claimed.set(minted, [...claimed.get(minted) ?? [], siblingSchema]);
672
+ }
673
+ const rivals = [...new Set(own.flatMap((name) => claimed.get(name) ?? []))];
674
+ if (rivals.length === 0) return {
675
+ context: fieldName,
676
+ reason: "unclaimed"
677
+ };
678
+ const element = unwrapNullableSchema(elementSchema);
679
+ const identical = (rival) => {
680
+ const sibling = unwrapNullableSchema(rival);
681
+ return isInlineObjectSchema(sibling) && isInlineObjectSchema(element) && schemaShapeKey(sibling) === schemaShapeKey(element);
682
+ };
683
+ if (rivals.every(identical)) return {
684
+ context: fieldName,
685
+ reason: "shared-shape"
686
+ };
687
+ return firstFreeElementContext(fieldName, kind, new Set(claimed.keys()), dependentNames, own);
688
+ }
689
+ /**
690
+ * The first fallback context under which the elements mint no claimed name:
691
+ * `<field>_item` / `<field>_value`, then `<field>_array_item` /
692
+ * `<field>_member_value`, then `<field>_array_item2`, `…3`, ... (the way
693
+ * `variantMemberKeys` numbers a variant's members). `mint` gives the names
694
+ * under a candidate and `previous` those under the context tried before the
695
+ * first candidate. The search stops with `no-progress` as soon as a
696
+ * candidate mints the same names as the one before it — the names do not
697
+ * depend on the context, so no candidate can avoid the claim — and with
698
+ * `cap` after `maxAttempts` candidates; both keep the last candidate tried.
699
+ */
700
+ function firstFreeElementContext(fieldName, kind, claimed, mint, previous, maxAttempts = 256) {
701
+ const fallback = kind === "item" ? "array_item" : "member_value";
702
+ let last = previous;
703
+ let candidate = fieldName;
704
+ for (let i = 0; i < maxAttempts; i++) {
705
+ candidate = i === 0 ? `${fieldName}_${kind}` : i === 1 ? `${fieldName}_${fallback}` : `${fieldName}_${fallback}${i}`;
706
+ const minted = mint(candidate);
707
+ if (minted.every((name) => !claimed.has(name))) return {
708
+ context: candidate,
709
+ reason: "free"
710
+ };
711
+ if (sameNames(minted, last)) return {
712
+ context: candidate,
713
+ reason: "no-progress"
714
+ };
715
+ last = minted;
716
+ }
717
+ return {
718
+ context: candidate,
719
+ reason: "cap"
720
+ };
721
+ }
722
+ /** The same names, in any order. */
723
+ function sameNames(a, b) {
724
+ if (a.length !== b.length) return false;
725
+ const sortedB = [...b].sort();
726
+ return [...a].sort().every((name, i) => name === sortedB[i]);
727
+ }
728
+ /**
729
+ * Every property the object described by `schema` exposes, by name: its own
730
+ * `properties`, those of its `allOf` members (following a member's `$ref`
731
+ * into `schemas` when given, as `extractAllOfModel` does), and those of
732
+ * inline `oneOf`/`anyOf` variants, whose fields the model extractors flatten
733
+ * in; the first definition of a name wins. This is the sibling set
734
+ * `elementContextName` decides from; the TypeRef builder and every extractor
735
+ * derive it through this one function so they agree.
736
+ */
737
+ function collectProperties(schema, schemas) {
738
+ const properties = /* @__PURE__ */ new Map();
739
+ const seen = /* @__PURE__ */ new Set();
740
+ const visit = (s, followRef) => {
741
+ if (!s || seen.has(s)) return;
742
+ seen.add(s);
743
+ if (s.$ref) {
744
+ if (followRef && schemas) visit(schemas[s.$ref.split("/").pop() ?? ""], true);
745
+ return;
746
+ }
747
+ for (const [key, value] of Object.entries(s.properties ?? {})) if (value && !properties.has(key)) properties.set(key, value);
748
+ for (const sub of s.allOf ?? []) visit(sub, true);
749
+ for (const variant of [...s.oneOf ?? [], ...s.anyOf ?? []]) visit(variant, false);
750
+ };
751
+ visit(schema, false);
752
+ return properties;
753
+ }
754
+ /**
376
755
  * Walk all component schemas and extract inline Model definitions for fields
377
- * that are objects with properties (or arrays of such objects).
756
+ * that are objects with properties, including objects reached through array
757
+ * items, `oneOf` variants, and map (`additionalProperties` /
758
+ * `patternProperties`) values.
378
759
  * These correspond to the ModelRef entries created by schemaToTypeRef.
379
760
  */
380
761
  function extractInlineModelsFromSchemas(schemas) {
381
762
  if (!schemas) return [];
382
763
  const inlineModels = [];
383
- for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, resolveSchemaName(schemaName));
764
+ for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, resolveSchemaName(schemaName), schemas, collectProperties(schema, schemas));
384
765
  return inlineModels;
385
766
  }
386
- function extractInlineModelsFromProperties(schema, results, parentName) {
767
+ /**
768
+ * Walk a schema's properties and materialise every inline object as a model.
769
+ * `componentSchemas` is optional and only feeds `buildFieldFromSchema`, so a
770
+ * field that `$ref`s a component can take its `example`/`examples` from it;
771
+ * callers without the component map at hand simply omit it.
772
+ * `siblingProperties` is the parent object's full property set, for callers
773
+ * that see more of it than this schema shows (see `elementContextName`).
774
+ * `reservedNames` is set only on the re-entry for a field's array items or
775
+ * map values: the inline names that field's siblings mint, which a union
776
+ * among the items or values must step around exactly as `schemaToTypeRef`
777
+ * does when it hands the same set down (`reservedNamesFor`).
778
+ *
779
+ * The recursion follows the document's own inline structure only: a `$ref`
780
+ * is resolved solely to read a component's property list for typing (an
781
+ * augmentation's inherited fields, a sibling set), never descended into, so
782
+ * the depth is bounded by the document's inline nesting and no component
783
+ * graph — self-extending, mutually extending, or reused at several levels —
784
+ * can make it run away.
785
+ */
786
+ function extractInlineModelsFromProperties(schema, results, parentName, componentSchemas, siblingProperties, reservedNames) {
387
787
  const properties = schema.properties ?? {};
388
788
  const allOfSchemas = schema.allOf ?? [];
389
- for (const sub of allOfSchemas) if (sub.properties) extractInlineModelsFromProperties(sub, results, parentName);
390
- for (const [fieldName, fieldSchema] of Object.entries(properties)) {
391
- if (!fieldSchema) continue;
392
- if (fieldSchema.properties && (fieldSchema.type === "object" || !fieldSchema.type)) {
789
+ const siblings = siblingProperties ?? collectProperties(schema);
790
+ const reservedFor = (fieldName) => reservedNames ?? reservedInlineNames(fieldName, siblings, parentName);
791
+ for (const sub of allOfSchemas) if (sub.properties) extractInlineModelsFromProperties(sub, results, parentName, componentSchemas, siblings, reservedNames);
792
+ for (const [fieldName, rawFieldSchema] of Object.entries(properties)) {
793
+ if (!rawFieldSchema) continue;
794
+ const fieldSchema = unwrapNullableField(rawFieldSchema);
795
+ if (isInlineObjectSchema(fieldSchema)) {
393
796
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
394
- results.push(buildInlineModel(modelName, fieldSchema));
395
- extractInlineModelsFromProperties(fieldSchema, results, modelName);
797
+ results.push(buildInlineModel(modelName, fieldSchema, componentSchemas));
798
+ extractInlineModelsFromProperties(fieldSchema, results, modelName, componentSchemas);
396
799
  }
397
- if (fieldSchema.type === "array" && fieldSchema.items) {
398
- const items = fieldSchema.items;
399
- if (items.properties && (items.type === "object" || !items.type)) {
400
- const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
401
- results.push(buildInlineModel(modelName, items));
402
- extractInlineModelsFromProperties(items, results, modelName);
403
- }
800
+ if (fieldSchema.type === "array" && fieldSchema.items) extractInlineModelsFromProperties({ properties: { [elementContextName(fieldName, "item", siblings, parentName, fieldSchema.items)]: fieldSchema.items } }, results, parentName, componentSchemas, void 0, reservedFor(fieldName));
801
+ if (fieldSchema.type === "object" && !fieldSchema.properties) {
802
+ const valueSchema = mapValueSchema(fieldSchema);
803
+ if (valueSchema) extractInlineModelsFromProperties({ properties: { [elementContextName(fieldName, "value", siblings, parentName, valueSchema)]: valueSchema } }, results, parentName, componentSchemas, void 0, reservedFor(fieldName));
404
804
  }
405
- if (fieldSchema.allOf && fieldSchema.allOf.every((s) => !s.$ref && !s.oneOf && !s.anyOf)) {
406
- const merged = mergeAllOfObjectSchema(fieldSchema);
407
- if (Object.keys(merged.properties ?? {}).length > 0) {
805
+ if (fieldSchema.allOf) {
806
+ const members = fieldSchema.allOf;
807
+ const emitMerged = () => {
808
+ const merged = mergeAugmentation(fieldSchema, componentSchemas);
809
+ if (merged.properties.length === 0) return;
408
810
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
409
- results.push(buildInlineModel(modelName, merged));
410
- extractInlineModelsFromProperties(merged, results, modelName);
411
- }
811
+ results.push(buildMergedModel(modelName, merged, componentSchemas));
812
+ const own = {};
813
+ for (const p of merged.properties) if (p.origin === null) own[p.name] = p.schema;
814
+ const mergedSiblings = new Map(merged.properties.map((p) => [p.name, p.schema]));
815
+ extractInlineModelsFromProperties({ properties: own }, results, modelName, componentSchemas, mergedSiblings);
816
+ };
817
+ const only = members.length === 1 ? members[0] : void 0;
818
+ if (members.some((m) => m.$ref)) {
819
+ if (members.some((m) => !m.$ref && (m.properties || m.type === "object"))) emitMerged();
820
+ } else if (only) extractInlineModelsFromProperties({ properties: { [fieldName]: {
821
+ ...only,
822
+ ...extractConstraints(fieldSchema)
823
+ } } }, results, parentName, componentSchemas, siblings, reservedNames);
824
+ else if (members.some((m) => m.properties)) emitMerged();
412
825
  }
413
- if (fieldSchema.oneOf) {
414
- const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
826
+ const unionVariants = fieldSchema.oneOf ?? fieldSchema.anyOf;
827
+ if (unionVariants) {
828
+ const inlineObjectVariants = unionVariants.filter((v) => isInlineObjectSchema(v));
415
829
  if (inlineObjectVariants.length > 0) {
416
830
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
417
831
  const existingNames = new Set(results.map((r) => r.name));
418
- const namingDiscProp = deriveOneOfNamingDiscriminator(inlineObjectVariants);
419
- const emittedNames = [];
832
+ const variantNames = nameInlineVariants(fieldSchema, modelName, reservedFor(fieldName));
420
833
  for (const variant of inlineObjectVariants) {
421
- const variantName = nameOneOfVariant(variant, modelName, emittedNames, namingDiscProp);
422
- emittedNames.push(variantName);
834
+ const variantName = variantNames.get(variant);
835
+ if (variantName === void 0) continue;
423
836
  if (!existingNames.has(variantName)) {
424
837
  existingNames.add(variantName);
425
- results.push(buildInlineModel(variantName, variant));
426
- extractInlineModelsFromProperties(variant, results, variantName);
838
+ results.push(buildInlineModel(variantName, variant, componentSchemas));
839
+ extractInlineModelsFromProperties(variant, results, variantName, componentSchemas);
840
+ extractInlineModelsFromProperties({ properties: variantMemberSchemas(variant, variantName) }, results, variantName, componentSchemas);
427
841
  }
428
842
  }
429
843
  }
430
844
  }
431
845
  }
432
846
  }
433
- /** Find a single string-const-valued property shared by every variant whose
434
- * values are all distinct — the implicit discriminator. Returns null when no
435
- * such property exists. Mirrors `deriveConstNamingDiscriminator` in schemas.ts. */
436
- function deriveOneOfNamingDiscriminator(variants) {
437
- if (variants.length < 2) return null;
438
- const candidates = Object.keys(variants[0]?.properties ?? {});
439
- for (const propName of candidates) {
440
- const values = variants.map((v) => readConstString(v.properties?.[propName]));
441
- if (values.some((v) => v === null)) continue;
442
- if (new Set(values).size !== values.length) continue;
443
- return propName;
444
- }
445
- return null;
446
- }
447
- function readConstString(prop) {
448
- if (!prop) return null;
449
- if (typeof prop.const === "string") return prop.const;
450
- if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
451
- return null;
452
- }
453
- /** Produce a per-variant model name. Variant 0 keeps the bare parent name;
454
- * later variants are prefixed by the const-derived label. Mirrors
455
- * `nameVariantModel` in schemas.ts. Falls back to a numeric suffix when no
456
- * discriminator is available, the const value PascalCases to nothing, or
457
- * the derived candidate collides with the parent or an already-emitted name. */
458
- function nameOneOfVariant(variant, parentName, alreadyEmitted, discriminatorProperty) {
459
- if (alreadyEmitted.length === 0) return parentName;
460
- if (discriminatorProperty) {
461
- const constValue = readConstString(variant.properties?.[discriminatorProperty]);
462
- if (constValue) {
463
- const prefix = toPascalCase(constValue);
464
- if (prefix) {
465
- const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
466
- if (!(candidate === parentName || alreadyEmitted.includes(candidate))) return candidate;
467
- }
468
- }
469
- }
470
- return `${parentName}${alreadyEmitted.length + 1}`;
471
- }
472
847
  /**
473
- * Merge the object members of an `allOf` (recursively) into a single
474
- * object-shaped schema, so an inline object expressed via `allOf` can be
475
- * materialized as one model — mirroring how schemaToTypeRef collapses such a
476
- * field into a single merged model ref. Earlier members win on key conflicts,
477
- * matching the merge order used elsewhere in the parser.
848
+ * Merge an `allOf` field into one object shape, remembering where each
849
+ * property comes from. A `$ref` member is resolved against `componentSchemas`
850
+ * by its component name, as `extractAllOfModel` does, and contributes its
851
+ * properties — its own nested `allOf` chain included — tagged with that
852
+ * component's model name, so the merged model can type them exactly as the
853
+ * component's own model does; inline members contribute properties tagged as
854
+ * the merged model's own. `required` is collected from every member. Earlier
855
+ * members win on key conflicts, matching the merge order used elsewhere in
856
+ * the parser; an unresolvable ref contributes nothing. The walk reads the
857
+ * components' property lists only — it never descends into a property — so
858
+ * it is finite for any component graph (the `seen` set only short-circuits a
859
+ * component reached twice within one merge).
478
860
  */
479
- function mergeAllOfObjectSchema(schema) {
480
- const properties = {};
861
+ function mergeAugmentation(schema, componentSchemas) {
862
+ const properties = [];
863
+ const seenNames = /* @__PURE__ */ new Set();
481
864
  const required = [];
482
- const visit = (s) => {
483
- if (s.properties) {
484
- for (const [key, value] of Object.entries(s.properties)) if (value && !(key in properties)) properties[key] = value;
865
+ const seen = /* @__PURE__ */ new Set();
866
+ const visit = (s, origin) => {
867
+ if (seen.has(s)) return;
868
+ seen.add(s);
869
+ if (s.$ref) {
870
+ const rawName = s.$ref.split("/").pop() ?? "";
871
+ const target = componentSchemas?.[rawName];
872
+ if (!target) return;
873
+ visit(target, origin ?? {
874
+ name: resolveSchemaName(rawName),
875
+ schema: target
876
+ });
877
+ return;
878
+ }
879
+ for (const [key, value] of Object.entries(s.properties ?? {})) {
880
+ if (!value || seenNames.has(key)) continue;
881
+ seenNames.add(key);
882
+ properties.push({
883
+ name: key,
884
+ schema: value,
885
+ origin
886
+ });
485
887
  }
486
888
  if (s.required) required.push(...s.required);
487
- if (s.allOf) for (const sub of s.allOf) visit(sub);
889
+ for (const sub of s.allOf ?? []) visit(sub, origin);
488
890
  };
489
- visit(schema);
891
+ visit(schema, null);
490
892
  return {
491
- type: "object",
492
893
  properties,
493
894
  required,
494
- description: schema.description
895
+ description: schema.description,
896
+ constraints: extractConstraints(schema)
897
+ };
898
+ }
899
+ /**
900
+ * Build the model for a merged `allOf` field. An inherited property is typed
901
+ * with its component as naming context and the component's own sibling set,
902
+ * so it carries the same TypeRef as in the component's model (`BaseMeta`,
903
+ * `BaseChild`, ...); the merged model's own properties are typed under its
904
+ * name with the full merged property set as siblings.
905
+ */
906
+ function buildMergedModel(name, merged, componentSchemas) {
907
+ const requiredSet = new Set(merged.required);
908
+ const mergedSiblings = new Map(merged.properties.map((p) => [p.name, p.schema]));
909
+ const componentSiblings = /* @__PURE__ */ new Map();
910
+ const fields = merged.properties.map((p) => {
911
+ if (p.origin === null) return buildFieldFromSchema(p.name, p.schema, name, requiredSet, componentSchemas, mergedSiblings);
912
+ let siblings = componentSiblings.get(p.origin.name);
913
+ if (!siblings) {
914
+ siblings = collectProperties(p.origin.schema, componentSchemas);
915
+ componentSiblings.set(p.origin.name, siblings);
916
+ }
917
+ return buildFieldFromSchema(p.name, p.schema, p.origin.name, requiredSet, componentSchemas, siblings);
918
+ });
919
+ return {
920
+ name,
921
+ description: merged.description,
922
+ fields,
923
+ ...merged.constraints ? { constraints: merged.constraints } : {}
924
+ };
925
+ }
926
+ /**
927
+ * Strip the nullable wrappers `schemaToTypeRef` unwraps before it names
928
+ * anything — the OAS 3.1 `type: [X, 'null']` array, the OAS 3.0
929
+ * `nullable: true` flag, and a `oneOf`/`anyOf` with a single non-null
930
+ * variant — so extraction dispatches on the same shape the TypeRef was built
931
+ * from. The unwrap keeps `contextName`/`parentModelName` semantics intact:
932
+ * `schemaToTypeRef` passes both through each of these wrappers unchanged.
933
+ */
934
+ function unwrapNullableSchema(schema) {
935
+ if (schema.$ref) return schema;
936
+ if (Array.isArray(schema.type)) {
937
+ const nonNullTypes = schema.type.filter((t) => t !== "null");
938
+ if (nonNullTypes.length !== 1) return schema;
939
+ return unwrapNullableSchema({
940
+ ...schema,
941
+ type: nonNullTypes[0],
942
+ nullable: false
943
+ });
944
+ }
945
+ if (schema.nullable && schema.type) return unwrapNullableSchema({
946
+ ...schema,
947
+ nullable: false
948
+ });
949
+ const variants = schema.allOf ? void 0 : schema.oneOf ?? schema.anyOf;
950
+ if (variants) {
951
+ const nullVariant = variants.find(isNullSchema$1);
952
+ const nonNullVariants = variants.filter((v) => v !== nullVariant);
953
+ const only = nonNullVariants[0];
954
+ if (nullVariant && nonNullVariants.length === 1 && only) return unwrapNullableSchema(only);
955
+ }
956
+ return schema;
957
+ }
958
+ /**
959
+ * `unwrapNullableSchema` for a field, carrying the wrapper's limits onto the
960
+ * branch it unwraps to. The type-array and `nullable: true` forms unwrap by
961
+ * spreading, so every keyword survives; the union form swaps in the bare
962
+ * branch, which would drop a `minProperties` written beside
963
+ * `anyOf: [{ type: 'object', … }, { type: 'null' }]`. `extractConstraints`
964
+ * conjoins that wrapper's own keywords with its branch's as the field's
965
+ * limits, so the model built from the branch must carry the same ones or
966
+ * `Model.constraints` would omit what the field declares.
967
+ */
968
+ function unwrapNullableField(schema) {
969
+ const inner = unwrapNullableSchema(schema);
970
+ if (inner === schema || schema.allOf || !(schema.oneOf ?? schema.anyOf)) return inner;
971
+ return {
972
+ ...inner,
973
+ ...extractConstraints(schema)
495
974
  };
496
975
  }
497
- function buildInlineModel(name, schema) {
976
+ /** The `{ type: 'null' }` member that marks a `oneOf`/`anyOf` as nullable — same test as schemaToTypeRef. */
977
+ function isNullSchema$1(schema) {
978
+ return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
979
+ }
980
+ /**
981
+ * The schema that types a map's values, in the precedence `schemaToTypeRef`
982
+ * uses: a non-empty `additionalProperties` object wins, otherwise the first
983
+ * `patternProperties` entry. `additionalProperties: {}` means "any value"
984
+ * (typed `unknown`, nothing to extract) and does not fall through to
985
+ * `patternProperties`, matching the type-ref branch.
986
+ */
987
+ function mapValueSchema(schema) {
988
+ const additional = schema.additionalProperties;
989
+ if (additional && typeof additional === "object") return Object.keys(additional).length > 0 ? additional : null;
990
+ return (schema.patternProperties ? Object.values(schema.patternProperties) : [])[0] ?? null;
991
+ }
992
+ function buildInlineModel(name, schema, componentSchemas) {
498
993
  const requiredSet = new Set(schema.required ?? []);
499
994
  const fields = [];
995
+ const siblings = collectProperties(schema);
500
996
  for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
501
997
  if (!fieldSchema) continue;
502
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
998
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, componentSchemas, siblings));
503
999
  }
504
1000
  return {
505
1001
  name,
506
1002
  description: schema.description,
507
- fields
1003
+ fields,
1004
+ ...constraintsEntry(schema)
508
1005
  };
509
1006
  }
510
1007
  //#endregion
@@ -512,9 +1009,21 @@ function buildInlineModel(name, schema) {
512
1009
  /**
513
1010
  * Module-level transform set during extractSchemas(). Used by schemaToTypeRef()
514
1011
  * to apply the same name transform to $ref model/enum references.
1012
+ *
1013
+ * Receives the cleaned PascalCase name and, when the name is a
1014
+ * `components.schemas` key, the raw key exactly as spelled in the document.
1015
+ * Names oagen synthesises (inline enums, request-body variant models,
1016
+ * title-derived names) pass no second argument.
515
1017
  */
516
1018
  let activeSchemaNameTransform = null;
517
1019
  /**
1020
+ * Module-level set of raw `components.schemas` keys for the spec being
1021
+ * extracted. resolveSchemaName() consults it to decide whether the name it was
1022
+ * handed is a real component key (forwarded to the transform as
1023
+ * `originalName`) or a name oagen synthesised (no `originalName`).
1024
+ */
1025
+ let activeComponentSchemaKeys = /* @__PURE__ */ new Set();
1026
+ /**
518
1027
  * Module-level set of resolved schema names whose components/schemas entry is
519
1028
  * an enum (i.e. has `enum:`). Populated by extractSchemas() so schemaToTypeRef()
520
1029
  * can classify a `$ref` to a top-level enum schema as `kind: 'enum'` instead of
@@ -522,33 +1031,308 @@ let activeSchemaNameTransform = null;
522
1031
  * ref, even when the target was a shared enum like `PaginationOrder`.
523
1032
  */
524
1033
  let activeEnumSchemaNames = /* @__PURE__ */ new Set();
525
- /** Apply cleanSchemaName + the active transform (if any) to a raw schema name. */
1034
+ /**
1035
+ * Subset of `activeEnumSchemaNames` whose component schema also admits
1036
+ * `null` — via an OAS 3.1 `type: [..., 'null']` array, the OAS 3.0
1037
+ * `nullable: true` flag, or a literal `null` member in `enum:`. A `$ref` to
1038
+ * one of these resolves to `nullable(enum)` so the component's own
1039
+ * nullability survives the reference, matching the shape an inline
1040
+ * `oneOf: [{ $ref }, { type: 'null' }]` already produces.
1041
+ */
1042
+ let activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
1043
+ /** Drop `null` from an `enum:` list — it marks nullability, it is not a member. */
1044
+ function nonNullEnumValues(values) {
1045
+ return (values ?? []).filter((v) => v !== null);
1046
+ }
1047
+ /** True when an enum schema admits `null` by any OAS 3.0 / 3.1 spelling. */
1048
+ function isNullableEnumSchema(schema) {
1049
+ if (schema.nullable === true) return true;
1050
+ if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
1051
+ return Array.isArray(schema.enum) && schema.enum.includes(null);
1052
+ }
1053
+ /** Wrap `ref` in `nullable` unless it already is one, so a nullable enum `$ref` is never double-wrapped. */
1054
+ function asNullable(ref) {
1055
+ return ref.kind === "nullable" ? ref : {
1056
+ kind: "nullable",
1057
+ inner: ref
1058
+ };
1059
+ }
1060
+ /**
1061
+ * Apply cleanSchemaName + the active transform (if any) to a raw schema name.
1062
+ * When `rawName` is a `components.schemas` key the transform also receives it
1063
+ * as `originalName`, so a consumer can opt out of normalization by returning
1064
+ * the original spelling. Names oagen synthesises (request-body variant
1065
+ * models, title-derived names, ...) are not component keys and get no
1066
+ * `originalName`.
1067
+ */
526
1068
  function resolveSchemaName(rawName) {
527
1069
  let name = cleanSchemaName(toPascalCase(rawName));
528
- if (activeSchemaNameTransform) name = activeSchemaNameTransform(name);
1070
+ if (activeSchemaNameTransform) {
1071
+ const originalName = activeComponentSchemaKeys.has(rawName) ? rawName : void 0;
1072
+ name = activeSchemaNameTransform(name, originalName);
1073
+ }
529
1074
  return name;
530
1075
  }
1076
+ /** Validation keywords copied through unchanged when they hold a number. */
1077
+ const NUMERIC_CONSTRAINT_KEYS = [
1078
+ "multipleOf",
1079
+ "minLength",
1080
+ "maxLength",
1081
+ "minItems",
1082
+ "maxItems",
1083
+ "minContains",
1084
+ "maxContains",
1085
+ "minProperties",
1086
+ "maxProperties"
1087
+ ];
1088
+ /**
1089
+ * Collect the JSON Schema validation keywords declared directly on `schema`
1090
+ * into a `Constraints` object, or `undefined` when it declares none — so a
1091
+ * schema without limits yields no `constraints` key on its IR node at all.
1092
+ *
1093
+ * Numeric bounds are normalized to the OAS 3.1 shape. OAS 3.0 spells an
1094
+ * exclusive bound as `exclusiveMinimum: true` beside `minimum: n`; that pair
1095
+ * becomes `exclusiveMinimum: n` with no `minimum`. A boolean `false`, or a
1096
+ * `true` with no bound to apply to, is dropped. The 3.1 numeric form passes
1097
+ * through, including the legal-but-unusual case of both an inclusive and an
1098
+ * exclusive bound on the same side. `uniqueItems: false` restates the default
1099
+ * and is dropped too.
1100
+ *
1101
+ * Only the schema's own keywords are read: limits nested inside array `items`
1102
+ * or map values are not collected. The one exception is a nullable wrapper —
1103
+ * a `oneOf`/`anyOf` with a single non-null, non-`$ref` branch — that declares
1104
+ * none itself: it takes that branch's keywords, since the IR collapses it to
1105
+ * `nullable<T>`. Values of the wrong type (a string `maxLength`, say) are
1106
+ * ignored rather than carried.
1107
+ */
1108
+ function extractConstraints(schema) {
1109
+ let out = ownConstraints(schema);
1110
+ for (const member of schema.allOf ?? []) out = combineConstraints(out, extractConstraints(member));
1111
+ const branch = singleNonNullBranch(schema);
1112
+ return branch ? combineConstraints(out, extractConstraints(branch)) : out;
1113
+ }
1114
+ /** The keywords declared strictly on `schema`, normalized — without the nullable-branch fallback. */
1115
+ function ownConstraints(schema) {
1116
+ const out = {};
1117
+ const lower = normalizeBound(schema.minimum, schema.exclusiveMinimum);
1118
+ if (lower.inclusive !== void 0) out.minimum = lower.inclusive;
1119
+ if (lower.exclusive !== void 0) out.exclusiveMinimum = lower.exclusive;
1120
+ const upper = normalizeBound(schema.maximum, schema.exclusiveMaximum);
1121
+ if (upper.inclusive !== void 0) out.maximum = upper.inclusive;
1122
+ if (upper.exclusive !== void 0) out.exclusiveMaximum = upper.exclusive;
1123
+ for (const key of NUMERIC_CONSTRAINT_KEYS) {
1124
+ const value = schema[key];
1125
+ if (typeof value === "number") out[key] = value;
1126
+ }
1127
+ if (typeof schema.pattern === "string") out.pattern = schema.pattern;
1128
+ if (schema.uniqueItems === true) out.uniqueItems = true;
1129
+ const dependentRequired = readDependentRequired(schema.dependentRequired);
1130
+ if (dependentRequired) out.dependentRequired = dependentRequired;
1131
+ return Object.keys(out).length > 0 ? out : void 0;
1132
+ }
1133
+ /** The one non-null, non-`$ref` branch of a `oneOf`/`anyOf`, or `undefined` for anything else. */
1134
+ function singleNonNullBranch(schema) {
1135
+ const variants = schema.oneOf ?? schema.anyOf;
1136
+ if (!variants) return void 0;
1137
+ const nonNull = variants.filter((v) => !isNullSchema(v));
1138
+ return nonNull.length === 1 && !nonNull[0].$ref ? nonNull[0] : void 0;
1139
+ }
1140
+ /** True for the `{ type: 'null' }` / `{ type: ['null'] }` branch of a nullable wrapper. */
1141
+ function isNullSchema(schema) {
1142
+ return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
1143
+ }
1144
+ /**
1145
+ * `{ constraints }` when `schema` declares any validation keyword, `{}`
1146
+ * otherwise — spread into an IR node so the key stays absent rather than
1147
+ * present-but-undefined.
1148
+ */
1149
+ function constraintsEntry(schema) {
1150
+ const constraints = extractConstraints(schema);
1151
+ return constraints ? { constraints } : {};
1152
+ }
1153
+ /** Keywords where the stricter of two declarations is the larger value. */
1154
+ const MIN_KEYS = [
1155
+ "minimum",
1156
+ "exclusiveMinimum",
1157
+ "minLength",
1158
+ "minItems",
1159
+ "minContains",
1160
+ "minProperties"
1161
+ ];
1162
+ /** Keywords where the stricter of two declarations is the smaller value. */
1163
+ const MAX_KEYS = [
1164
+ "maximum",
1165
+ "exclusiveMaximum",
1166
+ "maxLength",
1167
+ "maxItems",
1168
+ "maxContains",
1169
+ "maxProperties"
1170
+ ];
1171
+ /**
1172
+ * Conjoin two constraint sets the way `allOf` conjoins schemas: an instance
1173
+ * must satisfy both, so every `min*` takes the larger value, every `max*` the
1174
+ * smaller, `uniqueItems` holds if either side demands it, and
1175
+ * `dependentRequired` merges per property with the union of its names
1176
+ * (deduplicated, `a`'s order first). A keyword declared on one side only is
1177
+ * kept as is, and an absent side yields the other unchanged.
1178
+ *
1179
+ * `multipleOf` and `pattern` keep `a`'s value when the two differ: a true
1180
+ * conjunction would need the least common multiple, or a lookahead regex
1181
+ * joining both patterns, and neither is worth carrying into the IR.
1182
+ *
1183
+ * Not for the parameter `$ref` fallback in extractParams: keywords written
1184
+ * beside a `$ref` override the target's there, which is a different operation.
1185
+ */
1186
+ function combineConstraints(a, b) {
1187
+ if (!a) return b;
1188
+ if (!b) return a;
1189
+ const out = {
1190
+ ...a,
1191
+ ...b
1192
+ };
1193
+ for (const key of MIN_KEYS) {
1194
+ const [x, y] = [a[key], b[key]];
1195
+ if (x !== void 0 && y !== void 0) out[key] = Math.max(x, y);
1196
+ }
1197
+ for (const key of MAX_KEYS) {
1198
+ const [x, y] = [a[key], b[key]];
1199
+ if (x !== void 0 && y !== void 0) out[key] = Math.min(x, y);
1200
+ }
1201
+ if (a.uniqueItems || b.uniqueItems) out.uniqueItems = true;
1202
+ if (a.multipleOf !== void 0 && b.multipleOf !== void 0) out.multipleOf = a.multipleOf;
1203
+ if (a.pattern !== void 0 && b.pattern !== void 0) out.pattern = a.pattern;
1204
+ if (a.dependentRequired && b.dependentRequired) out.dependentRequired = unionDependentRequired(a.dependentRequired, b.dependentRequired);
1205
+ return out;
1206
+ }
1207
+ /** Per-property union of two `dependentRequired` maps, deduplicated and keeping `a`'s order first. */
1208
+ function unionDependentRequired(a, b) {
1209
+ const merged = /* @__PURE__ */ new Map();
1210
+ for (const [property, names] of Object.entries(a)) merged.set(property, [...names]);
1211
+ for (const [property, names] of Object.entries(b)) {
1212
+ const existing = merged.get(property);
1213
+ if (!existing) {
1214
+ merged.set(property, [...names]);
1215
+ continue;
1216
+ }
1217
+ for (const name of names) if (!existing.includes(name)) existing.push(name);
1218
+ }
1219
+ return Object.fromEntries(merged);
1220
+ }
1221
+ /**
1222
+ * Resolve one side's bound from `minimum` / `maximum` plus its `exclusive*`
1223
+ * companion into the OAS 3.1 shape: a numeric companion is the exclusive
1224
+ * bound itself, `true` turns the inclusive bound into the exclusive one, and
1225
+ * `false` or absent leaves the inclusive bound as is.
1226
+ */
1227
+ function normalizeBound(inclusive, exclusive) {
1228
+ const bound = typeof inclusive === "number" ? inclusive : void 0;
1229
+ if (typeof exclusive === "number") return {
1230
+ inclusive: bound,
1231
+ exclusive
1232
+ };
1233
+ if (exclusive === true) return { exclusive: bound };
1234
+ return { inclusive: bound };
1235
+ }
1236
+ /** Copy `dependentRequired` when it is an object whose values are string arrays; anything else is ignored. */
1237
+ function readDependentRequired(value) {
1238
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1239
+ const entries = [];
1240
+ for (const [property, deps] of Object.entries(value)) if (Array.isArray(deps) && deps.every((d) => typeof d === "string")) entries.push([property, [...deps]]);
1241
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
1242
+ }
1243
+ /**
1244
+ * Narrow a raw `examples` value to a non-empty list, or undefined. Guards
1245
+ * against documents that put an OAS Example Object map (or anything else)
1246
+ * where JSON Schema expects an array, and against an empty list, so the IR
1247
+ * carries `examples` only when there is at least one value.
1248
+ */
1249
+ function nonEmptyExamples(value) {
1250
+ return Array.isArray(value) && value.length > 0 ? value : void 0;
1251
+ }
1252
+ /**
1253
+ * Pick the single `example` the IR carries: the document's own `example`
1254
+ * when it is set (including an explicit `null`), otherwise the first entry
1255
+ * of the resolved `examples` list.
1256
+ */
1257
+ function pickExample(example, examples) {
1258
+ return example !== void 0 ? example : examples?.[0];
1259
+ }
1260
+ /**
1261
+ * Resolve `example` and `examples` across ordered precedence levels: for a
1262
+ * parameter its own declaration, its inline schema, then the component its
1263
+ * `$ref` points at; for a field its own schema, then its `$ref` target.
1264
+ *
1265
+ * Each value walks the levels independently. `examples` is the first
1266
+ * non-empty list. `example` comes from the first level that has either an
1267
+ * `example` or a list, preferring that level's own `example` over its first
1268
+ * list entry — so a schema-level `example` can never mask the parameter's
1269
+ * own `examples` map. A level with neither falls through; an explicit
1270
+ * `null` is a value and stops the walk.
1271
+ */
1272
+ function resolveExamples(levels) {
1273
+ let example;
1274
+ let examples;
1275
+ for (const level of levels) {
1276
+ if (examples === void 0) examples = level.examples;
1277
+ if (example === void 0) example = pickExample(level.example, level.examples);
1278
+ }
1279
+ return {
1280
+ example,
1281
+ examples
1282
+ };
1283
+ }
1284
+ /**
1285
+ * If a schema is a `$ref` to `#/components/schemas/X`, return the referenced
1286
+ * component schema. Used to surface schema-level metadata (default, example,
1287
+ * examples, deprecated, description) that would otherwise be invisible at the
1288
+ * field or parameter level.
1289
+ *
1290
+ * Refs are not pre-resolved by the bundler (`dereference: false` in refs.ts),
1291
+ * so without this, a param like `schema: { $ref: '.../PaginationOrder' }`
1292
+ * silently loses the target's `default: desc` and the SDK stops emitting the
1293
+ * spec-mandated default value in generated method signatures.
1294
+ */
1295
+ function resolveComponentSchemaRef(schema, componentSchemas) {
1296
+ if (!schema || !componentSchemas) return void 0;
1297
+ const ref = schema.$ref;
1298
+ if (!ref) return void 0;
1299
+ const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
1300
+ if (!match) return void 0;
1301
+ return componentSchemas[match[1]];
1302
+ }
531
1303
  function extractSchemas(schemas, options) {
532
1304
  const enums = [];
533
1305
  if (!schemas) return {
534
1306
  models: [],
535
1307
  enums
536
1308
  };
1309
+ activeComponentSchemaKeys = new Set(Object.keys(schemas));
537
1310
  if (options?.schemaNameTransform) {
538
1311
  const transform = options.schemaNameTransform;
539
- const cleanedNames = Object.keys(schemas).map((n) => cleanSchemaName(toPascalCase(n)));
540
- const transformedToOriginals = /* @__PURE__ */ new Map();
541
- for (const cleaned of cleanedNames) {
542
- const transformed = transform(cleaned);
543
- if (!transformedToOriginals.has(transformed)) transformedToOriginals.set(transformed, []);
544
- transformedToOriginals.get(transformed).push(cleaned);
545
- }
546
- const unsafeToTransform = /* @__PURE__ */ new Set();
547
- for (const [, originals] of transformedToOriginals) if (originals.length > 1) for (const n of originals) unsafeToTransform.add(n);
548
- activeSchemaNameTransform = (name) => unsafeToTransform.has(name) ? name : transform(name);
1312
+ const transformedToRawKeys = /* @__PURE__ */ new Map();
1313
+ for (const raw of Object.keys(schemas)) {
1314
+ const transformed = transform(cleanSchemaName(toPascalCase(raw)), raw);
1315
+ if (!transformedToRawKeys.has(transformed)) transformedToRawKeys.set(transformed, []);
1316
+ transformedToRawKeys.get(transformed).push(raw);
1317
+ }
1318
+ const unsafeRawKeys = /* @__PURE__ */ new Set();
1319
+ const unsafeCleanedNames = /* @__PURE__ */ new Set();
1320
+ for (const [, rawKeys] of transformedToRawKeys) if (rawKeys.length > 1) for (const raw of rawKeys) {
1321
+ unsafeRawKeys.add(raw);
1322
+ unsafeCleanedNames.add(cleanSchemaName(toPascalCase(raw)));
1323
+ }
1324
+ activeSchemaNameTransform = (name, originalName) => {
1325
+ return (originalName !== void 0 ? unsafeRawKeys.has(originalName) : unsafeCleanedNames.has(name)) ? name : transform(name, originalName);
1326
+ };
549
1327
  } else activeSchemaNameTransform = null;
550
1328
  activeEnumSchemaNames = /* @__PURE__ */ new Set();
551
- for (const [name, schema] of Object.entries(schemas)) if (schema.enum) activeEnumSchemaNames.add(resolveSchemaName(name));
1329
+ activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
1330
+ for (const [name, schema] of Object.entries(schemas)) {
1331
+ if (!schema.enum) continue;
1332
+ const resolved = resolveSchemaName(name);
1333
+ activeEnumSchemaNames.add(resolved);
1334
+ if (isNullableEnumSchema(schema)) activeNullableEnumSchemaNames.add(resolved);
1335
+ }
552
1336
  const modelsByCleanName = /* @__PURE__ */ new Map();
553
1337
  for (const [name, schema] of Object.entries(schemas)) {
554
1338
  const pascalName = resolveSchemaName(name);
@@ -559,7 +1343,7 @@ function extractSchemas(schemas, options) {
559
1343
  if (existing) {
560
1344
  if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
561
1345
  } else modelsByCleanName.set(pascalName, model);
562
- for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName)) {
1346
+ for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName, schemas)) {
563
1347
  const existingInline = modelsByCleanName.get(inlineModel.name);
564
1348
  if (existingInline) {
565
1349
  if (inlineModel.fields.length > existingInline.fields.length) modelsByCleanName.set(inlineModel.name, inlineModel);
@@ -584,6 +1368,8 @@ function extractSchemas(schemas, options) {
584
1368
  function clearSchemaNameTransform() {
585
1369
  activeSchemaNameTransform = null;
586
1370
  activeEnumSchemaNames = /* @__PURE__ */ new Set();
1371
+ activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
1372
+ activeComponentSchemaKeys = /* @__PURE__ */ new Set();
587
1373
  }
588
1374
  /**
589
1375
  * Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
@@ -619,14 +1405,17 @@ function collectInlineEnums(fields, enums) {
619
1405
  * that schemaToTypeRef turns into model refs but aren't extracted from components.
620
1406
  */
621
1407
  function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
1408
+ const directRefs = directModelRefNames(ref);
622
1409
  walkTypeRef(ref, { model: (modelRef) => {
623
1410
  if (modelNames.has(modelRef.name)) return;
1411
+ if (!directRefs.has(modelRef.name)) return;
624
1412
  if (Object.keys(schemas).some((k) => resolveSchemaName(k) === modelRef.name)) return;
625
1413
  const parentSchema = findSchemaByName(parentModelName, schemas);
626
1414
  if (!parentSchema) return;
627
1415
  const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
628
1416
  if (!fieldSchema) return;
629
- const extracted = extractNestedSchema(modelRef.name, fieldSchema);
1417
+ const reserved = reservedInlineNames(fieldName, collectProperties(parentSchema, schemas), parentModelName);
1418
+ const extracted = extractNestedSchema(unionBaseName(modelRef.name, fieldName, fieldSchema, parentModelName, reserved), fieldSchema, schemas, reserved);
630
1419
  for (const m of extracted) if (!modelNames.has(m.name)) {
631
1420
  models.push(m);
632
1421
  modelNames.add(m.name);
@@ -634,6 +1423,35 @@ function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, mod
634
1423
  }
635
1424
  } });
636
1425
  }
1426
+ /**
1427
+ * The base name `extractNestedSchema` should name a field's `oneOf` variants
1428
+ * from: the field's qualified inline name when naming the variants from it
1429
+ * yields `refName` (the ref the walk is resolving), else `refName` itself,
1430
+ * which is right for every non-union field and for a union whose variant 0 is
1431
+ * bare.
1432
+ */
1433
+ function unionBaseName(refName, fieldName, fieldSchema, parentModelName, reserved) {
1434
+ if (!fieldSchema.oneOf) return refName;
1435
+ const fromField = qualifyInlineModelName(toPascalCase(fieldName), parentModelName);
1436
+ return [...nameInlineVariants(fieldSchema, fromField, reserved).values()].includes(refName) ? fromField : refName;
1437
+ }
1438
+ /**
1439
+ * The model refs a field's TypeRef produces directly: the ref itself, the
1440
+ * items of a (possibly nullable) array, or each (possibly nullable) variant of
1441
+ * a union — the shapes `extractNestedSchema` can name from a ref. Refs nested
1442
+ * any deeper are excluded.
1443
+ */
1444
+ function directModelRefNames(ref) {
1445
+ const names = /* @__PURE__ */ new Set();
1446
+ let root = ref;
1447
+ while (root.kind === "nullable" || root.kind === "array") root = root.kind === "nullable" ? root.inner : root.items;
1448
+ const leaves = root.kind === "union" ? root.variants : [root];
1449
+ for (const leaf of leaves) {
1450
+ const inner = leaf.kind === "nullable" ? leaf.inner : leaf;
1451
+ if (inner.kind === "model") names.add(inner.name);
1452
+ }
1453
+ return names;
1454
+ }
637
1455
  /** Find a component schema by PascalCase name. */
638
1456
  function findSchemaByName(pascalName, schemas) {
639
1457
  for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
@@ -662,104 +1480,278 @@ function findNestedFieldSchema(fieldName, parentSchema, schemas) {
662
1480
  }
663
1481
  return null;
664
1482
  }
665
- /** Extract a model (and nested models) from an inline field schema. */
666
- function extractNestedSchema(name, schema) {
1483
+ /**
1484
+ * Extract a model (and nested models) from an inline field schema. `reserved`
1485
+ * holds the inline names the field's sibling properties mint
1486
+ * (`reservedInlineNames`), which a `oneOf` variant must not take.
1487
+ */
1488
+ function extractNestedSchema(name, schema, schemas, reserved = NO_RESERVED) {
667
1489
  if (schema.oneOf) {
668
1490
  const models = [];
669
- const constNamingDiscriminator = deriveConstNamingDiscriminator(schema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type)));
1491
+ const variantNames = nameInlineVariants(schema, name, reserved);
670
1492
  for (const variant of schema.oneOf) {
671
- if (variant.$ref) continue;
672
- if (variant.properties && (variant.type === "object" || !variant.type)) {
673
- const requiredSet = new Set(variant.required ?? []);
674
- const fields = [];
675
- for (const [fn, fs] of Object.entries(variant.properties)) {
676
- if (!fs) continue;
677
- fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
678
- }
679
- const variantName = nameVariantModel(variant, name, models, constNamingDiscriminator);
680
- models.push({
681
- name: variantName,
682
- description: variant.description,
683
- fields
684
- });
1493
+ const variantName = variantNames.get(variant);
1494
+ if (variantName === void 0) continue;
1495
+ const requiredSet = new Set(variant.required ?? []);
1496
+ const siblings = collectProperties(variant);
1497
+ const fields = [];
1498
+ for (const [fn, fs] of Object.entries(variant.properties ?? {})) {
1499
+ if (!fs) continue;
1500
+ fields.push(buildFieldFromSchema(fn, fs, variantName, requiredSet, schemas, siblings));
685
1501
  }
1502
+ models.push({
1503
+ name: variantName,
1504
+ description: variant.description,
1505
+ fields,
1506
+ ...constraintsEntry(variant)
1507
+ });
686
1508
  }
687
1509
  return models;
688
1510
  }
689
1511
  if (schema.properties && (schema.type === "object" || !schema.type)) {
690
1512
  const requiredSet = new Set(schema.required ?? []);
1513
+ const siblings = collectProperties(schema);
691
1514
  const fields = [];
692
1515
  for (const [fn, fs] of Object.entries(schema.properties)) {
693
1516
  if (!fs) continue;
694
- fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
1517
+ fields.push(buildFieldFromSchema(fn, fs, name, requiredSet, schemas, siblings));
695
1518
  }
696
1519
  return [{
697
1520
  name,
698
1521
  description: schema.description,
699
- fields
1522
+ fields,
1523
+ ...constraintsEntry(schema)
700
1524
  }];
701
1525
  }
702
1526
  if (schema.type === "array" && schema.items?.properties) {
703
1527
  const requiredSet = new Set(schema.items.required ?? []);
1528
+ const siblings = collectProperties(schema.items);
704
1529
  const fields = [];
705
1530
  for (const [fn, fs] of Object.entries(schema.items.properties)) {
706
1531
  if (!fs) continue;
707
- fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
1532
+ fields.push(buildFieldFromSchema(fn, fs, name, requiredSet, schemas, siblings));
708
1533
  }
709
1534
  return [{
710
1535
  name,
711
1536
  description: schema.items.description,
712
- fields
1537
+ fields,
1538
+ ...constraintsEntry(schema.items)
713
1539
  }];
714
1540
  }
715
1541
  return [];
716
1542
  }
717
1543
  /**
718
- * Derive a meaningful name for a oneOf object variant past the first,
719
- * falling back to the numeric-suffix scheme (`Foo2`, `Foo3`) when no
720
- * better signal is available. The "better signal" is a const-valued
721
- * discriminator property that distinguishes one variant from the others.
722
- *
723
- * For a schema like `ApiKey.owner` whose oneOf has two variants — one
724
- * with `type: { const: "organization" }` and one with `type: { const:
725
- * "user" }` — this returns `ApiKeyOwner` for the first variant (the
726
- * baseline name that the union TypeRef will reference, so consumer-
727
- * visible typed fields stay backward-compatible) and `UserApiKeyOwner`
728
- * for the second instead of `ApiKeyOwner2`. SDK consumers reading the
729
- * generated types can tell the variants apart at a glance.
730
- *
731
- * The first variant always keeps the bare parent name because
732
- * `schemaToTypeRef` builds union variants whose model refs collapse to
733
- * `qualifyInlineModelName(field, parent)` — so the first model name
734
- * must match for the union's degenerate-collapse to land on a valid
735
- * type. Renaming variant 0 would orphan it.
736
- *
737
- * Falls back to numeric suffix when:
738
- * - The variant has no discriminator property (no shared const
739
- * property across all object variants), OR
740
- * - The const value PascalCases to an empty/whitespace name, OR
741
- * - The derived name collides with the parent name or an already-
742
- * emitted variant name (numeric keeps uniqueness).
743
- */
744
- function nameVariantModel(variant, parentName, alreadyEmitted, discriminator) {
745
- if (alreadyEmitted.length === 0) return parentName;
1544
+ * The prefix a variant's `title` contributes to its name: the title trimmed
1545
+ * and PascalCased, or null when there is no title or it PascalCases to
1546
+ * nothing.
1547
+ */
1548
+ function variantTitlePrefix(variant) {
1549
+ const title = typeof variant["title"] === "string" ? variant["title"].trim() : "";
1550
+ return title ? toPascalCase(title) || null : null;
1551
+ }
1552
+ /** `<Prefix><Base>`, or `base` unchanged when it already starts with the prefix (no doubling). */
1553
+ function prefixedVariantName(prefix, base) {
1554
+ return base.startsWith(prefix) ? base : `${prefix}${base}`;
1555
+ }
1556
+ /**
1557
+ * The properties `variant` declares that no other inline object variant of
1558
+ * the same union declares, in the variant's own `properties` order.
1559
+ */
1560
+ function distinguishingProperties(variant, siblings) {
1561
+ const declaredElsewhere = /* @__PURE__ */ new Set();
1562
+ for (const sibling of siblings) {
1563
+ if (sibling === variant) continue;
1564
+ for (const property of Object.keys(sibling.properties ?? {})) declaredElsewhere.add(property);
1565
+ }
1566
+ return Object.keys(variant.properties ?? {}).filter((property) => !declaredElsewhere.has(property));
1567
+ }
1568
+ /**
1569
+ * Name an inline object variant of a `oneOf`/`anyOf` that has no better
1570
+ * signal: it is not variant 0 (which keeps the union's base name) and no
1571
+ * const discriminator gave it a prefix. The name is derived from the
1572
+ * variant's own content, never from where it sits in the document (beyond
1573
+ * its position among the variants, which only the final counter reads), so
1574
+ * an SDK user can tell the variants apart and reordering them changes nothing
1575
+ * but which one is variant 0. In order, taking the first candidate that is
1576
+ * neither the base name nor a name already given to an earlier variant of
1577
+ * the same union:
1578
+ *
1579
+ * 1. the variant's `title` (trimmed and PascalCased): `<Title><Base>`
1580
+ * (`CircleDrawingShape`), the same prefix form a const discriminator
1581
+ * produces, including the rule that a base already starting with the
1582
+ * prefix is not doubled (that candidate equals the base, so it falls
1583
+ * through); a title an earlier variant already took falls through too;
1584
+ * 2. a distinguishing property — the first property, in the variant's own
1585
+ * order, that it declares and no other inline object variant declares:
1586
+ * `<Base>With<Property>` (`DrawingShapeWithSide`); a property that
1587
+ * PascalCases to nothing is skipped;
1588
+ * 3. the position counter, `<Base><position + 1>` (`DrawingShape2`), which
1589
+ * now remains only for variants whose property sets are identical, and
1590
+ * which keeps counting past a taken name (`DrawingShape3`).
1591
+ *
1592
+ * Every candidate is also checked against `reserved`: the inline names the
1593
+ * parent's other properties mint (`reservedInlineNames`), so a variant can
1594
+ * never take a name another field's model already has — beside a
1595
+ * `shape_with_side` object, the `{ side }` variant of `shape` is not
1596
+ * `DrawingShapeWithSide` but the next candidate. Read from sibling names and
1597
+ * content only, so document order plays no part.
1598
+ *
1599
+ * `siblings` are the inline object variants of the union, this one included;
1600
+ * `emitted` are the names already given to the earlier ones, in order. Every
1601
+ * site that names a variant — the union TypeRef, the component-field,
1602
+ * request-body and response extractors, and `mintedInlineNames` — goes
1603
+ * through this function (via `nameInlineVariant`), so they cannot drift.
1604
+ */
1605
+ function nameUndiscriminatedVariant(variant, siblings, base, emitted, reserved = NO_RESERVED) {
1606
+ const taken = (candidate) => candidate === base || emitted.includes(candidate) || reserved.has(candidate);
1607
+ const titlePrefix = variantTitlePrefix(variant);
1608
+ if (titlePrefix !== null) {
1609
+ const candidate = prefixedVariantName(titlePrefix, base);
1610
+ if (!taken(candidate)) return candidate;
1611
+ }
1612
+ for (const property of distinguishingProperties(variant, siblings)) {
1613
+ const word = toPascalCase(property);
1614
+ if (!word) continue;
1615
+ const candidate = `${base}With${word}`;
1616
+ if (!taken(candidate)) return candidate;
1617
+ }
1618
+ for (let position = emitted.length + 1;; position++) {
1619
+ const candidate = `${base}${position}`;
1620
+ if (!taken(candidate)) return candidate;
1621
+ }
1622
+ }
1623
+ /** No reserved names: for a union with no sibling properties (a request body or response that is itself a union). */
1624
+ const NO_RESERVED = /* @__PURE__ */ new Set();
1625
+ /**
1626
+ * True when the union's inline object variants (two or more) each carry a
1627
+ * title and no two titles PascalCase to the same prefix: the one case in
1628
+ * which variant 0 is named from its title rather than keeping the base name.
1629
+ */
1630
+ function allDistinctlyTitled(siblings) {
1631
+ if (siblings.length < 2) return false;
1632
+ const prefixes = siblings.map(variantTitlePrefix);
1633
+ return prefixes.every((prefix) => prefix !== null) && new Set(prefixes).size === prefixes.length;
1634
+ }
1635
+ /**
1636
+ * The name of one inline object variant of a `oneOf`/`anyOf`, given the
1637
+ * union's base name (the field's qualified inline name, e.g. `ApiKeyOwner`),
1638
+ * the inline object variants of the union (`siblings`, this one included),
1639
+ * the names already given to the earlier variants (`emitted`, in order), and
1640
+ * the const-valued property that distinguishes the variants when there is
1641
+ * one (`deriveConstNamingDiscriminator`).
1642
+ *
1643
+ * Variant 0 keeps the bare base name: the union TypeRef and every extractor
1644
+ * reference it under that name, and a consumer's typed field keeps the name
1645
+ * it always had. The one exception: when the union has no const discriminator
1646
+ * and every variant carries a distinct title, variant 0 is named from its
1647
+ * title like the others (`CircleDrawingShape` beside `SquareDrawingShape`)
1648
+ * instead of being the only one left bare. Whether the union is discriminated
1649
+ * is read from the variants themselves here, not from `discriminator`, so a
1650
+ * caller forcing a property (an explicit `discriminator:` without `mapping:`)
1651
+ * still agrees with the extractors on variant 0.
1652
+ *
1653
+ * A later variant takes `<ConstValue><Base>` from the discriminator
1654
+ * (`UserApiKeyOwner`) when the value PascalCases to something and the name is
1655
+ * neither the base, nor already emitted, nor minted by one of the parent's
1656
+ * other properties (`reserved`), and otherwise falls to
1657
+ * `nameUndiscriminatedVariant`: its title, a distinguishing property, then
1658
+ * the position counter, each checked the same way.
1659
+ */
1660
+ function nameInlineVariant(variant, siblings, base, emitted, discriminator, reserved = NO_RESERVED) {
1661
+ if (emitted.length === 0) {
1662
+ const titlePrefix = deriveConstNamingDiscriminator([...siblings]) === null && allDistinctlyTitled(siblings) ? variantTitlePrefix(variant) : null;
1663
+ return titlePrefix === null ? base : prefixedVariantName(titlePrefix, base);
1664
+ }
746
1665
  if (discriminator) {
747
1666
  const constValue = getConstPropertyValue(variant, discriminator.property);
748
- if (constValue) {
749
- const prefix = toPascalCase(constValue);
750
- if (prefix) {
751
- const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
752
- if (!(candidate === parentName || alreadyEmitted.some((m) => m.name === candidate))) return candidate;
1667
+ const prefix = constValue ? toPascalCase(constValue) : "";
1668
+ if (prefix) {
1669
+ const candidate = prefixedVariantName(prefix, base);
1670
+ if (candidate !== base && !emitted.includes(candidate) && !reserved.has(candidate)) return candidate;
1671
+ }
1672
+ }
1673
+ return nameUndiscriminatedVariant(variant, siblings, base, emitted, reserved);
1674
+ }
1675
+ /**
1676
+ * Name every inline object variant of a `oneOf`/`anyOf` schema from the
1677
+ * union's base name, keyed by variant schema identity; null, non-object and
1678
+ * `$ref` variants get no entry. Positions are local to this variant list, so
1679
+ * the union's TypeRef entries and the extracted models agree no matter what
1680
+ * else has been extracted. `reserved` holds the inline names the parent's
1681
+ * other properties mint (`reservedInlineNames`); every site that has a
1682
+ * sibling set passes the same one, so the reference and the model step
1683
+ * around them identically.
1684
+ *
1685
+ * One union shape is named differently: a `oneOf` with no explicit
1686
+ * `discriminator:` whose every variant carries a `$ref` or a `title` and pins
1687
+ * the same const property (`titleMappedDiscriminatorProperty`). Its
1688
+ * discriminator mapping has always named an inline variant by its title
1689
+ * (`resolveSchemaName(title)`: `Circle`, `Square`, run through the project's
1690
+ * schema-name transform like a component), and `schemaToTypeRef` references
1691
+ * the variant by that mapped name, so the title is the model name here too —
1692
+ * unless it is the base, already emitted, or reserved by a sibling, in which
1693
+ * case the variant takes a context-qualified name (its title as a prefix on
1694
+ * the base, `CircleDrawingMarker`, else a distinguishing property, else the
1695
+ * counter) and the mapping, which is read from this map, follows it. A kept
1696
+ * title is context-free: it is the same whatever field, array or map the
1697
+ * union sits under, so it names one shape wherever it appears, as a
1698
+ * component name would. A second union beside it using the same title for a
1699
+ * variant of the identical shape (`schemaShapeKey`) therefore shares the
1700
+ * model instead of stepping around it; a sibling giving the same title a
1701
+ * different shape reserves it (`reservedInlineNames`), and since both sides
1702
+ * read their siblings in isolation the decision is symmetric: when the
1703
+ * shapes differ, both unions fall back to their context-qualified names
1704
+ * (`CircleDrawingShape`, `CircleDrawingMarker`) and neither keeps the bare
1705
+ * `Circle`; when they are identical, both keep `Circle` and one model is
1706
+ * emitted. `elementContextName` never lets a context-free name drive an
1707
+ * items/values step-aside, which no context could satisfy.
1708
+ * `nameInlineVariantEntries` reports which names are context-free; a
1709
+ * fallen-back title name is context-dependent.
1710
+ *
1711
+ * This is the list-level entry point every site uses: `schemaToTypeRef` (via
1712
+ * `nameInlineObjectVariants`), `extractNestedSchema`,
1713
+ * `extractInlineModelDeep`, `extractInlineModelsFromProperties` and
1714
+ * `mintedInlineNames`.
1715
+ */
1716
+ function nameInlineVariants(union, base, reserved = NO_RESERVED) {
1717
+ const names = /* @__PURE__ */ new Map();
1718
+ for (const [variant, entry] of nameInlineVariantEntries(union, base, reserved)) names.set(variant, entry.name);
1719
+ return names;
1720
+ }
1721
+ /** `nameInlineVariants`, also telling for each name whether it is context-free. */
1722
+ function nameInlineVariantEntries(union, base, reserved = NO_RESERVED) {
1723
+ const nonNullVariants = (union.oneOf ?? union.anyOf ?? []).filter((v) => !isNullVariantSchema(v));
1724
+ const inlineObjectVariants = nonNullVariants.filter((v) => isInlineObjectSchema(v));
1725
+ const discriminator = deriveConstNamingDiscriminator(inlineObjectVariants);
1726
+ const titleMapped = union.oneOf !== void 0 && !union.discriminator && titleMappedDiscriminatorProperty(nonNullVariants) !== null;
1727
+ const emitted = [];
1728
+ const entries = /* @__PURE__ */ new Map();
1729
+ for (const variant of inlineObjectVariants) {
1730
+ let name = null;
1731
+ let contextFree = false;
1732
+ if (titleMapped) {
1733
+ const titled = resolveVariantModelName(variant);
1734
+ if (titled !== null) {
1735
+ if (titled !== base && !emitted.includes(titled) && !reserved.has(titled)) {
1736
+ name = titled;
1737
+ contextFree = true;
1738
+ } else name = nameUndiscriminatedVariant(variant, inlineObjectVariants, base, emitted, reserved);
753
1739
  }
754
1740
  }
1741
+ name ??= nameInlineVariant(variant, inlineObjectVariants, base, emitted, discriminator, reserved);
1742
+ emitted.push(name);
1743
+ entries.set(variant, {
1744
+ name,
1745
+ contextFree
1746
+ });
755
1747
  }
756
- return `${parentName}${alreadyEmitted.length + 1}`;
1748
+ return entries;
757
1749
  }
758
1750
  /**
759
1751
  * Returns a discriminator descriptor when every inline object variant in a
760
1752
  * oneOf has a string-const value on the same property — the signature of a
761
1753
  * discriminated union without an explicit `discriminator:` keyword. Used by
762
- * `nameVariantModel` to derive variant names from the const values.
1754
+ * `nameInlineVariant` to derive variant names from the const values.
763
1755
  *
764
1756
  * Distinct from `detectConstPropertyDiscriminator` (which requires every
765
1757
  * variant to carry a `$ref` or `title` so a name mapping can be built):
@@ -777,18 +1769,96 @@ function deriveConstNamingDiscriminator(variants) {
777
1769
  }
778
1770
  return null;
779
1771
  }
780
- /** Build a single Field from a schema property entry. Shared across all extraction sites. */
781
- function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet) {
1772
+ /**
1773
+ * True for a schema that `schemaToTypeRef` turns into an inline model reference:
1774
+ * it declares `properties` directly (no `$ref`) and is an object — whether by
1775
+ * `type: 'object'`, by omitting `type`, or by an OAS 3.1 nullable array such as
1776
+ * `type: ['object', 'null']`. Every inline-model extractor must use the same
1777
+ * test, or a field's reference points at a model nobody materializes.
1778
+ */
1779
+ function isInlineObjectSchema(schema) {
1780
+ if (!schema || schema.$ref || !schema.properties) return false;
1781
+ const { type } = schema;
1782
+ return type === void 0 || type === "object" || Array.isArray(type) && type.includes("object");
1783
+ }
1784
+ /** The `{ type: 'null' }` member that marks a `oneOf`/`anyOf` as nullable; never a variant to name. */
1785
+ function isNullVariantSchema(schema) {
1786
+ return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
1787
+ }
1788
+ /**
1789
+ * The synthetic context names a multi-type inline variant's non-object
1790
+ * members are typed and extracted under, chosen so the models they name can
1791
+ * never collide with a model named from one of the variant's own properties.
1792
+ * An `array` member's items go under `item` (`<Variant>Item`), falling back
1793
+ * to `array_item` (`<Variant>ArrayItem`) and then `array_item2`,
1794
+ * `array_item3`, ... when a property already claims that name; any other
1795
+ * member goes under `value` (`<Variant>Value`), then `member_value`,
1796
+ * `member_value2`, .... A candidate is skipped when the model name
1797
+ * `qualifyInlineModelName` derives from it equals one derived from a
1798
+ * property — compared after qualification, so a property `items`, whose
1799
+ * name singularizes to `<Variant>Item`, blocks `item` as well.
1800
+ * This is the single source of these names for the TypeRef builder
1801
+ * (`inlineVariantTypeRef`) and the extractors (`variantMemberSchemas`).
1802
+ */
1803
+ function variantMemberKeys(variant, variantName) {
1804
+ const modelNameFor = (key) => qualifyInlineModelName(toPascalCase(key), variantName);
1805
+ const claimed = new Set(Object.keys(variant.properties ?? {}).map(modelNameFor));
1806
+ const pick = (preferred, fallback) => {
1807
+ for (let i = 0;; i++) {
1808
+ const candidate = i === 0 ? preferred : i === 1 ? fallback : `${fallback}${i}`;
1809
+ if (!claimed.has(modelNameFor(candidate))) return candidate;
1810
+ }
1811
+ };
1812
+ return {
1813
+ item: pick("item", "array_item"),
1814
+ value: pick("value", "member_value")
1815
+ };
1816
+ }
1817
+ /**
1818
+ * The schemas carried by the non-object members of a multi-type inline
1819
+ * variant (`type: ['object', 'array']` with `items`), keyed by the synthetic
1820
+ * context name from `variantMemberKeys` that `schemaToTypeRef` types them
1821
+ * under, so an inline object item is `<Variant>Item` (or `<Variant>ArrayItem`
1822
+ * when `item` is taken) and its own nested objects `<Variant>ItemChild`.
1823
+ * Every extractor re-enters with these under the variant's name so the
1824
+ * models line up with the refs by construction. A map member cannot occur
1825
+ * here: a variant with `properties` is typed as a model, never as a map.
1826
+ */
1827
+ function variantMemberSchemas(variant, variantName) {
1828
+ const members = {};
1829
+ if (Array.isArray(variant.type) && variant.type.includes("array") && variant.items) members[variantMemberKeys(variant, variantName).item] = variant.items;
1830
+ return members;
1831
+ }
1832
+ /**
1833
+ * Build a single Field from a schema property entry. Shared across all
1834
+ * extraction sites. Pass `componentSchemas` where they are at hand so a
1835
+ * field whose schema is a bare `$ref` can take `example`/`examples` from the
1836
+ * component it points at, as a parameter does. `siblingProperties` are the
1837
+ * other properties of the object the field belongs to (`collectProperties`);
1838
+ * array items and map values use them to step aside from a sibling that
1839
+ * would take their singular name (`elementContextName`).
1840
+ */
1841
+ function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet, componentSchemas, siblingProperties) {
1842
+ const refTarget = resolveComponentSchemaRef(fieldSchema, componentSchemas);
1843
+ const { example, examples } = resolveExamples([{
1844
+ example: fieldSchema.example,
1845
+ examples: nonEmptyExamples(fieldSchema.examples)
1846
+ }, {
1847
+ example: refTarget?.example,
1848
+ examples: nonEmptyExamples(refTarget?.examples)
1849
+ }]);
782
1850
  return {
783
1851
  name: fieldName,
784
- type: schemaToTypeRef(fieldSchema, fieldName, contextName),
1852
+ type: schemaToTypeRef(fieldSchema, fieldName, contextName, siblingProperties),
785
1853
  required: requiredSet.has(fieldName),
786
1854
  description: fieldSchema.description,
787
1855
  readOnly: fieldSchema.readOnly || void 0,
788
1856
  writeOnly: fieldSchema.writeOnly || void 0,
789
1857
  deprecated: fieldSchema.deprecated || void 0,
790
1858
  default: fieldSchema.default,
791
- example: fieldSchema.example
1859
+ example,
1860
+ ...examples ? { examples } : {},
1861
+ ...constraintsEntry(fieldSchema)
792
1862
  };
793
1863
  }
794
1864
  /**
@@ -825,39 +1895,27 @@ function buildSyntheticEnumRef(values, contextName, parentModelName) {
825
1895
  };
826
1896
  }
827
1897
  /**
828
- * Detect an implicit discriminator on an oneOf where every variant is an
829
- * object schema that pins the same property to a const value. Returns
830
- * { property, mapping } so the IR can represent the union as discriminated
831
- * without needing an explicit `discriminator:` key in the spec.
1898
+ * Detect an implicit discriminator on a `oneOf` whose every variant pins the
1899
+ * same property to a const value AND is nameable on its own — by a `$ref` to
1900
+ * a component or by a `title` — so the IR can represent the union as
1901
+ * discriminated, with a mapping to concrete names, without an explicit
1902
+ * `discriminator:` keyword. Returns the property, or null when any variant
1903
+ * doesn't fit the pattern or when the variants disagree on which property
1904
+ * carries the discriminator. The mapping itself is built by `schemaToTypeRef`
1905
+ * from the names `nameInlineVariants` gives the inline variants (their titles
1906
+ * unless taken) and the component names of `$ref` variants, so it can only
1907
+ * ever point at a model that exists.
832
1908
  *
833
- * Both shapes are supported:
1909
+ * Both const shapes are supported:
834
1910
  * { properties: { event: { const: "x" } }, required: ["event"] }
835
1911
  * { properties: { event: { type: "string", enum: ["x"] } } }
836
- *
837
- * Returns null when any variant doesn't fit the pattern or when variants
838
- * disagree on which property carries the discriminator.
839
1912
  */
840
- function detectConstPropertyDiscriminator(variants) {
1913
+ function titleMappedDiscriminatorProperty(variants) {
841
1914
  if (variants.length < 2) return null;
842
- let candidateProperty = null;
1915
+ if (!variants.every((v) => resolveVariantModelName(v) !== null)) return null;
843
1916
  const candidates = Object.keys(variants[0]?.properties ?? {});
844
- for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) {
845
- candidateProperty = propName;
846
- break;
847
- }
848
- if (!candidateProperty) return null;
849
- const mapping = {};
850
- for (const v of variants) {
851
- const value = getConstPropertyValue(v, candidateProperty);
852
- if (value === null) return null;
853
- const variantName = resolveVariantModelName(v);
854
- if (!variantName) return null;
855
- mapping[value] = variantName;
856
- }
857
- return {
858
- property: candidateProperty,
859
- mapping
860
- };
1917
+ for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) return propName;
1918
+ return null;
861
1919
  }
862
1920
  function getConstPropertyValue(schema, property) {
863
1921
  const prop = schema.properties?.[property];
@@ -883,42 +1941,96 @@ function mapVariantToDiscriminatorEntry(variant, discriminator) {
883
1941
  }
884
1942
  /**
885
1943
  * Derive a discriminator value → variant model name mapping for an inline-
886
- * variant `oneOf` whose explicit `discriminator:` has no `mapping:` clause.
887
- *
888
- * Each variant model name is reproduced via `nameVariantModel` so it matches
889
- * what `extractNestedSchema` registered when it walked the same `oneOf` to
890
- * pull variants out as named models. Variant 0 keeps the bare parent name
891
- * (e.g. `ApiKeyOwner`); subsequent variants get a const-derived prefix
892
- * (e.g. `UserApiKeyOwner` for the `type: const: user` variant).
893
- *
894
- * The "parent name" used here mirrors the one `collectNestedInlineModels`
895
- * passes to `extractNestedSchema`: it's the qualified inline model name
896
- * built from `contextName` (the field name) plus `parentModelName` (the
897
- * containing model). Returns null when any non-object variant is present
898
- * or when a variant lacks a const value on the discriminator property.
1944
+ * variant `oneOf` from the names `nameInlineVariants` gave its variants
1945
+ * (`inlineVariantNames`): the explicit `discriminator:` without a `mapping:`
1946
+ * clause, and the implicit const discriminator of anonymous inline variants
1947
+ * (`ApiKeyCreatedData.owner`: `ApiKeyCreatedDataOwner`,
1948
+ * `UserApiKeyCreatedDataOwner`). Reading the names rather than deriving them
1949
+ * again is what keeps the mapping pointing at models the extractors create.
1950
+ * Returns null without a context name, when any variant is not an inline
1951
+ * object, or when a variant lacks a const value on the property.
899
1952
  */
900
- function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, parentModelName) {
1953
+ function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, inlineVariantNames) {
901
1954
  if (variants.length === 0 || !contextName) return null;
902
- const inlineObjectVariants = variants.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
903
- if (inlineObjectVariants.length !== variants.length) return null;
904
- const baseInlineName = qualifyInlineModelName(toPascalCase(contextName), parentModelName);
905
- const namingDiscriminator = { property: discriminatorProperty };
906
1955
  const mapping = {};
907
- const emittedSoFar = [];
908
- for (const variant of inlineObjectVariants) {
1956
+ for (const variant of variants) {
1957
+ const variantName = inlineVariantNames.get(variant);
1958
+ if (variantName === void 0) return null;
909
1959
  const constValue = getConstPropertyValue(variant, discriminatorProperty);
910
1960
  if (constValue === null) return null;
911
- const variantName = nameVariantModel(variant, baseInlineName, emittedSoFar, namingDiscriminator);
912
1961
  mapping[constValue] = variantName;
913
- emittedSoFar.push({
914
- name: variantName,
915
- fields: []
916
- });
917
1962
  }
918
1963
  return mapping;
919
1964
  }
1965
+ /**
1966
+ * The names reserved for a union typed under `contextName`: the set an
1967
+ * enclosing field handed down (`reservedNames`, for a union among a field's
1968
+ * array items or map values), else the inline names the field's own sibling
1969
+ * properties mint (`reservedInlineNames`), else nothing. The extractor
1970
+ * derives the same set at each of those levels, so the reference and the
1971
+ * model step around a sibling's name identically.
1972
+ */
1973
+ function reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames) {
1974
+ if (reservedNames) return reservedNames;
1975
+ if (contextName === void 0) return NO_RESERVED;
1976
+ return reservedInlineNames(contextName, siblingProperties, parentModelName);
1977
+ }
1978
+ /**
1979
+ * Name the inline object variants of a field's `oneOf`/`anyOf` exactly as the
1980
+ * inline-model extractors do (`extractInlineModelsFromProperties` in
1981
+ * inline-models.ts, and `extractNestedSchema` / `extractInlineModelDeep`
1982
+ * here): the union's base name is the field's qualified inline name, the
1983
+ * names the field's sibling properties mint — or the set handed down from an
1984
+ * enclosing field — are reserved (`reservedNamesFor`), and
1985
+ * `nameInlineVariants` hands out the variant names from there. Returned keyed
1986
+ * by variant schema identity.
1987
+ */
1988
+ function nameInlineObjectVariants(union, contextName, parentModelName, siblingProperties, reservedNames) {
1989
+ return nameInlineVariants(union, qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName), reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames));
1990
+ }
1991
+ /**
1992
+ * The TypeRef for an inline object variant referenced by the name its
1993
+ * extracted model carries, keeping every other type the variant allows.
1994
+ * Mirrors what `schemaToTypeRef` builds for the same schema — one member per
1995
+ * non-null entry of a `type` array, `union`-wrapped when there is more than
1996
+ * one, `nullable`-wrapped when `null` or OAS 3.0 `nullable: true` is present
1997
+ * — with the `object` member swapped for the named ref:
1998
+ * `type: ['object', 'string']` → `union[model, string]`,
1999
+ * `['object', 'integer', 'null']` → `nullable<union[model, integer]>`,
2000
+ * `['object', 'null']` / `nullable: true` → `nullable<model>`.
2001
+ * A `oneOf: [X, { type: 'null' }]` variant has no `properties` of its own
2002
+ * and never reaches this path; `schemaToTypeRef` types it directly.
2003
+ *
2004
+ * Anything inline under a non-object member is named from the variant, never
2005
+ * from the union's field: an `array` member types its items under the
2006
+ * variant as parent with the context `variantMemberKeys` picks (`item` →
2007
+ * `<Variant>Item`, or `array_item` → `<Variant>ArrayItem` when a property
2008
+ * claims `item`), and any other member under its `value` key
2009
+ * (`<Variant>Value` for an inline enum, else `<Variant>MemberValue`). Naming
2010
+ * them from the field would hand variant 0's model name to every variant's
2011
+ * items.
2012
+ */
2013
+ function inlineVariantTypeRef(variant, name) {
2014
+ const modelRef = {
2015
+ kind: "model",
2016
+ name
2017
+ };
2018
+ const typeList = Array.isArray(variant.type) ? variant.type : [];
2019
+ const nullable = variant.nullable === true || typeList.includes("null");
2020
+ const keys = variantMemberKeys(variant, name);
2021
+ const members = typeList.filter((t) => t !== "null").map((t) => t === "object" ? modelRef : schemaToTypeRef({
2022
+ ...variant,
2023
+ type: t,
2024
+ nullable: false
2025
+ }, t === "array" ? keys.item : keys.value, name));
2026
+ const ref = members.length > 1 ? {
2027
+ kind: "union",
2028
+ variants: members
2029
+ } : modelRef;
2030
+ return nullable ? asNullable(ref) : ref;
2031
+ }
920
2032
  function extractEnum(name, schema) {
921
- const values = (schema.enum ?? []).map((v) => ({
2033
+ const values = nonNullEnumValues(schema.enum).map((v) => ({
922
2034
  name: toUpperSnakeCase(String(v)),
923
2035
  value: typeof v === "number" ? v : String(v),
924
2036
  description: void 0
@@ -934,9 +2046,10 @@ function extractModel(name, schema, schemas) {
934
2046
  if (schema.allOf) return extractAllOfModel(name, schema, schemas);
935
2047
  const requiredSet = new Set(schema.required ?? []);
936
2048
  const fields = [];
2049
+ const siblings = collectProperties(schema, schemas);
937
2050
  for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
938
2051
  if (!fieldSchema) continue;
939
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
2052
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, schemas, siblings));
940
2053
  }
941
2054
  if (fields.length === 0 && (schema.oneOf || schema.anyOf)) {
942
2055
  const variants = schema.oneOf ?? schema.anyOf ?? [];
@@ -947,7 +2060,7 @@ function extractModel(name, schema, schemas) {
947
2060
  for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
948
2061
  if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
949
2062
  seenFieldNames.add(fieldName);
950
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
2063
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
951
2064
  }
952
2065
  }
953
2066
  }
@@ -968,12 +2081,14 @@ function extractModel(name, schema, schemas) {
968
2081
  return {
969
2082
  name,
970
2083
  description: schema.description,
971
- fields
2084
+ fields,
2085
+ ...constraintsEntry(schema)
972
2086
  };
973
2087
  }
974
- function extractAllOfModel(name, schema, schemas) {
2088
+ function extractAllOfModel(name, schema, schemas, siblingProperties) {
975
2089
  const fields = [];
976
2090
  const requiredSet = /* @__PURE__ */ new Set();
2091
+ const siblings = siblingProperties ?? collectProperties(schema, schemas);
977
2092
  let resultDiscriminator;
978
2093
  for (const subSchema of schema.allOf ?? []) {
979
2094
  let resolved = subSchema;
@@ -983,7 +2098,7 @@ function extractAllOfModel(name, schema, schemas) {
983
2098
  if (refName && schemas[refName]) resolved = schemas[refName];
984
2099
  }
985
2100
  if (resolved.allOf) {
986
- const nested = extractAllOfModel(name, resolved, schemas);
2101
+ const nested = extractAllOfModel(name, resolved, schemas, siblings);
987
2102
  for (const f of nested.fields) {
988
2103
  fields.push({
989
2104
  ...f,
@@ -1003,7 +2118,7 @@ function extractAllOfModel(name, schema, schemas) {
1003
2118
  for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
1004
2119
  if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
1005
2120
  seenFieldNames.add(fieldName);
1006
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
2121
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
1007
2122
  }
1008
2123
  }
1009
2124
  }
@@ -1013,7 +2128,7 @@ function extractAllOfModel(name, schema, schemas) {
1013
2128
  const emptyRequired = /* @__PURE__ */ new Set();
1014
2129
  for (const [fieldName, fieldSchema] of Object.entries(resolved.properties)) {
1015
2130
  if (!fieldSchema) continue;
1016
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
2131
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
1017
2132
  }
1018
2133
  }
1019
2134
  }
@@ -1024,7 +2139,8 @@ function extractAllOfModel(name, schema, schemas) {
1024
2139
  name,
1025
2140
  description: schema.description,
1026
2141
  fields,
1027
- ...resultDiscriminator ? { discriminator: resultDiscriminator } : {}
2142
+ ...resultDiscriminator ? { discriminator: resultDiscriminator } : {},
2143
+ ...constraintsEntry(schema)
1028
2144
  };
1029
2145
  }
1030
2146
  /**
@@ -1095,18 +2211,23 @@ function detectAllOfVariantDiscriminator(variants, fallbackName) {
1095
2211
  mapping
1096
2212
  };
1097
2213
  }
1098
- function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
2214
+ function extractDiscriminatedAllOfVariantModels(schema, fallbackName, schemas) {
1099
2215
  const models = [];
1100
2216
  const seenNames = /* @__PURE__ */ new Set();
1101
2217
  const baseProperties = {};
1102
2218
  const baseRequired = /* @__PURE__ */ new Set();
2219
+ let baseConstraints = ownConstraints(schema);
1103
2220
  for (const subSchema of schema.allOf ?? []) {
1104
- if (subSchema.oneOf) continue;
2221
+ if (subSchema.oneOf) {
2222
+ baseConstraints = combineConstraints(baseConstraints, ownConstraints(subSchema));
2223
+ continue;
2224
+ }
1105
2225
  const flat = subSchema.allOf ? flattenVariantForExtraction(subSchema) : subSchema;
1106
2226
  if (flat.properties) {
1107
2227
  for (const [k, v] of Object.entries(flat.properties)) if (!(k in baseProperties) && v) baseProperties[k] = v;
1108
2228
  }
1109
2229
  if (flat.required) for (const r of flat.required) baseRequired.add(r);
2230
+ baseConstraints = combineConstraints(baseConstraints, extractConstraints(subSchema));
1110
2231
  }
1111
2232
  for (const subSchema of schema.allOf ?? []) for (const variant of subSchema.oneOf ?? []) {
1112
2233
  const flat = flattenVariantForExtraction(variant);
@@ -1116,10 +2237,11 @@ function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
1116
2237
  const merged = {
1117
2238
  type: "object",
1118
2239
  properties: mergedProperties,
1119
- required: [...new Set([...baseRequired, ...flat.required ?? []])]
2240
+ required: [.../* @__PURE__ */ new Set([...baseRequired, ...flat.required ?? []])],
2241
+ ...combineConstraints(baseConstraints, extractConstraints(variant))
1120
2242
  };
1121
2243
  const variantName = deriveDiscriminatedVariantName(variant, fallbackName);
1122
- for (const model of extractInlineModelDeep(variantName, merged)) {
2244
+ for (const model of extractInlineModelDeep(variantName, merged, schemas)) {
1123
2245
  if (seenNames.has(model.name)) continue;
1124
2246
  seenNames.add(model.name);
1125
2247
  models.push(model);
@@ -1225,16 +2347,27 @@ function deriveDiscriminatedVariantName(schema, fallbackName) {
1225
2347
  if (pascal.endsWith(fallbackName)) return pascal;
1226
2348
  return `${fallbackName}${pascal}`;
1227
2349
  }
1228
- function extractInlineModelDeep(name, schema) {
2350
+ function extractInlineModelDeep(name, schema, schemas) {
1229
2351
  const requiredSet = new Set(schema.required ?? []);
1230
2352
  const fields = [];
1231
2353
  const properties = schema.properties ?? {};
1232
2354
  const nestedModels = [];
2355
+ const siblings = collectProperties(schema);
1233
2356
  for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1234
2357
  if (!fieldSchema) continue;
1235
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
1236
- if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema));
1237
- if (fieldSchema.type === "array" && fieldSchema.items?.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema.items));
2358
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, schemas, siblings));
2359
+ if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema, schemas));
2360
+ if (fieldSchema.type === "array" && fieldSchema.items?.properties) {
2361
+ const itemContext = elementContextName(fieldName, "item", siblings, name, fieldSchema.items);
2362
+ nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, itemContext), fieldSchema.items, schemas));
2363
+ }
2364
+ if (fieldSchema.type === "object" && !fieldSchema.properties) {
2365
+ const valueSchema = mapValueSchema(fieldSchema);
2366
+ if (valueSchema && !valueSchema.$ref && valueSchema.properties) {
2367
+ const valueContext = elementContextName(fieldName, "value", siblings, name, valueSchema);
2368
+ nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, valueContext), valueSchema, schemas));
2369
+ }
2370
+ }
1238
2371
  if (fieldSchema.allOf) {
1239
2372
  const mergedProperties = {};
1240
2373
  const mergedRequired = [];
@@ -1245,22 +2378,20 @@ function extractInlineModelDeep(name, schema) {
1245
2378
  if (Object.keys(mergedProperties).length > 0) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), {
1246
2379
  type: "object",
1247
2380
  properties: mergedProperties,
1248
- required: mergedRequired
1249
- }));
2381
+ required: mergedRequired,
2382
+ ...extractConstraints(fieldSchema)
2383
+ }, schemas));
1250
2384
  }
1251
2385
  if (fieldSchema.oneOf) {
1252
- const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
2386
+ const inlineObjectVariants = fieldSchema.oneOf.filter((v) => isInlineObjectSchema(v));
1253
2387
  if (inlineObjectVariants.length > 0) {
1254
- const baseQualified = qualifyNestedInlineName(name, fieldName);
1255
- const namingDisc = deriveConstNamingDiscriminator(inlineObjectVariants);
1256
- const emitted = [];
2388
+ const variantNames = nameInlineVariants(fieldSchema, qualifyNestedInlineName(name, fieldName), reservedInlineNames(fieldName, siblings, name));
1257
2389
  for (const variant of inlineObjectVariants) {
1258
- const variantName = nameVariantModel(variant, baseQualified, emitted, namingDisc);
1259
- emitted.push({
1260
- name: variantName,
1261
- fields: []
1262
- });
1263
- nestedModels.push(...extractInlineModelDeep(variantName, variant));
2390
+ const variantName = variantNames.get(variant);
2391
+ if (variantName === void 0) continue;
2392
+ nestedModels.push(...extractInlineModelDeep(variantName, variant, schemas));
2393
+ const memberSchemas = variantMemberSchemas(variant, variantName);
2394
+ if (Object.keys(memberSchemas).length > 0) nestedModels.push(...extractInlineModelDeep(variantName, { properties: memberSchemas }, schemas).slice(1));
1264
2395
  }
1265
2396
  }
1266
2397
  }
@@ -1268,7 +2399,8 @@ function extractInlineModelDeep(name, schema) {
1268
2399
  return [{
1269
2400
  name,
1270
2401
  description: schema.description,
1271
- fields
2402
+ fields,
2403
+ ...constraintsEntry(schema)
1272
2404
  }, ...nestedModels];
1273
2405
  }
1274
2406
  function qualifyNestedInlineName(parentName, fieldName) {
@@ -1279,15 +2411,28 @@ function qualifyNestedInlineName(parentName, fieldName) {
1279
2411
  if (match && match[2]) return match[1] + singularize(match[2]);
1280
2412
  return qualified;
1281
2413
  }
1282
- function schemaToTypeRef(schema, contextName, parentModelName) {
2414
+ /**
2415
+ * `elementContextName` for an optional context: with no field name there is
2416
+ * nothing to decide, and the items / values keep an undefined context.
2417
+ */
2418
+ function elementContext(contextName, kind, siblingProperties, parentModelName, elementSchema) {
2419
+ return contextName === void 0 ? void 0 : elementContextName(contextName, kind, siblingProperties, parentModelName, elementSchema);
2420
+ }
2421
+ function schemaToTypeRef(schema, contextName, parentModelName, siblingProperties, reservedNames) {
1283
2422
  if (schema.$ref) {
1284
2423
  const segments = schema.$ref.split("/");
1285
2424
  const rawName = segments[segments.length - 1];
1286
2425
  const resolved = resolveSchemaName(rawName);
1287
- if (activeEnumSchemaNames.has(resolved)) return {
1288
- kind: "enum",
1289
- name: resolved
1290
- };
2426
+ if (activeEnumSchemaNames.has(resolved)) {
2427
+ const enumRef = {
2428
+ kind: "enum",
2429
+ name: resolved
2430
+ };
2431
+ return activeNullableEnumSchemaNames.has(resolved) ? {
2432
+ kind: "nullable",
2433
+ inner: enumRef
2434
+ } : enumRef;
2435
+ }
1291
2436
  return {
1292
2437
  kind: "model",
1293
2438
  name: resolved
@@ -1302,21 +2447,18 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1302
2447
  type: "unknown"
1303
2448
  }
1304
2449
  };
1305
- if (schema.type.includes("null") && nonNullTypes.length === 1) return {
1306
- kind: "nullable",
1307
- inner: schemaToTypeRef({
1308
- ...schema,
1309
- type: nonNullTypes[0],
1310
- nullable: false
1311
- }, contextName)
1312
- };
2450
+ if (schema.type.includes("null") && nonNullTypes.length === 1) return asNullable(schemaToTypeRef({
2451
+ ...schema,
2452
+ type: nonNullTypes[0],
2453
+ nullable: false
2454
+ }, contextName, parentModelName, siblingProperties, reservedNames));
1313
2455
  if (nonNullTypes.length > 1) {
1314
2456
  const ref = {
1315
2457
  kind: "union",
1316
2458
  variants: nonNullTypes.map((t) => schemaToTypeRef({
1317
2459
  ...schema,
1318
2460
  type: t
1319
- }, contextName))
2461
+ }, contextName, parentModelName, siblingProperties, reservedNames))
1320
2462
  };
1321
2463
  if (schema.type.includes("null")) return {
1322
2464
  kind: "nullable",
@@ -1325,13 +2467,10 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1325
2467
  return ref;
1326
2468
  }
1327
2469
  }
1328
- if (schema.nullable && schema.type) return {
1329
- kind: "nullable",
1330
- inner: schemaToTypeRef({
1331
- ...schema,
1332
- nullable: false
1333
- }, contextName)
1334
- };
2470
+ if (schema.nullable && schema.type) return asNullable(schemaToTypeRef({
2471
+ ...schema,
2472
+ nullable: false
2473
+ }, contextName, parentModelName, siblingProperties, reservedNames));
1335
2474
  if (schema.allOf) {
1336
2475
  const refItem = schema.allOf.find((s) => s.$ref);
1337
2476
  if (refItem) {
@@ -1341,7 +2480,7 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1341
2480
  };
1342
2481
  return schemaToTypeRef(refItem, contextName, parentModelName);
1343
2482
  }
1344
- if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName);
2483
+ if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName, siblingProperties, reservedNames);
1345
2484
  if (schema.allOf.some((s) => s.properties)) return {
1346
2485
  kind: "model",
1347
2486
  name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
@@ -1350,12 +2489,9 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1350
2489
  if (schema.oneOf || schema.anyOf) {
1351
2490
  const compositionKind = schema.oneOf ? "oneOf" : "anyOf";
1352
2491
  const rawVariants = schema.oneOf ?? schema.anyOf ?? [];
1353
- const nullVariant = rawVariants.find((v) => v.type === "null" || Array.isArray(v.type) && v.type.length === 1 && v.type[0] === "null");
2492
+ const nullVariant = rawVariants.find(isNullVariantSchema);
1354
2493
  const nonNullVariants = rawVariants.filter((v) => v !== nullVariant);
1355
- if (nullVariant && nonNullVariants.length === 1) return {
1356
- kind: "nullable",
1357
- inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
1358
- };
2494
+ if (nullVariant && nonNullVariants.length === 1) return asNullable(schemaToTypeRef(nonNullVariants[0], contextName, parentModelName, siblingProperties, reservedNames));
1359
2495
  const literalStrings = collectLiteralStringConsts(nonNullVariants);
1360
2496
  if (literalStrings !== null && literalStrings.length >= 2) {
1361
2497
  const enumRef = buildSyntheticEnumRef(literalStrings, contextName, parentModelName);
@@ -1364,13 +2500,14 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1364
2500
  inner: enumRef
1365
2501
  } : enumRef;
1366
2502
  }
2503
+ const inlineVariantNames = nameInlineObjectVariants(schema, contextName, parentModelName, siblingProperties, reservedNames);
1367
2504
  let resolvedDiscriminator;
1368
2505
  if (schema.discriminator) {
1369
2506
  const property = schema.discriminator.propertyName;
1370
2507
  const explicitMapping = schema.discriminator.mapping ?? {};
1371
2508
  let mapping = Object.fromEntries(Object.entries(explicitMapping).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]));
1372
2509
  if (Object.keys(mapping).length === 0) {
1373
- const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName, parentModelName);
2510
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName, inlineVariantNames);
1374
2511
  if (inlineMapping) mapping = inlineMapping;
1375
2512
  }
1376
2513
  resolvedDiscriminator = {
@@ -1378,12 +2515,22 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1378
2515
  mapping
1379
2516
  };
1380
2517
  } else if (compositionKind === "oneOf") {
1381
- const synthetic = detectConstPropertyDiscriminator(nonNullVariants);
1382
- if (synthetic) resolvedDiscriminator = synthetic;
1383
- else {
2518
+ const titleProperty = titleMappedDiscriminatorProperty(nonNullVariants);
2519
+ if (titleProperty) {
2520
+ const mapping = {};
2521
+ for (const v of nonNullVariants) {
2522
+ const value = getConstPropertyValue(v, titleProperty);
2523
+ const mappedName = inlineVariantNames.get(v) ?? resolveVariantModelName(v);
2524
+ if (value !== null && mappedName !== null) mapping[value] = mappedName;
2525
+ }
2526
+ resolvedDiscriminator = {
2527
+ property: titleProperty,
2528
+ mapping
2529
+ };
2530
+ } else {
1384
2531
  const namingDisc = deriveConstNamingDiscriminator(nonNullVariants);
1385
2532
  if (namingDisc) {
1386
- const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName, parentModelName);
2533
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName, inlineVariantNames);
1387
2534
  if (inlineMapping) resolvedDiscriminator = {
1388
2535
  property: namingDisc.property,
1389
2536
  mapping: inlineMapping
@@ -1394,12 +2541,11 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1394
2541
  const variants = [];
1395
2542
  for (const v of rawVariants) {
1396
2543
  if (v === nullVariant) continue;
1397
- const inlineMappingName = resolvedDiscriminator && !v.$ref && v.properties && (v.type === "object" || !v.type) ? mapVariantToDiscriminatorEntry(v, resolvedDiscriminator) : null;
1398
- if (inlineMappingName) variants.push({
1399
- kind: "model",
1400
- name: inlineMappingName
1401
- });
1402
- else variants.push(schemaToTypeRef(v, contextName, parentModelName));
2544
+ const inlineName = inlineVariantNames.get(v);
2545
+ if (inlineName) {
2546
+ const mappedName = resolvedDiscriminator ? mapVariantToDiscriminatorEntry(v, resolvedDiscriminator) : null;
2547
+ variants.push(inlineVariantTypeRef(v, mappedName ?? inlineName));
2548
+ } else variants.push(schemaToTypeRef(v, contextName, parentModelName));
1403
2549
  }
1404
2550
  const hasNull = !!nullVariant;
1405
2551
  const union = {
@@ -1442,30 +2588,30 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1442
2588
  };
1443
2589
  }
1444
2590
  }
1445
- if (schema.enum && schema.enum.length === 1) {
1446
- const v = schema.enum[0];
1447
- return {
2591
+ const enumValues = nonNullEnumValues(schema.enum);
2592
+ const enumAdmitsNull = schema.enum !== void 0 && enumValues.length !== schema.enum.length;
2593
+ if (schema.enum && enumValues.length === 1) {
2594
+ const v = enumValues[0];
2595
+ const literal = {
1448
2596
  kind: "literal",
1449
2597
  value: typeof v === "number" ? v : String(v)
1450
2598
  };
2599
+ return enumAdmitsNull ? asNullable(literal) : literal;
1451
2600
  }
1452
2601
  if (schema.enum) {
1453
2602
  const baseName = toPascalCase(contextName ?? "UnknownEnum");
1454
2603
  const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
1455
2604
  const qualifiedName = cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName;
1456
- return {
2605
+ const enumRef = {
1457
2606
  kind: "enum",
1458
2607
  name: activeSchemaNameTransform ? activeSchemaNameTransform(qualifiedName) : qualifiedName,
1459
- values: schema.enum.map((v) => typeof v === "number" ? v : String(v))
2608
+ values: enumValues.map((v) => typeof v === "number" ? v : String(v))
1460
2609
  };
2610
+ return enumAdmitsNull ? asNullable(enumRef) : enumRef;
1461
2611
  }
1462
- if (schema.type === "array" && schema.items) return {
1463
- kind: "array",
1464
- items: schemaToTypeRef(schema.items, contextName, parentModelName)
1465
- };
1466
- if (!schema.type && schema.items) return {
2612
+ if ((schema.type === "array" || !schema.type) && schema.items) return {
1467
2613
  kind: "array",
1468
- items: schemaToTypeRef(schema.items, contextName, parentModelName)
2614
+ items: schemaToTypeRef(schema.items, elementContext(contextName, "item", siblingProperties, parentModelName, schema.items), parentModelName, void 0, reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames))
1469
2615
  };
1470
2616
  if (schema.type === "object" && schema.properties) return {
1471
2617
  kind: "model",
@@ -1476,12 +2622,8 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1476
2622
  kind: "primitive",
1477
2623
  type: "unknown"
1478
2624
  };
1479
- if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
1480
- if (Object.keys(schema.additionalProperties).length > 0) valueType = schemaToTypeRef(schema.additionalProperties, contextName);
1481
- } else if (schema.patternProperties) {
1482
- const patterns = Object.values(schema.patternProperties);
1483
- if (patterns.length > 0) valueType = schemaToTypeRef(patterns[0], contextName);
1484
- }
2625
+ const valueSchema = mapValueSchema(schema);
2626
+ if (valueSchema) valueType = schemaToTypeRef(valueSchema, elementContext(contextName, "value", siblingProperties, parentModelName, valueSchema), parentModelName, void 0, reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames));
1485
2627
  return {
1486
2628
  kind: "map",
1487
2629
  valueType
@@ -1581,11 +2723,11 @@ function classifyAndExtractResponse(schema, contextName, componentSchemas) {
1581
2723
  isPaginated: false
1582
2724
  };
1583
2725
  const envelope = detectListEnvelope(schema);
1584
- if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath);
1585
- if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName);
2726
+ if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath, componentSchemas);
2727
+ if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName, componentSchemas);
1586
2728
  return extractDirectResource(schema, contextName, componentSchemas);
1587
2729
  }
1588
- const KNOWN_DATA_PATHS = new Set([
2730
+ const KNOWN_DATA_PATHS = /* @__PURE__ */ new Set([
1589
2731
  "data",
1590
2732
  "items",
1591
2733
  "results",
@@ -1643,33 +2785,27 @@ function detectListEnvelope(schema) {
1643
2785
  dataPath: null
1644
2786
  };
1645
2787
  }
1646
- function extractListResponse(schema, contextName, dataPath) {
2788
+ function extractListResponse(schema, contextName, dataPath, componentSchemas) {
1647
2789
  let itemTypeRef = {
1648
2790
  kind: "primitive",
1649
2791
  type: "string"
1650
2792
  };
1651
2793
  const inlineModels = [];
1652
- const propSources = [];
1653
- if (schema.allOf) {
1654
- for (const sub of schema.allOf) if (sub.properties) propSources.push(sub.properties);
1655
- }
1656
- if (schema.properties) propSources.push(schema.properties);
1657
- for (const props of propSources) {
1658
- const dataProp = props[dataPath];
1659
- if (!dataProp) continue;
1660
- if (dataProp.type === "array" && dataProp.items) {
1661
- const items = dataProp.items;
1662
- if (items.$ref) itemTypeRef = schemaToTypeRef(items, contextName);
1663
- else if (items.type === "object" && items.properties) {
1664
- const itemName = contextName.replace(/Response$/, "") + "Item";
1665
- itemTypeRef = {
1666
- kind: "model",
1667
- name: itemName
1668
- };
1669
- inlineModels.push(...extractInlineModel(itemName, items));
1670
- }
2794
+ const { properties, required } = collectEnvelopeProperties(schema, componentSchemas);
2795
+ const dataProp = properties[dataPath];
2796
+ if (dataProp?.type === "array" && dataProp.items) {
2797
+ const items = dataProp.items;
2798
+ if (items.$ref) itemTypeRef = schemaToTypeRef(items, contextName);
2799
+ else if (items.type === "object" && items.properties) {
2800
+ const itemName = contextName.replace(/Response$/, "") + "Item";
2801
+ itemTypeRef = {
2802
+ kind: "model",
2803
+ name: itemName
2804
+ };
2805
+ inlineModels.push(...extractInlineModel(itemName, items, componentSchemas));
1671
2806
  }
1672
2807
  }
2808
+ const { envelopeFields, envelopeCompanions } = extractEnvelopeCompanions(properties, required, dataPath, contextName.replace(/Response$/, ""), componentSchemas);
1673
2809
  return {
1674
2810
  response: {
1675
2811
  kind: "array",
@@ -1678,7 +2814,117 @@ function extractListResponse(schema, contextName, dataPath) {
1678
2814
  inlineModels,
1679
2815
  isPaginated: true,
1680
2816
  dataPath,
1681
- itemType: itemTypeRef
2817
+ itemType: itemTypeRef,
2818
+ envelopeFields,
2819
+ envelopeCompanions: envelopeCompanions.length > 0 ? envelopeCompanions : void 0
2820
+ };
2821
+ }
2822
+ /**
2823
+ * Every property the envelope declares, merged across its `allOf` branches,
2824
+ * with the `required` lists that go with them.
2825
+ *
2826
+ * A `$ref` branch is resolved against the component schemas so fields an
2827
+ * envelope inherits from a shared base (`trace_id` on every response, say)
2828
+ * are not silently dropped. A property declared by more than one branch keeps
2829
+ * both declarations' shape when both are inline objects — `pagination.next`
2830
+ * from one branch and `pagination.total` from another both belong to the
2831
+ * response — and otherwise takes the later declaration, as `allOf` merging
2832
+ * does elsewhere in the parser.
2833
+ */
2834
+ function collectEnvelopeProperties(schema, componentSchemas) {
2835
+ const properties = {};
2836
+ const required = [];
2837
+ const seenRefs = /* @__PURE__ */ new Set();
2838
+ const visit = (s) => {
2839
+ if (s.$ref) {
2840
+ if (seenRefs.has(s.$ref)) return;
2841
+ seenRefs.add(s.$ref);
2842
+ const refName = s.$ref.split("/").pop();
2843
+ const resolved = refName ? componentSchemas?.[refName] : void 0;
2844
+ if (resolved) visit(resolved);
2845
+ return;
2846
+ }
2847
+ for (const sub of s.allOf ?? []) visit(sub);
2848
+ for (const [key, prop] of Object.entries(s.properties ?? {})) {
2849
+ if (!prop) continue;
2850
+ const existing = properties[key];
2851
+ properties[key] = existing ? mergePropertyDeclarations(existing, prop) : prop;
2852
+ }
2853
+ if (s.required) required.push(...s.required);
2854
+ };
2855
+ visit(schema);
2856
+ return {
2857
+ properties,
2858
+ required
2859
+ };
2860
+ }
2861
+ /**
2862
+ * Combine two `allOf` declarations of one property. Inline objects merge their
2863
+ * fields; an array keeps the earlier `items` when the later declaration only
2864
+ * constrains it (`minItems: 1`), so `data: User[]` is not downgraded to
2865
+ * `string[]`; anything else takes the later declaration.
2866
+ */
2867
+ function mergePropertyDeclarations(earlier, later) {
2868
+ if (isInlineObjectSchema(earlier) && isInlineObjectSchema(later)) return {
2869
+ ...earlier,
2870
+ ...later,
2871
+ properties: {
2872
+ ...earlier.properties,
2873
+ ...later.properties
2874
+ },
2875
+ required: [...earlier.required ?? [], ...later.required ?? []]
2876
+ };
2877
+ if (earlier.type === "array" && later.type === "array") return {
2878
+ ...earlier,
2879
+ ...later,
2880
+ items: later.items ?? earlier.items
2881
+ };
2882
+ return later;
2883
+ }
2884
+ /**
2885
+ * Type a list envelope's companion properties as fields, lifting any inline
2886
+ * object among them (and anything nested inside it) to a model.
2887
+ *
2888
+ * Models come from `extractInlineModelsFromProperties`, the extractor that
2889
+ * mirrors `schemaToTypeRef` form for form — plain, nullable and `allOf`
2890
+ * objects, arrays of objects, and every `oneOf`/`anyOf` object variant — so a
2891
+ * field's reference always has a model behind it.
2892
+ *
2893
+ * Each companion is extracted twice: once under the operation's name, which
2894
+ * is how its models enter the IR (`ListUsersListMetadata`), and once under no
2895
+ * parent, which yields the name each model would carry if named from its
2896
+ * property alone (`ListMetadata`). Both runs walk the same code path, so the
2897
+ * two model lists line up index for index. `resolveEnvelopeCompanionNames`
2898
+ * later picks, per companion, which of the two names survives — the bare one
2899
+ * only where every copy of that companion across the API agrees structurally.
2900
+ */
2901
+ function extractEnvelopeCompanions(properties, required, dataPath, operationName, componentSchemas) {
2902
+ const envelopeFields = [];
2903
+ const envelopeCompanions = [];
2904
+ const requiredSet = new Set(required);
2905
+ const siblings = collectProperties({ properties });
2906
+ for (const [key, propSchema] of Object.entries(properties)) {
2907
+ if (key === dataPath) continue;
2908
+ const field = buildFieldFromSchema(key, propSchema, operationName, requiredSet, componentSchemas, siblings);
2909
+ envelopeFields.push(field);
2910
+ const single = {
2911
+ type: "object",
2912
+ properties: { [key]: propSchema }
2913
+ };
2914
+ const models = [];
2915
+ extractInlineModelsFromProperties(single, models, operationName, componentSchemas, siblings);
2916
+ if (models.length === 0) continue;
2917
+ const bare = [];
2918
+ extractInlineModelsFromProperties(single, bare, void 0, componentSchemas, siblings);
2919
+ envelopeCompanions.push({
2920
+ field,
2921
+ models,
2922
+ bareNames: bare.map((m) => m.name)
2923
+ });
2924
+ }
2925
+ return {
2926
+ envelopeFields,
2927
+ envelopeCompanions
1682
2928
  };
1683
2929
  }
1684
2930
  function hasDiscriminantConstField(schema) {
@@ -1724,7 +2970,7 @@ function isSingleResourceWrapper(schema) {
1724
2970
  if (propSchema.oneOf) return propSchema.oneOf.some((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
1725
2971
  return false;
1726
2972
  }
1727
- function extractWrappedResource(schema, contextName) {
2973
+ function extractWrappedResource(schema, contextName, componentSchemas) {
1728
2974
  const props = schema.properties;
1729
2975
  const propSchema = props[schema.required && schema.required.length === 1 ? schema.required[0] : Object.keys(props)[0]];
1730
2976
  const inlineModels = [];
@@ -1733,7 +2979,7 @@ function extractWrappedResource(schema, contextName) {
1733
2979
  const objectVariant = propSchema.oneOf.find((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
1734
2980
  if (objectVariant) {
1735
2981
  const modelName = deriveModelName(objectVariant, resourceName);
1736
- inlineModels.push(...extractInlineModel(modelName, objectVariant));
2982
+ inlineModels.push(...extractInlineModel(modelName, objectVariant, componentSchemas));
1737
2983
  const hasNullVariant = propSchema.oneOf.some((v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null"));
1738
2984
  const modelRef = {
1739
2985
  kind: "model",
@@ -1750,7 +2996,7 @@ function extractWrappedResource(schema, contextName) {
1750
2996
  }
1751
2997
  }
1752
2998
  const modelName = deriveModelName(propSchema, resourceName);
1753
- inlineModels.push(...extractInlineModel(modelName, propSchema));
2999
+ inlineModels.push(...extractInlineModel(modelName, propSchema, componentSchemas));
1754
3000
  return {
1755
3001
  response: {
1756
3002
  kind: "model",
@@ -1764,7 +3010,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1764
3010
  const inlineModels = [];
1765
3011
  if (schema.properties && (schema.type === "object" || !schema.type)) {
1766
3012
  const modelName = deriveModelName(schema, contextName);
1767
- inlineModels.push(...extractInlineModel(modelName, schema));
3013
+ inlineModels.push(...extractInlineModel(modelName, schema, componentSchemas));
1768
3014
  return {
1769
3015
  response: {
1770
3016
  kind: "model",
@@ -1811,10 +3057,11 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1811
3057
  const merged = {
1812
3058
  type: "object",
1813
3059
  properties: mergedProperties,
1814
- required: mergedRequired
3060
+ required: mergedRequired,
3061
+ ...extractConstraints(schema)
1815
3062
  };
1816
3063
  const modelName = mergedRefFields ? toPascalCase(contextName) : deriveModelName(merged, contextName);
1817
- inlineModels.push(...extractInlineModel(modelName, merged));
3064
+ inlineModels.push(...extractInlineModel(modelName, merged, componentSchemas));
1818
3065
  return {
1819
3066
  response: {
1820
3067
  kind: "model",
@@ -1825,12 +3072,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1825
3072
  };
1826
3073
  }
1827
3074
  }
1828
- if (schema.oneOf) {
1829
- for (const variant of schema.oneOf) if (variant.properties && (variant.type === "object" || !variant.type)) {
1830
- const modelName = deriveModelName(variant, contextName);
1831
- if (!new Set(inlineModels.map((m) => m.name)).has(modelName)) inlineModels.push(...extractInlineModel(modelName, variant));
1832
- }
1833
- }
3075
+ if (schema.oneOf) extractInlineModelsFromProperties({ properties: { [contextName]: schema } }, inlineModels, void 0, componentSchemas);
1834
3076
  if (schema.type === "array" && schema.items) {
1835
3077
  const items = schema.items;
1836
3078
  if (items.$ref) return {
@@ -1843,7 +3085,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1843
3085
  };
1844
3086
  if (items.properties && (items.type === "object" || !items.type)) {
1845
3087
  const itemName = contextName.replace(/Response$/, "") + "Item";
1846
- inlineModels.push(...extractInlineModel(itemName, items));
3088
+ inlineModels.push(...extractInlineModel(itemName, items, componentSchemas));
1847
3089
  return {
1848
3090
  response: {
1849
3091
  kind: "array",
@@ -1873,70 +3115,288 @@ function deriveModelName(schema, fallback) {
1873
3115
  return toPascalCase(fallback);
1874
3116
  }
1875
3117
  /**
1876
- * Qualify a nested inline model name with its parent to avoid generic names.
1877
- * e.g., parent="Connection" + field="domains" → "ConnectionDomain" (singularized)
1878
- * Only qualifies when the field name alone would be too generic.
3118
+ * Materialise an inline response object (and every nested inline object
3119
+ * under it) as models. `componentSchemas` only feeds `buildFieldFromSchema`,
3120
+ * so a response field that `$ref`s a component takes the component's
3121
+ * `example`/`examples` exactly as a request-body or component-model field
3122
+ * does; nested models get the same map so the fallback reaches every level.
1879
3123
  */
1880
- function qualifyNestedName(parentName, fieldName) {
1881
- const pascalField = toPascalCase(fieldName);
1882
- if (pascalField.startsWith(parentName)) return pascalField;
1883
- const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
1884
- const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
1885
- if (match && match[2]) {
1886
- const trailing = singularize(match[2]);
1887
- return match[1] + trailing;
1888
- }
1889
- return qualified;
1890
- }
1891
- function extractInlineModel(name, schema) {
3124
+ function extractInlineModel(name, schema, componentSchemas) {
1892
3125
  const requiredSet = new Set(schema.required ?? []);
1893
3126
  const fields = [];
1894
3127
  const properties = schema.properties ?? {};
1895
3128
  const nestedModels = [];
3129
+ const siblings = collectProperties(schema);
1896
3130
  for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1897
3131
  if (!fieldSchema) continue;
1898
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
1899
- if (fieldSchema.type === "object" && fieldSchema.properties) {
1900
- const nestedName = qualifyNestedName(name, fieldName);
1901
- nestedModels.push(...extractInlineModel(nestedName, fieldSchema));
1902
- }
1903
- if (fieldSchema.type === "array" && fieldSchema.items) {
1904
- const items = fieldSchema.items;
1905
- if (items.type === "object" && items.properties) {
1906
- const nestedName = qualifyNestedName(name, fieldName);
1907
- nestedModels.push(...extractInlineModel(nestedName, items));
1908
- }
3132
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, componentSchemas, siblings));
3133
+ }
3134
+ extractInlineModelsFromProperties(schema, nestedModels, name, componentSchemas);
3135
+ return [{
3136
+ name,
3137
+ description: schema.description,
3138
+ fields,
3139
+ ...constraintsEntry(schema)
3140
+ }, ...nestedModels];
3141
+ }
3142
+ //#endregion
3143
+ //#region src/parser/security.ts
3144
+ /**
3145
+ * Convert a document- or operation-level `security` list into the IR shape,
3146
+ * preserving what the list says rather than flattening it:
3147
+ *
3148
+ * - absent (or not a list) → `undefined`: nothing declared here.
3149
+ * - `[]` → `[]`: no authentication required.
3150
+ * - `[{ a: [] }, { b: [] }]` → `[[a], [b]]`: either scheme suffices.
3151
+ * - `[{ a: [], b: [] }]` → `[[a, b]]`: both schemes are required together.
3152
+ * - `[{ a: [] }, {}]` → `[[a], []]`: the empty set is an anonymous alternative,
3153
+ * which is how a document marks authentication optional.
3154
+ *
3155
+ * An entry that is not an object (`null`, say) is not a requirement object.
3156
+ * Reading it as `{}` would declare an anonymous alternative and make the
3157
+ * operation look open, so it is dropped with an `[oagen] Warning:` naming
3158
+ * `context` (the operation, or the document). A non-empty list left with no
3159
+ * valid entry is treated as undeclared (`undefined`) rather than as `[]`:
3160
+ * the document never said "no authentication", so the operation keeps
3161
+ * inheriting the default.
3162
+ *
3163
+ * The previous flattening collapsed the alternatives onto one list and turned
3164
+ * `[]` into `undefined`, so "no auth" read the same as "use the default" and
3165
+ * "either of" the same as "all of" (#182).
3166
+ */
3167
+ function extractSecurityRequirements(security, context) {
3168
+ if (!Array.isArray(security)) return void 0;
3169
+ const sets = [];
3170
+ for (const entry of security) {
3171
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
3172
+ console.warn(`[oagen] Warning: Ignoring security requirement ${JSON.stringify(entry)} on ${context} — not a requirement object`);
3173
+ continue;
1909
3174
  }
1910
- if (fieldSchema.allOf) {
1911
- const mergedProperties = {};
1912
- const mergedRequired = [];
1913
- for (const sub of fieldSchema.allOf) {
1914
- if (sub.properties) Object.assign(mergedProperties, sub.properties);
1915
- if (sub.required) mergedRequired.push(...sub.required);
1916
- }
1917
- if (Object.keys(mergedProperties).length > 0) {
1918
- const nestedName = qualifyNestedName(name, fieldName);
1919
- const merged = {
1920
- type: "object",
1921
- properties: mergedProperties,
1922
- required: mergedRequired
1923
- };
1924
- if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, merged));
3175
+ sets.push(Object.entries(entry).map(([schemeName, scopes]) => ({
3176
+ schemeName,
3177
+ scopes: Array.isArray(scopes) ? scopes : []
3178
+ })));
3179
+ }
3180
+ if (security.length > 0 && sets.length === 0) {
3181
+ console.warn(`[oagen] Warning: No valid security requirement on ${context}; treating security as undeclared`);
3182
+ return;
3183
+ }
3184
+ return sets;
3185
+ }
3186
+ //#endregion
3187
+ //#region src/parser/envelope-companion-names.ts
3188
+ /**
3189
+ * Decide the final name of every lifted companion model, across all list
3190
+ * operations at once, and return the models to merge into the IR.
3191
+ *
3192
+ * The same envelope recurs on every list endpoint of an API, so naming each
3193
+ * copy after its operation would mint one identical `ListMetadata` per
3194
+ * endpoint. But naming by property alone is only safe while the copies agree:
3195
+ * `mergeInlineResponseModels` keeps the largest model of a name, so a
3196
+ * `pagination` with three fields would silently replace an unrelated
3197
+ * `Pagination` component, or absorb another endpoint's two-field `pagination`.
3198
+ *
3199
+ * So a companion takes its bare name only when that cannot change any other
3200
+ * type's shape:
3201
+ *
3202
+ * Copies are compared structurally — field names, requiredness and each
3203
+ * field's shape, with nested models resolved and inline enums compared by
3204
+ * value — never by name alone. A copy collapses onto a target shape only when
3205
+ * the target *admits* every value the copy can hold: the same fields with the
3206
+ * same types, where the target may be looser (a field the copy requires may be
3207
+ * optional on the target, a non-null copy field may be nullable there) but
3208
+ * never stricter. Widening a response type loses no data a reader can see;
3209
+ * narrowing it would promise fields the wire may omit.
3210
+ *
3211
+ * - When a component model of that name exists — compared after the parser's
3212
+ * own renaming, so `PaginationDto` → `Pagination` is caught — the copies it
3213
+ * admits collapse onto it (the component stays as written); the others keep
3214
+ * their operation-qualified name.
3215
+ * - Otherwise the most common exact shape (first declared, on a tie) becomes
3216
+ * the target: it and every copy it admits take the bare name; any other
3217
+ * copy keeps its operation-qualified name.
3218
+ *
3219
+ * The comparison covers the companion's whole field type, so a `oneOf`
3220
+ * companion is judged on every variant, not just the first, and a copy folds
3221
+ * among copies only when its lifted models line up name for name with the
3222
+ * target's (same variants, same order); onto a component, only a companion
3223
+ * whose field references exactly its own lifted model can fold.
3224
+ *
3225
+ * Only the target's definition survives a collapse. The admitted copies'
3226
+ * models are dropped rather than handed to the merge pass, which keeps the
3227
+ * first model of a name and would let a stricter copy declared earlier
3228
+ * narrow the shared type for everyone; their field references are repointed
3229
+ * at the target's models instead.
3230
+ *
3231
+ * Enums declared inline in a companion stay operation-qualified, exactly like
3232
+ * enums declared inline in a parameter.
3233
+ */
3234
+ function resolveEnvelopeCompanionNames(companions, componentModels) {
3235
+ const byBareName = /* @__PURE__ */ new Map();
3236
+ /** Copies folded onto another definition; their own models are not emitted. */
3237
+ const folded = /* @__PURE__ */ new Set();
3238
+ for (const companion of companions) {
3239
+ if (companion.models.length === 0) continue;
3240
+ const bare = companion.bareNames[0];
3241
+ const group = byBareName.get(bare) ?? [];
3242
+ group.push(companion);
3243
+ byBareName.set(bare, group);
3244
+ }
3245
+ const componentMap = new Map((componentModels ?? []).map((m) => [m.name, m]));
3246
+ const localMap = (c) => new Map(c.models.map((m) => [m.name, m]));
3247
+ const unwrap = (t) => t.kind === "nullable" ? t.inner : t;
3248
+ for (const [bare, group] of byBareName) {
3249
+ let folds;
3250
+ let finalNames;
3251
+ let owner;
3252
+ const component = componentMap.get(bare);
3253
+ if (component) {
3254
+ const targetType = {
3255
+ kind: "model",
3256
+ name: component.name
3257
+ };
3258
+ finalNames = [component.name];
3259
+ folds = (c) => {
3260
+ const refs = collectModelRefs(c.field.type);
3261
+ if (refs.length !== 1 || refs[0] !== c.models[0].name) return false;
3262
+ return typeAdmits(targetType, componentMap, unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set());
3263
+ };
3264
+ } else {
3265
+ const counts = /* @__PURE__ */ new Map();
3266
+ for (const c of group) {
3267
+ const key = `${fingerprintType$1(unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set())}|${c.bareNames.join(",")}`;
3268
+ const seen = counts.get(key);
3269
+ if (seen) seen.n += 1;
3270
+ else counts.set(key, {
3271
+ n: 1,
3272
+ first: c
3273
+ });
1925
3274
  }
3275
+ let best = {
3276
+ n: 0,
3277
+ first: group[0]
3278
+ };
3279
+ for (const entry of counts.values()) if (entry.n > best.n) best = entry;
3280
+ owner = best.first;
3281
+ finalNames = owner.bareNames;
3282
+ const ownerType = unwrap(owner.field.type);
3283
+ const ownerMap = localMap(owner);
3284
+ const ownerNames = owner.bareNames;
3285
+ 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());
1926
3286
  }
1927
- if (fieldSchema.oneOf) {
1928
- const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
1929
- if (objectVariant) {
1930
- const nestedName = qualifyNestedName(name, fieldName);
1931
- if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, objectVariant));
3287
+ const collapsing = group.filter(folds);
3288
+ for (const companion of collapsing) {
3289
+ renameCompanion(companion, finalNames);
3290
+ if (companion !== owner) folded.add(companion);
3291
+ }
3292
+ }
3293
+ return companions.filter((c) => !folded.has(c)).flatMap((c) => c.models);
3294
+ }
3295
+ /**
3296
+ * Rename a companion's models to `names` (index-aligned with `models`) and
3297
+ * repoint every reference, in its field and inside its models.
3298
+ */
3299
+ function renameCompanion(companion, names) {
3300
+ const renames = /* @__PURE__ */ new Map();
3301
+ companion.models.forEach((model, i) => {
3302
+ const name = names[i];
3303
+ if (name !== void 0 && model.name !== name) renames.set(model.name, name);
3304
+ });
3305
+ if (renames.size === 0) return;
3306
+ const repoint = (ref) => {
3307
+ walkTypeRef(ref, { model: (r) => {
3308
+ const renamed = renames.get(r.name);
3309
+ if (renamed !== void 0) r.name = renamed;
3310
+ } });
3311
+ };
3312
+ repoint(companion.field.type);
3313
+ for (const model of companion.models) {
3314
+ model.name = renames.get(model.name) ?? model.name;
3315
+ for (const field of model.fields) repoint(field.type);
3316
+ }
3317
+ }
3318
+ /**
3319
+ * Whether `target` admits every value `copy` can hold: the same field names,
3320
+ * each field's type admitted (see `typeAdmits`), and no field the target
3321
+ * requires that the copy leaves optional. Nested models resolve through their
3322
+ * respective maps and are compared the same way; a model neither map resolves
3323
+ * is compared by name.
3324
+ */
3325
+ function admits(target, targetMap, copy, copyMap, visiting) {
3326
+ const key = `${target.name}|${copy.name}`;
3327
+ if (visiting.has(key)) return true;
3328
+ visiting.add(key);
3329
+ try {
3330
+ if (target.fields.length !== copy.fields.length) return false;
3331
+ const targetFields = new Map(target.fields.map((f) => [f.name, f]));
3332
+ for (const field of copy.fields) {
3333
+ const expected = targetFields.get(field.name);
3334
+ if (!expected) return false;
3335
+ if (expected.required && !field.required) return false;
3336
+ if (!typeAdmits(expected.type, targetMap, field.type, copyMap, visiting)) return false;
3337
+ }
3338
+ return true;
3339
+ } finally {
3340
+ visiting.delete(key);
3341
+ }
3342
+ }
3343
+ /**
3344
+ * Whether `target` admits every value of `copy`'s type. A nullable target
3345
+ * admits a nullable or a non-null copy; a non-null target admits only a
3346
+ * non-null copy. An enum with known values admits a copy whose values are a
3347
+ * subset of its own. Everything else must match exactly, by fingerprint.
3348
+ */
3349
+ function typeAdmits(target, targetMap, copy, copyMap, visiting) {
3350
+ if (target.kind === "nullable") return typeAdmits(target.inner, targetMap, copy.kind === "nullable" ? copy.inner : copy, copyMap, visiting);
3351
+ if (copy.kind === "nullable") return false;
3352
+ if (target.kind !== copy.kind) return false;
3353
+ switch (target.kind) {
3354
+ case "model": {
3355
+ const t = targetMap.get(target.name);
3356
+ const c = copyMap.get(copy.name);
3357
+ if (t && c) return admits(t, targetMap, c, copyMap, visiting);
3358
+ return target.name === copy.name;
3359
+ }
3360
+ case "array": return typeAdmits(target.items, targetMap, copy.items, copyMap, visiting);
3361
+ case "enum": {
3362
+ const c = copy;
3363
+ if (target.values && c.values) {
3364
+ const allowed = new Set(target.values.map((v) => `${typeof v}:${String(v)}`));
3365
+ return c.values.every((v) => allowed.has(`${typeof v}:${String(v)}`));
1932
3366
  }
3367
+ return target.name === c.name;
3368
+ }
3369
+ default: return fingerprintType$1(target, targetMap, /* @__PURE__ */ new Set()) === fingerprintType$1(copy, copyMap, /* @__PURE__ */ new Set());
3370
+ }
3371
+ }
3372
+ /**
3373
+ * Structural fingerprint of a companion model: field names, requiredness, and
3374
+ * each field's shape, with models nested inside the companion compared by
3375
+ * shape rather than by (operation-qualified) name, and inline enums compared
3376
+ * by their values. Models outside the companion are compared by name.
3377
+ */
3378
+ function fingerprintModel(model, local, visiting) {
3379
+ if (visiting.has(model.name)) return `rec:${model.name}`;
3380
+ visiting.add(model.name);
3381
+ const fields = [...model.fields].sort((a, b) => a.name.localeCompare(b.name)).map((f) => `${f.name}${f.required ? "!" : "?"}:${fingerprintType$1(f.type, local, visiting)}`);
3382
+ visiting.delete(model.name);
3383
+ return `{${fields.join(",")}}`;
3384
+ }
3385
+ function fingerprintType$1(ref, local, visiting) {
3386
+ switch (ref.kind) {
3387
+ case "primitive": return `p:${ref.type}${ref.format ? `:${ref.format}` : ""}`;
3388
+ case "literal": return `l:${typeof ref.value}:${String(ref.value)}`;
3389
+ case "nullable": return `n<${fingerprintType$1(ref.inner, local, visiting)}>`;
3390
+ case "array": return `a<${fingerprintType$1(ref.items, local, visiting)}>`;
3391
+ case "map": return `m<${ref.keyType ? `${fingerprintType$1(ref.keyType, local, visiting)},` : ""}${fingerprintType$1(ref.valueType, local, visiting)}>`;
3392
+ case "union": return `u:${ref.compositionKind ?? ""}<${ref.variants.map((v) => fingerprintType$1(v, local, visiting)).sort().join(",")}>`;
3393
+ case "enum": return ref.values ? `e<${[...ref.values].map((v) => `${typeof v}:${String(v)}`).sort().join("|")}>` : `e:${ref.name}`;
3394
+ case "model": {
3395
+ const nested = local.get(ref.name);
3396
+ return nested ? fingerprintModel(nested, local, visiting) : `ref:${ref.name}`;
1933
3397
  }
3398
+ default: return assertNever(ref);
1934
3399
  }
1935
- return [{
1936
- name,
1937
- description: schema.description,
1938
- fields
1939
- }, ...nestedModels];
1940
3400
  }
1941
3401
  //#endregion
1942
3402
  //#region src/parser/operations.ts
@@ -1964,6 +3424,60 @@ function resolveParameterRef(raw, componentParameters, operationContext) {
1964
3424
  }
1965
3425
  return current;
1966
3426
  }
3427
+ /**
3428
+ * Resolve a raw request body to a concrete Request Body Object, following
3429
+ * `$ref` chains into `components/requestBodies` with a cycle guard. Throws on
3430
+ * an unresolvable ref so a malformed spec fails at parse time instead of
3431
+ * silently losing the body — and with it the body-ownership rule that keeps
3432
+ * duplicated secrets out of the query string.
3433
+ */
3434
+ function resolveRequestBodyRef(raw, componentRequestBodies, operationContext) {
3435
+ if (!raw) return void 0;
3436
+ const seenRefs = /* @__PURE__ */ new Set();
3437
+ let current = raw;
3438
+ while ("$ref" in current) {
3439
+ if (seenRefs.has(current.$ref)) throw new Error(`Circular request body $ref chain starting at "${current.$ref}" in ${operationContext}.`);
3440
+ seenRefs.add(current.$ref);
3441
+ const refName = /^#\/components\/requestBodies\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
3442
+ const resolved = refName ? componentRequestBodies?.[refName] : void 0;
3443
+ if (!resolved) throw new Error(`Unresolved request body $ref "${current.$ref}" in ${operationContext}: no matching entry under components/requestBodies.`);
3444
+ current = resolved;
3445
+ }
3446
+ return current;
3447
+ }
3448
+ /**
3449
+ * Resolve a raw response header to a concrete Header Object, following `$ref`
3450
+ * chains into `components/headers` with a cycle guard.
3451
+ *
3452
+ * Unlike parameters and request bodies, a header that cannot be resolved is
3453
+ * warned about and dropped rather than failing the parse: a response header
3454
+ * is informational, so losing one costs an emitter a typed accessor, not a
3455
+ * wire contract, and should not block generating the rest of the SDK.
3456
+ */
3457
+ function resolveHeaderRef(raw, componentHeaders, headerName, operationContext) {
3458
+ const seenRefs = /* @__PURE__ */ new Set();
3459
+ let current = raw;
3460
+ let description;
3461
+ while ("$ref" in current) {
3462
+ if (description === void 0 && typeof current.description === "string") description = current.description;
3463
+ if (seenRefs.has(current.$ref)) {
3464
+ console.warn(`[oagen] Warning: Circular header $ref chain starting at "${current.$ref}" for response header "${headerName}" in ${operationContext}. Skipping the header.`);
3465
+ return;
3466
+ }
3467
+ seenRefs.add(current.$ref);
3468
+ const refName = /^#\/components\/headers\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
3469
+ const resolved = refName ? componentHeaders?.[refName] : void 0;
3470
+ if (!resolved) {
3471
+ console.warn(`[oagen] Warning: Unresolved header $ref "${current.$ref}" for response header "${headerName}" in ${operationContext}: no matching entry under components/headers. Skipping the header.`);
3472
+ return;
3473
+ }
3474
+ current = resolved;
3475
+ }
3476
+ return description === void 0 ? current : {
3477
+ ...current,
3478
+ description
3479
+ };
3480
+ }
1967
3481
  const HTTP_METHODS = [
1968
3482
  "get",
1969
3483
  "post",
@@ -1974,26 +3488,34 @@ const HTTP_METHODS = [
1974
3488
  "options",
1975
3489
  "trace"
1976
3490
  ];
1977
- function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters) {
3491
+ function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters, componentRequestBodies, componentModels, documentTags, componentExamples, componentHeaders) {
1978
3492
  if (!paths) return {
1979
3493
  services: [],
1980
3494
  inlineModels: []
1981
3495
  };
3496
+ const pathModelBaseNames = resolvePathModelBaseNames(paths, componentModels, componentRequestBodies);
1982
3497
  const serviceMap = /* @__PURE__ */ new Map();
1983
3498
  const inlineModels = [];
3499
+ const envelopeCompanions = [];
3500
+ const tagServiceByOperation = /* @__PURE__ */ new Map();
3501
+ const headerModelRegistry = new Map((componentModels ?? []).map((model) => [model.name, { fingerprint: modelFingerprint(model) }]));
1984
3502
  for (const [path, pathItem] of Object.entries(paths)) {
1985
3503
  const pathLevelParams = pathItem.parameters ?? [];
1986
3504
  for (const method of HTTP_METHODS) {
1987
3505
  const op = pathItem[method];
1988
3506
  if (!op) continue;
1989
- const serviceName = inferServiceName(path, op.tags?.[0]);
1990
- const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters);
3507
+ const tag = op.tags?.[0];
3508
+ const serviceName = inferServiceName(path, tag);
3509
+ 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);
1991
3510
  inlineModels.push(...opModels);
3511
+ if (opCompanions) envelopeCompanions.push(...opCompanions);
3512
+ if (tag) tagServiceByOperation.set(operation, serviceName);
1992
3513
  const ops = serviceMap.get(serviceName) ?? [];
1993
3514
  ops.push(operation);
1994
3515
  serviceMap.set(serviceName, ops);
1995
3516
  }
1996
3517
  }
3518
+ inlineModels.push(...resolveEnvelopeCompanionNames(envelopeCompanions, componentModels));
1997
3519
  for (const [serviceName, operations] of serviceMap) {
1998
3520
  const pathGroups = /* @__PURE__ */ new Map();
1999
3521
  for (const op of operations) {
@@ -2015,14 +3537,73 @@ function extractOperations(paths, operationIdTransform, componentSchemas, compon
2015
3537
  }
2016
3538
  }
2017
3539
  for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
3540
+ const declaredTags = indexDocumentTags(documentTags);
2018
3541
  return {
2019
- services: Array.from(serviceMap.entries()).map(([name, operations]) => ({
2020
- name,
2021
- operations
2022
- })),
3542
+ services: Array.from(serviceMap.entries()).map(([name, operations]) => {
3543
+ const description = resolveServiceDescription(name, operations, tagServiceByOperation, declaredTags);
3544
+ return description === void 0 ? {
3545
+ name,
3546
+ operations
3547
+ } : {
3548
+ name,
3549
+ description,
3550
+ operations
3551
+ };
3552
+ }),
2023
3553
  inlineModels
2024
3554
  };
2025
3555
  }
3556
+ /**
3557
+ * Index the document's top-level `tags[]` by the service name each tag
3558
+ * produces, so a tag spelled `user_management`, `User Management`, or
3559
+ * `user-management` all map to the `UserManagement` service. Every named tag
3560
+ * is indexed — with its description when it declares a non-blank one, and
3561
+ * `undefined` otherwise — because a declared-but-undescribed tag still marks
3562
+ * its service as one that must not borrow another tag's wording. When several
3563
+ * tags normalize to the same service name, the first described one wins.
3564
+ *
3565
+ * `parseSpec` ignored `tags` entirely before descriptions were read, so a
3566
+ * document with a malformed value — `tags` written as an object, a `null`
3567
+ * entry, a numeric `name` — used to parse and must keep parsing. Anything
3568
+ * that cannot be matched is skipped rather than thrown on.
3569
+ */
3570
+ function indexDocumentTags(documentTags) {
3571
+ const index = /* @__PURE__ */ new Map();
3572
+ if (!Array.isArray(documentTags)) return index;
3573
+ for (const tag of documentTags) {
3574
+ if (typeof tag !== "object" || tag === null) continue;
3575
+ const { name, description } = tag;
3576
+ if (typeof name !== "string" || name === "") continue;
3577
+ const serviceName = toPascalCase(name);
3578
+ if (index.get(serviceName) !== void 0) continue;
3579
+ const described = typeof description === "string" && description.trim() !== "";
3580
+ index.set(serviceName, described ? description : void 0);
3581
+ }
3582
+ return index;
3583
+ }
3584
+ /**
3585
+ * Pick a service's description from the document's tags.
3586
+ *
3587
+ * A service whose own name is a declared tag is described by that tag alone:
3588
+ * its description when it has one, nothing otherwise. Another tag's wording is
3589
+ * never borrowed for it — operations tagged `api_keys` that the path-prefix
3590
+ * split merges into `UserManagement` must not label that service "API keys".
3591
+ *
3592
+ * A service whose own name is not declared is one the split carved out of a
3593
+ * tag (e.g. `OrganizationDomains` from operations tagged `organizations`); it
3594
+ * inherits the description of the first described tag its operations carry.
3595
+ * Path-named services whose operations have no tag carry nothing and stay
3596
+ * undescribed.
3597
+ */
3598
+ function resolveServiceDescription(serviceName, operations, tagServiceByOperation, declaredTags) {
3599
+ if (declaredTags.has(serviceName)) return declaredTags.get(serviceName);
3600
+ for (const op of operations) {
3601
+ const tagService = tagServiceByOperation.get(op);
3602
+ if (tagService === void 0) continue;
3603
+ const description = declaredTags.get(tagService);
3604
+ if (description !== void 0) return description;
3605
+ }
3606
+ }
2026
3607
  function inferServiceName(path, tag) {
2027
3608
  if (tag) return toPascalCase(tag);
2028
3609
  const first = path.split("/").filter(Boolean)[0] ?? "Default";
@@ -2041,6 +3622,24 @@ function inferServiceName(path, tag) {
2041
3622
  function normalizeOperationIdForNaming(operationId) {
2042
3623
  return operationId.replace(/\[\d+\]\s*$/, "");
2043
3624
  }
3625
+ /**
3626
+ * The inline request body or response model name of an operation that has an
3627
+ * `operationId`. The single source of that name: extractRequestBody and
3628
+ * deriveResponseName use it to name, resolvePathModelBaseNames to reserve.
3629
+ */
3630
+ function operationIdModelName(operationId, suffix) {
3631
+ return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(operationId)) + suffix);
3632
+ }
3633
+ /**
3634
+ * The stem every model derived from an `operationId` starts with: its
3635
+ * `Request` and `Response` models, a list's `Item`, nested inline models under
3636
+ * those (`…ResponseOwner`) and envelope companions (`…ListMetadata`). Every
3637
+ * step of cleanSchemaName is a global replace or acts on the leading word, so
3638
+ * this is a true prefix of each of those names. Used to reserve them all.
3639
+ */
3640
+ function operationIdModelBase(operationId) {
3641
+ return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(operationId)));
3642
+ }
2044
3643
  function inferOperationName(method, path, operationId, operationIdTransform) {
2045
3644
  if (operationId) {
2046
3645
  const normalized = normalizeOperationIdForNaming(operationId);
@@ -2185,10 +3784,10 @@ function extractParameterGroups(op, allIRParams, operationContext) {
2185
3784
  * the request body. Emitters use the group's presence to generate sum-type
2186
3785
  * interfaces and custom JSON marshalling instead of flat optional fields.
2187
3786
  */
2188
- function extractBodyParameterGroups(op, operationContext, componentSchemas) {
3787
+ function extractBodyParameterGroups(op, requestBody, operationContext, componentSchemas) {
2189
3788
  const raw = op["x-mutually-exclusive-body-groups"];
2190
3789
  if (!raw || typeof raw !== "object") return void 0;
2191
- const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
3790
+ const bodySchema = requestBody?.content?.["application/json"]?.schema;
2192
3791
  const variantFieldSchemas = collectVariantFieldSchemas(bodySchema, componentSchemas);
2193
3792
  const groups = [];
2194
3793
  for (const [groupName, groupDef] of Object.entries(raw)) {
@@ -2228,8 +3827,10 @@ function extractBodyParameterGroups(op, operationContext, componentSchemas) {
2228
3827
  }
2229
3828
  return groups.length > 0 ? groups : void 0;
2230
3829
  }
2231
- function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters) {
2232
- const opLabel = op.operationId ?? `${method.toUpperCase()} ${path}`;
3830
+ function buildOperation(method, path, op, pathLevelParams, pathModelBaseName, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies, componentExamples, componentHeaders, headerModelRegistry = /* @__PURE__ */ new Map()) {
3831
+ const opLabel = operationLabel(op, path, method);
3832
+ const operationName = inferOperationName(method, path, op.operationId, operationIdTransform);
3833
+ const requestBodyObject = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
2233
3834
  const paramsByIdentity = /* @__PURE__ */ new Map();
2234
3835
  for (const raw of [...pathLevelParams, ...op.parameters ?? []]) {
2235
3836
  const param = resolveParameterRef(raw, componentParameters, opLabel);
@@ -2237,14 +3838,28 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2237
3838
  }
2238
3839
  const allParams = [...paramsByIdentity.values()];
2239
3840
  const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
3841
+ const groupedQueryNames = collectParameterGroupMemberNames(op);
3842
+ const bodyOwnedNames = /* @__PURE__ */ new Set();
3843
+ if (BODY_CARRYING_METHODS.has(method)) {
3844
+ const bodySchema = selectRequestBodyContent(requestBodyObject).schema;
3845
+ for (const name of collectBodyPropertyNames(bodySchema, componentSchemas)) if (!groupedQueryNames.has(name)) bodyOwnedNames.add(name);
3846
+ }
3847
+ const wireParams = bodyOwnedNames.size > 0 ? allParams.filter((p) => !(p.in === "query" && bodyOwnedNames.has(p.name))) : allParams;
2240
3848
  const opContext = serviceName;
2241
- const pathParams = extractParams(allParams, "path", opContext, componentSchemas);
2242
- const queryParams = extractParams(allParams, "query", opContext, componentSchemas);
2243
- const headerParams = extractParams(allParams, "header", opContext, componentSchemas).filter((p) => p.name.toLowerCase() !== "idempotency-key");
2244
- const cookieParams = extractParams(allParams, "cookie", opContext, componentSchemas);
3849
+ const pathParams = extractParams(wireParams, "path", opContext, componentSchemas, componentExamples, opLabel);
3850
+ const queryParams = extractParams(wireParams, "query", opContext, componentSchemas, componentExamples, opLabel);
3851
+ const headerParams = extractParams(wireParams, "header", opContext, componentSchemas, componentExamples, opLabel).filter((p) => p.name.toLowerCase() !== "idempotency-key");
3852
+ const cookieParams = extractParams(wireParams, "cookie", opContext, componentSchemas, componentExamples, opLabel);
3853
+ const bodyOwnedQueryParams = bodyOwnedNames.size > 0 ? extractParams(allParams.filter((p) => p.in === "query" && bodyOwnedNames.has(p.name)), "query", opContext, componentSchemas, componentExamples, opLabel) : [];
2245
3854
  const reqBodyModels = [];
2246
- const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
2247
- const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method, componentSchemas);
3855
+ const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(requestBodyObject, op, pathModelBaseName, reqBodyModels, componentSchemas);
3856
+ const { response, responseHeaders, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType, envelopeFields, envelopeCompanions } = extractResponses(op.responses, op, pathModelBaseName, path, method, componentSchemas, {
3857
+ componentHeaders,
3858
+ componentSchemas,
3859
+ serviceName: opContext,
3860
+ operationQualifier: toPascalCase(operationName),
3861
+ registry: headerModelRegistry
3862
+ });
2248
3863
  inlineModels.push(...reqBodyModels);
2249
3864
  const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
2250
3865
  let pagination;
@@ -2255,31 +3870,36 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2255
3870
  param: paginationFromParams?.param ?? "after",
2256
3871
  limitParam: paginationFromParams?.limitParam,
2257
3872
  dataPath: responseDataPath ?? paginationFromParams?.dataPath,
2258
- itemType
3873
+ itemType,
3874
+ envelopeFields
2259
3875
  };
2260
3876
  } else if (paginationFromParams) pagination = paginationFromParams;
2261
- const security = extractOperationSecurity(op.security);
3877
+ const security = extractSecurityRequirements(op.security, opLabel);
2262
3878
  const queryParamGroups = extractParameterGroups(op, [
2263
3879
  ...pathParams,
2264
3880
  ...queryParams,
2265
3881
  ...headerParams,
2266
3882
  ...cookieParams
2267
3883
  ], opLabel);
2268
- const bodyParamGroups = extractBodyParameterGroups(op, opLabel, componentSchemas);
3884
+ const bodyParamGroups = extractBodyParameterGroups(op, requestBodyObject, opLabel, componentSchemas);
2269
3885
  const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
3886
+ const summary = presentText(op.summary);
2270
3887
  return {
2271
3888
  operation: {
2272
- name: inferOperationName(method, path, op.operationId, operationIdTransform),
2273
- description: buildDescription(op.summary, op.description),
3889
+ name: operationName,
3890
+ ...summary !== void 0 ? { summary } : {},
3891
+ description: buildDescription(summary, op.description),
2274
3892
  httpMethod: method,
2275
3893
  path,
2276
3894
  pathParams,
2277
3895
  queryParams,
3896
+ bodyOwnedQueryParams: bodyOwnedQueryParams.length > 0 ? bodyOwnedQueryParams : void 0,
2278
3897
  headerParams,
2279
3898
  cookieParams: cookieParams.length > 0 ? cookieParams : void 0,
2280
3899
  requestBody,
2281
3900
  requestBodyEncoding,
2282
3901
  response,
3902
+ ...responseHeaders && { responseHeaders },
2283
3903
  successResponses,
2284
3904
  errors,
2285
3905
  pagination,
@@ -2289,31 +3909,49 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2289
3909
  security,
2290
3910
  parameterGroups
2291
3911
  },
2292
- inlineModels
3912
+ inlineModels,
3913
+ envelopeCompanions
2293
3914
  };
2294
3915
  }
2295
3916
  /**
2296
- * Extract per-operation security requirements from an OpenAPI security directive.
2297
- * Returns undefined when the operation uses the spec-level default security.
2298
- *
2299
- * OpenAPI security format: `security: [{ schemeName: [scope1, scope2] }]`
3917
+ * Combine an operation's `summary` and `description` into one doc text.
3918
+ * Empty or whitespace-only text counts as absent, so `summary: "List pets"`
3919
+ * with `description: ""` yields `"List pets"` rather than `""`. Text that has
3920
+ * content is kept verbatim, surrounding whitespace included.
2300
3921
  */
2301
- function extractOperationSecurity(security) {
2302
- if (!security || security.length === 0) return void 0;
2303
- const requirements = [];
2304
- for (const entry of security) for (const [schemeName, scopes] of Object.entries(entry)) requirements.push({
2305
- schemeName,
2306
- scopes: scopes ?? []
2307
- });
2308
- return requirements.length > 0 ? requirements : void 0;
2309
- }
2310
3922
  function buildDescription(summary, description) {
2311
- if (summary && description && summary !== description) return `${summary}\n\n${description}`;
2312
- return description ?? summary;
3923
+ const presentSummary = presentText(summary);
3924
+ const presentDescription = presentText(description);
3925
+ if (presentSummary && presentDescription && presentSummary !== presentDescription) return `${presentSummary}\n\n${presentDescription}`;
3926
+ return presentDescription ?? presentSummary;
3927
+ }
3928
+ /** The text itself when it has non-whitespace content, otherwise `undefined`. */
3929
+ function presentText(text) {
3930
+ return typeof text === "string" && text.trim() !== "" ? text : void 0;
2313
3931
  }
2314
- function extractParams(params, location, operationContext, componentSchemas) {
3932
+ function extractParams(params, location, operationContext, componentSchemas, componentExamples, opLabel) {
2315
3933
  return params.filter((p) => p.in === location).map((p) => {
2316
- const refTarget = resolveParamSchemaRef(p.schema, componentSchemas);
3934
+ const refTarget = resolveComponentSchemaRef(p.schema, componentSchemas);
3935
+ const { example, examples } = resolveExamples([
3936
+ {
3937
+ example: p.example,
3938
+ examples: exampleObjectValues(p.examples, componentExamples, `parameter "${p.name}" in ${opLabel ?? location}`)
3939
+ },
3940
+ {
3941
+ example: p.schema?.example,
3942
+ examples: nonEmptyExamples(p.schema?.examples)
3943
+ },
3944
+ {
3945
+ example: refTarget?.example,
3946
+ examples: nonEmptyExamples(refTarget?.examples)
3947
+ }
3948
+ ]);
3949
+ const own = p.schema ? extractConstraints(p.schema) : void 0;
3950
+ const inherited = refTarget ? extractConstraints(refTarget) : void 0;
3951
+ const constraints = own && inherited ? {
3952
+ ...inherited,
3953
+ ...own
3954
+ } : own ?? inherited;
2317
3955
  return {
2318
3956
  name: p.name,
2319
3957
  type: p.schema ? schemaToTypeRef(p.schema, p.name, operationContext ? toPascalCase(operationContext) : void 0) : {
@@ -2321,32 +3959,74 @@ function extractParams(params, location, operationContext, componentSchemas) {
2321
3959
  type: "string"
2322
3960
  },
2323
3961
  required: p.required ?? false,
2324
- description: p.description,
3962
+ description: firstDescription(p.description, p.schema?.description, refTarget?.description),
2325
3963
  deprecated: p.deprecated || p.schema?.deprecated || refTarget?.deprecated || void 0,
2326
3964
  default: p.schema?.default ?? refTarget?.default,
2327
- example: p.example ?? p.schema?.example ?? refTarget?.example,
3965
+ example,
3966
+ ...examples ? { examples } : {},
2328
3967
  style: p.style,
2329
- explode: p.explode
3968
+ explode: p.explode,
3969
+ ...constraints ? { constraints } : {}
2330
3970
  };
2331
3971
  });
2332
3972
  }
2333
3973
  /**
2334
- * If a parameter's schema is a `$ref` to `#/components/schemas/X`, return the
2335
- * referenced component schema. Used to surface schema-level metadata (default,
2336
- * example, deprecated) that wouldn't otherwise be visible at the param level.
2337
- *
2338
- * Refs are not pre-resolved by the bundler (`dereference: false` in refs.ts),
2339
- * so without this, a param like `schema: { $ref: '.../PaginationOrder' }`
2340
- * silently loses the target's `default: desc` and the SDK stops emitting the
2341
- * spec-mandated default value in generated method signatures.
3974
+ * First non-empty description in precedence order. A parameter that declares
3975
+ * `description: ""` is treated as undocumented so its schema's text still
3976
+ * surfaces, and the result is `undefined` rather than `""` when nothing in
3977
+ * the chain is documented.
2342
3978
  */
2343
- function resolveParamSchemaRef(schema, componentSchemas) {
2344
- if (!schema || !componentSchemas) return void 0;
2345
- const ref = schema.$ref;
2346
- if (!ref) return void 0;
2347
- const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
2348
- if (!match) return void 0;
2349
- return componentSchemas[match[1]];
3979
+ function firstDescription(...candidates) {
3980
+ return candidates.find((candidate) => typeof candidate === "string" && candidate.length > 0);
3981
+ }
3982
+ /**
3983
+ * Flatten a parameter's `examples` map of Example Objects to the inline
3984
+ * values it carries, in document order. A `$ref` entry is resolved against
3985
+ * `components.examples` first (see `resolveExampleRef`). Entries that point
3986
+ * elsewhere (`externalValue`) or have no `value` are skipped; `null` is a
3987
+ * legitimate value and kept. Returns undefined when nothing remains so the
3988
+ * caller can fall through to the schema's `examples`.
3989
+ */
3990
+ function exampleObjectValues(examples, componentExamples, context) {
3991
+ if (!examples || typeof examples !== "object" || Array.isArray(examples)) return void 0;
3992
+ const values = [];
3993
+ for (const raw of Object.values(examples)) {
3994
+ const entry = resolveExampleRef(raw, componentExamples, context);
3995
+ if (!entry || typeof entry !== "object") continue;
3996
+ if (entry.externalValue !== void 0 || entry.value === void 0) continue;
3997
+ values.push(entry.value);
3998
+ }
3999
+ return values.length > 0 ? values : void 0;
4000
+ }
4001
+ /**
4002
+ * Follow an Example Object's `$ref` into `components.examples`, including a
4003
+ * component that is itself a `$ref`, with a cycle guard. The bundler keeps
4004
+ * refs (`dereference: false`), so without this every `$ref` entry would be
4005
+ * dropped and a map made only of refs would silently yield nothing — the
4006
+ * parameter would then take its `examples` from the schema instead of the
4007
+ * values the document points at. A ref that can't be resolved is skipped
4008
+ * with a warning rather than failing the parse: a missing example is a doc
4009
+ * gap, not a broken API surface.
4010
+ */
4011
+ function resolveExampleRef(raw, componentExamples, context) {
4012
+ const seenRefs = /* @__PURE__ */ new Set();
4013
+ let current = raw;
4014
+ while (current && typeof current === "object" && current.$ref !== void 0) {
4015
+ const ref = current.$ref;
4016
+ if (seenRefs.has(ref)) {
4017
+ console.warn(`[oagen] Warning: Circular example $ref chain starting at "${ref}" on ${context}; skipping`);
4018
+ return;
4019
+ }
4020
+ seenRefs.add(ref);
4021
+ const refName = /^#\/components\/examples\/(.+)$/.exec(ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
4022
+ const resolved = refName ? componentExamples?.[refName] : void 0;
4023
+ if (!resolved) {
4024
+ console.warn(`[oagen] Warning: Unresolved example $ref "${ref}" on ${context}; skipping`);
4025
+ return;
4026
+ }
4027
+ current = resolved;
4028
+ }
4029
+ return current;
2350
4030
  }
2351
4031
  /**
2352
4032
  * Collect the property schemas of every `oneOf` branch reachable from a request
@@ -2397,26 +4077,91 @@ function resolveSchemaRefObject(schema, componentSchemas) {
2397
4077
  if (!match) return void 0;
2398
4078
  return componentSchemas[match[1]];
2399
4079
  }
2400
- function extractRequestBody(body, op, inlineModels) {
2401
- if (!body?.content) return {};
2402
- let encoding = "json";
2403
- let schema;
2404
- if (body.content["application/json"]?.schema) {
2405
- encoding = "json";
2406
- schema = body.content["application/json"].schema;
2407
- } else if (body.content["multipart/form-data"]?.schema) {
2408
- encoding = "form-data";
2409
- schema = body.content["multipart/form-data"].schema;
2410
- } else if (body.content["application/x-www-form-urlencoded"]?.schema) {
2411
- encoding = "form-urlencoded";
2412
- schema = body.content["application/x-www-form-urlencoded"].schema;
2413
- } else if (body.content["application/octet-stream"]) {
2414
- encoding = "binary";
2415
- schema = body.content["application/octet-stream"].schema;
2416
- } else if (body.content["text/plain"]) {
2417
- encoding = "text";
2418
- schema = body.content["text/plain"].schema;
4080
+ /** HTTP methods whose request body is the primary parameter carrier. */
4081
+ const BODY_CARRYING_METHODS = /* @__PURE__ */ new Set([
4082
+ "post",
4083
+ "put",
4084
+ "patch"
4085
+ ]);
4086
+ /**
4087
+ * Pick the request body's schema and wire encoding from its content map, in
4088
+ * priority order. JSON wins over form encodings; binary and text bodies may
4089
+ * legitimately carry no schema at all.
4090
+ */
4091
+ function selectRequestBodyContent(body) {
4092
+ const content = body?.content;
4093
+ if (!content) return { encoding: "json" };
4094
+ if (content["application/json"]?.schema) return {
4095
+ encoding: "json",
4096
+ schema: content["application/json"].schema
4097
+ };
4098
+ if (content["multipart/form-data"]?.schema) return {
4099
+ encoding: "form-data",
4100
+ schema: content["multipart/form-data"].schema
4101
+ };
4102
+ if (content["application/x-www-form-urlencoded"]?.schema) return {
4103
+ encoding: "form-urlencoded",
4104
+ schema: content["application/x-www-form-urlencoded"].schema
4105
+ };
4106
+ if (content["application/octet-stream"]) return {
4107
+ encoding: "binary",
4108
+ schema: content["application/octet-stream"].schema
4109
+ };
4110
+ if (content["text/plain"]) return {
4111
+ encoding: "text",
4112
+ schema: content["text/plain"].schema
4113
+ };
4114
+ return { encoding: "json" };
4115
+ }
4116
+ /** Parameter names referenced by any `x-mutually-exclusive-parameter-groups` variant. */
4117
+ function collectParameterGroupMemberNames(op) {
4118
+ const names = /* @__PURE__ */ new Set();
4119
+ const raw = op["x-mutually-exclusive-parameter-groups"];
4120
+ if (!raw || typeof raw !== "object") return names;
4121
+ for (const groupDef of Object.values(raw)) {
4122
+ const variants = groupDef && typeof groupDef === "object" ? groupDef.variants : void 0;
4123
+ if (!variants || typeof variants !== "object") continue;
4124
+ for (const members of Object.values(variants)) {
4125
+ if (!Array.isArray(members)) continue;
4126
+ for (const member of members) if (typeof member === "string") names.add(member);
4127
+ }
4128
+ }
4129
+ return names;
4130
+ }
4131
+ /**
4132
+ * Every property name a request body is guaranteed to accept, following
4133
+ * `$ref`s and descending through composition. `allOf` members all apply, so
4134
+ * their properties union. `oneOf` / `anyOf` alternatives are choices, so only
4135
+ * a property every non-null alternative declares counts — otherwise a caller
4136
+ * on another branch would have no slot for the value in either the body or
4137
+ * the query. Used to decide which `in: query` declarations are body-owned.
4138
+ */
4139
+ function collectBodyPropertyNames(schema, componentSchemas, seen = /* @__PURE__ */ new Set()) {
4140
+ if (!schema) return /* @__PURE__ */ new Set();
4141
+ const ref = schema.$ref;
4142
+ if (ref) {
4143
+ if (seen.has(ref)) return /* @__PURE__ */ new Set();
4144
+ return collectBodyPropertyNames(resolveSchemaRefObject(schema, componentSchemas), componentSchemas, /* @__PURE__ */ new Set([...seen, ref]));
4145
+ }
4146
+ const names = new Set(Object.keys(schema.properties ?? {}));
4147
+ for (const sub of schema.allOf ?? []) for (const name of collectBodyPropertyNames(sub, componentSchemas, seen)) names.add(name);
4148
+ for (const alternatives of [schema.oneOf, schema.anyOf]) {
4149
+ const branches = (alternatives ?? []).filter((alt) => alt.type !== "null");
4150
+ if (branches.length === 0) continue;
4151
+ let common;
4152
+ for (const branch of branches) {
4153
+ const branchNames = collectBodyPropertyNames(branch, componentSchemas, seen);
4154
+ common = common ? new Set([...common].filter((name) => branchNames.has(name))) : branchNames;
4155
+ }
4156
+ for (const name of common ?? []) names.add(name);
2419
4157
  }
4158
+ return names;
4159
+ }
4160
+ function extractRequestBody(body, op, pathModelBaseName, inlineModels, componentSchemas) {
4161
+ if (!body?.content) return {};
4162
+ const selected = selectRequestBodyContent(body);
4163
+ const encoding = selected.encoding;
4164
+ let schema = selected.schema;
2420
4165
  if (!schema) {
2421
4166
  if (encoding === "binary") return {
2422
4167
  body: {
@@ -2435,24 +4180,26 @@ function extractRequestBody(body, op, inlineModels) {
2435
4180
  };
2436
4181
  return {};
2437
4182
  }
2438
- const contextName = cleanSchemaName(op?.operationId ? toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Request" : "RequestBody");
4183
+ const contextName = op?.operationId ? operationIdModelName(op.operationId, "Request") : pathModelBaseName + "Request";
2439
4184
  if (schema.oneOf && schema.discriminator) {
2440
4185
  const merged = flattenDiscriminatedUnionBody(schema);
2441
4186
  if (merged) schema = merged;
2442
4187
  }
2443
4188
  if (schema.properties && (schema.type === "object" || !schema.type)) {
2444
4189
  const requiredSet = new Set(schema.required ?? []);
4190
+ const siblings = collectProperties(schema);
2445
4191
  const fields = [];
2446
4192
  for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
2447
4193
  if (!fieldSchema) continue;
2448
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet));
4194
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet, componentSchemas, siblings));
2449
4195
  }
2450
4196
  inlineModels.push({
2451
4197
  name: contextName,
2452
4198
  description: void 0,
2453
- fields
4199
+ fields,
4200
+ ...constraintsEntry(schema)
2454
4201
  });
2455
- extractInlineModelsFromProperties(schema, inlineModels, contextName);
4202
+ extractInlineModelsFromProperties(schema, inlineModels, contextName, componentSchemas);
2456
4203
  return {
2457
4204
  body: {
2458
4205
  kind: "model",
@@ -2463,23 +4210,29 @@ function extractRequestBody(body, op, inlineModels) {
2463
4210
  }
2464
4211
  if (schema.oneOf) {
2465
4212
  const variants = [];
4213
+ const inlineObjectVariants = schema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
4214
+ const contentNamed = [];
2466
4215
  for (const variant of schema.oneOf) if (variant.$ref) {
2467
4216
  const ref = schemaToTypeRef(variant, contextName);
2468
4217
  variants.push(ref);
2469
4218
  } else if (variant.type === "null") {} else if (variant.properties && (variant.type === "object" || !variant.type)) {
2470
- const variantName = deriveOneOfVariantName(variant, contextName, inlineModels);
4219
+ const named = deriveOneOfVariantName(variant, contextName, inlineModels, inlineObjectVariants, contentNamed, Boolean(op?.operationId));
4220
+ const variantName = named.name;
4221
+ if (named.fromContent) contentNamed.push(variantName);
2471
4222
  const requiredSet = new Set(variant.required ?? []);
4223
+ const siblings = collectProperties(variant);
2472
4224
  const fields = [];
2473
4225
  for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
2474
4226
  if (!fieldSchema) continue;
2475
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet));
4227
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet, componentSchemas, siblings));
2476
4228
  }
2477
4229
  inlineModels.push({
2478
4230
  name: variantName,
2479
4231
  description: variant.description,
2480
- fields
4232
+ fields,
4233
+ ...constraintsEntry(variant)
2481
4234
  });
2482
- extractInlineModelsFromProperties(variant, inlineModels, variantName);
4235
+ extractInlineModelsFromProperties(variant, inlineModels, variantName, componentSchemas);
2483
4236
  variants.push({
2484
4237
  kind: "model",
2485
4238
  name: variantName
@@ -2565,38 +4318,451 @@ function flattenDiscriminatedUnionBody(schema) {
2565
4318
  type: "object",
2566
4319
  properties,
2567
4320
  required: order.filter((name) => name === discProp || requiredCounts[name] === variantCount),
2568
- ...schema.description ? { description: schema.description } : {}
4321
+ ...schema.description ? { description: schema.description } : {},
4322
+ ...extractConstraints(schema)
2569
4323
  };
2570
4324
  }
2571
- /** Derive a unique variant name for a oneOf inline model. */
2572
- function deriveOneOfVariantName(variant, baseName, existingModels) {
4325
+ /**
4326
+ * Derive a unique variant name for a request-body `oneOf` inline model.
4327
+ *
4328
+ * A variant with a string-const property is named after it
4329
+ * (`RadarEmailChallengeCodeSessionAuthenticateRequest`). Any other variant is
4330
+ * named from its content by the same rule every inline union variant follows
4331
+ * (`nameInlineVariant`): the first const-less variant keeps the body's name
4332
+ * (or its title, when every inline variant of the union is titled), and each
4333
+ * later one takes its title, a distinguishing property
4334
+ * (`CreateThingRequestWithB`) or, when its property set repeats an earlier
4335
+ * variant's, the position counter (`CreateThingRequest2`). `siblings` are the
4336
+ * union's inline object variants and `contentNamed` the names its const-less
4337
+ * variants have taken so far; `fromContent` tells the caller to record this
4338
+ * one there. A clash with a model minted elsewhere still bumps the counter:
4339
+ * on the base for a content-named variant (`CreateThingRequest2`), on the
4340
+ * const-derived name for a const-named one (`FooCreateThingRequest2`) — so a
4341
+ * variant titled `Foo` and a later one pinning `kind: foo` in the same body
4342
+ * never share a model.
4343
+ *
4344
+ * `fromOperationId` says where `baseName` came from. An operationId-derived
4345
+ * base is already in cleanSchemaName form and its variants go through
4346
+ * resolveSchemaName, so they follow the same project-level naming policy as
4347
+ * component schemas. A path-derived base is used verbatim: it is deliberately
4348
+ * not cleaned (see derivePathModelBaseName) and resolvePathModelBaseNames
4349
+ * reserves names by that exact prefix, so cleaning a variant here would let it
4350
+ * slip past the reservation — `NotesPostRequest` became `NotePostRequest` and
4351
+ * collided with a component of that name.
4352
+ */
4353
+ function deriveOneOfVariantName(variant, baseName, existingModels, siblings, contentNamed, fromOperationId) {
4354
+ const finalize = (name) => fromOperationId ? resolveSchemaName(name) : name;
4355
+ const existingNames = new Set(existingModels.map((m) => m.name));
4356
+ const unique = (name, stem) => {
4357
+ let candidate = name;
4358
+ for (let suffix = 2; existingNames.has(candidate); suffix++) candidate = `${stem}${suffix}`;
4359
+ return candidate;
4360
+ };
2573
4361
  if (variant.properties) {
2574
- for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string") return resolveSchemaName(toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request");
4362
+ for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string") {
4363
+ const derivedName = finalize(toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request");
4364
+ return {
4365
+ name: unique(derivedName, derivedName),
4366
+ fromContent: false
4367
+ };
4368
+ }
2575
4369
  }
2576
- const existingNames = new Set(existingModels.map((m) => m.name));
2577
- const transformedBaseName = resolveSchemaName(baseName);
2578
- let name = transformedBaseName;
2579
- let suffix = 2;
2580
- while (existingNames.has(name)) {
2581
- name = `${transformedBaseName}${suffix}`;
2582
- suffix++;
4370
+ const finalBaseName = finalize(baseName);
4371
+ return {
4372
+ name: unique(nameInlineVariant(variant, siblings, finalBaseName, contentNamed, null), finalBaseName),
4373
+ fromContent: true
4374
+ };
4375
+ }
4376
+ /** Human-readable label for an operation in diagnostics: its operationId, else `METHOD /path`. */
4377
+ function operationLabel(op, path, method) {
4378
+ return op?.operationId ?? `${method.toUpperCase()} ${path}`;
4379
+ }
4380
+ /** Shape of a model, independent of field order: name, requiredness, and type of every field. */
4381
+ function modelFingerprint(model) {
4382
+ return model.fields.map((f) => `${f.name}${f.required ? "!" : "?"}:${stableStringify(f.type)}`).sort().join("|");
4383
+ }
4384
+ /** `JSON.stringify` with object keys sorted, so equal shapes serialize equally. */
4385
+ function stableStringify(value) {
4386
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
4387
+ if (value !== null && typeof value === "object") {
4388
+ const record = value;
4389
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
2583
4390
  }
2584
- return name;
4391
+ return JSON.stringify(value) ?? "undefined";
4392
+ }
4393
+ /**
4394
+ * Type a header's schema and materialize the inline models it names, under a
4395
+ * property name that does not collide with a different shape anywhere in the
4396
+ * spec.
4397
+ *
4398
+ * Header models are named from the service and header name alone, so an
4399
+ * inline `X-Meta` on a `Documents` operation is `DocumentsXMeta` — the same
4400
+ * name a component schema, or the same header on a 429 or on another
4401
+ * `Documents` operation, may already own with a different shape. Left alone,
4402
+ * the later merge keeps one definition for the name and every other
4403
+ * reference silently gets the wrong fields (a request body using the
4404
+ * component would even change type without any emitter reading headers).
4405
+ *
4406
+ * The first owner of a name keeps it: component models are seeded before any
4407
+ * operation, operations are visited in path order, and a single operation's
4408
+ * responses in ascending status-code order (integer-like keys iterate that
4409
+ * way), so the lowest code wins regardless of where it appears in the
4410
+ * document. A later header with a different shape — at any depth, a nested
4411
+ * object that differs counts — is re-derived under the first free candidate:
4412
+ *
4413
+ * 1. the bare header name `DocumentsXMeta`
4414
+ * 2. when the name is taken inside this operation, the status code
4415
+ * (`DocumentsXMeta429`); when taken by a component or another
4416
+ * operation, the operation (`DocumentsCreateDocumentXMeta`)
4417
+ * 3. the other of those two
4418
+ * 4. operation and status code `DocumentsCreateDocumentXMeta429`
4419
+ * 5. a numeric suffix `DocumentsXMeta2`, `DocumentsXMeta3`, ...
4420
+ *
4421
+ * Both the type and the models come from the same property name, so nested
4422
+ * models (`DocumentsXMeta429Origin`) line up by construction. A shape
4423
+ * identical to the name's current owner shares it and materializes nothing
4424
+ * new, which is also how a header reuses a matching component model.
4425
+ *
4426
+ * Treating the header as a one-property object reuses the field-level
4427
+ * extractor, whose names come from the same `qualifyInlineModelName(name,
4428
+ * parent)` rule `schemaToTypeRef` applies — the trick the inline request-body
4429
+ * path uses.
4430
+ */
4431
+ function typeHeaderSchema(schema, name, statusCode, ctx, models) {
4432
+ const parentName = ctx.serviceName ? toPascalCase(ctx.serviceName) : void 0;
4433
+ const byStatus = `${name}${statusCode}`;
4434
+ const byOperation = `${ctx.operationQualifier}${name}`;
4435
+ const byBoth = `${ctx.operationQualifier}${name}${statusCode}`;
4436
+ let candidates = [
4437
+ name,
4438
+ byStatus,
4439
+ byOperation,
4440
+ byBoth
4441
+ ];
4442
+ for (let attempt = 0;; attempt++) {
4443
+ const propertyName = attempt < candidates.length ? candidates[attempt] : `${name}${attempt - candidates.length + 2}`;
4444
+ const trial = [];
4445
+ extractInlineModelsFromProperties({ properties: { [propertyName]: schema } }, trial, parentName, ctx.componentSchemas);
4446
+ const fingerprints = trial.map((model) => [model, modelFingerprint(model)]);
4447
+ const collisions = fingerprints.flatMap(([model, fingerprint]) => {
4448
+ const registered = ctx.registry.get(model.name);
4449
+ return registered !== void 0 && registered.fingerprint !== fingerprint ? [registered] : [];
4450
+ });
4451
+ if (collisions.length > 0) {
4452
+ if (attempt === 0 && !collisions.every((c) => c.operation === ctx.operationQualifier)) candidates = [
4453
+ name,
4454
+ byOperation,
4455
+ byStatus,
4456
+ byBoth
4457
+ ];
4458
+ continue;
4459
+ }
4460
+ for (const [model, fingerprint] of fingerprints) {
4461
+ if (ctx.registry.has(model.name)) continue;
4462
+ ctx.registry.set(model.name, {
4463
+ fingerprint,
4464
+ operation: ctx.operationQualifier
4465
+ });
4466
+ models.push(model);
4467
+ }
4468
+ return schemaToTypeRef(schema, propertyName, parentName);
4469
+ }
4470
+ }
4471
+ /**
4472
+ * Read a Response Object's `headers` map into IR response headers, resolving
4473
+ * `components/headers` refs. Returns `undefined` rather than `[]` when nothing
4474
+ * survives so callers can leave the `headers` key off entirely.
4475
+ *
4476
+ * An object-shaped header schema (directly, inside an array, or as a oneOf
4477
+ * variant) becomes a `ModelRef`, so the model it names is appended to
4478
+ * `models` — otherwise the ref would dangle with nothing to emit.
4479
+ */
4480
+ function extractResponseHeaders(headers, operationLabel, statusCode, ctx, models) {
4481
+ if (!headers) return void 0;
4482
+ const result = [];
4483
+ for (const [name, raw] of Object.entries(headers)) {
4484
+ if (name.toLowerCase() === "content-type") continue;
4485
+ const header = resolveHeaderRef(raw, ctx.componentHeaders, name, operationLabel);
4486
+ if (!header) continue;
4487
+ const contentSchema = header.content ? Object.values(header.content)[0]?.schema : void 0;
4488
+ const schema = header.schema ?? contentSchema;
4489
+ const type = schema ? typeHeaderSchema(schema, name, statusCode, ctx, models) : {
4490
+ kind: "primitive",
4491
+ type: "string"
4492
+ };
4493
+ result.push({
4494
+ name,
4495
+ type,
4496
+ required: header.required ?? false,
4497
+ description: header.description,
4498
+ deprecated: header.deprecated || void 0
4499
+ });
4500
+ }
4501
+ return result.length > 0 ? result : void 0;
4502
+ }
4503
+ /**
4504
+ * Build the prefix (`NotesByIdPinPost`) that names both inline models — the
4505
+ * request body (`…Request`) and the response (`…Response`) — of an operation
4506
+ * that has no `operationId`, from the only thing that identifies it: its path
4507
+ * and method.
4508
+ *
4509
+ * Every non-parameter segment is PascalCased in order and every parameter
4510
+ * segment becomes `By` + PascalCase(param name); then the optional
4511
+ * disambiguator and the method are appended. The whole is normalized with
4512
+ * toPascalCase so it is a fixed point of that function: responses.ts re-cases
4513
+ * the response name in `deriveModelName`, and without this step `POST /a/b`
4514
+ * named its body `ABPostRequest` but its response `AbPostResponse`.
4515
+ *
4516
+ * GET /widgets → WidgetsGet
4517
+ * POST /notes → NotesPost
4518
+ * POST /notes/{id}/pin → NotesByIdPinPost
4519
+ * GET /users/{user_id}/sessions → UsersByUserIdSessionsGet
4520
+ *
4521
+ * This tells the common path shapes apart (list vs. item, sibling and nested
4522
+ * sub-resources) but is not unique by construction: `/foo-bar` and `/foo/bar`,
4523
+ * `/notes/by-id` and `/notes/{id}`, `/notes` and `/notes/` all flatten to the
4524
+ * same words. `resolvePathModelBaseNames` detects such collisions: the path
4525
+ * spelled most plainly keeps the plain name and every other one gets a
4526
+ * `disambiguator` derived from its raw path.
4527
+ *
4528
+ * A single-segment path keeps the name the old first-segment rule produced.
4529
+ * The result is deliberately not passed through cleanSchemaName (the old
4530
+ * fallback wasn't either): the segments are the resource names as the API
4531
+ * spells them.
4532
+ */
4533
+ function derivePathModelBaseName(path, method, disambiguator = "") {
4534
+ const parts = path.split("/").filter(Boolean).map((segment) => segment.startsWith("{") ? "By" + toPascalCase(segment.replace(/[{}]/g, "")) : toPascalCase(segment));
4535
+ return toPascalCase([
4536
+ parts.length > 0 ? parts.join("") : "Unknown",
4537
+ disambiguator,
4538
+ method
4539
+ ].filter(Boolean).join("_"));
4540
+ }
4541
+ /**
4542
+ * Decide the path-derived model prefix of every operation that has no
4543
+ * `operationId`, keyed by `${method} ${path}`.
4544
+ *
4545
+ * A prefix collides when two distinct raw paths with the same method flatten
4546
+ * to it (see derivePathModelBaseName), or when a reserved name starts with
4547
+ * it. The reserved names are every resolved component model name and, for
4548
+ * every operation with an `operationId`, its model stem
4549
+ * (operationIdModelBase). A prefix is the stem of several emitted names —
4550
+ * `…Request`, `…Response`, a list's `…Item`, nested inline models such as
4551
+ * `…ResponseOwner`, envelope companions such as `…ListMetadata` — so the
4552
+ * check is on the stem, not on an enumeration of suffixes: an unnamed
4553
+ * `GET /audit/events` must not overwrite `components/schemas/AuditEventsGetItem`
4554
+ * any more than an unnamed `POST /audit/events` may overwrite the models of
4555
+ * a `POST /legacy-audit` whose operationId is `auditEventsPost`. Reserved
4556
+ * names never change.
4557
+ *
4558
+ * This is deliberately a superset: a component named `AuditEventsGetter`, or
4559
+ * an operationId `auditEventsGetSummary`, also pushes `GET /audit/events`
4560
+ * onto a hashed name although no emitted name would coincide. Determinism
4561
+ * and never sharing a name wrongly matter more than the rare extra hash, and
4562
+ * the reservation is unconditional — an inline body is never merged into a
4563
+ * same-named component even when the shapes agree — so the rule stays
4564
+ * simple and order-free. Every name derived from a path-based prefix uses it
4565
+ * verbatim (body `oneOf` variants included; see deriveOneOfVariantName), and
4566
+ * as a second line of defence the check also runs on the cleaned spellings
4567
+ * of the prefix (see prefixSpellings), so `GET /notes` yields to a component
4568
+ * `NoteGetItem` although its own item model is `NotesGetItem`. Names that
4569
+ * embed the prefix behind a const value — a body `oneOf` variant pinning
4570
+ * `kind: email` is `EmailTagsPostRequest`, a nested variant
4571
+ * `<Const><Parent><Field>` — are checked too, with each const the operation
4572
+ * can mint prepended to every spelling (see collectOperationConstPrefixes);
4573
+ * the hash then lands inside those names as well (`EmailTags151Dd5PostRequest`).
4574
+ *
4575
+ * These names are types SDK users read and import, so a collision costs the
4576
+ * plain name only where it must. Within a group of paths that flatten to one
4577
+ * prefix, the path spelled most plainly (see pathDeviation: no trailing
4578
+ * slash, `{id}` rather than a literal `by-id`) is canonical and keeps the
4579
+ * plain prefix; every other path gets a six-character disambiguator, the
4580
+ * start of the SHA-1 of its raw path, inserted before the method
4581
+ * (`POST /foo-bar` next to `POST /foo/bar` → `FooBar546F12Post`). When two
4582
+ * paths tie for plainest, or the prefix is reserved, nobody is canonical and
4583
+ * every path in the group is disambiguated. The outcome depends only on the
4584
+ * document's paths, operationIds and component names, never on their order,
4585
+ * and a path that stops colliding returns to its plain name.
4586
+ */
4587
+ function resolvePathModelBaseNames(paths, componentModels, componentRequestBodies) {
4588
+ const candidates = [];
4589
+ const rawPathsByPlainName = /* @__PURE__ */ new Map();
4590
+ const constPrefixesByPlainName = /* @__PURE__ */ new Map();
4591
+ const reservedNames = /* @__PURE__ */ new Set();
4592
+ for (const model of componentModels ?? []) reservedNames.add(model.name);
4593
+ for (const [path, pathItem] of Object.entries(paths)) for (const method of HTTP_METHODS) {
4594
+ const op = pathItem[method];
4595
+ if (!op) continue;
4596
+ if (op.operationId) {
4597
+ reservedNames.add(operationIdModelBase(op.operationId));
4598
+ continue;
4599
+ }
4600
+ const plain = derivePathModelBaseName(path, method);
4601
+ candidates.push({
4602
+ key: `${method} ${path}`,
4603
+ path,
4604
+ method,
4605
+ plain
4606
+ });
4607
+ const rawPaths = rawPathsByPlainName.get(plain) ?? /* @__PURE__ */ new Set();
4608
+ rawPaths.add(path);
4609
+ rawPathsByPlainName.set(plain, rawPaths);
4610
+ const constPrefixes = constPrefixesByPlainName.get(plain) ?? /* @__PURE__ */ new Set();
4611
+ for (const prefix of collectOperationConstPrefixes(op, componentRequestBodies, operationLabel(op, path, method))) constPrefixes.add(prefix);
4612
+ constPrefixesByPlainName.set(plain, constPrefixes);
4613
+ }
4614
+ const reservedList = [...reservedNames];
4615
+ const canonicalByPlainName = /* @__PURE__ */ new Map();
4616
+ for (const [plain, rawPaths] of rawPathsByPlainName) {
4617
+ const spellings = prefixSpellings(plain);
4618
+ const reserved = ["", ...constPrefixesByPlainName.get(plain) ?? []].flatMap((constPrefix) => spellings.map((spelling) => constPrefix + spelling)).some((stem) => reservedList.some((name) => name.startsWith(stem)));
4619
+ canonicalByPlainName.set(plain, reserved ? void 0 : canonicalPath(rawPaths));
4620
+ }
4621
+ const resolved = /* @__PURE__ */ new Map();
4622
+ for (const { key, path, method, plain } of candidates) {
4623
+ const keepsPlainName = canonicalByPlainName.get(plain) === path;
4624
+ resolved.set(key, keepsPlainName ? plain : derivePathModelBaseName(path, method, pathDisambiguator(path)));
4625
+ }
4626
+ return resolved;
4627
+ }
4628
+ /**
4629
+ * The one path in a group that is spelled most plainly by pathDeviation, or
4630
+ * undefined when two or more tie for it, so the choice never depends on the
4631
+ * order the paths were written in.
4632
+ */
4633
+ function canonicalPath(rawPaths) {
4634
+ let best;
4635
+ let bestScore;
4636
+ let tied = false;
4637
+ for (const path of rawPaths) {
4638
+ const score = pathDeviation(path);
4639
+ const cmp = bestScore ? compareDeviation(score, bestScore) : -1;
4640
+ if (cmp < 0) {
4641
+ best = path;
4642
+ bestScore = score;
4643
+ tied = false;
4644
+ } else if (cmp === 0) tied = true;
4645
+ }
4646
+ return tied ? void 0 : best;
4647
+ }
4648
+ /**
4649
+ * How far a raw path's spelling is from the plain prefix it flattens to:
4650
+ * `[trailing slash, literal segments with any character outside a-z0-9]`,
4651
+ * compared lexicographically. `/notes` scores [0, 0] and `/notes/` [1, 0];
4652
+ * `/notes/{id}` scores [0, 0] and `/notes/by-id` [0, 1].
4653
+ */
4654
+ function pathDeviation(rawPath) {
4655
+ return [rawPath.length > 1 && rawPath.endsWith("/") ? 1 : 0, rawPath.split("/").filter(Boolean).filter((segment) => !segment.startsWith("{") && /[^a-z0-9]/.test(segment)).length];
4656
+ }
4657
+ function compareDeviation(a, b) {
4658
+ return a[0] - b[0] || a[1] - b[1];
4659
+ }
4660
+ /** The first six hex characters of the SHA-1 of a raw path. */
4661
+ function pathDisambiguator(rawPath) {
4662
+ return createHash("sha1").update(rawPath).digest("hex").slice(0, 6);
4663
+ }
4664
+ /**
4665
+ * The spellings a path-based prefix could take if a name derived from it were
4666
+ * run through one of the parser's cleaning steps: the prefix itself (every
4667
+ * path-derived name uses it verbatim), cleanSchemaName's form (lead-word
4668
+ * singularization plus backend and ListItem-marker stripping),
4669
+ * resolveSchemaName's form (the same plus the project's schemaNameTransform),
4670
+ * and stripListItemMarkers alone, which nested-model and inline-enum
4671
+ * qualification apply to a parent name. Reserving by every spelling is the
4672
+ * second line of defence behind "use the base verbatim": a cleaning step added
4673
+ * later cannot let a derived name slip past the reservation. Empty spellings
4674
+ * are dropped so a degenerate prefix cannot match every reserved name.
4675
+ */
4676
+ function prefixSpellings(prefix) {
4677
+ const spellings = [
4678
+ prefix,
4679
+ cleanSchemaName(prefix),
4680
+ resolveSchemaName(prefix),
4681
+ stripListItemMarkers(prefix)
4682
+ ];
4683
+ return [...new Set(spellings)].filter((spelling) => spelling.length > 0);
4684
+ }
4685
+ /**
4686
+ * The PascalCase const values an operationId-less operation can prepend to a
4687
+ * name minted from its prefix, read from the schemas its request body and 2xx
4688
+ * responses declare inline. A body `oneOf` variant pinning `kind: email`
4689
+ * yields `EmailTagsPostRequest` (deriveOneOfVariantName); a nested
4690
+ * `oneOf`/`anyOf` object variant yields `<Const><Parent><Field>`
4691
+ * (nameOneOfVariant, nameVariantModel). Every string const or single-value
4692
+ * enum of every inline object variant is collected, top level and nested — a
4693
+ * superset of whichever property each of those picks as the discriminator, so
4694
+ * the reservation can never be narrower than the naming. `$ref` targets are
4695
+ * components with names of their own and are not followed. A request body
4696
+ * `$ref` that cannot be resolved is left for buildOperation to report.
4697
+ *
4698
+ * Names that do not embed the prefix at all — a response `oneOf` variant
4699
+ * named from its bare const (`Email`, see deriveModelName in responses.ts)
4700
+ * or a `title`-mapped variant — are outside what a disambiguator can
4701
+ * separate and are not reserved here.
4702
+ */
4703
+ function collectOperationConstPrefixes(op, componentRequestBodies, opLabel) {
4704
+ const prefixes = /* @__PURE__ */ new Set();
4705
+ let body;
4706
+ try {
4707
+ body = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
4708
+ } catch {
4709
+ body = void 0;
4710
+ }
4711
+ collectConstPrefixes(selectRequestBodyContent(body).schema, prefixes, 0);
4712
+ for (const [statusCode, response] of Object.entries(op.responses ?? {})) {
4713
+ const code = parseInt(statusCode, 10);
4714
+ if (code < 200 || code >= 300) continue;
4715
+ for (const media of Object.values(response.content ?? {})) collectConstPrefixes(media?.schema, prefixes, 0);
4716
+ }
4717
+ return prefixes;
4718
+ }
4719
+ function collectConstPrefixes(schema, out, depth) {
4720
+ if (!schema || schema.$ref || depth > 16) return;
4721
+ for (const variants of [schema.oneOf, schema.anyOf]) for (const variant of variants ?? []) {
4722
+ if (variant.$ref || !variant.properties) continue;
4723
+ for (const property of Object.values(variant.properties)) {
4724
+ const value = constStringOf(property);
4725
+ const prefix = value === null ? "" : toPascalCase(value);
4726
+ if (prefix) out.add(prefix);
4727
+ }
4728
+ }
4729
+ const children = [
4730
+ ...schema.oneOf ?? [],
4731
+ ...schema.anyOf ?? [],
4732
+ ...schema.allOf ?? [],
4733
+ schema.items,
4734
+ ...Object.values(schema.properties ?? {}),
4735
+ typeof schema.additionalProperties === "object" ? schema.additionalProperties : void 0
4736
+ ];
4737
+ for (const child of children) collectConstPrefixes(child, out, depth + 1);
4738
+ }
4739
+ /** A property's pinned string value: a string `const`, or a single-value string `enum`. */
4740
+ function constStringOf(property) {
4741
+ if (!property) return null;
4742
+ if (typeof property.const === "string") return property.const;
4743
+ if (Array.isArray(property.enum) && property.enum.length === 1 && typeof property.enum[0] === "string") return property.enum[0];
4744
+ return null;
2585
4745
  }
2586
- function deriveResponseName(op, path, method) {
2587
- if (op?.operationId) return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Response");
2588
- return toPascalCase(path.split("/").filter(Boolean)[0] ?? "Unknown") + toPascalCase(method) + "Response";
4746
+ function deriveResponseName(op, pathModelBaseName) {
4747
+ if (op?.operationId) return operationIdModelName(op.operationId, "Response");
4748
+ return pathModelBaseName + "Response";
2589
4749
  }
2590
- function extractResponses(responses, op, path, method, componentSchemas) {
4750
+ function extractResponses(responses, op, pathModelBaseName, path, method, componentSchemas, headerContext = {
4751
+ operationQualifier: "",
4752
+ registry: /* @__PURE__ */ new Map()
4753
+ }) {
2591
4754
  const errors = [];
2592
4755
  let response = {
2593
4756
  kind: "primitive",
2594
4757
  type: "string"
2595
4758
  };
4759
+ let responseHeaders;
2596
4760
  let inlineModels = [];
2597
4761
  let isPaginated = false;
2598
4762
  let dataPath;
2599
4763
  let itemType;
4764
+ let envelopeFields;
4765
+ let envelopeCompanions;
2600
4766
  if (!responses) return {
2601
4767
  response,
2602
4768
  errors,
@@ -2604,8 +4770,11 @@ function extractResponses(responses, op, path, method, componentSchemas) {
2604
4770
  isPaginated
2605
4771
  };
2606
4772
  const allSuccessResponses = [];
4773
+ const opLabel = operationLabel(op, path ?? "/", method ?? "get");
4774
+ const headerModels = [];
2607
4775
  for (const [statusCode, resp] of Object.entries(responses)) {
2608
4776
  const code = parseInt(statusCode, 10);
4777
+ const headers = code >= 200 ? extractResponseHeaders(resp.headers, opLabel, code, headerContext, headerModels) : void 0;
2609
4778
  if (code >= 200 && code < 300) {
2610
4779
  let extractedType = {
2611
4780
  kind: "primitive",
@@ -2613,62 +4782,75 @@ function extractResponses(responses, op, path, method, componentSchemas) {
2613
4782
  };
2614
4783
  const jsonContent = resp.content?.["application/json"];
2615
4784
  if (jsonContent?.schema) {
2616
- const contextName = deriveResponseName(op, path ?? "/", method ?? "get");
4785
+ const contextName = deriveResponseName(op, pathModelBaseName);
2617
4786
  const result = classifyAndExtractResponse(jsonContent.schema, contextName, componentSchemas);
2618
4787
  extractedType = result.response;
2619
4788
  inlineModels = result.inlineModels;
2620
4789
  isPaginated = result.isPaginated;
2621
4790
  dataPath = result.dataPath;
2622
4791
  itemType = result.itemType;
2623
- } else if (resp.content) if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) extractedType = {
2624
- kind: "primitive",
2625
- type: "string",
2626
- format: "binary"
2627
- };
2628
- else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) extractedType = {
2629
- kind: "primitive",
2630
- type: "string"
2631
- };
2632
- else {
2633
- const firstKey = Object.keys(resp.content)[0];
2634
- if (firstKey) {
2635
- const firstContent = resp.content[firstKey];
2636
- if (firstContent?.schema) extractedType = schemaToTypeRef(firstContent.schema, deriveResponseName(op, path ?? "/", method ?? "get"));
4792
+ envelopeFields = result.envelopeFields;
4793
+ envelopeCompanions = result.envelopeCompanions;
4794
+ } else if (resp.content) {
4795
+ if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) extractedType = {
4796
+ kind: "primitive",
4797
+ type: "string",
4798
+ format: "binary"
4799
+ };
4800
+ else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) extractedType = {
4801
+ kind: "primitive",
4802
+ type: "string"
4803
+ };
4804
+ else {
4805
+ const firstKey = Object.keys(resp.content)[0];
4806
+ if (firstKey) {
4807
+ const firstContent = resp.content[firstKey];
4808
+ if (firstContent?.schema) extractedType = schemaToTypeRef(firstContent.schema, deriveResponseName(op, pathModelBaseName));
4809
+ }
2637
4810
  }
2638
4811
  }
2639
4812
  allSuccessResponses.push({
2640
4813
  statusCode: code,
2641
- type: extractedType
4814
+ type: extractedType,
4815
+ ...headers && { headers }
2642
4816
  });
2643
4817
  } else if (code >= 300 && code < 400) allSuccessResponses.push({
2644
4818
  statusCode: code,
2645
4819
  type: {
2646
4820
  kind: "primitive",
2647
4821
  type: "unknown"
2648
- }
4822
+ },
4823
+ ...headers && { headers }
2649
4824
  });
2650
4825
  else if (code >= 400) {
2651
4826
  const jsonContent = resp.content?.["application/json"];
2652
4827
  const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
2653
4828
  errors.push({
2654
4829
  statusCode: code,
2655
- type
4830
+ type,
4831
+ ...headers && { headers }
2656
4832
  });
2657
4833
  }
2658
4834
  }
2659
4835
  if (allSuccessResponses.length > 0) {
2660
4836
  const sorted = [...allSuccessResponses].sort((a, b) => a.statusCode - b.statusCode);
2661
- response = (sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0]).type;
4837
+ const primary = sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0];
4838
+ response = primary.type;
4839
+ responseHeaders = primary.headers;
2662
4840
  }
2663
4841
  const successResponses = allSuccessResponses.length > 1 ? allSuccessResponses : void 0;
4842
+ if (headerModels.length > 0) inlineModels = [...inlineModels, ...headerModels];
2664
4843
  return {
2665
4844
  response,
4845
+ responseHeaders,
2666
4846
  successResponses,
2667
4847
  errors,
2668
4848
  inlineModels,
2669
4849
  isPaginated,
2670
4850
  dataPath,
2671
- itemType
4851
+ itemType,
4852
+ envelopeFields,
4853
+ envelopeCompanions
2672
4854
  };
2673
4855
  }
2674
4856
  //#endregion
@@ -2677,10 +4859,25 @@ function keepLargestByName(models) {
2677
4859
  const byName = /* @__PURE__ */ new Map();
2678
4860
  for (const model of models) {
2679
4861
  const existing = byName.get(model.name);
4862
+ if (existing && !haveSameFields(existing, model)) {
4863
+ const kept = model.fields.length === existing.fields.length ? "keeping the first definition" : "keeping the one with more fields";
4864
+ 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}`);
4865
+ }
2680
4866
  if (!existing || model.fields.length > existing.fields.length) byName.set(model.name, model);
2681
4867
  }
2682
4868
  return [...byName.values()];
2683
4869
  }
4870
+ /**
4871
+ * Same field names with the same types, in any order. Types are compared by
4872
+ * their JSON serialization: the parser builds a given shape's TypeRef with the
4873
+ * same key order every time, so equal shapes serialize identically.
4874
+ */
4875
+ function haveSameFields(a, b) {
4876
+ const signatures = (m) => new Set(m.fields.map((f) => `${f.name}:${JSON.stringify(f.type)}`));
4877
+ const aSignatures = signatures(a);
4878
+ const bSignatures = signatures(b);
4879
+ return aSignatures.size === bSignatures.size && [...aSignatures].every((s) => bSignatures.has(s));
4880
+ }
2684
4881
  function rewriteModelRef(ref, oldName, newName) {
2685
4882
  walkTypeRef(ref, { model: (r) => {
2686
4883
  if (r.name === oldName) r.name = newName;
@@ -2691,6 +4888,8 @@ function rewriteModelRefs(models, services, oldName, newName) {
2691
4888
  for (const service of services) for (const op of service.operations) {
2692
4889
  rewriteModelRef(op.response, oldName, newName);
2693
4890
  if (op.requestBody) rewriteModelRef(op.requestBody, oldName, newName);
4891
+ for (const field of op.pagination?.envelopeFields ?? []) rewriteModelRef(field.type, oldName, newName);
4892
+ for (const header of collectResponseHeaders(op)) rewriteModelRef(header.type, oldName, newName);
2694
4893
  }
2695
4894
  }
2696
4895
  function mergeInlineResponseModels(schemaModels, inlineModels) {
@@ -2713,15 +4912,25 @@ function mergeInlineResponseModels(schemaModels, inlineModels) {
2713
4912
  });
2714
4913
  return [...mergedSchemaModels, ...keepLargestByName(deduplicatedInlineModels)];
2715
4914
  }
2716
- function mergeFieldInlineModels(existingModels, fieldInlineModels) {
4915
+ /**
4916
+ * Fold the field extractor's inline models into the models gathered so far,
4917
+ * keeping the earlier definition on a name clash. `componentNames` tells a
4918
+ * clash with a real component schema apart from one between two inline
4919
+ * models, so the warning can say which happened and name both shapes.
4920
+ */
4921
+ function mergeFieldInlineModels(existingModels, fieldInlineModels, componentNames) {
2717
4922
  const modelNames = new Set(existingModels.map((m) => m.name));
2718
4923
  const modelsByName = new Map(existingModels.map((m) => [m.name, m]));
4924
+ const fieldList = (m) => m.fields.map((f) => f.name).join(", ") || "(no fields)";
2719
4925
  const deduplicatedFieldModels = fieldInlineModels.filter((model) => {
2720
4926
  if (!modelNames.has(model.name)) return true;
2721
4927
  const existing = modelsByName.get(model.name);
2722
4928
  const existingFieldNames = new Set(existing.fields.map((f) => f.name));
2723
4929
  const inlineFieldNames = new Set(model.fields.map((f) => f.name));
2724
- if (model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name))) console.warn(`[oagen] Warning: Inline model "${model.name}" has different fields than component schema — using component schema`);
4930
+ if (model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name))) {
4931
+ 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`);
4932
+ else console.warn(`[oagen] Warning: Inline model "${model.name}" has different fields than component schema — using component schema`);
4933
+ }
2725
4934
  return false;
2726
4935
  });
2727
4936
  return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
@@ -2753,7 +4962,7 @@ function collapseJsonSuffixModels(models, services) {
2753
4962
  if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
2754
4963
  if (!baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name))) continue;
2755
4964
  if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
2756
- if (isModelReferencedByOthers(model.name, normalizedModels, new Set([model.name, baseName]))) continue;
4965
+ if (isModelReferencedByOthers(model.name, normalizedModels, /* @__PURE__ */ new Set([model.name, baseName]))) continue;
2757
4966
  mergeCandidates.push({
2758
4967
  jsonModel: model,
2759
4968
  baseModel
@@ -2762,6 +4971,10 @@ function collapseJsonSuffixModels(models, services) {
2762
4971
  const toRemove = /* @__PURE__ */ new Set();
2763
4972
  for (const { jsonModel, baseModel } of mergeCandidates) {
2764
4973
  baseModel.fields = jsonModel.fields;
4974
+ if (jsonModel.constraints || baseModel.constraints) baseModel.constraints = {
4975
+ ...baseModel.constraints,
4976
+ ...jsonModel.constraints
4977
+ };
2765
4978
  toRemove.add(jsonModel.name);
2766
4979
  rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
2767
4980
  console.warn(`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`);
@@ -2797,16 +5010,82 @@ function collectInlineEnumsFromModels(models, enums) {
2797
5010
  }
2798
5011
  /**
2799
5012
  * Collect inline enum definitions from operation parameters (query, path, header)
2800
- * and promote them to top-level enums so emitters can generate typed enum files.
5013
+ * list-envelope companion fields, and response headers, and promote them to
5014
+ * top-level enums so emitters can generate typed enum files.
2801
5015
  */
2802
5016
  function collectInlineEnumsFromOperations(services, enums) {
2803
5017
  const enumNames = new Set(enums.map((e) => e.name));
2804
- for (const service of services) for (const op of service.operations) for (const param of [
2805
- ...op.pathParams,
2806
- ...op.queryParams,
2807
- ...op.headerParams,
2808
- ...op.cookieParams ?? []
2809
- ]) collectInlineEnumFromRef(param.type, enums, enumNames);
5018
+ for (const service of services) for (const op of service.operations) {
5019
+ for (const param of [
5020
+ ...op.pathParams,
5021
+ ...op.queryParams,
5022
+ ...op.bodyOwnedQueryParams ?? [],
5023
+ ...op.headerParams,
5024
+ ...op.cookieParams ?? []
5025
+ ]) collectInlineEnumFromRef(param.type, enums, enumNames);
5026
+ for (const field of op.pagination?.envelopeFields ?? []) collectInlineEnumFromRef(field.type, enums, enumNames);
5027
+ for (const { headers, qualifier } of responseHeaderGroups(op)) for (const header of headers) collectHeaderEnums(header, qualifier, enums, enumNames);
5028
+ }
5029
+ }
5030
+ /**
5031
+ * Header lists to walk, each with the status-code qualifier that keeps a
5032
+ * colliding enum name apart. When `successResponses` exists the primary list
5033
+ * is the same array as its entry there, so the entries are walked with their
5034
+ * own codes instead of `responseHeaders`; a lone 2xx carries no status code
5035
+ * in the IR, so its headers get no qualifier.
5036
+ */
5037
+ function responseHeaderGroups(op) {
5038
+ const groups = [];
5039
+ if (op.successResponses) {
5040
+ for (const entry of op.successResponses) if (entry.headers) groups.push({
5041
+ headers: entry.headers,
5042
+ qualifier: String(entry.statusCode)
5043
+ });
5044
+ } else if (op.responseHeaders) groups.push({ headers: op.responseHeaders });
5045
+ for (const error of op.errors) if (error.headers) groups.push({
5046
+ headers: error.headers,
5047
+ qualifier: String(error.statusCode)
5048
+ });
5049
+ return groups;
5050
+ }
5051
+ /**
5052
+ * Promote a header's inline enums. A header enum is named from the service
5053
+ * and header name alone, so `X-Policy` allowing `fast`/`slow` on a 200 and
5054
+ * `retry`/`stop` on a 429 would share one name and whichever was collected
5055
+ * first would silently win; a request parameter named like the header
5056
+ * collides the same way. When the name is already taken by a different value
5057
+ * set, the header's ref is renamed to a distinct name before it is promoted.
5058
+ * A taken name with the same values is shared, as before.
5059
+ */
5060
+ function collectHeaderEnums(header, qualifier, enums, enumNames) {
5061
+ walkTypeRef(header.type, { enum: (ref) => {
5062
+ if (!ref.values) return;
5063
+ const existing = enums.find((e) => e.name === ref.name);
5064
+ if (existing && !sameEnumValues(existing, ref.values)) ref.name = distinctEnumName(ref.name, ref.values, qualifier, enums);
5065
+ collectInlineEnumFromRef(ref, enums, enumNames);
5066
+ } });
5067
+ }
5068
+ /** Order-insensitive: a reordering of the same members is the same type. */
5069
+ function sameEnumValues(existing, values) {
5070
+ if (existing.values.length !== values.length) return false;
5071
+ const have = new Set(existing.values.map((v) => v.value));
5072
+ return values.every((v) => have.has(v));
5073
+ }
5074
+ /**
5075
+ * The first name not taken by a different value set: the base qualified by
5076
+ * the status code (`DocumentsXPolicy429`), then numeric suffixes
5077
+ * (`DocumentsXPolicy2`, `DocumentsXPolicy3`, ...). Deterministic for a given
5078
+ * spec, since it depends only on declaration order.
5079
+ */
5080
+ function distinctEnumName(base, values, qualifier, enums) {
5081
+ const isUsable = (name) => {
5082
+ const taken = enums.find((e) => e.name === name);
5083
+ return !taken || sameEnumValues(taken, values);
5084
+ };
5085
+ if (qualifier && isUsable(`${base}${qualifier}`)) return `${base}${qualifier}`;
5086
+ let suffix = 2;
5087
+ while (!isUsable(`${base}${suffix}`)) suffix++;
5088
+ return `${base}${suffix}`;
2810
5089
  }
2811
5090
  //#endregion
2812
5091
  //#region src/parser/parameter-group-names.ts
@@ -3080,10 +5359,13 @@ function validateModelRefs(spec) {
3080
5359
  for (const p of [
3081
5360
  ...op.pathParams,
3082
5361
  ...op.queryParams,
5362
+ ...op.bodyOwnedQueryParams ?? [],
3083
5363
  ...op.headerParams
3084
5364
  ]) walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
3085
5365
  if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
3086
5366
  walkRef(op.response, `${service.name}.${op.name}.response`);
5367
+ for (const field of op.pagination?.envelopeFields ?? []) walkRef(field.type, `${service.name}.${op.name}.pagination.${field.name}`);
5368
+ for (const header of collectResponseHeaders(op)) walkRef(header.type, `${service.name}.${op.name}.headers.${header.name}`);
3087
5369
  }
3088
5370
  }
3089
5371
  //#endregion
@@ -3094,11 +5376,12 @@ async function parseSpec(specPath, options) {
3094
5376
  const version = spec.openapi ?? "";
3095
5377
  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}\`.`);
3096
5378
  const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
3097
- const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters);
5379
+ 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);
3098
5380
  const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
3099
5381
  const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
5382
+ const componentNames = new Set(Object.keys(spec.components?.schemas ?? {}).map((schemaName) => resolveSchemaName(schemaName)));
3100
5383
  clearSchemaNameTransform();
3101
- const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels), services);
5384
+ const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels, componentNames), services);
3102
5385
  applyFieldHints(finalModels, options?.fieldHints);
3103
5386
  collectInlineEnumsFromModels(finalModels, enums);
3104
5387
  collectInlineEnumsFromOperations(services, enums);
@@ -3117,6 +5400,7 @@ async function parseSpec(specPath, options) {
3117
5400
  models: finalModels,
3118
5401
  enums,
3119
5402
  auth,
5403
+ security: extractSecurityRequirements(spec.security, "the document"),
3120
5404
  sdk: defaultSdkBehavior()
3121
5405
  };
3122
5406
  assignParameterGroupWrapperNames(result);
@@ -3139,20 +5423,47 @@ function applyFieldHints(models, fieldHints) {
3139
5423
  }
3140
5424
  }
3141
5425
  }
3142
- /** Extract authentication schemes from OpenAPI securitySchemes. */
5426
+ /**
5427
+ * Extract authentication schemes from OpenAPI securitySchemes.
5428
+ *
5429
+ * Document order is preserved. Each entry carries its `securitySchemes` key as
5430
+ * `schemeName` so emitters can join the requirements under `ApiSpec.security`
5431
+ * and `Operation.security` back to it.
5432
+ * Schemes the IR cannot represent are omitted with a warning rather than silently.
5433
+ */
3143
5434
  function extractAuthSchemes(securitySchemes) {
3144
5435
  if (!securitySchemes) return void 0;
3145
5436
  const schemes = [];
3146
- for (const [, scheme] of Object.entries(securitySchemes)) if (scheme.type === "http" && scheme.scheme === "bearer") schemes.push({ kind: "bearer" });
3147
- else if (scheme.type === "apiKey" && scheme.in && scheme.name) schemes.push({
3148
- kind: "apiKey",
3149
- in: scheme.in,
3150
- name: scheme.name
3151
- });
3152
- else if (scheme.type === "oauth2" && scheme.flows) schemes.push({
3153
- kind: "oauth2",
3154
- flows: scheme.flows
3155
- });
5437
+ for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
5438
+ const common = scheme.description !== void 0 ? {
5439
+ schemeName,
5440
+ description: scheme.description
5441
+ } : { schemeName };
5442
+ const httpScheme = scheme.type === "http" ? scheme.scheme?.toLowerCase() : void 0;
5443
+ if (httpScheme === "bearer") schemes.push({
5444
+ kind: "bearer",
5445
+ ...common
5446
+ });
5447
+ else if (httpScheme === "basic") schemes.push({
5448
+ kind: "basic",
5449
+ ...common
5450
+ });
5451
+ else if (scheme.type === "apiKey" && scheme.in && scheme.name) schemes.push({
5452
+ kind: "apiKey",
5453
+ ...common,
5454
+ in: scheme.in,
5455
+ name: scheme.name
5456
+ });
5457
+ else if (scheme.type === "oauth2" && scheme.flows) schemes.push({
5458
+ kind: "oauth2",
5459
+ ...common,
5460
+ flows: scheme.flows
5461
+ });
5462
+ else {
5463
+ const detail = scheme.type === "http" ? `type: http, scheme: ${scheme.scheme}` : `type: ${scheme.type}`;
5464
+ console.warn(`[oagen] Warning: Unsupported security scheme "${schemeName}" (${detail}) omitted from spec.auth`);
5465
+ }
5466
+ }
3156
5467
  return schemes.length > 0 ? schemes : void 0;
3157
5468
  }
3158
5469
  //#endregion