@workos/oagen 0.30.2 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +2885 -651
  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-BO5FieX5.mjs → diff-CAsypxXV.mjs} +107 -89
  10. package/dist/{diff-BO5FieX5.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 +2877 -546
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/{overlay-DWAHPofk.mjs → overlay-Dj3WWlNP.mjs} +44 -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 +2740 -527
  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 +251 -36
  26. package/dist/smoke.d.mts.map +1 -1
  27. package/dist/smoke.mjs +2740 -527
  28. package/dist/smoke.mjs.map +1 -1
  29. package/dist/{tree-sitter-CDKm3n64.mjs → tree-sitter-C7Hk-NDu.mjs} +35 -7
  30. package/dist/tree-sitter-C7Hk-NDu.mjs.map +1 -0
  31. package/dist/{types-BcbGLNkh.d.mts → types-I7W7CNl1.d.mts} +219 -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 +170 -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-nZSxhCfC.d.mts +0 -123
  43. package/dist/differ-nZSxhCfC.d.mts.map +0 -1
  44. package/dist/overlay-DWAHPofk.mjs.map +0 -1
  45. package/dist/tree-sitter-CDKm3n64.mjs.map +0 -1
  46. package/dist/types-BcbGLNkh.d.mts.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,9 +1,10 @@
1
- import { A as validateModelHints, C as assertNever, D as collectModelRefs, E as collectFieldDependencies, O as collectRequestBodyModels, S as SpecParseError, T as collectEnumRefs, _ as ConfigVersionMismatchError, a as singularize, b as OagenError, c as stripListItemMarkers, d as toPascalCase, f as toSnakeCase, g as ConfigLoadError, h as ConfigError, j as walkTypeRef, k as mapTypeRef, l as toCamelCase, m as CommandError, n as ACRONYM_SET, o as splitWords, p as toUpperSnakeCase, r as cleanSchemaName, s as stripBackendPrefixes, u as toKebabCase, v as ExtractorError, w as assignModelsToServices, x as RegistryError, y as InternalError } from "./tree-sitter-CDKm3n64.mjs";
2
- import { _ as findResolvedMethodCollisions, a as registerEmitter, c as integrateGeneratedFiles, d as applyFileHeaders, f as buildEmitterContext, g as deriveMethodName, h as generateFiles, i as getEmitter, l as mapFilesForTargetIntegration, m as generateAllFiles, n as buildDirectionIndex, o as generate, p as collectReferencedNames, r as directionOf, t as diffSpecs, u as treeShakeFiles, v as resolveMountTarget, y as resolveOperations } from "./diff-BO5FieX5.mjs";
1
+ import { A as mapTypeRef, C as assertNever, D as collectModelRefs, E as collectFieldDependencies, M as walkTypeRef, O as collectRequestBodyModels, S as SpecParseError, T as collectEnumRefs, _ as ConfigVersionMismatchError, a as singularize, b as OagenError, c as stripListItemMarkers, d as toPascalCase, f as toSnakeCase, g as ConfigLoadError, h as ConfigError, j as validateModelHints, k as collectResponseHeaders, l as toCamelCase, m as CommandError, n as ACRONYM_SET, o as splitWords, p as toUpperSnakeCase, r as cleanSchemaName, s as stripBackendPrefixes, u as toKebabCase, v as ExtractorError, w as assignModelsToServices, x as RegistryError, y as InternalError } from "./tree-sitter-C7Hk-NDu.mjs";
2
+ import { _ as findResolvedMethodCollisions, a as registerEmitter, c as integrateGeneratedFiles, d as applyFileHeaders, f as buildEmitterContext, g as deriveMethodName, h as generateFiles, i as getEmitter, l as mapFilesForTargetIntegration, m as generateAllFiles, n as buildDirectionIndex, o as generate, p as collectReferencedNames, r as directionOf, t as diffSpecs, u as treeShakeFiles, v as resolveMountTarget, y as resolveOperations } from "./diff-CAsypxXV.mjs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { BaseResolver, Oas3_1Types, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
7
+ import { createHash } from "node:crypto";
7
8
  //#region src/ir/sdk-behavior.ts
8
9
  /**
9
10
  * Canonical SDK behavior defaults. Per-language or per-project overrides can
@@ -143,14 +144,21 @@ function deepMerge(target, source) {
143
144
  //#region src/parser/refs.ts
144
145
  async function loadAndBundleSpec(specPath) {
145
146
  const absolutePath = resolve(specPath);
147
+ const content = await readFile(absolutePath, "utf-8");
148
+ const document = makeDocumentFromString(content, pathToFileURL(absolutePath).href);
149
+ const config = await createConfig({});
150
+ const resolver = new BaseResolver();
146
151
  const result = await bundleDocument({
147
- document: makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href),
148
- config: await createConfig({}),
152
+ document,
153
+ config,
149
154
  types: Oas3_1Types,
150
- externalRefResolver: new BaseResolver(),
155
+ externalRefResolver: resolver,
151
156
  dereference: false
152
157
  });
153
- 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.`);
158
+ if (result.problems.some((p) => p.severity === "error")) {
159
+ const errors = result.problems.filter((p) => p.severity === "error").map((p) => p.message).join("\n");
160
+ throw new SpecParseError(`Failed to parse spec: ${errors}`, `Check the OpenAPI spec at "${absolutePath}" for syntax errors. Run a linter such as \`npx @redocly/cli lint ${specPath}\` to identify issues.`);
161
+ }
154
162
  return {
155
163
  parsed: result.bundle.parsed,
156
164
  specPath: absolutePath
@@ -176,138 +184,598 @@ function qualifyInlineModelName(baseName, parentName) {
176
184
  return `${cleanParent}${baseName}`;
177
185
  }
178
186
  /**
187
+ * The inline model or enum names `schemaToTypeRef` would mint for `schema`
188
+ * typed under `contextName` with `parentModelName`; empty when it mints none
189
+ * (a `$ref`, a primitive, an array or map of those). Mirrors the naming
190
+ * branches of `schemaToTypeRef`: an inline object in any form it recognises
191
+ * (`properties`, an `allOf` with object members) takes
192
+ * `qualifyInlineModelName`; a `oneOf`/`anyOf` mints one model per inline
193
+ * object variant, named exactly as the union references them
194
+ * (`nameInlineVariants`: `DrawingLayer`, `DrawingLayerWithOpacity`, or
195
+ * `CircleDrawingLayer` when every variant is titled); an inline enum, or a
196
+ * `oneOf` of string consts that collapses into one, takes the parent-prefixed
197
+ * enum name, which is never singularized; an array or map claims whatever its
198
+ * items or values would.
199
+ */
200
+ function mintedInlineNames(contextName, schema, parentModelName) {
201
+ return mintedInlineNameEntries(contextName, schema, parentModelName).map((entry) => entry.name);
202
+ }
203
+ /** `mintedInlineNames`, also telling for each name whether it is context-free. */
204
+ function mintedInlineNameEntries(contextName, schema, parentModelName) {
205
+ const s = unwrapNullableSchema(schema);
206
+ if (s.$ref) return [];
207
+ const base = toPascalCase(contextName);
208
+ const modelName = () => [{
209
+ name: qualifyInlineModelName(base, parentModelName),
210
+ contextFree: false
211
+ }];
212
+ const enumName = () => {
213
+ const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
214
+ return [{
215
+ name: cleanParent && !base.startsWith(cleanParent) ? `${cleanParent}${base}` : base,
216
+ contextFree: false
217
+ }];
218
+ };
219
+ if (s.allOf) {
220
+ const only = s.allOf.length === 1 ? s.allOf[0] : void 0;
221
+ if (only) return mintedInlineNameEntries(contextName, only, parentModelName);
222
+ return s.allOf.some((m) => !m.$ref && (m.properties || m.type === "object")) ? modelName() : [];
223
+ }
224
+ const variants = s.oneOf ?? s.anyOf;
225
+ if (variants) {
226
+ const nonNull = variants.filter((v) => !isNullSchema$1(v));
227
+ if (nonNull.length >= 2 && nonNull.every((v) => typeof v.const === "string")) return enumName();
228
+ return [...nameInlineVariantEntries(s, modelName()[0].name)].map(([variant, entry]) => ({
229
+ ...entry,
230
+ schema: variant
231
+ }));
232
+ }
233
+ if (s.const !== void 0) return [];
234
+ if ((s.enum ?? []).filter((v) => v !== null).length >= 2) return enumName();
235
+ if (isInlineObjectSchema(s)) return modelName();
236
+ if (s.items && (s.type === "array" || !s.type)) return mintedInlineNameEntries(contextName, s.items, parentModelName);
237
+ if (s.type === "object" && !s.properties) {
238
+ const value = mapValueSchema(s);
239
+ return value ? mintedInlineNameEntries(contextName, value, parentModelName) : [];
240
+ }
241
+ return [];
242
+ }
243
+ /**
244
+ * The inline names the other properties of an object mint
245
+ * (`mintedInlineNameEntries`) which a `oneOf`/`anyOf` under `fieldName` must
246
+ * not give one of its variants (`nameInlineVariants`): beside a
247
+ * `shape_with_side` object, the `{ side }` variant of `shape` cannot become
248
+ * `DrawingShapeWithSide`. Every context-dependent sibling name is reserved.
249
+ * A context-free name — a title-mapped variant's title — names one shape
250
+ * wherever it appears, so it is shared, not reserved, when this field gives
251
+ * the same title a variant of the identical shape (`schemaShapeKey`:
252
+ * documentation keywords ignored, property names verbatim — the rule array
253
+ * items already use to share a singular sibling's model); a sibling giving
254
+ * the same title a different shape, or one this field does not use, reserves
255
+ * it, so this union's variant falls back to its context-qualified name. Each
256
+ * sibling's names are read in isolation, without the reservations that
257
+ * sibling would itself apply, so the set depends on sibling names and content
258
+ * only, two unions beside each other cannot chase each other's names, and
259
+ * the shape test is symmetric: when `Circle` means `{ radius }` on one side
260
+ * and `{ diameter }` on the other, both fall back (`CircleDrawingShape`,
261
+ * `CircleDrawingMarker`) and neither keeps the bare `Circle`. Empty without a
262
+ * sibling set, as for a request body or response that is itself a union.
263
+ */
264
+ function reservedInlineNames(fieldName, siblings, parentModelName) {
265
+ const reserved = /* @__PURE__ */ new Set();
266
+ if (!siblings) return reserved;
267
+ const ownShapes = /* @__PURE__ */ new Map();
268
+ const fieldSchema = siblings.get(fieldName);
269
+ if (fieldSchema) for (const own of mintedInlineNameEntries(fieldName, fieldSchema, parentModelName)) {
270
+ if (!own.contextFree || !own.schema) continue;
271
+ ownShapes.set(own.name, [...ownShapes.get(own.name) ?? [], schemaShapeKey(own.schema)]);
272
+ }
273
+ for (const [siblingName, siblingSchema] of siblings) {
274
+ if (siblingName === fieldName) continue;
275
+ for (const minted of mintedInlineNameEntries(siblingName, siblingSchema, parentModelName)) {
276
+ if (!minted.contextFree) {
277
+ reserved.add(minted.name);
278
+ continue;
279
+ }
280
+ const shapes = ownShapes.get(minted.name);
281
+ const siblingShape = minted.schema ? schemaShapeKey(minted.schema) : void 0;
282
+ if (!(shapes !== void 0 && siblingShape !== void 0 && shapes.every((s) => s === siblingShape))) reserved.add(minted.name);
283
+ }
284
+ }
285
+ return reserved;
286
+ }
287
+ /** Documentation keywords that do not shape a model; ignored by `schemaShapeKey` at a schema's own level. */
288
+ const DOC_KEYWORDS = /* @__PURE__ */ new Set([
289
+ "description",
290
+ "title",
291
+ "example",
292
+ "examples",
293
+ "summary",
294
+ "externalDocs",
295
+ "deprecated"
296
+ ]);
297
+ /** Keywords whose value maps NAMES (not keywords) to schemas: names are kept verbatim, values recurse as schemas. */
298
+ const NAMED_SCHEMA_MAPS = /* @__PURE__ */ new Set([
299
+ "properties",
300
+ "patternProperties",
301
+ "dependentSchemas",
302
+ "$defs",
303
+ "definitions"
304
+ ]);
305
+ /** Keywords whose value is a single schema. */
306
+ const SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
307
+ "items",
308
+ "additionalProperties",
309
+ "additionalItems",
310
+ "contains",
311
+ "propertyNames",
312
+ "not",
313
+ "if",
314
+ "then",
315
+ "else",
316
+ "unevaluatedProperties",
317
+ "unevaluatedItems"
318
+ ]);
319
+ /** Keywords whose value is an ordered list of schemas. */
320
+ const SCHEMA_LIST_KEYWORDS = /* @__PURE__ */ new Set([
321
+ "allOf",
322
+ "oneOf",
323
+ "anyOf",
324
+ "prefixItems"
325
+ ]);
326
+ /** Keywords whose list value is a set: compared order-insensitively. */
327
+ const UNORDERED_LIST_KEYWORDS = /* @__PURE__ */ new Set(["required", "type"]);
328
+ /**
329
+ * A canonical rendering of a schema's shape, for deciding whether two inline
330
+ * schemas describe the same model: every structural keyword (`type`,
331
+ * `properties`, `required`, `items`, `additionalProperties`,
332
+ * `patternProperties`, `allOf`/`oneOf`/`anyOf`, `enum`, `const`, `format`,
333
+ * `nullable`, ...) recursively, with keywords sorted and the `required` and
334
+ * `type` lists order-insensitive. Documentation keywords (`description`,
335
+ * `title`, `example`, `examples`, `summary`, `externalDocs`, `deprecated`)
336
+ * and vendor extensions (`x-*`) are ignored, and so is `nullable: false`,
337
+ * which the nullable unwrap leaves behind — but only where they are a
338
+ * schema's own keywords. Inside `properties`, `patternProperties`,
339
+ * `dependentSchemas`, `dependentRequired` and `$defs` the keys are property
340
+ * names, kept verbatim: a field named `description` or `title` is part of the
341
+ * shape. Enum, const and default values are compared as plain data, in
342
+ * order, as the IR keeps them.
343
+ */
344
+ function schemaShapeKey(schema) {
345
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
346
+ const dataKey = (value) => {
347
+ if (Array.isArray(value)) return value.map(dataKey);
348
+ if (!isRecord(value)) return value;
349
+ const out = {};
350
+ for (const key of Object.keys(value).sort()) out[key] = dataKey(value[key]);
351
+ return out;
352
+ };
353
+ const schemaKey = (value) => {
354
+ if (!isRecord(value)) return value;
355
+ const out = {};
356
+ for (const keyword of Object.keys(value).sort()) {
357
+ const v = value[keyword];
358
+ if (DOC_KEYWORDS.has(keyword) || keyword.startsWith("x-")) continue;
359
+ if (keyword === "nullable" && v === false) continue;
360
+ if (NAMED_SCHEMA_MAPS.has(keyword) && isRecord(v)) {
361
+ const named = {};
362
+ for (const name of Object.keys(v).sort()) named[name] = schemaKey(v[name]);
363
+ out[keyword] = named;
364
+ } else if (keyword === "dependentRequired" && isRecord(v)) {
365
+ const named = {};
366
+ for (const name of Object.keys(v).sort()) {
367
+ const names = v[name];
368
+ named[name] = Array.isArray(names) ? names.map(String).sort() : dataKey(names);
369
+ }
370
+ out[keyword] = named;
371
+ } else if (SCHEMA_KEYWORDS.has(keyword)) out[keyword] = schemaKey(v);
372
+ else if (SCHEMA_LIST_KEYWORDS.has(keyword) && Array.isArray(v)) out[keyword] = v.map(schemaKey);
373
+ else if (UNORDERED_LIST_KEYWORDS.has(keyword) && Array.isArray(v)) out[keyword] = v.map(String).sort();
374
+ else out[keyword] = dataKey(v);
375
+ }
376
+ return out;
377
+ };
378
+ return JSON.stringify(schemaKey(schema));
379
+ }
380
+ /**
381
+ * The context name an array field's items (`kind: 'item'`) or a map field's
382
+ * values (`kind: 'value'`) are typed and extracted under. By default it is
383
+ * the field itself, so the inline model is the parent plus the singularized
384
+ * field (`layers` on `Drawing` → `DrawingLayer`).
385
+ *
386
+ * When a sibling property of the same object would mint that very name —
387
+ * `layer` as an inline object, an inline enum, an inline union with object
388
+ * variants (any of whose variant names counts), or an array or map of such
389
+ * (`mintedInlineNames`); a primitive `email` beside `emails` claims nothing
390
+ * — the elements step aside to
391
+ * `<field>_item` / `<field>_value`, which qualify to `<Parent><Field>Item` /
392
+ * `<Parent><Field>Value` (`DrawingLayersItem`, `DrawingLayersValue`),
393
+ * matching the `<Variant>Item` convention. Should a sibling mint that name
394
+ * too (a property `layers_item`), the fallback keeps going the way
395
+ * `variantMemberKeys` does for variants: `<field>_array_item`, then
396
+ * `<field>_array_item2`, `<field>_array_item3`, ... for items
397
+ * (`DrawingLayersArrayItem`, ...) and `<field>_member_value`,
398
+ * `<field>_member_value2`, ... for values, taking the first candidate whose
399
+ * minted name no sibling claims.
400
+ *
401
+ * Identical-shape sharing: when every sibling claiming the singular name is
402
+ * an inline object whose shape is structurally identical to the items or
403
+ * values (`schemaShapeKey`: same properties, required, types, nested inline
404
+ * schemas and enum values; description, title and example ignored), the
405
+ * fields share one model under the singular name. They are the same shape,
406
+ * so one model is the right SDK type and a duplicate would only add noise;
407
+ * should a later spec version make the shapes diverge, the split happens
408
+ * then, which is the moment the item type genuinely changes. This
409
+ * deliberately departs from a literal one-model-per-field reading for the
410
+ * identical case.
411
+ *
412
+ * Only context-dependent names take part: a title-mapped variant's title
413
+ * (`MintedInlineName.contextFree`) is the same under every context, so no
414
+ * candidate could step away from it, and such a name is shared by design —
415
+ * two sibling arrays whose items carry the same titled variants both
416
+ * reference `Circle` and `Square` (`nameInlineVariants`). The search always
417
+ * terminates: it stops as soon as a candidate changes nothing about the names
418
+ * (`firstFreeElementContext`'s no-progress rule), and
419
+ * `MAX_ELEMENT_CONTEXT_ATTEMPTS` caps it as a backstop; both return the last
420
+ * candidate tried, deterministically.
421
+ *
422
+ * Decided from sibling names and schema content alone, so document order
423
+ * plays no part. `schemaToTypeRef` and every extractor call this with the
424
+ * same sibling set (`collectProperties`); otherwise the reference and the
425
+ * model disagree.
426
+ */
427
+ function elementContextName(fieldName, kind, siblings, parentModelName, elementSchema) {
428
+ return elementContextDecision(fieldName, kind, siblings, parentModelName, elementSchema).context;
429
+ }
430
+ /** `elementContextName`, with the reason it decided as it did (see `ElementContextReason`). */
431
+ function elementContextDecision(fieldName, kind, siblings, parentModelName, elementSchema) {
432
+ if (!siblings) return {
433
+ context: fieldName,
434
+ reason: "no-siblings"
435
+ };
436
+ const dependentNames = (candidate) => mintedInlineNameEntries(candidate, elementSchema, parentModelName).filter((entry) => !entry.contextFree).map((entry) => entry.name);
437
+ const own = dependentNames(fieldName);
438
+ if (own.length === 0) return {
439
+ context: fieldName,
440
+ reason: "nothing-minted"
441
+ };
442
+ const claimed = /* @__PURE__ */ new Map();
443
+ for (const [siblingName, siblingSchema] of siblings) {
444
+ if (siblingName === fieldName) continue;
445
+ for (const minted of mintedInlineNames(siblingName, siblingSchema, parentModelName)) claimed.set(minted, [...claimed.get(minted) ?? [], siblingSchema]);
446
+ }
447
+ const rivals = [...new Set(own.flatMap((name) => claimed.get(name) ?? []))];
448
+ if (rivals.length === 0) return {
449
+ context: fieldName,
450
+ reason: "unclaimed"
451
+ };
452
+ const element = unwrapNullableSchema(elementSchema);
453
+ const identical = (rival) => {
454
+ const sibling = unwrapNullableSchema(rival);
455
+ return isInlineObjectSchema(sibling) && isInlineObjectSchema(element) && schemaShapeKey(sibling) === schemaShapeKey(element);
456
+ };
457
+ if (rivals.every(identical)) return {
458
+ context: fieldName,
459
+ reason: "shared-shape"
460
+ };
461
+ return firstFreeElementContext(fieldName, kind, new Set(claimed.keys()), dependentNames, own);
462
+ }
463
+ /**
464
+ * The first fallback context under which the elements mint no claimed name:
465
+ * `<field>_item` / `<field>_value`, then `<field>_array_item` /
466
+ * `<field>_member_value`, then `<field>_array_item2`, `…3`, ... (the way
467
+ * `variantMemberKeys` numbers a variant's members). `mint` gives the names
468
+ * under a candidate and `previous` those under the context tried before the
469
+ * first candidate. The search stops with `no-progress` as soon as a
470
+ * candidate mints the same names as the one before it — the names do not
471
+ * depend on the context, so no candidate can avoid the claim — and with
472
+ * `cap` after `maxAttempts` candidates; both keep the last candidate tried.
473
+ */
474
+ function firstFreeElementContext(fieldName, kind, claimed, mint, previous, maxAttempts = 256) {
475
+ const fallback = kind === "item" ? "array_item" : "member_value";
476
+ let last = previous;
477
+ let candidate = fieldName;
478
+ for (let i = 0; i < maxAttempts; i++) {
479
+ candidate = i === 0 ? `${fieldName}_${kind}` : i === 1 ? `${fieldName}_${fallback}` : `${fieldName}_${fallback}${i}`;
480
+ const minted = mint(candidate);
481
+ if (minted.every((name) => !claimed.has(name))) return {
482
+ context: candidate,
483
+ reason: "free"
484
+ };
485
+ if (sameNames(minted, last)) return {
486
+ context: candidate,
487
+ reason: "no-progress"
488
+ };
489
+ last = minted;
490
+ }
491
+ return {
492
+ context: candidate,
493
+ reason: "cap"
494
+ };
495
+ }
496
+ /** The same names, in any order. */
497
+ function sameNames(a, b) {
498
+ if (a.length !== b.length) return false;
499
+ const sortedB = [...b].sort();
500
+ return [...a].sort().every((name, i) => name === sortedB[i]);
501
+ }
502
+ /**
503
+ * Every property the object described by `schema` exposes, by name: its own
504
+ * `properties`, those of its `allOf` members (following a member's `$ref`
505
+ * into `schemas` when given, as `extractAllOfModel` does), and those of
506
+ * inline `oneOf`/`anyOf` variants, whose fields the model extractors flatten
507
+ * in; the first definition of a name wins. This is the sibling set
508
+ * `elementContextName` decides from; the TypeRef builder and every extractor
509
+ * derive it through this one function so they agree.
510
+ */
511
+ function collectProperties(schema, schemas) {
512
+ const properties = /* @__PURE__ */ new Map();
513
+ const seen = /* @__PURE__ */ new Set();
514
+ const visit = (s, followRef) => {
515
+ if (!s || seen.has(s)) return;
516
+ seen.add(s);
517
+ if (s.$ref) {
518
+ if (followRef && schemas) visit(schemas[s.$ref.split("/").pop() ?? ""], true);
519
+ return;
520
+ }
521
+ for (const [key, value] of Object.entries(s.properties ?? {})) if (value && !properties.has(key)) properties.set(key, value);
522
+ for (const sub of s.allOf ?? []) visit(sub, true);
523
+ for (const variant of [...s.oneOf ?? [], ...s.anyOf ?? []]) visit(variant, false);
524
+ };
525
+ visit(schema, false);
526
+ return properties;
527
+ }
528
+ /**
179
529
  * Walk all component schemas and extract inline Model definitions for fields
180
- * that are objects with properties (or arrays of such objects).
530
+ * that are objects with properties, including objects reached through array
531
+ * items, `oneOf` variants, and map (`additionalProperties` /
532
+ * `patternProperties`) values.
181
533
  * These correspond to the ModelRef entries created by schemaToTypeRef.
182
534
  */
183
535
  function extractInlineModelsFromSchemas(schemas) {
184
536
  if (!schemas) return [];
185
537
  const inlineModels = [];
186
- for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, resolveSchemaName(schemaName));
538
+ for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, resolveSchemaName(schemaName), schemas, collectProperties(schema, schemas));
187
539
  return inlineModels;
188
540
  }
189
- function extractInlineModelsFromProperties(schema, results, parentName) {
541
+ /**
542
+ * Walk a schema's properties and materialise every inline object as a model.
543
+ * `componentSchemas` is optional and only feeds `buildFieldFromSchema`, so a
544
+ * field that `$ref`s a component can take its `example`/`examples` from it;
545
+ * callers without the component map at hand simply omit it.
546
+ * `siblingProperties` is the parent object's full property set, for callers
547
+ * that see more of it than this schema shows (see `elementContextName`).
548
+ * `reservedNames` is set only on the re-entry for a field's array items or
549
+ * map values: the inline names that field's siblings mint, which a union
550
+ * among the items or values must step around exactly as `schemaToTypeRef`
551
+ * does when it hands the same set down (`reservedNamesFor`).
552
+ *
553
+ * The recursion follows the document's own inline structure only: a `$ref`
554
+ * is resolved solely to read a component's property list for typing (an
555
+ * augmentation's inherited fields, a sibling set), never descended into, so
556
+ * the depth is bounded by the document's inline nesting and no component
557
+ * graph — self-extending, mutually extending, or reused at several levels —
558
+ * can make it run away.
559
+ */
560
+ function extractInlineModelsFromProperties(schema, results, parentName, componentSchemas, siblingProperties, reservedNames) {
190
561
  const properties = schema.properties ?? {};
191
562
  const allOfSchemas = schema.allOf ?? [];
192
- for (const sub of allOfSchemas) if (sub.properties) extractInlineModelsFromProperties(sub, results, parentName);
193
- for (const [fieldName, fieldSchema] of Object.entries(properties)) {
194
- if (!fieldSchema) continue;
195
- if (fieldSchema.properties && (fieldSchema.type === "object" || !fieldSchema.type)) {
563
+ const siblings = siblingProperties ?? collectProperties(schema);
564
+ const reservedFor = (fieldName) => reservedNames ?? reservedInlineNames(fieldName, siblings, parentName);
565
+ for (const sub of allOfSchemas) if (sub.properties) extractInlineModelsFromProperties(sub, results, parentName, componentSchemas, siblings, reservedNames);
566
+ for (const [fieldName, rawFieldSchema] of Object.entries(properties)) {
567
+ if (!rawFieldSchema) continue;
568
+ const fieldSchema = unwrapNullableField(rawFieldSchema);
569
+ if (isInlineObjectSchema(fieldSchema)) {
196
570
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
197
- results.push(buildInlineModel(modelName, fieldSchema));
198
- extractInlineModelsFromProperties(fieldSchema, results, modelName);
571
+ results.push(buildInlineModel(modelName, fieldSchema, componentSchemas));
572
+ extractInlineModelsFromProperties(fieldSchema, results, modelName, componentSchemas);
199
573
  }
200
- if (fieldSchema.type === "array" && fieldSchema.items) {
201
- const items = fieldSchema.items;
202
- if (items.properties && (items.type === "object" || !items.type)) {
203
- const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
204
- results.push(buildInlineModel(modelName, items));
205
- extractInlineModelsFromProperties(items, results, modelName);
206
- }
574
+ if (fieldSchema.type === "array" && fieldSchema.items) extractInlineModelsFromProperties({ properties: { [elementContextName(fieldName, "item", siblings, parentName, fieldSchema.items)]: fieldSchema.items } }, results, parentName, componentSchemas, void 0, reservedFor(fieldName));
575
+ if (fieldSchema.type === "object" && !fieldSchema.properties) {
576
+ const valueSchema = mapValueSchema(fieldSchema);
577
+ if (valueSchema) extractInlineModelsFromProperties({ properties: { [elementContextName(fieldName, "value", siblings, parentName, valueSchema)]: valueSchema } }, results, parentName, componentSchemas, void 0, reservedFor(fieldName));
207
578
  }
208
- if (fieldSchema.allOf && fieldSchema.allOf.every((s) => !s.$ref && !s.oneOf && !s.anyOf)) {
209
- const merged = mergeAllOfObjectSchema(fieldSchema);
210
- if (Object.keys(merged.properties ?? {}).length > 0) {
579
+ if (fieldSchema.allOf) {
580
+ const members = fieldSchema.allOf;
581
+ const emitMerged = () => {
582
+ const merged = mergeAugmentation(fieldSchema, componentSchemas);
583
+ if (merged.properties.length === 0) return;
211
584
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
212
- results.push(buildInlineModel(modelName, merged));
213
- extractInlineModelsFromProperties(merged, results, modelName);
214
- }
585
+ results.push(buildMergedModel(modelName, merged, componentSchemas));
586
+ const own = {};
587
+ for (const p of merged.properties) if (p.origin === null) own[p.name] = p.schema;
588
+ const mergedSiblings = new Map(merged.properties.map((p) => [p.name, p.schema]));
589
+ extractInlineModelsFromProperties({ properties: own }, results, modelName, componentSchemas, mergedSiblings);
590
+ };
591
+ const only = members.length === 1 ? members[0] : void 0;
592
+ if (members.some((m) => m.$ref)) {
593
+ if (members.some((m) => !m.$ref && (m.properties || m.type === "object"))) emitMerged();
594
+ } else if (only) extractInlineModelsFromProperties({ properties: { [fieldName]: {
595
+ ...only,
596
+ ...extractConstraints(fieldSchema)
597
+ } } }, results, parentName, componentSchemas, siblings, reservedNames);
598
+ else if (members.some((m) => m.properties)) emitMerged();
215
599
  }
216
- if (fieldSchema.oneOf) {
217
- const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
600
+ const unionVariants = fieldSchema.oneOf ?? fieldSchema.anyOf;
601
+ if (unionVariants) {
602
+ const inlineObjectVariants = unionVariants.filter((v) => isInlineObjectSchema(v));
218
603
  if (inlineObjectVariants.length > 0) {
219
604
  const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
220
605
  const existingNames = new Set(results.map((r) => r.name));
221
- const namingDiscProp = deriveOneOfNamingDiscriminator(inlineObjectVariants);
222
- const emittedNames = [];
606
+ const variantNames = nameInlineVariants(fieldSchema, modelName, reservedFor(fieldName));
223
607
  for (const variant of inlineObjectVariants) {
224
- const variantName = nameOneOfVariant(variant, modelName, emittedNames, namingDiscProp);
225
- emittedNames.push(variantName);
608
+ const variantName = variantNames.get(variant);
609
+ if (variantName === void 0) continue;
226
610
  if (!existingNames.has(variantName)) {
227
611
  existingNames.add(variantName);
228
- results.push(buildInlineModel(variantName, variant));
229
- extractInlineModelsFromProperties(variant, results, variantName);
612
+ results.push(buildInlineModel(variantName, variant, componentSchemas));
613
+ extractInlineModelsFromProperties(variant, results, variantName, componentSchemas);
614
+ extractInlineModelsFromProperties({ properties: variantMemberSchemas(variant, variantName) }, results, variantName, componentSchemas);
230
615
  }
231
616
  }
232
617
  }
233
618
  }
234
619
  }
235
620
  }
236
- /** Find a single string-const-valued property shared by every variant whose
237
- * values are all distinct — the implicit discriminator. Returns null when no
238
- * such property exists. Mirrors `deriveConstNamingDiscriminator` in schemas.ts. */
239
- function deriveOneOfNamingDiscriminator(variants) {
240
- if (variants.length < 2) return null;
241
- const candidates = Object.keys(variants[0]?.properties ?? {});
242
- for (const propName of candidates) {
243
- const values = variants.map((v) => readConstString(v.properties?.[propName]));
244
- if (values.some((v) => v === null)) continue;
245
- if (new Set(values).size !== values.length) continue;
246
- return propName;
247
- }
248
- return null;
249
- }
250
- function readConstString(prop) {
251
- if (!prop) return null;
252
- if (typeof prop.const === "string") return prop.const;
253
- if (Array.isArray(prop.enum) && prop.enum.length === 1 && typeof prop.enum[0] === "string") return prop.enum[0];
254
- return null;
255
- }
256
- /** Produce a per-variant model name. Variant 0 keeps the bare parent name;
257
- * later variants are prefixed by the const-derived label. Mirrors
258
- * `nameVariantModel` in schemas.ts. Falls back to a numeric suffix when no
259
- * discriminator is available, the const value PascalCases to nothing, or
260
- * the derived candidate collides with the parent or an already-emitted name. */
261
- function nameOneOfVariant(variant, parentName, alreadyEmitted, discriminatorProperty) {
262
- if (alreadyEmitted.length === 0) return parentName;
263
- if (discriminatorProperty) {
264
- const constValue = readConstString(variant.properties?.[discriminatorProperty]);
265
- if (constValue) {
266
- const prefix = toPascalCase(constValue);
267
- if (prefix) {
268
- const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
269
- if (!(candidate === parentName || alreadyEmitted.includes(candidate))) return candidate;
270
- }
271
- }
272
- }
273
- return `${parentName}${alreadyEmitted.length + 1}`;
274
- }
275
621
  /**
276
- * Merge the object members of an `allOf` (recursively) into a single
277
- * object-shaped schema, so an inline object expressed via `allOf` can be
278
- * materialized as one model — mirroring how schemaToTypeRef collapses such a
279
- * field into a single merged model ref. Earlier members win on key conflicts,
280
- * matching the merge order used elsewhere in the parser.
622
+ * Merge an `allOf` field into one object shape, remembering where each
623
+ * property comes from. A `$ref` member is resolved against `componentSchemas`
624
+ * by its component name, as `extractAllOfModel` does, and contributes its
625
+ * properties — its own nested `allOf` chain included — tagged with that
626
+ * component's model name, so the merged model can type them exactly as the
627
+ * component's own model does; inline members contribute properties tagged as
628
+ * the merged model's own. `required` is collected from every member. Earlier
629
+ * members win on key conflicts, matching the merge order used elsewhere in
630
+ * the parser; an unresolvable ref contributes nothing. The walk reads the
631
+ * components' property lists only — it never descends into a property — so
632
+ * it is finite for any component graph (the `seen` set only short-circuits a
633
+ * component reached twice within one merge).
281
634
  */
282
- function mergeAllOfObjectSchema(schema) {
283
- const properties = {};
635
+ function mergeAugmentation(schema, componentSchemas) {
636
+ const properties = [];
637
+ const seenNames = /* @__PURE__ */ new Set();
284
638
  const required = [];
285
- const visit = (s) => {
286
- if (s.properties) {
287
- for (const [key, value] of Object.entries(s.properties)) if (value && !(key in properties)) properties[key] = value;
639
+ const seen = /* @__PURE__ */ new Set();
640
+ const visit = (s, origin) => {
641
+ if (seen.has(s)) return;
642
+ seen.add(s);
643
+ if (s.$ref) {
644
+ const rawName = s.$ref.split("/").pop() ?? "";
645
+ const target = componentSchemas?.[rawName];
646
+ if (!target) return;
647
+ visit(target, origin ?? {
648
+ name: resolveSchemaName(rawName),
649
+ schema: target
650
+ });
651
+ return;
652
+ }
653
+ for (const [key, value] of Object.entries(s.properties ?? {})) {
654
+ if (!value || seenNames.has(key)) continue;
655
+ seenNames.add(key);
656
+ properties.push({
657
+ name: key,
658
+ schema: value,
659
+ origin
660
+ });
288
661
  }
289
662
  if (s.required) required.push(...s.required);
290
- if (s.allOf) for (const sub of s.allOf) visit(sub);
663
+ for (const sub of s.allOf ?? []) visit(sub, origin);
291
664
  };
292
- visit(schema);
665
+ visit(schema, null);
293
666
  return {
294
- type: "object",
295
667
  properties,
296
668
  required,
297
- description: schema.description
669
+ description: schema.description,
670
+ constraints: extractConstraints(schema)
298
671
  };
299
672
  }
300
- function buildInlineModel(name, schema) {
673
+ /**
674
+ * Build the model for a merged `allOf` field. An inherited property is typed
675
+ * with its component as naming context and the component's own sibling set,
676
+ * so it carries the same TypeRef as in the component's model (`BaseMeta`,
677
+ * `BaseChild`, ...); the merged model's own properties are typed under its
678
+ * name with the full merged property set as siblings.
679
+ */
680
+ function buildMergedModel(name, merged, componentSchemas) {
681
+ const requiredSet = new Set(merged.required);
682
+ const mergedSiblings = new Map(merged.properties.map((p) => [p.name, p.schema]));
683
+ const componentSiblings = /* @__PURE__ */ new Map();
684
+ const fields = merged.properties.map((p) => {
685
+ if (p.origin === null) return buildFieldFromSchema(p.name, p.schema, name, requiredSet, componentSchemas, mergedSiblings);
686
+ let siblings = componentSiblings.get(p.origin.name);
687
+ if (!siblings) {
688
+ siblings = collectProperties(p.origin.schema, componentSchemas);
689
+ componentSiblings.set(p.origin.name, siblings);
690
+ }
691
+ return buildFieldFromSchema(p.name, p.schema, p.origin.name, requiredSet, componentSchemas, siblings);
692
+ });
693
+ return {
694
+ name,
695
+ description: merged.description,
696
+ fields,
697
+ ...merged.constraints ? { constraints: merged.constraints } : {}
698
+ };
699
+ }
700
+ /**
701
+ * Strip the nullable wrappers `schemaToTypeRef` unwraps before it names
702
+ * anything — the OAS 3.1 `type: [X, 'null']` array, the OAS 3.0
703
+ * `nullable: true` flag, and a `oneOf`/`anyOf` with a single non-null
704
+ * variant — so extraction dispatches on the same shape the TypeRef was built
705
+ * from. The unwrap keeps `contextName`/`parentModelName` semantics intact:
706
+ * `schemaToTypeRef` passes both through each of these wrappers unchanged.
707
+ */
708
+ function unwrapNullableSchema(schema) {
709
+ if (schema.$ref) return schema;
710
+ if (Array.isArray(schema.type)) {
711
+ const nonNullTypes = schema.type.filter((t) => t !== "null");
712
+ if (nonNullTypes.length !== 1) return schema;
713
+ return unwrapNullableSchema({
714
+ ...schema,
715
+ type: nonNullTypes[0],
716
+ nullable: false
717
+ });
718
+ }
719
+ if (schema.nullable && schema.type) return unwrapNullableSchema({
720
+ ...schema,
721
+ nullable: false
722
+ });
723
+ const variants = schema.allOf ? void 0 : schema.oneOf ?? schema.anyOf;
724
+ if (variants) {
725
+ const nullVariant = variants.find(isNullSchema$1);
726
+ const nonNullVariants = variants.filter((v) => v !== nullVariant);
727
+ const only = nonNullVariants[0];
728
+ if (nullVariant && nonNullVariants.length === 1 && only) return unwrapNullableSchema(only);
729
+ }
730
+ return schema;
731
+ }
732
+ /**
733
+ * `unwrapNullableSchema` for a field, carrying the wrapper's limits onto the
734
+ * branch it unwraps to. The type-array and `nullable: true` forms unwrap by
735
+ * spreading, so every keyword survives; the union form swaps in the bare
736
+ * branch, which would drop a `minProperties` written beside
737
+ * `anyOf: [{ type: 'object', … }, { type: 'null' }]`. `extractConstraints`
738
+ * conjoins that wrapper's own keywords with its branch's as the field's
739
+ * limits, so the model built from the branch must carry the same ones or
740
+ * `Model.constraints` would omit what the field declares.
741
+ */
742
+ function unwrapNullableField(schema) {
743
+ const inner = unwrapNullableSchema(schema);
744
+ if (inner === schema || schema.allOf || !(schema.oneOf ?? schema.anyOf)) return inner;
745
+ return {
746
+ ...inner,
747
+ ...extractConstraints(schema)
748
+ };
749
+ }
750
+ /** The `{ type: 'null' }` member that marks a `oneOf`/`anyOf` as nullable — same test as schemaToTypeRef. */
751
+ function isNullSchema$1(schema) {
752
+ return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
753
+ }
754
+ /**
755
+ * The schema that types a map's values, in the precedence `schemaToTypeRef`
756
+ * uses: a non-empty `additionalProperties` object wins, otherwise the first
757
+ * `patternProperties` entry. `additionalProperties: {}` means "any value"
758
+ * (typed `unknown`, nothing to extract) and does not fall through to
759
+ * `patternProperties`, matching the type-ref branch.
760
+ */
761
+ function mapValueSchema(schema) {
762
+ const additional = schema.additionalProperties;
763
+ if (additional && typeof additional === "object") return Object.keys(additional).length > 0 ? additional : null;
764
+ return (schema.patternProperties ? Object.values(schema.patternProperties) : [])[0] ?? null;
765
+ }
766
+ function buildInlineModel(name, schema, componentSchemas) {
301
767
  const requiredSet = new Set(schema.required ?? []);
302
768
  const fields = [];
769
+ const siblings = collectProperties(schema);
303
770
  for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
304
771
  if (!fieldSchema) continue;
305
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
772
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, componentSchemas, siblings));
306
773
  }
307
774
  return {
308
775
  name,
309
776
  description: schema.description,
310
- fields
777
+ fields,
778
+ ...constraintsEntry(schema)
311
779
  };
312
780
  }
313
781
  //#endregion
@@ -315,9 +783,21 @@ function buildInlineModel(name, schema) {
315
783
  /**
316
784
  * Module-level transform set during extractSchemas(). Used by schemaToTypeRef()
317
785
  * to apply the same name transform to $ref model/enum references.
786
+ *
787
+ * Receives the cleaned PascalCase name and, when the name is a
788
+ * `components.schemas` key, the raw key exactly as spelled in the document.
789
+ * Names oagen synthesises (inline enums, request-body variant models,
790
+ * title-derived names) pass no second argument.
318
791
  */
319
792
  let activeSchemaNameTransform = null;
320
793
  /**
794
+ * Module-level set of raw `components.schemas` keys for the spec being
795
+ * extracted. resolveSchemaName() consults it to decide whether the name it was
796
+ * handed is a real component key (forwarded to the transform as
797
+ * `originalName`) or a name oagen synthesised (no `originalName`).
798
+ */
799
+ let activeComponentSchemaKeys = /* @__PURE__ */ new Set();
800
+ /**
321
801
  * Module-level set of resolved schema names whose components/schemas entry is
322
802
  * an enum (i.e. has `enum:`). Populated by extractSchemas() so schemaToTypeRef()
323
803
  * can classify a `$ref` to a top-level enum schema as `kind: 'enum'` instead of
@@ -325,33 +805,308 @@ let activeSchemaNameTransform = null;
325
805
  * ref, even when the target was a shared enum like `PaginationOrder`.
326
806
  */
327
807
  let activeEnumSchemaNames = /* @__PURE__ */ new Set();
328
- /** Apply cleanSchemaName + the active transform (if any) to a raw schema name. */
808
+ /**
809
+ * Subset of `activeEnumSchemaNames` whose component schema also admits
810
+ * `null` — via an OAS 3.1 `type: [..., 'null']` array, the OAS 3.0
811
+ * `nullable: true` flag, or a literal `null` member in `enum:`. A `$ref` to
812
+ * one of these resolves to `nullable(enum)` so the component's own
813
+ * nullability survives the reference, matching the shape an inline
814
+ * `oneOf: [{ $ref }, { type: 'null' }]` already produces.
815
+ */
816
+ let activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
817
+ /** Drop `null` from an `enum:` list — it marks nullability, it is not a member. */
818
+ function nonNullEnumValues(values) {
819
+ return (values ?? []).filter((v) => v !== null);
820
+ }
821
+ /** True when an enum schema admits `null` by any OAS 3.0 / 3.1 spelling. */
822
+ function isNullableEnumSchema(schema) {
823
+ if (schema.nullable === true) return true;
824
+ if (Array.isArray(schema.type) && schema.type.includes("null")) return true;
825
+ return Array.isArray(schema.enum) && schema.enum.includes(null);
826
+ }
827
+ /** Wrap `ref` in `nullable` unless it already is one, so a nullable enum `$ref` is never double-wrapped. */
828
+ function asNullable(ref) {
829
+ return ref.kind === "nullable" ? ref : {
830
+ kind: "nullable",
831
+ inner: ref
832
+ };
833
+ }
834
+ /**
835
+ * Apply cleanSchemaName + the active transform (if any) to a raw schema name.
836
+ * When `rawName` is a `components.schemas` key the transform also receives it
837
+ * as `originalName`, so a consumer can opt out of normalization by returning
838
+ * the original spelling. Names oagen synthesises (request-body variant
839
+ * models, title-derived names, ...) are not component keys and get no
840
+ * `originalName`.
841
+ */
329
842
  function resolveSchemaName(rawName) {
330
843
  let name = cleanSchemaName(toPascalCase(rawName));
331
- if (activeSchemaNameTransform) name = activeSchemaNameTransform(name);
844
+ if (activeSchemaNameTransform) {
845
+ const originalName = activeComponentSchemaKeys.has(rawName) ? rawName : void 0;
846
+ name = activeSchemaNameTransform(name, originalName);
847
+ }
332
848
  return name;
333
849
  }
850
+ /** Validation keywords copied through unchanged when they hold a number. */
851
+ const NUMERIC_CONSTRAINT_KEYS = [
852
+ "multipleOf",
853
+ "minLength",
854
+ "maxLength",
855
+ "minItems",
856
+ "maxItems",
857
+ "minContains",
858
+ "maxContains",
859
+ "minProperties",
860
+ "maxProperties"
861
+ ];
862
+ /**
863
+ * Collect the JSON Schema validation keywords declared directly on `schema`
864
+ * into a `Constraints` object, or `undefined` when it declares none — so a
865
+ * schema without limits yields no `constraints` key on its IR node at all.
866
+ *
867
+ * Numeric bounds are normalized to the OAS 3.1 shape. OAS 3.0 spells an
868
+ * exclusive bound as `exclusiveMinimum: true` beside `minimum: n`; that pair
869
+ * becomes `exclusiveMinimum: n` with no `minimum`. A boolean `false`, or a
870
+ * `true` with no bound to apply to, is dropped. The 3.1 numeric form passes
871
+ * through, including the legal-but-unusual case of both an inclusive and an
872
+ * exclusive bound on the same side. `uniqueItems: false` restates the default
873
+ * and is dropped too.
874
+ *
875
+ * Only the schema's own keywords are read: limits nested inside array `items`
876
+ * or map values are not collected. The one exception is a nullable wrapper —
877
+ * a `oneOf`/`anyOf` with a single non-null, non-`$ref` branch — that declares
878
+ * none itself: it takes that branch's keywords, since the IR collapses it to
879
+ * `nullable<T>`. Values of the wrong type (a string `maxLength`, say) are
880
+ * ignored rather than carried.
881
+ */
882
+ function extractConstraints(schema) {
883
+ let out = ownConstraints(schema);
884
+ for (const member of schema.allOf ?? []) out = combineConstraints(out, extractConstraints(member));
885
+ const branch = singleNonNullBranch(schema);
886
+ return branch ? combineConstraints(out, extractConstraints(branch)) : out;
887
+ }
888
+ /** The keywords declared strictly on `schema`, normalized — without the nullable-branch fallback. */
889
+ function ownConstraints(schema) {
890
+ const out = {};
891
+ const lower = normalizeBound(schema.minimum, schema.exclusiveMinimum);
892
+ if (lower.inclusive !== void 0) out.minimum = lower.inclusive;
893
+ if (lower.exclusive !== void 0) out.exclusiveMinimum = lower.exclusive;
894
+ const upper = normalizeBound(schema.maximum, schema.exclusiveMaximum);
895
+ if (upper.inclusive !== void 0) out.maximum = upper.inclusive;
896
+ if (upper.exclusive !== void 0) out.exclusiveMaximum = upper.exclusive;
897
+ for (const key of NUMERIC_CONSTRAINT_KEYS) {
898
+ const value = schema[key];
899
+ if (typeof value === "number") out[key] = value;
900
+ }
901
+ if (typeof schema.pattern === "string") out.pattern = schema.pattern;
902
+ if (schema.uniqueItems === true) out.uniqueItems = true;
903
+ const dependentRequired = readDependentRequired(schema.dependentRequired);
904
+ if (dependentRequired) out.dependentRequired = dependentRequired;
905
+ return Object.keys(out).length > 0 ? out : void 0;
906
+ }
907
+ /** The one non-null, non-`$ref` branch of a `oneOf`/`anyOf`, or `undefined` for anything else. */
908
+ function singleNonNullBranch(schema) {
909
+ const variants = schema.oneOf ?? schema.anyOf;
910
+ if (!variants) return void 0;
911
+ const nonNull = variants.filter((v) => !isNullSchema(v));
912
+ return nonNull.length === 1 && !nonNull[0].$ref ? nonNull[0] : void 0;
913
+ }
914
+ /** True for the `{ type: 'null' }` / `{ type: ['null'] }` branch of a nullable wrapper. */
915
+ function isNullSchema(schema) {
916
+ return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
917
+ }
918
+ /**
919
+ * `{ constraints }` when `schema` declares any validation keyword, `{}`
920
+ * otherwise — spread into an IR node so the key stays absent rather than
921
+ * present-but-undefined.
922
+ */
923
+ function constraintsEntry(schema) {
924
+ const constraints = extractConstraints(schema);
925
+ return constraints ? { constraints } : {};
926
+ }
927
+ /** Keywords where the stricter of two declarations is the larger value. */
928
+ const MIN_KEYS = [
929
+ "minimum",
930
+ "exclusiveMinimum",
931
+ "minLength",
932
+ "minItems",
933
+ "minContains",
934
+ "minProperties"
935
+ ];
936
+ /** Keywords where the stricter of two declarations is the smaller value. */
937
+ const MAX_KEYS = [
938
+ "maximum",
939
+ "exclusiveMaximum",
940
+ "maxLength",
941
+ "maxItems",
942
+ "maxContains",
943
+ "maxProperties"
944
+ ];
945
+ /**
946
+ * Conjoin two constraint sets the way `allOf` conjoins schemas: an instance
947
+ * must satisfy both, so every `min*` takes the larger value, every `max*` the
948
+ * smaller, `uniqueItems` holds if either side demands it, and
949
+ * `dependentRequired` merges per property with the union of its names
950
+ * (deduplicated, `a`'s order first). A keyword declared on one side only is
951
+ * kept as is, and an absent side yields the other unchanged.
952
+ *
953
+ * `multipleOf` and `pattern` keep `a`'s value when the two differ: a true
954
+ * conjunction would need the least common multiple, or a lookahead regex
955
+ * joining both patterns, and neither is worth carrying into the IR.
956
+ *
957
+ * Not for the parameter `$ref` fallback in extractParams: keywords written
958
+ * beside a `$ref` override the target's there, which is a different operation.
959
+ */
960
+ function combineConstraints(a, b) {
961
+ if (!a) return b;
962
+ if (!b) return a;
963
+ const out = {
964
+ ...a,
965
+ ...b
966
+ };
967
+ for (const key of MIN_KEYS) {
968
+ const [x, y] = [a[key], b[key]];
969
+ if (x !== void 0 && y !== void 0) out[key] = Math.max(x, y);
970
+ }
971
+ for (const key of MAX_KEYS) {
972
+ const [x, y] = [a[key], b[key]];
973
+ if (x !== void 0 && y !== void 0) out[key] = Math.min(x, y);
974
+ }
975
+ if (a.uniqueItems || b.uniqueItems) out.uniqueItems = true;
976
+ if (a.multipleOf !== void 0 && b.multipleOf !== void 0) out.multipleOf = a.multipleOf;
977
+ if (a.pattern !== void 0 && b.pattern !== void 0) out.pattern = a.pattern;
978
+ if (a.dependentRequired && b.dependentRequired) out.dependentRequired = unionDependentRequired(a.dependentRequired, b.dependentRequired);
979
+ return out;
980
+ }
981
+ /** Per-property union of two `dependentRequired` maps, deduplicated and keeping `a`'s order first. */
982
+ function unionDependentRequired(a, b) {
983
+ const merged = /* @__PURE__ */ new Map();
984
+ for (const [property, names] of Object.entries(a)) merged.set(property, [...names]);
985
+ for (const [property, names] of Object.entries(b)) {
986
+ const existing = merged.get(property);
987
+ if (!existing) {
988
+ merged.set(property, [...names]);
989
+ continue;
990
+ }
991
+ for (const name of names) if (!existing.includes(name)) existing.push(name);
992
+ }
993
+ return Object.fromEntries(merged);
994
+ }
995
+ /**
996
+ * Resolve one side's bound from `minimum` / `maximum` plus its `exclusive*`
997
+ * companion into the OAS 3.1 shape: a numeric companion is the exclusive
998
+ * bound itself, `true` turns the inclusive bound into the exclusive one, and
999
+ * `false` or absent leaves the inclusive bound as is.
1000
+ */
1001
+ function normalizeBound(inclusive, exclusive) {
1002
+ const bound = typeof inclusive === "number" ? inclusive : void 0;
1003
+ if (typeof exclusive === "number") return {
1004
+ inclusive: bound,
1005
+ exclusive
1006
+ };
1007
+ if (exclusive === true) return { exclusive: bound };
1008
+ return { inclusive: bound };
1009
+ }
1010
+ /** Copy `dependentRequired` when it is an object whose values are string arrays; anything else is ignored. */
1011
+ function readDependentRequired(value) {
1012
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
1013
+ const entries = [];
1014
+ for (const [property, deps] of Object.entries(value)) if (Array.isArray(deps) && deps.every((d) => typeof d === "string")) entries.push([property, [...deps]]);
1015
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
1016
+ }
1017
+ /**
1018
+ * Narrow a raw `examples` value to a non-empty list, or undefined. Guards
1019
+ * against documents that put an OAS Example Object map (or anything else)
1020
+ * where JSON Schema expects an array, and against an empty list, so the IR
1021
+ * carries `examples` only when there is at least one value.
1022
+ */
1023
+ function nonEmptyExamples(value) {
1024
+ return Array.isArray(value) && value.length > 0 ? value : void 0;
1025
+ }
1026
+ /**
1027
+ * Pick the single `example` the IR carries: the document's own `example`
1028
+ * when it is set (including an explicit `null`), otherwise the first entry
1029
+ * of the resolved `examples` list.
1030
+ */
1031
+ function pickExample(example, examples) {
1032
+ return example !== void 0 ? example : examples?.[0];
1033
+ }
1034
+ /**
1035
+ * Resolve `example` and `examples` across ordered precedence levels: for a
1036
+ * parameter its own declaration, its inline schema, then the component its
1037
+ * `$ref` points at; for a field its own schema, then its `$ref` target.
1038
+ *
1039
+ * Each value walks the levels independently. `examples` is the first
1040
+ * non-empty list. `example` comes from the first level that has either an
1041
+ * `example` or a list, preferring that level's own `example` over its first
1042
+ * list entry — so a schema-level `example` can never mask the parameter's
1043
+ * own `examples` map. A level with neither falls through; an explicit
1044
+ * `null` is a value and stops the walk.
1045
+ */
1046
+ function resolveExamples(levels) {
1047
+ let example;
1048
+ let examples;
1049
+ for (const level of levels) {
1050
+ if (examples === void 0) examples = level.examples;
1051
+ if (example === void 0) example = pickExample(level.example, level.examples);
1052
+ }
1053
+ return {
1054
+ example,
1055
+ examples
1056
+ };
1057
+ }
1058
+ /**
1059
+ * If a schema is a `$ref` to `#/components/schemas/X`, return the referenced
1060
+ * component schema. Used to surface schema-level metadata (default, example,
1061
+ * examples, deprecated, description) that would otherwise be invisible at the
1062
+ * field or parameter level.
1063
+ *
1064
+ * Refs are not pre-resolved by the bundler (`dereference: false` in refs.ts),
1065
+ * so without this, a param like `schema: { $ref: '.../PaginationOrder' }`
1066
+ * silently loses the target's `default: desc` and the SDK stops emitting the
1067
+ * spec-mandated default value in generated method signatures.
1068
+ */
1069
+ function resolveComponentSchemaRef(schema, componentSchemas) {
1070
+ if (!schema || !componentSchemas) return void 0;
1071
+ const ref = schema.$ref;
1072
+ if (!ref) return void 0;
1073
+ const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
1074
+ if (!match) return void 0;
1075
+ return componentSchemas[match[1]];
1076
+ }
334
1077
  function extractSchemas(schemas, options) {
335
1078
  const enums = [];
336
1079
  if (!schemas) return {
337
1080
  models: [],
338
1081
  enums
339
1082
  };
1083
+ activeComponentSchemaKeys = new Set(Object.keys(schemas));
340
1084
  if (options?.schemaNameTransform) {
341
1085
  const transform = options.schemaNameTransform;
342
- const cleanedNames = Object.keys(schemas).map((n) => cleanSchemaName(toPascalCase(n)));
343
- const transformedToOriginals = /* @__PURE__ */ new Map();
344
- for (const cleaned of cleanedNames) {
345
- const transformed = transform(cleaned);
346
- if (!transformedToOriginals.has(transformed)) transformedToOriginals.set(transformed, []);
347
- transformedToOriginals.get(transformed).push(cleaned);
348
- }
349
- const unsafeToTransform = /* @__PURE__ */ new Set();
350
- for (const [, originals] of transformedToOriginals) if (originals.length > 1) for (const n of originals) unsafeToTransform.add(n);
351
- activeSchemaNameTransform = (name) => unsafeToTransform.has(name) ? name : transform(name);
1086
+ const transformedToRawKeys = /* @__PURE__ */ new Map();
1087
+ for (const raw of Object.keys(schemas)) {
1088
+ const transformed = transform(cleanSchemaName(toPascalCase(raw)), raw);
1089
+ if (!transformedToRawKeys.has(transformed)) transformedToRawKeys.set(transformed, []);
1090
+ transformedToRawKeys.get(transformed).push(raw);
1091
+ }
1092
+ const unsafeRawKeys = /* @__PURE__ */ new Set();
1093
+ const unsafeCleanedNames = /* @__PURE__ */ new Set();
1094
+ for (const [, rawKeys] of transformedToRawKeys) if (rawKeys.length > 1) for (const raw of rawKeys) {
1095
+ unsafeRawKeys.add(raw);
1096
+ unsafeCleanedNames.add(cleanSchemaName(toPascalCase(raw)));
1097
+ }
1098
+ activeSchemaNameTransform = (name, originalName) => {
1099
+ return (originalName !== void 0 ? unsafeRawKeys.has(originalName) : unsafeCleanedNames.has(name)) ? name : transform(name, originalName);
1100
+ };
352
1101
  } else activeSchemaNameTransform = null;
353
1102
  activeEnumSchemaNames = /* @__PURE__ */ new Set();
354
- for (const [name, schema] of Object.entries(schemas)) if (schema.enum) activeEnumSchemaNames.add(resolveSchemaName(name));
1103
+ activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
1104
+ for (const [name, schema] of Object.entries(schemas)) {
1105
+ if (!schema.enum) continue;
1106
+ const resolved = resolveSchemaName(name);
1107
+ activeEnumSchemaNames.add(resolved);
1108
+ if (isNullableEnumSchema(schema)) activeNullableEnumSchemaNames.add(resolved);
1109
+ }
355
1110
  const modelsByCleanName = /* @__PURE__ */ new Map();
356
1111
  for (const [name, schema] of Object.entries(schemas)) {
357
1112
  const pascalName = resolveSchemaName(name);
@@ -362,7 +1117,7 @@ function extractSchemas(schemas, options) {
362
1117
  if (existing) {
363
1118
  if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
364
1119
  } else modelsByCleanName.set(pascalName, model);
365
- for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName)) {
1120
+ for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName, schemas)) {
366
1121
  const existingInline = modelsByCleanName.get(inlineModel.name);
367
1122
  if (existingInline) {
368
1123
  if (inlineModel.fields.length > existingInline.fields.length) modelsByCleanName.set(inlineModel.name, inlineModel);
@@ -387,6 +1142,8 @@ function extractSchemas(schemas, options) {
387
1142
  function clearSchemaNameTransform() {
388
1143
  activeSchemaNameTransform = null;
389
1144
  activeEnumSchemaNames = /* @__PURE__ */ new Set();
1145
+ activeNullableEnumSchemaNames = /* @__PURE__ */ new Set();
1146
+ activeComponentSchemaKeys = /* @__PURE__ */ new Set();
390
1147
  }
391
1148
  /**
392
1149
  * Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
@@ -422,14 +1179,17 @@ function collectInlineEnums(fields, enums) {
422
1179
  * that schemaToTypeRef turns into model refs but aren't extracted from components.
423
1180
  */
424
1181
  function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
1182
+ const directRefs = directModelRefNames(ref);
425
1183
  walkTypeRef(ref, { model: (modelRef) => {
426
1184
  if (modelNames.has(modelRef.name)) return;
1185
+ if (!directRefs.has(modelRef.name)) return;
427
1186
  if (Object.keys(schemas).some((k) => resolveSchemaName(k) === modelRef.name)) return;
428
1187
  const parentSchema = findSchemaByName(parentModelName, schemas);
429
1188
  if (!parentSchema) return;
430
1189
  const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
431
1190
  if (!fieldSchema) return;
432
- const extracted = extractNestedSchema(modelRef.name, fieldSchema);
1191
+ const reserved = reservedInlineNames(fieldName, collectProperties(parentSchema, schemas), parentModelName);
1192
+ const extracted = extractNestedSchema(unionBaseName(modelRef.name, fieldName, fieldSchema, parentModelName, reserved), fieldSchema, schemas, reserved);
433
1193
  for (const m of extracted) if (!modelNames.has(m.name)) {
434
1194
  models.push(m);
435
1195
  modelNames.add(m.name);
@@ -437,6 +1197,35 @@ function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, mod
437
1197
  }
438
1198
  } });
439
1199
  }
1200
+ /**
1201
+ * The base name `extractNestedSchema` should name a field's `oneOf` variants
1202
+ * from: the field's qualified inline name when naming the variants from it
1203
+ * yields `refName` (the ref the walk is resolving), else `refName` itself,
1204
+ * which is right for every non-union field and for a union whose variant 0 is
1205
+ * bare.
1206
+ */
1207
+ function unionBaseName(refName, fieldName, fieldSchema, parentModelName, reserved) {
1208
+ if (!fieldSchema.oneOf) return refName;
1209
+ const fromField = qualifyInlineModelName(toPascalCase(fieldName), parentModelName);
1210
+ return [...nameInlineVariants(fieldSchema, fromField, reserved).values()].includes(refName) ? fromField : refName;
1211
+ }
1212
+ /**
1213
+ * The model refs a field's TypeRef produces directly: the ref itself, the
1214
+ * items of a (possibly nullable) array, or each (possibly nullable) variant of
1215
+ * a union — the shapes `extractNestedSchema` can name from a ref. Refs nested
1216
+ * any deeper are excluded.
1217
+ */
1218
+ function directModelRefNames(ref) {
1219
+ const names = /* @__PURE__ */ new Set();
1220
+ let root = ref;
1221
+ while (root.kind === "nullable" || root.kind === "array") root = root.kind === "nullable" ? root.inner : root.items;
1222
+ const leaves = root.kind === "union" ? root.variants : [root];
1223
+ for (const leaf of leaves) {
1224
+ const inner = leaf.kind === "nullable" ? leaf.inner : leaf;
1225
+ if (inner.kind === "model") names.add(inner.name);
1226
+ }
1227
+ return names;
1228
+ }
440
1229
  /** Find a component schema by PascalCase name. */
441
1230
  function findSchemaByName(pascalName, schemas) {
442
1231
  for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
@@ -465,104 +1254,278 @@ function findNestedFieldSchema(fieldName, parentSchema, schemas) {
465
1254
  }
466
1255
  return null;
467
1256
  }
468
- /** Extract a model (and nested models) from an inline field schema. */
469
- function extractNestedSchema(name, schema) {
1257
+ /**
1258
+ * Extract a model (and nested models) from an inline field schema. `reserved`
1259
+ * holds the inline names the field's sibling properties mint
1260
+ * (`reservedInlineNames`), which a `oneOf` variant must not take.
1261
+ */
1262
+ function extractNestedSchema(name, schema, schemas, reserved = NO_RESERVED) {
470
1263
  if (schema.oneOf) {
471
1264
  const models = [];
472
- const constNamingDiscriminator = deriveConstNamingDiscriminator(schema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type)));
1265
+ const variantNames = nameInlineVariants(schema, name, reserved);
473
1266
  for (const variant of schema.oneOf) {
474
- if (variant.$ref) continue;
475
- if (variant.properties && (variant.type === "object" || !variant.type)) {
476
- const requiredSet = new Set(variant.required ?? []);
477
- const fields = [];
478
- for (const [fn, fs] of Object.entries(variant.properties)) {
479
- if (!fs) continue;
480
- fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
481
- }
482
- const variantName = nameVariantModel(variant, name, models, constNamingDiscriminator);
483
- models.push({
484
- name: variantName,
485
- description: variant.description,
486
- fields
487
- });
1267
+ const variantName = variantNames.get(variant);
1268
+ if (variantName === void 0) continue;
1269
+ const requiredSet = new Set(variant.required ?? []);
1270
+ const siblings = collectProperties(variant);
1271
+ const fields = [];
1272
+ for (const [fn, fs] of Object.entries(variant.properties ?? {})) {
1273
+ if (!fs) continue;
1274
+ fields.push(buildFieldFromSchema(fn, fs, variantName, requiredSet, schemas, siblings));
488
1275
  }
1276
+ models.push({
1277
+ name: variantName,
1278
+ description: variant.description,
1279
+ fields,
1280
+ ...constraintsEntry(variant)
1281
+ });
489
1282
  }
490
1283
  return models;
491
1284
  }
492
1285
  if (schema.properties && (schema.type === "object" || !schema.type)) {
493
1286
  const requiredSet = new Set(schema.required ?? []);
1287
+ const siblings = collectProperties(schema);
494
1288
  const fields = [];
495
1289
  for (const [fn, fs] of Object.entries(schema.properties)) {
496
1290
  if (!fs) continue;
497
- fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
1291
+ fields.push(buildFieldFromSchema(fn, fs, name, requiredSet, schemas, siblings));
498
1292
  }
499
1293
  return [{
500
1294
  name,
501
1295
  description: schema.description,
502
- fields
1296
+ fields,
1297
+ ...constraintsEntry(schema)
503
1298
  }];
504
1299
  }
505
1300
  if (schema.type === "array" && schema.items?.properties) {
506
1301
  const requiredSet = new Set(schema.items.required ?? []);
1302
+ const siblings = collectProperties(schema.items);
507
1303
  const fields = [];
508
1304
  for (const [fn, fs] of Object.entries(schema.items.properties)) {
509
1305
  if (!fs) continue;
510
- fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
1306
+ fields.push(buildFieldFromSchema(fn, fs, name, requiredSet, schemas, siblings));
511
1307
  }
512
1308
  return [{
513
1309
  name,
514
1310
  description: schema.items.description,
515
- fields
1311
+ fields,
1312
+ ...constraintsEntry(schema.items)
516
1313
  }];
517
1314
  }
518
1315
  return [];
519
1316
  }
520
1317
  /**
521
- * Derive a meaningful name for a oneOf object variant past the first,
522
- * falling back to the numeric-suffix scheme (`Foo2`, `Foo3`) when no
523
- * better signal is available. The "better signal" is a const-valued
524
- * discriminator property that distinguishes one variant from the others.
525
- *
526
- * For a schema like `ApiKey.owner` whose oneOf has two variants — one
527
- * with `type: { const: "organization" }` and one with `type: { const:
528
- * "user" }` — this returns `ApiKeyOwner` for the first variant (the
529
- * baseline name that the union TypeRef will reference, so consumer-
530
- * visible typed fields stay backward-compatible) and `UserApiKeyOwner`
531
- * for the second instead of `ApiKeyOwner2`. SDK consumers reading the
532
- * generated types can tell the variants apart at a glance.
533
- *
534
- * The first variant always keeps the bare parent name because
535
- * `schemaToTypeRef` builds union variants whose model refs collapse to
536
- * `qualifyInlineModelName(field, parent)` — so the first model name
537
- * must match for the union's degenerate-collapse to land on a valid
538
- * type. Renaming variant 0 would orphan it.
539
- *
540
- * Falls back to numeric suffix when:
541
- * - The variant has no discriminator property (no shared const
542
- * property across all object variants), OR
543
- * - The const value PascalCases to an empty/whitespace name, OR
544
- * - The derived name collides with the parent name or an already-
545
- * emitted variant name (numeric keeps uniqueness).
546
- */
547
- function nameVariantModel(variant, parentName, alreadyEmitted, discriminator) {
548
- if (alreadyEmitted.length === 0) return parentName;
1318
+ * The prefix a variant's `title` contributes to its name: the title trimmed
1319
+ * and PascalCased, or null when there is no title or it PascalCases to
1320
+ * nothing.
1321
+ */
1322
+ function variantTitlePrefix(variant) {
1323
+ const title = typeof variant["title"] === "string" ? variant["title"].trim() : "";
1324
+ return title ? toPascalCase(title) || null : null;
1325
+ }
1326
+ /** `<Prefix><Base>`, or `base` unchanged when it already starts with the prefix (no doubling). */
1327
+ function prefixedVariantName(prefix, base) {
1328
+ return base.startsWith(prefix) ? base : `${prefix}${base}`;
1329
+ }
1330
+ /**
1331
+ * The properties `variant` declares that no other inline object variant of
1332
+ * the same union declares, in the variant's own `properties` order.
1333
+ */
1334
+ function distinguishingProperties(variant, siblings) {
1335
+ const declaredElsewhere = /* @__PURE__ */ new Set();
1336
+ for (const sibling of siblings) {
1337
+ if (sibling === variant) continue;
1338
+ for (const property of Object.keys(sibling.properties ?? {})) declaredElsewhere.add(property);
1339
+ }
1340
+ return Object.keys(variant.properties ?? {}).filter((property) => !declaredElsewhere.has(property));
1341
+ }
1342
+ /**
1343
+ * Name an inline object variant of a `oneOf`/`anyOf` that has no better
1344
+ * signal: it is not variant 0 (which keeps the union's base name) and no
1345
+ * const discriminator gave it a prefix. The name is derived from the
1346
+ * variant's own content, never from where it sits in the document (beyond
1347
+ * its position among the variants, which only the final counter reads), so
1348
+ * an SDK user can tell the variants apart and reordering them changes nothing
1349
+ * but which one is variant 0. In order, taking the first candidate that is
1350
+ * neither the base name nor a name already given to an earlier variant of
1351
+ * the same union:
1352
+ *
1353
+ * 1. the variant's `title` (trimmed and PascalCased): `<Title><Base>`
1354
+ * (`CircleDrawingShape`), the same prefix form a const discriminator
1355
+ * produces, including the rule that a base already starting with the
1356
+ * prefix is not doubled (that candidate equals the base, so it falls
1357
+ * through); a title an earlier variant already took falls through too;
1358
+ * 2. a distinguishing property — the first property, in the variant's own
1359
+ * order, that it declares and no other inline object variant declares:
1360
+ * `<Base>With<Property>` (`DrawingShapeWithSide`); a property that
1361
+ * PascalCases to nothing is skipped;
1362
+ * 3. the position counter, `<Base><position + 1>` (`DrawingShape2`), which
1363
+ * now remains only for variants whose property sets are identical, and
1364
+ * which keeps counting past a taken name (`DrawingShape3`).
1365
+ *
1366
+ * Every candidate is also checked against `reserved`: the inline names the
1367
+ * parent's other properties mint (`reservedInlineNames`), so a variant can
1368
+ * never take a name another field's model already has — beside a
1369
+ * `shape_with_side` object, the `{ side }` variant of `shape` is not
1370
+ * `DrawingShapeWithSide` but the next candidate. Read from sibling names and
1371
+ * content only, so document order plays no part.
1372
+ *
1373
+ * `siblings` are the inline object variants of the union, this one included;
1374
+ * `emitted` are the names already given to the earlier ones, in order. Every
1375
+ * site that names a variant — the union TypeRef, the component-field,
1376
+ * request-body and response extractors, and `mintedInlineNames` — goes
1377
+ * through this function (via `nameInlineVariant`), so they cannot drift.
1378
+ */
1379
+ function nameUndiscriminatedVariant(variant, siblings, base, emitted, reserved = NO_RESERVED) {
1380
+ const taken = (candidate) => candidate === base || emitted.includes(candidate) || reserved.has(candidate);
1381
+ const titlePrefix = variantTitlePrefix(variant);
1382
+ if (titlePrefix !== null) {
1383
+ const candidate = prefixedVariantName(titlePrefix, base);
1384
+ if (!taken(candidate)) return candidate;
1385
+ }
1386
+ for (const property of distinguishingProperties(variant, siblings)) {
1387
+ const word = toPascalCase(property);
1388
+ if (!word) continue;
1389
+ const candidate = `${base}With${word}`;
1390
+ if (!taken(candidate)) return candidate;
1391
+ }
1392
+ for (let position = emitted.length + 1;; position++) {
1393
+ const candidate = `${base}${position}`;
1394
+ if (!taken(candidate)) return candidate;
1395
+ }
1396
+ }
1397
+ /** No reserved names: for a union with no sibling properties (a request body or response that is itself a union). */
1398
+ const NO_RESERVED = /* @__PURE__ */ new Set();
1399
+ /**
1400
+ * True when the union's inline object variants (two or more) each carry a
1401
+ * title and no two titles PascalCase to the same prefix: the one case in
1402
+ * which variant 0 is named from its title rather than keeping the base name.
1403
+ */
1404
+ function allDistinctlyTitled(siblings) {
1405
+ if (siblings.length < 2) return false;
1406
+ const prefixes = siblings.map(variantTitlePrefix);
1407
+ return prefixes.every((prefix) => prefix !== null) && new Set(prefixes).size === prefixes.length;
1408
+ }
1409
+ /**
1410
+ * The name of one inline object variant of a `oneOf`/`anyOf`, given the
1411
+ * union's base name (the field's qualified inline name, e.g. `ApiKeyOwner`),
1412
+ * the inline object variants of the union (`siblings`, this one included),
1413
+ * the names already given to the earlier variants (`emitted`, in order), and
1414
+ * the const-valued property that distinguishes the variants when there is
1415
+ * one (`deriveConstNamingDiscriminator`).
1416
+ *
1417
+ * Variant 0 keeps the bare base name: the union TypeRef and every extractor
1418
+ * reference it under that name, and a consumer's typed field keeps the name
1419
+ * it always had. The one exception: when the union has no const discriminator
1420
+ * and every variant carries a distinct title, variant 0 is named from its
1421
+ * title like the others (`CircleDrawingShape` beside `SquareDrawingShape`)
1422
+ * instead of being the only one left bare. Whether the union is discriminated
1423
+ * is read from the variants themselves here, not from `discriminator`, so a
1424
+ * caller forcing a property (an explicit `discriminator:` without `mapping:`)
1425
+ * still agrees with the extractors on variant 0.
1426
+ *
1427
+ * A later variant takes `<ConstValue><Base>` from the discriminator
1428
+ * (`UserApiKeyOwner`) when the value PascalCases to something and the name is
1429
+ * neither the base, nor already emitted, nor minted by one of the parent's
1430
+ * other properties (`reserved`), and otherwise falls to
1431
+ * `nameUndiscriminatedVariant`: its title, a distinguishing property, then
1432
+ * the position counter, each checked the same way.
1433
+ */
1434
+ function nameInlineVariant(variant, siblings, base, emitted, discriminator, reserved = NO_RESERVED) {
1435
+ if (emitted.length === 0) {
1436
+ const titlePrefix = deriveConstNamingDiscriminator([...siblings]) === null && allDistinctlyTitled(siblings) ? variantTitlePrefix(variant) : null;
1437
+ return titlePrefix === null ? base : prefixedVariantName(titlePrefix, base);
1438
+ }
549
1439
  if (discriminator) {
550
1440
  const constValue = getConstPropertyValue(variant, discriminator.property);
551
- if (constValue) {
552
- const prefix = toPascalCase(constValue);
553
- if (prefix) {
554
- const candidate = parentName.startsWith(prefix) ? parentName : `${prefix}${parentName}`;
555
- if (!(candidate === parentName || alreadyEmitted.some((m) => m.name === candidate))) return candidate;
1441
+ const prefix = constValue ? toPascalCase(constValue) : "";
1442
+ if (prefix) {
1443
+ const candidate = prefixedVariantName(prefix, base);
1444
+ if (candidate !== base && !emitted.includes(candidate) && !reserved.has(candidate)) return candidate;
1445
+ }
1446
+ }
1447
+ return nameUndiscriminatedVariant(variant, siblings, base, emitted, reserved);
1448
+ }
1449
+ /**
1450
+ * Name every inline object variant of a `oneOf`/`anyOf` schema from the
1451
+ * union's base name, keyed by variant schema identity; null, non-object and
1452
+ * `$ref` variants get no entry. Positions are local to this variant list, so
1453
+ * the union's TypeRef entries and the extracted models agree no matter what
1454
+ * else has been extracted. `reserved` holds the inline names the parent's
1455
+ * other properties mint (`reservedInlineNames`); every site that has a
1456
+ * sibling set passes the same one, so the reference and the model step
1457
+ * around them identically.
1458
+ *
1459
+ * One union shape is named differently: a `oneOf` with no explicit
1460
+ * `discriminator:` whose every variant carries a `$ref` or a `title` and pins
1461
+ * the same const property (`titleMappedDiscriminatorProperty`). Its
1462
+ * discriminator mapping has always named an inline variant by its title
1463
+ * (`resolveSchemaName(title)`: `Circle`, `Square`, run through the project's
1464
+ * schema-name transform like a component), and `schemaToTypeRef` references
1465
+ * the variant by that mapped name, so the title is the model name here too —
1466
+ * unless it is the base, already emitted, or reserved by a sibling, in which
1467
+ * case the variant takes a context-qualified name (its title as a prefix on
1468
+ * the base, `CircleDrawingMarker`, else a distinguishing property, else the
1469
+ * counter) and the mapping, which is read from this map, follows it. A kept
1470
+ * title is context-free: it is the same whatever field, array or map the
1471
+ * union sits under, so it names one shape wherever it appears, as a
1472
+ * component name would. A second union beside it using the same title for a
1473
+ * variant of the identical shape (`schemaShapeKey`) therefore shares the
1474
+ * model instead of stepping around it; a sibling giving the same title a
1475
+ * different shape reserves it (`reservedInlineNames`), and since both sides
1476
+ * read their siblings in isolation the decision is symmetric: when the
1477
+ * shapes differ, both unions fall back to their context-qualified names
1478
+ * (`CircleDrawingShape`, `CircleDrawingMarker`) and neither keeps the bare
1479
+ * `Circle`; when they are identical, both keep `Circle` and one model is
1480
+ * emitted. `elementContextName` never lets a context-free name drive an
1481
+ * items/values step-aside, which no context could satisfy.
1482
+ * `nameInlineVariantEntries` reports which names are context-free; a
1483
+ * fallen-back title name is context-dependent.
1484
+ *
1485
+ * This is the list-level entry point every site uses: `schemaToTypeRef` (via
1486
+ * `nameInlineObjectVariants`), `extractNestedSchema`,
1487
+ * `extractInlineModelDeep`, `extractInlineModelsFromProperties` and
1488
+ * `mintedInlineNames`.
1489
+ */
1490
+ function nameInlineVariants(union, base, reserved = NO_RESERVED) {
1491
+ const names = /* @__PURE__ */ new Map();
1492
+ for (const [variant, entry] of nameInlineVariantEntries(union, base, reserved)) names.set(variant, entry.name);
1493
+ return names;
1494
+ }
1495
+ /** `nameInlineVariants`, also telling for each name whether it is context-free. */
1496
+ function nameInlineVariantEntries(union, base, reserved = NO_RESERVED) {
1497
+ const nonNullVariants = (union.oneOf ?? union.anyOf ?? []).filter((v) => !isNullVariantSchema(v));
1498
+ const inlineObjectVariants = nonNullVariants.filter((v) => isInlineObjectSchema(v));
1499
+ const discriminator = deriveConstNamingDiscriminator(inlineObjectVariants);
1500
+ const titleMapped = union.oneOf !== void 0 && !union.discriminator && titleMappedDiscriminatorProperty(nonNullVariants) !== null;
1501
+ const emitted = [];
1502
+ const entries = /* @__PURE__ */ new Map();
1503
+ for (const variant of inlineObjectVariants) {
1504
+ let name = null;
1505
+ let contextFree = false;
1506
+ if (titleMapped) {
1507
+ const titled = resolveVariantModelName(variant);
1508
+ if (titled !== null) {
1509
+ if (titled !== base && !emitted.includes(titled) && !reserved.has(titled)) {
1510
+ name = titled;
1511
+ contextFree = true;
1512
+ } else name = nameUndiscriminatedVariant(variant, inlineObjectVariants, base, emitted, reserved);
556
1513
  }
557
1514
  }
1515
+ name ??= nameInlineVariant(variant, inlineObjectVariants, base, emitted, discriminator, reserved);
1516
+ emitted.push(name);
1517
+ entries.set(variant, {
1518
+ name,
1519
+ contextFree
1520
+ });
558
1521
  }
559
- return `${parentName}${alreadyEmitted.length + 1}`;
1522
+ return entries;
560
1523
  }
561
1524
  /**
562
1525
  * Returns a discriminator descriptor when every inline object variant in a
563
1526
  * oneOf has a string-const value on the same property — the signature of a
564
1527
  * discriminated union without an explicit `discriminator:` keyword. Used by
565
- * `nameVariantModel` to derive variant names from the const values.
1528
+ * `nameInlineVariant` to derive variant names from the const values.
566
1529
  *
567
1530
  * Distinct from `detectConstPropertyDiscriminator` (which requires every
568
1531
  * variant to carry a `$ref` or `title` so a name mapping can be built):
@@ -580,18 +1543,96 @@ function deriveConstNamingDiscriminator(variants) {
580
1543
  }
581
1544
  return null;
582
1545
  }
583
- /** Build a single Field from a schema property entry. Shared across all extraction sites. */
584
- function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet) {
1546
+ /**
1547
+ * True for a schema that `schemaToTypeRef` turns into an inline model reference:
1548
+ * it declares `properties` directly (no `$ref`) and is an object — whether by
1549
+ * `type: 'object'`, by omitting `type`, or by an OAS 3.1 nullable array such as
1550
+ * `type: ['object', 'null']`. Every inline-model extractor must use the same
1551
+ * test, or a field's reference points at a model nobody materializes.
1552
+ */
1553
+ function isInlineObjectSchema(schema) {
1554
+ if (!schema || schema.$ref || !schema.properties) return false;
1555
+ const { type } = schema;
1556
+ return type === void 0 || type === "object" || Array.isArray(type) && type.includes("object");
1557
+ }
1558
+ /** The `{ type: 'null' }` member that marks a `oneOf`/`anyOf` as nullable; never a variant to name. */
1559
+ function isNullVariantSchema(schema) {
1560
+ return schema.type === "null" || Array.isArray(schema.type) && schema.type.length === 1 && schema.type[0] === "null";
1561
+ }
1562
+ /**
1563
+ * The synthetic context names a multi-type inline variant's non-object
1564
+ * members are typed and extracted under, chosen so the models they name can
1565
+ * never collide with a model named from one of the variant's own properties.
1566
+ * An `array` member's items go under `item` (`<Variant>Item`), falling back
1567
+ * to `array_item` (`<Variant>ArrayItem`) and then `array_item2`,
1568
+ * `array_item3`, ... when a property already claims that name; any other
1569
+ * member goes under `value` (`<Variant>Value`), then `member_value`,
1570
+ * `member_value2`, .... A candidate is skipped when the model name
1571
+ * `qualifyInlineModelName` derives from it equals one derived from a
1572
+ * property — compared after qualification, so a property `items`, whose
1573
+ * name singularizes to `<Variant>Item`, blocks `item` as well.
1574
+ * This is the single source of these names for the TypeRef builder
1575
+ * (`inlineVariantTypeRef`) and the extractors (`variantMemberSchemas`).
1576
+ */
1577
+ function variantMemberKeys(variant, variantName) {
1578
+ const modelNameFor = (key) => qualifyInlineModelName(toPascalCase(key), variantName);
1579
+ const claimed = new Set(Object.keys(variant.properties ?? {}).map(modelNameFor));
1580
+ const pick = (preferred, fallback) => {
1581
+ for (let i = 0;; i++) {
1582
+ const candidate = i === 0 ? preferred : i === 1 ? fallback : `${fallback}${i}`;
1583
+ if (!claimed.has(modelNameFor(candidate))) return candidate;
1584
+ }
1585
+ };
1586
+ return {
1587
+ item: pick("item", "array_item"),
1588
+ value: pick("value", "member_value")
1589
+ };
1590
+ }
1591
+ /**
1592
+ * The schemas carried by the non-object members of a multi-type inline
1593
+ * variant (`type: ['object', 'array']` with `items`), keyed by the synthetic
1594
+ * context name from `variantMemberKeys` that `schemaToTypeRef` types them
1595
+ * under, so an inline object item is `<Variant>Item` (or `<Variant>ArrayItem`
1596
+ * when `item` is taken) and its own nested objects `<Variant>ItemChild`.
1597
+ * Every extractor re-enters with these under the variant's name so the
1598
+ * models line up with the refs by construction. A map member cannot occur
1599
+ * here: a variant with `properties` is typed as a model, never as a map.
1600
+ */
1601
+ function variantMemberSchemas(variant, variantName) {
1602
+ const members = {};
1603
+ if (Array.isArray(variant.type) && variant.type.includes("array") && variant.items) members[variantMemberKeys(variant, variantName).item] = variant.items;
1604
+ return members;
1605
+ }
1606
+ /**
1607
+ * Build a single Field from a schema property entry. Shared across all
1608
+ * extraction sites. Pass `componentSchemas` where they are at hand so a
1609
+ * field whose schema is a bare `$ref` can take `example`/`examples` from the
1610
+ * component it points at, as a parameter does. `siblingProperties` are the
1611
+ * other properties of the object the field belongs to (`collectProperties`);
1612
+ * array items and map values use them to step aside from a sibling that
1613
+ * would take their singular name (`elementContextName`).
1614
+ */
1615
+ function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet, componentSchemas, siblingProperties) {
1616
+ const refTarget = resolveComponentSchemaRef(fieldSchema, componentSchemas);
1617
+ const { example, examples } = resolveExamples([{
1618
+ example: fieldSchema.example,
1619
+ examples: nonEmptyExamples(fieldSchema.examples)
1620
+ }, {
1621
+ example: refTarget?.example,
1622
+ examples: nonEmptyExamples(refTarget?.examples)
1623
+ }]);
585
1624
  return {
586
1625
  name: fieldName,
587
- type: schemaToTypeRef(fieldSchema, fieldName, contextName),
1626
+ type: schemaToTypeRef(fieldSchema, fieldName, contextName, siblingProperties),
588
1627
  required: requiredSet.has(fieldName),
589
1628
  description: fieldSchema.description,
590
1629
  readOnly: fieldSchema.readOnly || void 0,
591
1630
  writeOnly: fieldSchema.writeOnly || void 0,
592
1631
  deprecated: fieldSchema.deprecated || void 0,
593
1632
  default: fieldSchema.default,
594
- example: fieldSchema.example
1633
+ example,
1634
+ ...examples ? { examples } : {},
1635
+ ...constraintsEntry(fieldSchema)
595
1636
  };
596
1637
  }
597
1638
  /**
@@ -628,39 +1669,27 @@ function buildSyntheticEnumRef(values, contextName, parentModelName) {
628
1669
  };
629
1670
  }
630
1671
  /**
631
- * Detect an implicit discriminator on an oneOf where every variant is an
632
- * object schema that pins the same property to a const value. Returns
633
- * { property, mapping } so the IR can represent the union as discriminated
634
- * without needing an explicit `discriminator:` key in the spec.
1672
+ * Detect an implicit discriminator on a `oneOf` whose every variant pins the
1673
+ * same property to a const value AND is nameable on its own — by a `$ref` to
1674
+ * a component or by a `title` — so the IR can represent the union as
1675
+ * discriminated, with a mapping to concrete names, without an explicit
1676
+ * `discriminator:` keyword. Returns the property, or null when any variant
1677
+ * doesn't fit the pattern or when the variants disagree on which property
1678
+ * carries the discriminator. The mapping itself is built by `schemaToTypeRef`
1679
+ * from the names `nameInlineVariants` gives the inline variants (their titles
1680
+ * unless taken) and the component names of `$ref` variants, so it can only
1681
+ * ever point at a model that exists.
635
1682
  *
636
- * Both shapes are supported:
1683
+ * Both const shapes are supported:
637
1684
  * { properties: { event: { const: "x" } }, required: ["event"] }
638
1685
  * { properties: { event: { type: "string", enum: ["x"] } } }
639
- *
640
- * Returns null when any variant doesn't fit the pattern or when variants
641
- * disagree on which property carries the discriminator.
642
1686
  */
643
- function detectConstPropertyDiscriminator(variants) {
1687
+ function titleMappedDiscriminatorProperty(variants) {
644
1688
  if (variants.length < 2) return null;
645
- let candidateProperty = null;
1689
+ if (!variants.every((v) => resolveVariantModelName(v) !== null)) return null;
646
1690
  const candidates = Object.keys(variants[0]?.properties ?? {});
647
- for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) {
648
- candidateProperty = propName;
649
- break;
650
- }
651
- if (!candidateProperty) return null;
652
- const mapping = {};
653
- for (const v of variants) {
654
- const value = getConstPropertyValue(v, candidateProperty);
655
- if (value === null) return null;
656
- const variantName = resolveVariantModelName(v);
657
- if (!variantName) return null;
658
- mapping[value] = variantName;
659
- }
660
- return {
661
- property: candidateProperty,
662
- mapping
663
- };
1691
+ for (const propName of candidates) if (variants.every((v) => getConstPropertyValue(v, propName) !== null)) return propName;
1692
+ return null;
664
1693
  }
665
1694
  function getConstPropertyValue(schema, property) {
666
1695
  const prop = schema.properties?.[property];
@@ -686,42 +1715,96 @@ function mapVariantToDiscriminatorEntry(variant, discriminator) {
686
1715
  }
687
1716
  /**
688
1717
  * Derive a discriminator value → variant model name mapping for an inline-
689
- * variant `oneOf` whose explicit `discriminator:` has no `mapping:` clause.
690
- *
691
- * Each variant model name is reproduced via `nameVariantModel` so it matches
692
- * what `extractNestedSchema` registered when it walked the same `oneOf` to
693
- * pull variants out as named models. Variant 0 keeps the bare parent name
694
- * (e.g. `ApiKeyOwner`); subsequent variants get a const-derived prefix
695
- * (e.g. `UserApiKeyOwner` for the `type: const: user` variant).
696
- *
697
- * The "parent name" used here mirrors the one `collectNestedInlineModels`
698
- * passes to `extractNestedSchema`: it's the qualified inline model name
699
- * built from `contextName` (the field name) plus `parentModelName` (the
700
- * containing model). Returns null when any non-object variant is present
701
- * or when a variant lacks a const value on the discriminator property.
1718
+ * variant `oneOf` from the names `nameInlineVariants` gave its variants
1719
+ * (`inlineVariantNames`): the explicit `discriminator:` without a `mapping:`
1720
+ * clause, and the implicit const discriminator of anonymous inline variants
1721
+ * (`ApiKeyCreatedData.owner`: `ApiKeyCreatedDataOwner`,
1722
+ * `UserApiKeyCreatedDataOwner`). Reading the names rather than deriving them
1723
+ * again is what keeps the mapping pointing at models the extractors create.
1724
+ * Returns null without a context name, when any variant is not an inline
1725
+ * object, or when a variant lacks a const value on the property.
702
1726
  */
703
- function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, parentModelName) {
1727
+ function deriveInlineVariantMapping(variants, discriminatorProperty, contextName, inlineVariantNames) {
704
1728
  if (variants.length === 0 || !contextName) return null;
705
- const inlineObjectVariants = variants.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
706
- if (inlineObjectVariants.length !== variants.length) return null;
707
- const baseInlineName = qualifyInlineModelName(toPascalCase(contextName), parentModelName);
708
- const namingDiscriminator = { property: discriminatorProperty };
709
1729
  const mapping = {};
710
- const emittedSoFar = [];
711
- for (const variant of inlineObjectVariants) {
1730
+ for (const variant of variants) {
1731
+ const variantName = inlineVariantNames.get(variant);
1732
+ if (variantName === void 0) return null;
712
1733
  const constValue = getConstPropertyValue(variant, discriminatorProperty);
713
1734
  if (constValue === null) return null;
714
- const variantName = nameVariantModel(variant, baseInlineName, emittedSoFar, namingDiscriminator);
715
1735
  mapping[constValue] = variantName;
716
- emittedSoFar.push({
717
- name: variantName,
718
- fields: []
719
- });
720
1736
  }
721
1737
  return mapping;
722
1738
  }
1739
+ /**
1740
+ * The names reserved for a union typed under `contextName`: the set an
1741
+ * enclosing field handed down (`reservedNames`, for a union among a field's
1742
+ * array items or map values), else the inline names the field's own sibling
1743
+ * properties mint (`reservedInlineNames`), else nothing. The extractor
1744
+ * derives the same set at each of those levels, so the reference and the
1745
+ * model step around a sibling's name identically.
1746
+ */
1747
+ function reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames) {
1748
+ if (reservedNames) return reservedNames;
1749
+ if (contextName === void 0) return NO_RESERVED;
1750
+ return reservedInlineNames(contextName, siblingProperties, parentModelName);
1751
+ }
1752
+ /**
1753
+ * Name the inline object variants of a field's `oneOf`/`anyOf` exactly as the
1754
+ * inline-model extractors do (`extractInlineModelsFromProperties` in
1755
+ * inline-models.ts, and `extractNestedSchema` / `extractInlineModelDeep`
1756
+ * here): the union's base name is the field's qualified inline name, the
1757
+ * names the field's sibling properties mint — or the set handed down from an
1758
+ * enclosing field — are reserved (`reservedNamesFor`), and
1759
+ * `nameInlineVariants` hands out the variant names from there. Returned keyed
1760
+ * by variant schema identity.
1761
+ */
1762
+ function nameInlineObjectVariants(union, contextName, parentModelName, siblingProperties, reservedNames) {
1763
+ return nameInlineVariants(union, qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName), reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames));
1764
+ }
1765
+ /**
1766
+ * The TypeRef for an inline object variant referenced by the name its
1767
+ * extracted model carries, keeping every other type the variant allows.
1768
+ * Mirrors what `schemaToTypeRef` builds for the same schema — one member per
1769
+ * non-null entry of a `type` array, `union`-wrapped when there is more than
1770
+ * one, `nullable`-wrapped when `null` or OAS 3.0 `nullable: true` is present
1771
+ * — with the `object` member swapped for the named ref:
1772
+ * `type: ['object', 'string']` → `union[model, string]`,
1773
+ * `['object', 'integer', 'null']` → `nullable<union[model, integer]>`,
1774
+ * `['object', 'null']` / `nullable: true` → `nullable<model>`.
1775
+ * A `oneOf: [X, { type: 'null' }]` variant has no `properties` of its own
1776
+ * and never reaches this path; `schemaToTypeRef` types it directly.
1777
+ *
1778
+ * Anything inline under a non-object member is named from the variant, never
1779
+ * from the union's field: an `array` member types its items under the
1780
+ * variant as parent with the context `variantMemberKeys` picks (`item` →
1781
+ * `<Variant>Item`, or `array_item` → `<Variant>ArrayItem` when a property
1782
+ * claims `item`), and any other member under its `value` key
1783
+ * (`<Variant>Value` for an inline enum, else `<Variant>MemberValue`). Naming
1784
+ * them from the field would hand variant 0's model name to every variant's
1785
+ * items.
1786
+ */
1787
+ function inlineVariantTypeRef(variant, name) {
1788
+ const modelRef = {
1789
+ kind: "model",
1790
+ name
1791
+ };
1792
+ const typeList = Array.isArray(variant.type) ? variant.type : [];
1793
+ const nullable = variant.nullable === true || typeList.includes("null");
1794
+ const keys = variantMemberKeys(variant, name);
1795
+ const members = typeList.filter((t) => t !== "null").map((t) => t === "object" ? modelRef : schemaToTypeRef({
1796
+ ...variant,
1797
+ type: t,
1798
+ nullable: false
1799
+ }, t === "array" ? keys.item : keys.value, name));
1800
+ const ref = members.length > 1 ? {
1801
+ kind: "union",
1802
+ variants: members
1803
+ } : modelRef;
1804
+ return nullable ? asNullable(ref) : ref;
1805
+ }
723
1806
  function extractEnum(name, schema) {
724
- const values = (schema.enum ?? []).map((v) => ({
1807
+ const values = nonNullEnumValues(schema.enum).map((v) => ({
725
1808
  name: toUpperSnakeCase(String(v)),
726
1809
  value: typeof v === "number" ? v : String(v),
727
1810
  description: void 0
@@ -737,9 +1820,10 @@ function extractModel(name, schema, schemas) {
737
1820
  if (schema.allOf) return extractAllOfModel(name, schema, schemas);
738
1821
  const requiredSet = new Set(schema.required ?? []);
739
1822
  const fields = [];
1823
+ const siblings = collectProperties(schema, schemas);
740
1824
  for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
741
1825
  if (!fieldSchema) continue;
742
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
1826
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, schemas, siblings));
743
1827
  }
744
1828
  if (fields.length === 0 && (schema.oneOf || schema.anyOf)) {
745
1829
  const variants = schema.oneOf ?? schema.anyOf ?? [];
@@ -750,7 +1834,7 @@ function extractModel(name, schema, schemas) {
750
1834
  for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
751
1835
  if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
752
1836
  seenFieldNames.add(fieldName);
753
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
1837
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
754
1838
  }
755
1839
  }
756
1840
  }
@@ -771,12 +1855,14 @@ function extractModel(name, schema, schemas) {
771
1855
  return {
772
1856
  name,
773
1857
  description: schema.description,
774
- fields
1858
+ fields,
1859
+ ...constraintsEntry(schema)
775
1860
  };
776
1861
  }
777
- function extractAllOfModel(name, schema, schemas) {
1862
+ function extractAllOfModel(name, schema, schemas, siblingProperties) {
778
1863
  const fields = [];
779
1864
  const requiredSet = /* @__PURE__ */ new Set();
1865
+ const siblings = siblingProperties ?? collectProperties(schema, schemas);
780
1866
  let resultDiscriminator;
781
1867
  for (const subSchema of schema.allOf ?? []) {
782
1868
  let resolved = subSchema;
@@ -786,7 +1872,7 @@ function extractAllOfModel(name, schema, schemas) {
786
1872
  if (refName && schemas[refName]) resolved = schemas[refName];
787
1873
  }
788
1874
  if (resolved.allOf) {
789
- const nested = extractAllOfModel(name, resolved, schemas);
1875
+ const nested = extractAllOfModel(name, resolved, schemas, siblings);
790
1876
  for (const f of nested.fields) {
791
1877
  fields.push({
792
1878
  ...f,
@@ -806,7 +1892,7 @@ function extractAllOfModel(name, schema, schemas) {
806
1892
  for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
807
1893
  if (!fieldSchema || seenFieldNames.has(fieldName)) continue;
808
1894
  seenFieldNames.add(fieldName);
809
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
1895
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
810
1896
  }
811
1897
  }
812
1898
  }
@@ -816,7 +1902,7 @@ function extractAllOfModel(name, schema, schemas) {
816
1902
  const emptyRequired = /* @__PURE__ */ new Set();
817
1903
  for (const [fieldName, fieldSchema] of Object.entries(resolved.properties)) {
818
1904
  if (!fieldSchema) continue;
819
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
1905
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired, schemas, siblings));
820
1906
  }
821
1907
  }
822
1908
  }
@@ -827,7 +1913,8 @@ function extractAllOfModel(name, schema, schemas) {
827
1913
  name,
828
1914
  description: schema.description,
829
1915
  fields,
830
- ...resultDiscriminator ? { discriminator: resultDiscriminator } : {}
1916
+ ...resultDiscriminator ? { discriminator: resultDiscriminator } : {},
1917
+ ...constraintsEntry(schema)
831
1918
  };
832
1919
  }
833
1920
  /**
@@ -898,18 +1985,23 @@ function detectAllOfVariantDiscriminator(variants, fallbackName) {
898
1985
  mapping
899
1986
  };
900
1987
  }
901
- function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
1988
+ function extractDiscriminatedAllOfVariantModels(schema, fallbackName, schemas) {
902
1989
  const models = [];
903
1990
  const seenNames = /* @__PURE__ */ new Set();
904
1991
  const baseProperties = {};
905
1992
  const baseRequired = /* @__PURE__ */ new Set();
1993
+ let baseConstraints = ownConstraints(schema);
906
1994
  for (const subSchema of schema.allOf ?? []) {
907
- if (subSchema.oneOf) continue;
1995
+ if (subSchema.oneOf) {
1996
+ baseConstraints = combineConstraints(baseConstraints, ownConstraints(subSchema));
1997
+ continue;
1998
+ }
908
1999
  const flat = subSchema.allOf ? flattenVariantForExtraction(subSchema) : subSchema;
909
2000
  if (flat.properties) {
910
2001
  for (const [k, v] of Object.entries(flat.properties)) if (!(k in baseProperties) && v) baseProperties[k] = v;
911
2002
  }
912
2003
  if (flat.required) for (const r of flat.required) baseRequired.add(r);
2004
+ baseConstraints = combineConstraints(baseConstraints, extractConstraints(subSchema));
913
2005
  }
914
2006
  for (const subSchema of schema.allOf ?? []) for (const variant of subSchema.oneOf ?? []) {
915
2007
  const flat = flattenVariantForExtraction(variant);
@@ -919,10 +2011,11 @@ function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
919
2011
  const merged = {
920
2012
  type: "object",
921
2013
  properties: mergedProperties,
922
- required: [...new Set([...baseRequired, ...flat.required ?? []])]
2014
+ required: [.../* @__PURE__ */ new Set([...baseRequired, ...flat.required ?? []])],
2015
+ ...combineConstraints(baseConstraints, extractConstraints(variant))
923
2016
  };
924
2017
  const variantName = deriveDiscriminatedVariantName(variant, fallbackName);
925
- for (const model of extractInlineModelDeep(variantName, merged)) {
2018
+ for (const model of extractInlineModelDeep(variantName, merged, schemas)) {
926
2019
  if (seenNames.has(model.name)) continue;
927
2020
  seenNames.add(model.name);
928
2021
  models.push(model);
@@ -1028,16 +2121,27 @@ function deriveDiscriminatedVariantName(schema, fallbackName) {
1028
2121
  if (pascal.endsWith(fallbackName)) return pascal;
1029
2122
  return `${fallbackName}${pascal}`;
1030
2123
  }
1031
- function extractInlineModelDeep(name, schema) {
2124
+ function extractInlineModelDeep(name, schema, schemas) {
1032
2125
  const requiredSet = new Set(schema.required ?? []);
1033
2126
  const fields = [];
1034
2127
  const properties = schema.properties ?? {};
1035
2128
  const nestedModels = [];
2129
+ const siblings = collectProperties(schema);
1036
2130
  for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1037
2131
  if (!fieldSchema) continue;
1038
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
1039
- if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema));
1040
- if (fieldSchema.type === "array" && fieldSchema.items?.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema.items));
2132
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, schemas, siblings));
2133
+ if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema, schemas));
2134
+ if (fieldSchema.type === "array" && fieldSchema.items?.properties) {
2135
+ const itemContext = elementContextName(fieldName, "item", siblings, name, fieldSchema.items);
2136
+ nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, itemContext), fieldSchema.items, schemas));
2137
+ }
2138
+ if (fieldSchema.type === "object" && !fieldSchema.properties) {
2139
+ const valueSchema = mapValueSchema(fieldSchema);
2140
+ if (valueSchema && !valueSchema.$ref && valueSchema.properties) {
2141
+ const valueContext = elementContextName(fieldName, "value", siblings, name, valueSchema);
2142
+ nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, valueContext), valueSchema, schemas));
2143
+ }
2144
+ }
1041
2145
  if (fieldSchema.allOf) {
1042
2146
  const mergedProperties = {};
1043
2147
  const mergedRequired = [];
@@ -1048,22 +2152,20 @@ function extractInlineModelDeep(name, schema) {
1048
2152
  if (Object.keys(mergedProperties).length > 0) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), {
1049
2153
  type: "object",
1050
2154
  properties: mergedProperties,
1051
- required: mergedRequired
1052
- }));
2155
+ required: mergedRequired,
2156
+ ...extractConstraints(fieldSchema)
2157
+ }, schemas));
1053
2158
  }
1054
2159
  if (fieldSchema.oneOf) {
1055
- const inlineObjectVariants = fieldSchema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
2160
+ const inlineObjectVariants = fieldSchema.oneOf.filter((v) => isInlineObjectSchema(v));
1056
2161
  if (inlineObjectVariants.length > 0) {
1057
- const baseQualified = qualifyNestedInlineName(name, fieldName);
1058
- const namingDisc = deriveConstNamingDiscriminator(inlineObjectVariants);
1059
- const emitted = [];
2162
+ const variantNames = nameInlineVariants(fieldSchema, qualifyNestedInlineName(name, fieldName), reservedInlineNames(fieldName, siblings, name));
1060
2163
  for (const variant of inlineObjectVariants) {
1061
- const variantName = nameVariantModel(variant, baseQualified, emitted, namingDisc);
1062
- emitted.push({
1063
- name: variantName,
1064
- fields: []
1065
- });
1066
- nestedModels.push(...extractInlineModelDeep(variantName, variant));
2164
+ const variantName = variantNames.get(variant);
2165
+ if (variantName === void 0) continue;
2166
+ nestedModels.push(...extractInlineModelDeep(variantName, variant, schemas));
2167
+ const memberSchemas = variantMemberSchemas(variant, variantName);
2168
+ if (Object.keys(memberSchemas).length > 0) nestedModels.push(...extractInlineModelDeep(variantName, { properties: memberSchemas }, schemas).slice(1));
1067
2169
  }
1068
2170
  }
1069
2171
  }
@@ -1071,7 +2173,8 @@ function extractInlineModelDeep(name, schema) {
1071
2173
  return [{
1072
2174
  name,
1073
2175
  description: schema.description,
1074
- fields
2176
+ fields,
2177
+ ...constraintsEntry(schema)
1075
2178
  }, ...nestedModels];
1076
2179
  }
1077
2180
  function qualifyNestedInlineName(parentName, fieldName) {
@@ -1082,15 +2185,28 @@ function qualifyNestedInlineName(parentName, fieldName) {
1082
2185
  if (match && match[2]) return match[1] + singularize(match[2]);
1083
2186
  return qualified;
1084
2187
  }
1085
- function schemaToTypeRef(schema, contextName, parentModelName) {
2188
+ /**
2189
+ * `elementContextName` for an optional context: with no field name there is
2190
+ * nothing to decide, and the items / values keep an undefined context.
2191
+ */
2192
+ function elementContext(contextName, kind, siblingProperties, parentModelName, elementSchema) {
2193
+ return contextName === void 0 ? void 0 : elementContextName(contextName, kind, siblingProperties, parentModelName, elementSchema);
2194
+ }
2195
+ function schemaToTypeRef(schema, contextName, parentModelName, siblingProperties, reservedNames) {
1086
2196
  if (schema.$ref) {
1087
2197
  const segments = schema.$ref.split("/");
1088
2198
  const rawName = segments[segments.length - 1];
1089
2199
  const resolved = resolveSchemaName(rawName);
1090
- if (activeEnumSchemaNames.has(resolved)) return {
1091
- kind: "enum",
1092
- name: resolved
1093
- };
2200
+ if (activeEnumSchemaNames.has(resolved)) {
2201
+ const enumRef = {
2202
+ kind: "enum",
2203
+ name: resolved
2204
+ };
2205
+ return activeNullableEnumSchemaNames.has(resolved) ? {
2206
+ kind: "nullable",
2207
+ inner: enumRef
2208
+ } : enumRef;
2209
+ }
1094
2210
  return {
1095
2211
  kind: "model",
1096
2212
  name: resolved
@@ -1105,21 +2221,18 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1105
2221
  type: "unknown"
1106
2222
  }
1107
2223
  };
1108
- if (schema.type.includes("null") && nonNullTypes.length === 1) return {
1109
- kind: "nullable",
1110
- inner: schemaToTypeRef({
1111
- ...schema,
1112
- type: nonNullTypes[0],
1113
- nullable: false
1114
- }, contextName)
1115
- };
2224
+ if (schema.type.includes("null") && nonNullTypes.length === 1) return asNullable(schemaToTypeRef({
2225
+ ...schema,
2226
+ type: nonNullTypes[0],
2227
+ nullable: false
2228
+ }, contextName, parentModelName, siblingProperties, reservedNames));
1116
2229
  if (nonNullTypes.length > 1) {
1117
2230
  const ref = {
1118
2231
  kind: "union",
1119
2232
  variants: nonNullTypes.map((t) => schemaToTypeRef({
1120
2233
  ...schema,
1121
2234
  type: t
1122
- }, contextName))
2235
+ }, contextName, parentModelName, siblingProperties, reservedNames))
1123
2236
  };
1124
2237
  if (schema.type.includes("null")) return {
1125
2238
  kind: "nullable",
@@ -1128,13 +2241,10 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1128
2241
  return ref;
1129
2242
  }
1130
2243
  }
1131
- if (schema.nullable && schema.type) return {
1132
- kind: "nullable",
1133
- inner: schemaToTypeRef({
1134
- ...schema,
1135
- nullable: false
1136
- }, contextName)
1137
- };
2244
+ if (schema.nullable && schema.type) return asNullable(schemaToTypeRef({
2245
+ ...schema,
2246
+ nullable: false
2247
+ }, contextName, parentModelName, siblingProperties, reservedNames));
1138
2248
  if (schema.allOf) {
1139
2249
  const refItem = schema.allOf.find((s) => s.$ref);
1140
2250
  if (refItem) {
@@ -1144,7 +2254,7 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1144
2254
  };
1145
2255
  return schemaToTypeRef(refItem, contextName, parentModelName);
1146
2256
  }
1147
- if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName);
2257
+ if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName, siblingProperties, reservedNames);
1148
2258
  if (schema.allOf.some((s) => s.properties)) return {
1149
2259
  kind: "model",
1150
2260
  name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
@@ -1153,12 +2263,9 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1153
2263
  if (schema.oneOf || schema.anyOf) {
1154
2264
  const compositionKind = schema.oneOf ? "oneOf" : "anyOf";
1155
2265
  const rawVariants = schema.oneOf ?? schema.anyOf ?? [];
1156
- const nullVariant = rawVariants.find((v) => v.type === "null" || Array.isArray(v.type) && v.type.length === 1 && v.type[0] === "null");
2266
+ const nullVariant = rawVariants.find(isNullVariantSchema);
1157
2267
  const nonNullVariants = rawVariants.filter((v) => v !== nullVariant);
1158
- if (nullVariant && nonNullVariants.length === 1) return {
1159
- kind: "nullable",
1160
- inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
1161
- };
2268
+ if (nullVariant && nonNullVariants.length === 1) return asNullable(schemaToTypeRef(nonNullVariants[0], contextName, parentModelName, siblingProperties, reservedNames));
1162
2269
  const literalStrings = collectLiteralStringConsts(nonNullVariants);
1163
2270
  if (literalStrings !== null && literalStrings.length >= 2) {
1164
2271
  const enumRef = buildSyntheticEnumRef(literalStrings, contextName, parentModelName);
@@ -1167,13 +2274,14 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1167
2274
  inner: enumRef
1168
2275
  } : enumRef;
1169
2276
  }
2277
+ const inlineVariantNames = nameInlineObjectVariants(schema, contextName, parentModelName, siblingProperties, reservedNames);
1170
2278
  let resolvedDiscriminator;
1171
2279
  if (schema.discriminator) {
1172
2280
  const property = schema.discriminator.propertyName;
1173
2281
  const explicitMapping = schema.discriminator.mapping ?? {};
1174
2282
  let mapping = Object.fromEntries(Object.entries(explicitMapping).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]));
1175
2283
  if (Object.keys(mapping).length === 0) {
1176
- const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName, parentModelName);
2284
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, property, contextName, inlineVariantNames);
1177
2285
  if (inlineMapping) mapping = inlineMapping;
1178
2286
  }
1179
2287
  resolvedDiscriminator = {
@@ -1181,12 +2289,22 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1181
2289
  mapping
1182
2290
  };
1183
2291
  } else if (compositionKind === "oneOf") {
1184
- const synthetic = detectConstPropertyDiscriminator(nonNullVariants);
1185
- if (synthetic) resolvedDiscriminator = synthetic;
1186
- else {
2292
+ const titleProperty = titleMappedDiscriminatorProperty(nonNullVariants);
2293
+ if (titleProperty) {
2294
+ const mapping = {};
2295
+ for (const v of nonNullVariants) {
2296
+ const value = getConstPropertyValue(v, titleProperty);
2297
+ const mappedName = inlineVariantNames.get(v) ?? resolveVariantModelName(v);
2298
+ if (value !== null && mappedName !== null) mapping[value] = mappedName;
2299
+ }
2300
+ resolvedDiscriminator = {
2301
+ property: titleProperty,
2302
+ mapping
2303
+ };
2304
+ } else {
1187
2305
  const namingDisc = deriveConstNamingDiscriminator(nonNullVariants);
1188
2306
  if (namingDisc) {
1189
- const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName, parentModelName);
2307
+ const inlineMapping = deriveInlineVariantMapping(nonNullVariants, namingDisc.property, contextName, inlineVariantNames);
1190
2308
  if (inlineMapping) resolvedDiscriminator = {
1191
2309
  property: namingDisc.property,
1192
2310
  mapping: inlineMapping
@@ -1197,12 +2315,11 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1197
2315
  const variants = [];
1198
2316
  for (const v of rawVariants) {
1199
2317
  if (v === nullVariant) continue;
1200
- const inlineMappingName = resolvedDiscriminator && !v.$ref && v.properties && (v.type === "object" || !v.type) ? mapVariantToDiscriminatorEntry(v, resolvedDiscriminator) : null;
1201
- if (inlineMappingName) variants.push({
1202
- kind: "model",
1203
- name: inlineMappingName
1204
- });
1205
- else variants.push(schemaToTypeRef(v, contextName, parentModelName));
2318
+ const inlineName = inlineVariantNames.get(v);
2319
+ if (inlineName) {
2320
+ const mappedName = resolvedDiscriminator ? mapVariantToDiscriminatorEntry(v, resolvedDiscriminator) : null;
2321
+ variants.push(inlineVariantTypeRef(v, mappedName ?? inlineName));
2322
+ } else variants.push(schemaToTypeRef(v, contextName, parentModelName));
1206
2323
  }
1207
2324
  const hasNull = !!nullVariant;
1208
2325
  const union = {
@@ -1245,30 +2362,30 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1245
2362
  };
1246
2363
  }
1247
2364
  }
1248
- if (schema.enum && schema.enum.length === 1) {
1249
- const v = schema.enum[0];
1250
- return {
2365
+ const enumValues = nonNullEnumValues(schema.enum);
2366
+ const enumAdmitsNull = schema.enum !== void 0 && enumValues.length !== schema.enum.length;
2367
+ if (schema.enum && enumValues.length === 1) {
2368
+ const v = enumValues[0];
2369
+ const literal = {
1251
2370
  kind: "literal",
1252
2371
  value: typeof v === "number" ? v : String(v)
1253
2372
  };
2373
+ return enumAdmitsNull ? asNullable(literal) : literal;
1254
2374
  }
1255
2375
  if (schema.enum) {
1256
2376
  const baseName = toPascalCase(contextName ?? "UnknownEnum");
1257
2377
  const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
1258
2378
  const qualifiedName = cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName;
1259
- return {
2379
+ const enumRef = {
1260
2380
  kind: "enum",
1261
2381
  name: activeSchemaNameTransform ? activeSchemaNameTransform(qualifiedName) : qualifiedName,
1262
- values: schema.enum.map((v) => typeof v === "number" ? v : String(v))
2382
+ values: enumValues.map((v) => typeof v === "number" ? v : String(v))
1263
2383
  };
2384
+ return enumAdmitsNull ? asNullable(enumRef) : enumRef;
1264
2385
  }
1265
- if (schema.type === "array" && schema.items) return {
1266
- kind: "array",
1267
- items: schemaToTypeRef(schema.items, contextName, parentModelName)
1268
- };
1269
- if (!schema.type && schema.items) return {
2386
+ if ((schema.type === "array" || !schema.type) && schema.items) return {
1270
2387
  kind: "array",
1271
- items: schemaToTypeRef(schema.items, contextName, parentModelName)
2388
+ items: schemaToTypeRef(schema.items, elementContext(contextName, "item", siblingProperties, parentModelName, schema.items), parentModelName, void 0, reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames))
1272
2389
  };
1273
2390
  if (schema.type === "object" && schema.properties) return {
1274
2391
  kind: "model",
@@ -1279,12 +2396,8 @@ function schemaToTypeRef(schema, contextName, parentModelName) {
1279
2396
  kind: "primitive",
1280
2397
  type: "unknown"
1281
2398
  };
1282
- if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
1283
- if (Object.keys(schema.additionalProperties).length > 0) valueType = schemaToTypeRef(schema.additionalProperties, contextName);
1284
- } else if (schema.patternProperties) {
1285
- const patterns = Object.values(schema.patternProperties);
1286
- if (patterns.length > 0) valueType = schemaToTypeRef(patterns[0], contextName);
1287
- }
2399
+ const valueSchema = mapValueSchema(schema);
2400
+ if (valueSchema) valueType = schemaToTypeRef(valueSchema, elementContext(contextName, "value", siblingProperties, parentModelName, valueSchema), parentModelName, void 0, reservedNamesFor(contextName, siblingProperties, parentModelName, reservedNames));
1288
2401
  return {
1289
2402
  kind: "map",
1290
2403
  valueType
@@ -1384,11 +2497,11 @@ function classifyAndExtractResponse(schema, contextName, componentSchemas) {
1384
2497
  isPaginated: false
1385
2498
  };
1386
2499
  const envelope = detectListEnvelope(schema);
1387
- if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath);
1388
- if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName);
2500
+ if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath, componentSchemas);
2501
+ if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName, componentSchemas);
1389
2502
  return extractDirectResource(schema, contextName, componentSchemas);
1390
2503
  }
1391
- const KNOWN_DATA_PATHS = new Set([
2504
+ const KNOWN_DATA_PATHS = /* @__PURE__ */ new Set([
1392
2505
  "data",
1393
2506
  "items",
1394
2507
  "results",
@@ -1446,33 +2559,27 @@ function detectListEnvelope(schema) {
1446
2559
  dataPath: null
1447
2560
  };
1448
2561
  }
1449
- function extractListResponse(schema, contextName, dataPath) {
2562
+ function extractListResponse(schema, contextName, dataPath, componentSchemas) {
1450
2563
  let itemTypeRef = {
1451
2564
  kind: "primitive",
1452
2565
  type: "string"
1453
2566
  };
1454
2567
  const inlineModels = [];
1455
- const propSources = [];
1456
- if (schema.allOf) {
1457
- for (const sub of schema.allOf) if (sub.properties) propSources.push(sub.properties);
1458
- }
1459
- if (schema.properties) propSources.push(schema.properties);
1460
- for (const props of propSources) {
1461
- const dataProp = props[dataPath];
1462
- if (!dataProp) continue;
1463
- if (dataProp.type === "array" && dataProp.items) {
1464
- const items = dataProp.items;
1465
- if (items.$ref) itemTypeRef = schemaToTypeRef(items, contextName);
1466
- else if (items.type === "object" && items.properties) {
1467
- const itemName = contextName.replace(/Response$/, "") + "Item";
1468
- itemTypeRef = {
1469
- kind: "model",
1470
- name: itemName
1471
- };
1472
- inlineModels.push(...extractInlineModel(itemName, items));
1473
- }
2568
+ const { properties, required } = collectEnvelopeProperties(schema, componentSchemas);
2569
+ const dataProp = properties[dataPath];
2570
+ if (dataProp?.type === "array" && dataProp.items) {
2571
+ const items = dataProp.items;
2572
+ if (items.$ref) itemTypeRef = schemaToTypeRef(items, contextName);
2573
+ else if (items.type === "object" && items.properties) {
2574
+ const itemName = contextName.replace(/Response$/, "") + "Item";
2575
+ itemTypeRef = {
2576
+ kind: "model",
2577
+ name: itemName
2578
+ };
2579
+ inlineModels.push(...extractInlineModel(itemName, items, componentSchemas));
1474
2580
  }
1475
2581
  }
2582
+ const { envelopeFields, envelopeCompanions } = extractEnvelopeCompanions(properties, required, dataPath, contextName.replace(/Response$/, ""), componentSchemas);
1476
2583
  return {
1477
2584
  response: {
1478
2585
  kind: "array",
@@ -1481,7 +2588,117 @@ function extractListResponse(schema, contextName, dataPath) {
1481
2588
  inlineModels,
1482
2589
  isPaginated: true,
1483
2590
  dataPath,
1484
- itemType: itemTypeRef
2591
+ itemType: itemTypeRef,
2592
+ envelopeFields,
2593
+ envelopeCompanions: envelopeCompanions.length > 0 ? envelopeCompanions : void 0
2594
+ };
2595
+ }
2596
+ /**
2597
+ * Every property the envelope declares, merged across its `allOf` branches,
2598
+ * with the `required` lists that go with them.
2599
+ *
2600
+ * A `$ref` branch is resolved against the component schemas so fields an
2601
+ * envelope inherits from a shared base (`trace_id` on every response, say)
2602
+ * are not silently dropped. A property declared by more than one branch keeps
2603
+ * both declarations' shape when both are inline objects — `pagination.next`
2604
+ * from one branch and `pagination.total` from another both belong to the
2605
+ * response — and otherwise takes the later declaration, as `allOf` merging
2606
+ * does elsewhere in the parser.
2607
+ */
2608
+ function collectEnvelopeProperties(schema, componentSchemas) {
2609
+ const properties = {};
2610
+ const required = [];
2611
+ const seenRefs = /* @__PURE__ */ new Set();
2612
+ const visit = (s) => {
2613
+ if (s.$ref) {
2614
+ if (seenRefs.has(s.$ref)) return;
2615
+ seenRefs.add(s.$ref);
2616
+ const refName = s.$ref.split("/").pop();
2617
+ const resolved = refName ? componentSchemas?.[refName] : void 0;
2618
+ if (resolved) visit(resolved);
2619
+ return;
2620
+ }
2621
+ for (const sub of s.allOf ?? []) visit(sub);
2622
+ for (const [key, prop] of Object.entries(s.properties ?? {})) {
2623
+ if (!prop) continue;
2624
+ const existing = properties[key];
2625
+ properties[key] = existing ? mergePropertyDeclarations(existing, prop) : prop;
2626
+ }
2627
+ if (s.required) required.push(...s.required);
2628
+ };
2629
+ visit(schema);
2630
+ return {
2631
+ properties,
2632
+ required
2633
+ };
2634
+ }
2635
+ /**
2636
+ * Combine two `allOf` declarations of one property. Inline objects merge their
2637
+ * fields; an array keeps the earlier `items` when the later declaration only
2638
+ * constrains it (`minItems: 1`), so `data: User[]` is not downgraded to
2639
+ * `string[]`; anything else takes the later declaration.
2640
+ */
2641
+ function mergePropertyDeclarations(earlier, later) {
2642
+ if (isInlineObjectSchema(earlier) && isInlineObjectSchema(later)) return {
2643
+ ...earlier,
2644
+ ...later,
2645
+ properties: {
2646
+ ...earlier.properties,
2647
+ ...later.properties
2648
+ },
2649
+ required: [...earlier.required ?? [], ...later.required ?? []]
2650
+ };
2651
+ if (earlier.type === "array" && later.type === "array") return {
2652
+ ...earlier,
2653
+ ...later,
2654
+ items: later.items ?? earlier.items
2655
+ };
2656
+ return later;
2657
+ }
2658
+ /**
2659
+ * Type a list envelope's companion properties as fields, lifting any inline
2660
+ * object among them (and anything nested inside it) to a model.
2661
+ *
2662
+ * Models come from `extractInlineModelsFromProperties`, the extractor that
2663
+ * mirrors `schemaToTypeRef` form for form — plain, nullable and `allOf`
2664
+ * objects, arrays of objects, and every `oneOf`/`anyOf` object variant — so a
2665
+ * field's reference always has a model behind it.
2666
+ *
2667
+ * Each companion is extracted twice: once under the operation's name, which
2668
+ * is how its models enter the IR (`ListUsersListMetadata`), and once under no
2669
+ * parent, which yields the name each model would carry if named from its
2670
+ * property alone (`ListMetadata`). Both runs walk the same code path, so the
2671
+ * two model lists line up index for index. `resolveEnvelopeCompanionNames`
2672
+ * later picks, per companion, which of the two names survives — the bare one
2673
+ * only where every copy of that companion across the API agrees structurally.
2674
+ */
2675
+ function extractEnvelopeCompanions(properties, required, dataPath, operationName, componentSchemas) {
2676
+ const envelopeFields = [];
2677
+ const envelopeCompanions = [];
2678
+ const requiredSet = new Set(required);
2679
+ const siblings = collectProperties({ properties });
2680
+ for (const [key, propSchema] of Object.entries(properties)) {
2681
+ if (key === dataPath) continue;
2682
+ const field = buildFieldFromSchema(key, propSchema, operationName, requiredSet, componentSchemas, siblings);
2683
+ envelopeFields.push(field);
2684
+ const single = {
2685
+ type: "object",
2686
+ properties: { [key]: propSchema }
2687
+ };
2688
+ const models = [];
2689
+ extractInlineModelsFromProperties(single, models, operationName, componentSchemas, siblings);
2690
+ if (models.length === 0) continue;
2691
+ const bare = [];
2692
+ extractInlineModelsFromProperties(single, bare, void 0, componentSchemas, siblings);
2693
+ envelopeCompanions.push({
2694
+ field,
2695
+ models,
2696
+ bareNames: bare.map((m) => m.name)
2697
+ });
2698
+ }
2699
+ return {
2700
+ envelopeFields,
2701
+ envelopeCompanions
1485
2702
  };
1486
2703
  }
1487
2704
  function hasDiscriminantConstField(schema) {
@@ -1527,7 +2744,7 @@ function isSingleResourceWrapper(schema) {
1527
2744
  if (propSchema.oneOf) return propSchema.oneOf.some((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
1528
2745
  return false;
1529
2746
  }
1530
- function extractWrappedResource(schema, contextName) {
2747
+ function extractWrappedResource(schema, contextName, componentSchemas) {
1531
2748
  const props = schema.properties;
1532
2749
  const propSchema = props[schema.required && schema.required.length === 1 ? schema.required[0] : Object.keys(props)[0]];
1533
2750
  const inlineModels = [];
@@ -1536,7 +2753,7 @@ function extractWrappedResource(schema, contextName) {
1536
2753
  const objectVariant = propSchema.oneOf.find((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
1537
2754
  if (objectVariant) {
1538
2755
  const modelName = deriveModelName(objectVariant, resourceName);
1539
- inlineModels.push(...extractInlineModel(modelName, objectVariant));
2756
+ inlineModels.push(...extractInlineModel(modelName, objectVariant, componentSchemas));
1540
2757
  const hasNullVariant = propSchema.oneOf.some((v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null"));
1541
2758
  const modelRef = {
1542
2759
  kind: "model",
@@ -1553,7 +2770,7 @@ function extractWrappedResource(schema, contextName) {
1553
2770
  }
1554
2771
  }
1555
2772
  const modelName = deriveModelName(propSchema, resourceName);
1556
- inlineModels.push(...extractInlineModel(modelName, propSchema));
2773
+ inlineModels.push(...extractInlineModel(modelName, propSchema, componentSchemas));
1557
2774
  return {
1558
2775
  response: {
1559
2776
  kind: "model",
@@ -1567,7 +2784,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1567
2784
  const inlineModels = [];
1568
2785
  if (schema.properties && (schema.type === "object" || !schema.type)) {
1569
2786
  const modelName = deriveModelName(schema, contextName);
1570
- inlineModels.push(...extractInlineModel(modelName, schema));
2787
+ inlineModels.push(...extractInlineModel(modelName, schema, componentSchemas));
1571
2788
  return {
1572
2789
  response: {
1573
2790
  kind: "model",
@@ -1614,10 +2831,11 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1614
2831
  const merged = {
1615
2832
  type: "object",
1616
2833
  properties: mergedProperties,
1617
- required: mergedRequired
2834
+ required: mergedRequired,
2835
+ ...extractConstraints(schema)
1618
2836
  };
1619
2837
  const modelName = mergedRefFields ? toPascalCase(contextName) : deriveModelName(merged, contextName);
1620
- inlineModels.push(...extractInlineModel(modelName, merged));
2838
+ inlineModels.push(...extractInlineModel(modelName, merged, componentSchemas));
1621
2839
  return {
1622
2840
  response: {
1623
2841
  kind: "model",
@@ -1628,12 +2846,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1628
2846
  };
1629
2847
  }
1630
2848
  }
1631
- if (schema.oneOf) {
1632
- for (const variant of schema.oneOf) if (variant.properties && (variant.type === "object" || !variant.type)) {
1633
- const modelName = deriveModelName(variant, contextName);
1634
- if (!new Set(inlineModels.map((m) => m.name)).has(modelName)) inlineModels.push(...extractInlineModel(modelName, variant));
1635
- }
1636
- }
2849
+ if (schema.oneOf) extractInlineModelsFromProperties({ properties: { [contextName]: schema } }, inlineModels, void 0, componentSchemas);
1637
2850
  if (schema.type === "array" && schema.items) {
1638
2851
  const items = schema.items;
1639
2852
  if (items.$ref) return {
@@ -1646,7 +2859,7 @@ function extractDirectResource(schema, contextName, componentSchemas) {
1646
2859
  };
1647
2860
  if (items.properties && (items.type === "object" || !items.type)) {
1648
2861
  const itemName = contextName.replace(/Response$/, "") + "Item";
1649
- inlineModels.push(...extractInlineModel(itemName, items));
2862
+ inlineModels.push(...extractInlineModel(itemName, items, componentSchemas));
1650
2863
  return {
1651
2864
  response: {
1652
2865
  kind: "array",
@@ -1676,70 +2889,288 @@ function deriveModelName(schema, fallback) {
1676
2889
  return toPascalCase(fallback);
1677
2890
  }
1678
2891
  /**
1679
- * Qualify a nested inline model name with its parent to avoid generic names.
1680
- * e.g., parent="Connection" + field="domains" → "ConnectionDomain" (singularized)
1681
- * Only qualifies when the field name alone would be too generic.
2892
+ * Materialise an inline response object (and every nested inline object
2893
+ * under it) as models. `componentSchemas` only feeds `buildFieldFromSchema`,
2894
+ * so a response field that `$ref`s a component takes the component's
2895
+ * `example`/`examples` exactly as a request-body or component-model field
2896
+ * does; nested models get the same map so the fallback reaches every level.
1682
2897
  */
1683
- function qualifyNestedName(parentName, fieldName) {
1684
- const pascalField = toPascalCase(fieldName);
1685
- if (pascalField.startsWith(parentName)) return pascalField;
1686
- const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
1687
- const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
1688
- if (match && match[2]) {
1689
- const trailing = singularize(match[2]);
1690
- return match[1] + trailing;
1691
- }
1692
- return qualified;
1693
- }
1694
- function extractInlineModel(name, schema) {
2898
+ function extractInlineModel(name, schema, componentSchemas) {
1695
2899
  const requiredSet = new Set(schema.required ?? []);
1696
2900
  const fields = [];
1697
2901
  const properties = schema.properties ?? {};
1698
2902
  const nestedModels = [];
2903
+ const siblings = collectProperties(schema);
1699
2904
  for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1700
2905
  if (!fieldSchema) continue;
1701
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
1702
- if (fieldSchema.type === "object" && fieldSchema.properties) {
1703
- const nestedName = qualifyNestedName(name, fieldName);
1704
- nestedModels.push(...extractInlineModel(nestedName, fieldSchema));
1705
- }
1706
- if (fieldSchema.type === "array" && fieldSchema.items) {
1707
- const items = fieldSchema.items;
1708
- if (items.type === "object" && items.properties) {
1709
- const nestedName = qualifyNestedName(name, fieldName);
1710
- nestedModels.push(...extractInlineModel(nestedName, items));
1711
- }
2906
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet, componentSchemas, siblings));
2907
+ }
2908
+ extractInlineModelsFromProperties(schema, nestedModels, name, componentSchemas);
2909
+ return [{
2910
+ name,
2911
+ description: schema.description,
2912
+ fields,
2913
+ ...constraintsEntry(schema)
2914
+ }, ...nestedModels];
2915
+ }
2916
+ //#endregion
2917
+ //#region src/parser/security.ts
2918
+ /**
2919
+ * Convert a document- or operation-level `security` list into the IR shape,
2920
+ * preserving what the list says rather than flattening it:
2921
+ *
2922
+ * - absent (or not a list) → `undefined`: nothing declared here.
2923
+ * - `[]` → `[]`: no authentication required.
2924
+ * - `[{ a: [] }, { b: [] }]` → `[[a], [b]]`: either scheme suffices.
2925
+ * - `[{ a: [], b: [] }]` → `[[a, b]]`: both schemes are required together.
2926
+ * - `[{ a: [] }, {}]` → `[[a], []]`: the empty set is an anonymous alternative,
2927
+ * which is how a document marks authentication optional.
2928
+ *
2929
+ * An entry that is not an object (`null`, say) is not a requirement object.
2930
+ * Reading it as `{}` would declare an anonymous alternative and make the
2931
+ * operation look open, so it is dropped with an `[oagen] Warning:` naming
2932
+ * `context` (the operation, or the document). A non-empty list left with no
2933
+ * valid entry is treated as undeclared (`undefined`) rather than as `[]`:
2934
+ * the document never said "no authentication", so the operation keeps
2935
+ * inheriting the default.
2936
+ *
2937
+ * The previous flattening collapsed the alternatives onto one list and turned
2938
+ * `[]` into `undefined`, so "no auth" read the same as "use the default" and
2939
+ * "either of" the same as "all of" (#182).
2940
+ */
2941
+ function extractSecurityRequirements(security, context) {
2942
+ if (!Array.isArray(security)) return void 0;
2943
+ const sets = [];
2944
+ for (const entry of security) {
2945
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
2946
+ console.warn(`[oagen] Warning: Ignoring security requirement ${JSON.stringify(entry)} on ${context} — not a requirement object`);
2947
+ continue;
1712
2948
  }
1713
- if (fieldSchema.allOf) {
1714
- const mergedProperties = {};
1715
- const mergedRequired = [];
1716
- for (const sub of fieldSchema.allOf) {
1717
- if (sub.properties) Object.assign(mergedProperties, sub.properties);
1718
- if (sub.required) mergedRequired.push(...sub.required);
1719
- }
1720
- if (Object.keys(mergedProperties).length > 0) {
1721
- const nestedName = qualifyNestedName(name, fieldName);
1722
- const merged = {
1723
- type: "object",
1724
- properties: mergedProperties,
1725
- required: mergedRequired
1726
- };
1727
- if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, merged));
2949
+ sets.push(Object.entries(entry).map(([schemeName, scopes]) => ({
2950
+ schemeName,
2951
+ scopes: Array.isArray(scopes) ? scopes : []
2952
+ })));
2953
+ }
2954
+ if (security.length > 0 && sets.length === 0) {
2955
+ console.warn(`[oagen] Warning: No valid security requirement on ${context}; treating security as undeclared`);
2956
+ return;
2957
+ }
2958
+ return sets;
2959
+ }
2960
+ //#endregion
2961
+ //#region src/parser/envelope-companion-names.ts
2962
+ /**
2963
+ * Decide the final name of every lifted companion model, across all list
2964
+ * operations at once, and return the models to merge into the IR.
2965
+ *
2966
+ * The same envelope recurs on every list endpoint of an API, so naming each
2967
+ * copy after its operation would mint one identical `ListMetadata` per
2968
+ * endpoint. But naming by property alone is only safe while the copies agree:
2969
+ * `mergeInlineResponseModels` keeps the largest model of a name, so a
2970
+ * `pagination` with three fields would silently replace an unrelated
2971
+ * `Pagination` component, or absorb another endpoint's two-field `pagination`.
2972
+ *
2973
+ * So a companion takes its bare name only when that cannot change any other
2974
+ * type's shape:
2975
+ *
2976
+ * Copies are compared structurally — field names, requiredness and each
2977
+ * field's shape, with nested models resolved and inline enums compared by
2978
+ * value — never by name alone. A copy collapses onto a target shape only when
2979
+ * the target *admits* every value the copy can hold: the same fields with the
2980
+ * same types, where the target may be looser (a field the copy requires may be
2981
+ * optional on the target, a non-null copy field may be nullable there) but
2982
+ * never stricter. Widening a response type loses no data a reader can see;
2983
+ * narrowing it would promise fields the wire may omit.
2984
+ *
2985
+ * - When a component model of that name exists — compared after the parser's
2986
+ * own renaming, so `PaginationDto` → `Pagination` is caught — the copies it
2987
+ * admits collapse onto it (the component stays as written); the others keep
2988
+ * their operation-qualified name.
2989
+ * - Otherwise the most common exact shape (first declared, on a tie) becomes
2990
+ * the target: it and every copy it admits take the bare name; any other
2991
+ * copy keeps its operation-qualified name.
2992
+ *
2993
+ * The comparison covers the companion's whole field type, so a `oneOf`
2994
+ * companion is judged on every variant, not just the first, and a copy folds
2995
+ * among copies only when its lifted models line up name for name with the
2996
+ * target's (same variants, same order); onto a component, only a companion
2997
+ * whose field references exactly its own lifted model can fold.
2998
+ *
2999
+ * Only the target's definition survives a collapse. The admitted copies'
3000
+ * models are dropped rather than handed to the merge pass, which keeps the
3001
+ * first model of a name and would let a stricter copy declared earlier
3002
+ * narrow the shared type for everyone; their field references are repointed
3003
+ * at the target's models instead.
3004
+ *
3005
+ * Enums declared inline in a companion stay operation-qualified, exactly like
3006
+ * enums declared inline in a parameter.
3007
+ */
3008
+ function resolveEnvelopeCompanionNames(companions, componentModels) {
3009
+ const byBareName = /* @__PURE__ */ new Map();
3010
+ /** Copies folded onto another definition; their own models are not emitted. */
3011
+ const folded = /* @__PURE__ */ new Set();
3012
+ for (const companion of companions) {
3013
+ if (companion.models.length === 0) continue;
3014
+ const bare = companion.bareNames[0];
3015
+ const group = byBareName.get(bare) ?? [];
3016
+ group.push(companion);
3017
+ byBareName.set(bare, group);
3018
+ }
3019
+ const componentMap = new Map((componentModels ?? []).map((m) => [m.name, m]));
3020
+ const localMap = (c) => new Map(c.models.map((m) => [m.name, m]));
3021
+ const unwrap = (t) => t.kind === "nullable" ? t.inner : t;
3022
+ for (const [bare, group] of byBareName) {
3023
+ let folds;
3024
+ let finalNames;
3025
+ let owner;
3026
+ const component = componentMap.get(bare);
3027
+ if (component) {
3028
+ const targetType = {
3029
+ kind: "model",
3030
+ name: component.name
3031
+ };
3032
+ finalNames = [component.name];
3033
+ folds = (c) => {
3034
+ const refs = collectModelRefs(c.field.type);
3035
+ if (refs.length !== 1 || refs[0] !== c.models[0].name) return false;
3036
+ return typeAdmits(targetType, componentMap, unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set());
3037
+ };
3038
+ } else {
3039
+ const counts = /* @__PURE__ */ new Map();
3040
+ for (const c of group) {
3041
+ const key = `${fingerprintType$1(unwrap(c.field.type), localMap(c), /* @__PURE__ */ new Set())}|${c.bareNames.join(",")}`;
3042
+ const seen = counts.get(key);
3043
+ if (seen) seen.n += 1;
3044
+ else counts.set(key, {
3045
+ n: 1,
3046
+ first: c
3047
+ });
1728
3048
  }
3049
+ let best = {
3050
+ n: 0,
3051
+ first: group[0]
3052
+ };
3053
+ for (const entry of counts.values()) if (entry.n > best.n) best = entry;
3054
+ owner = best.first;
3055
+ finalNames = owner.bareNames;
3056
+ const ownerType = unwrap(owner.field.type);
3057
+ const ownerMap = localMap(owner);
3058
+ const ownerNames = owner.bareNames;
3059
+ 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());
1729
3060
  }
1730
- if (fieldSchema.oneOf) {
1731
- const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
1732
- if (objectVariant) {
1733
- const nestedName = qualifyNestedName(name, fieldName);
1734
- if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, objectVariant));
3061
+ const collapsing = group.filter(folds);
3062
+ for (const companion of collapsing) {
3063
+ renameCompanion(companion, finalNames);
3064
+ if (companion !== owner) folded.add(companion);
3065
+ }
3066
+ }
3067
+ return companions.filter((c) => !folded.has(c)).flatMap((c) => c.models);
3068
+ }
3069
+ /**
3070
+ * Rename a companion's models to `names` (index-aligned with `models`) and
3071
+ * repoint every reference, in its field and inside its models.
3072
+ */
3073
+ function renameCompanion(companion, names) {
3074
+ const renames = /* @__PURE__ */ new Map();
3075
+ companion.models.forEach((model, i) => {
3076
+ const name = names[i];
3077
+ if (name !== void 0 && model.name !== name) renames.set(model.name, name);
3078
+ });
3079
+ if (renames.size === 0) return;
3080
+ const repoint = (ref) => {
3081
+ walkTypeRef(ref, { model: (r) => {
3082
+ const renamed = renames.get(r.name);
3083
+ if (renamed !== void 0) r.name = renamed;
3084
+ } });
3085
+ };
3086
+ repoint(companion.field.type);
3087
+ for (const model of companion.models) {
3088
+ model.name = renames.get(model.name) ?? model.name;
3089
+ for (const field of model.fields) repoint(field.type);
3090
+ }
3091
+ }
3092
+ /**
3093
+ * Whether `target` admits every value `copy` can hold: the same field names,
3094
+ * each field's type admitted (see `typeAdmits`), and no field the target
3095
+ * requires that the copy leaves optional. Nested models resolve through their
3096
+ * respective maps and are compared the same way; a model neither map resolves
3097
+ * is compared by name.
3098
+ */
3099
+ function admits(target, targetMap, copy, copyMap, visiting) {
3100
+ const key = `${target.name}|${copy.name}`;
3101
+ if (visiting.has(key)) return true;
3102
+ visiting.add(key);
3103
+ try {
3104
+ if (target.fields.length !== copy.fields.length) return false;
3105
+ const targetFields = new Map(target.fields.map((f) => [f.name, f]));
3106
+ for (const field of copy.fields) {
3107
+ const expected = targetFields.get(field.name);
3108
+ if (!expected) return false;
3109
+ if (expected.required && !field.required) return false;
3110
+ if (!typeAdmits(expected.type, targetMap, field.type, copyMap, visiting)) return false;
3111
+ }
3112
+ return true;
3113
+ } finally {
3114
+ visiting.delete(key);
3115
+ }
3116
+ }
3117
+ /**
3118
+ * Whether `target` admits every value of `copy`'s type. A nullable target
3119
+ * admits a nullable or a non-null copy; a non-null target admits only a
3120
+ * non-null copy. An enum with known values admits a copy whose values are a
3121
+ * subset of its own. Everything else must match exactly, by fingerprint.
3122
+ */
3123
+ function typeAdmits(target, targetMap, copy, copyMap, visiting) {
3124
+ if (target.kind === "nullable") return typeAdmits(target.inner, targetMap, copy.kind === "nullable" ? copy.inner : copy, copyMap, visiting);
3125
+ if (copy.kind === "nullable") return false;
3126
+ if (target.kind !== copy.kind) return false;
3127
+ switch (target.kind) {
3128
+ case "model": {
3129
+ const t = targetMap.get(target.name);
3130
+ const c = copyMap.get(copy.name);
3131
+ if (t && c) return admits(t, targetMap, c, copyMap, visiting);
3132
+ return target.name === copy.name;
3133
+ }
3134
+ case "array": return typeAdmits(target.items, targetMap, copy.items, copyMap, visiting);
3135
+ case "enum": {
3136
+ const c = copy;
3137
+ if (target.values && c.values) {
3138
+ const allowed = new Set(target.values.map((v) => `${typeof v}:${String(v)}`));
3139
+ return c.values.every((v) => allowed.has(`${typeof v}:${String(v)}`));
1735
3140
  }
3141
+ return target.name === c.name;
3142
+ }
3143
+ default: return fingerprintType$1(target, targetMap, /* @__PURE__ */ new Set()) === fingerprintType$1(copy, copyMap, /* @__PURE__ */ new Set());
3144
+ }
3145
+ }
3146
+ /**
3147
+ * Structural fingerprint of a companion model: field names, requiredness, and
3148
+ * each field's shape, with models nested inside the companion compared by
3149
+ * shape rather than by (operation-qualified) name, and inline enums compared
3150
+ * by their values. Models outside the companion are compared by name.
3151
+ */
3152
+ function fingerprintModel(model, local, visiting) {
3153
+ if (visiting.has(model.name)) return `rec:${model.name}`;
3154
+ visiting.add(model.name);
3155
+ const fields = [...model.fields].sort((a, b) => a.name.localeCompare(b.name)).map((f) => `${f.name}${f.required ? "!" : "?"}:${fingerprintType$1(f.type, local, visiting)}`);
3156
+ visiting.delete(model.name);
3157
+ return `{${fields.join(",")}}`;
3158
+ }
3159
+ function fingerprintType$1(ref, local, visiting) {
3160
+ switch (ref.kind) {
3161
+ case "primitive": return `p:${ref.type}${ref.format ? `:${ref.format}` : ""}`;
3162
+ case "literal": return `l:${typeof ref.value}:${String(ref.value)}`;
3163
+ case "nullable": return `n<${fingerprintType$1(ref.inner, local, visiting)}>`;
3164
+ case "array": return `a<${fingerprintType$1(ref.items, local, visiting)}>`;
3165
+ case "map": return `m<${ref.keyType ? `${fingerprintType$1(ref.keyType, local, visiting)},` : ""}${fingerprintType$1(ref.valueType, local, visiting)}>`;
3166
+ case "union": return `u:${ref.compositionKind ?? ""}<${ref.variants.map((v) => fingerprintType$1(v, local, visiting)).sort().join(",")}>`;
3167
+ case "enum": return ref.values ? `e<${[...ref.values].map((v) => `${typeof v}:${String(v)}`).sort().join("|")}>` : `e:${ref.name}`;
3168
+ case "model": {
3169
+ const nested = local.get(ref.name);
3170
+ return nested ? fingerprintModel(nested, local, visiting) : `ref:${ref.name}`;
1736
3171
  }
3172
+ default: return assertNever(ref);
1737
3173
  }
1738
- return [{
1739
- name,
1740
- description: schema.description,
1741
- fields
1742
- }, ...nestedModels];
1743
3174
  }
1744
3175
  //#endregion
1745
3176
  //#region src/parser/operations.ts
@@ -1788,6 +3219,39 @@ function resolveRequestBodyRef(raw, componentRequestBodies, operationContext) {
1788
3219
  }
1789
3220
  return current;
1790
3221
  }
3222
+ /**
3223
+ * Resolve a raw response header to a concrete Header Object, following `$ref`
3224
+ * chains into `components/headers` with a cycle guard.
3225
+ *
3226
+ * Unlike parameters and request bodies, a header that cannot be resolved is
3227
+ * warned about and dropped rather than failing the parse: a response header
3228
+ * is informational, so losing one costs an emitter a typed accessor, not a
3229
+ * wire contract, and should not block generating the rest of the SDK.
3230
+ */
3231
+ function resolveHeaderRef(raw, componentHeaders, headerName, operationContext) {
3232
+ const seenRefs = /* @__PURE__ */ new Set();
3233
+ let current = raw;
3234
+ let description;
3235
+ while ("$ref" in current) {
3236
+ if (description === void 0 && typeof current.description === "string") description = current.description;
3237
+ if (seenRefs.has(current.$ref)) {
3238
+ console.warn(`[oagen] Warning: Circular header $ref chain starting at "${current.$ref}" for response header "${headerName}" in ${operationContext}. Skipping the header.`);
3239
+ return;
3240
+ }
3241
+ seenRefs.add(current.$ref);
3242
+ const refName = /^#\/components\/headers\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
3243
+ const resolved = refName ? componentHeaders?.[refName] : void 0;
3244
+ if (!resolved) {
3245
+ console.warn(`[oagen] Warning: Unresolved header $ref "${current.$ref}" for response header "${headerName}" in ${operationContext}: no matching entry under components/headers. Skipping the header.`);
3246
+ return;
3247
+ }
3248
+ current = resolved;
3249
+ }
3250
+ return description === void 0 ? current : {
3251
+ ...current,
3252
+ description
3253
+ };
3254
+ }
1791
3255
  const HTTP_METHODS = [
1792
3256
  "get",
1793
3257
  "post",
@@ -1798,26 +3262,34 @@ const HTTP_METHODS = [
1798
3262
  "options",
1799
3263
  "trace"
1800
3264
  ];
1801
- function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters, componentRequestBodies) {
3265
+ function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters, componentRequestBodies, componentModels, documentTags, componentExamples, componentHeaders) {
1802
3266
  if (!paths) return {
1803
3267
  services: [],
1804
3268
  inlineModels: []
1805
3269
  };
3270
+ const pathModelBaseNames = resolvePathModelBaseNames(paths, componentModels, componentRequestBodies);
1806
3271
  const serviceMap = /* @__PURE__ */ new Map();
1807
3272
  const inlineModels = [];
3273
+ const envelopeCompanions = [];
3274
+ const tagServiceByOperation = /* @__PURE__ */ new Map();
3275
+ const headerModelRegistry = new Map((componentModels ?? []).map((model) => [model.name, { fingerprint: modelFingerprint(model) }]));
1808
3276
  for (const [path, pathItem] of Object.entries(paths)) {
1809
3277
  const pathLevelParams = pathItem.parameters ?? [];
1810
3278
  for (const method of HTTP_METHODS) {
1811
3279
  const op = pathItem[method];
1812
3280
  if (!op) continue;
1813
- const serviceName = inferServiceName(path, op.tags?.[0]);
1814
- const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies);
3281
+ const tag = op.tags?.[0];
3282
+ const serviceName = inferServiceName(path, tag);
3283
+ 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);
1815
3284
  inlineModels.push(...opModels);
3285
+ if (opCompanions) envelopeCompanions.push(...opCompanions);
3286
+ if (tag) tagServiceByOperation.set(operation, serviceName);
1816
3287
  const ops = serviceMap.get(serviceName) ?? [];
1817
3288
  ops.push(operation);
1818
3289
  serviceMap.set(serviceName, ops);
1819
3290
  }
1820
3291
  }
3292
+ inlineModels.push(...resolveEnvelopeCompanionNames(envelopeCompanions, componentModels));
1821
3293
  for (const [serviceName, operations] of serviceMap) {
1822
3294
  const pathGroups = /* @__PURE__ */ new Map();
1823
3295
  for (const op of operations) {
@@ -1839,14 +3311,73 @@ function extractOperations(paths, operationIdTransform, componentSchemas, compon
1839
3311
  }
1840
3312
  }
1841
3313
  for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
3314
+ const declaredTags = indexDocumentTags(documentTags);
1842
3315
  return {
1843
- services: Array.from(serviceMap.entries()).map(([name, operations]) => ({
1844
- name,
1845
- operations
1846
- })),
3316
+ services: Array.from(serviceMap.entries()).map(([name, operations]) => {
3317
+ const description = resolveServiceDescription(name, operations, tagServiceByOperation, declaredTags);
3318
+ return description === void 0 ? {
3319
+ name,
3320
+ operations
3321
+ } : {
3322
+ name,
3323
+ description,
3324
+ operations
3325
+ };
3326
+ }),
1847
3327
  inlineModels
1848
3328
  };
1849
3329
  }
3330
+ /**
3331
+ * Index the document's top-level `tags[]` by the service name each tag
3332
+ * produces, so a tag spelled `user_management`, `User Management`, or
3333
+ * `user-management` all map to the `UserManagement` service. Every named tag
3334
+ * is indexed — with its description when it declares a non-blank one, and
3335
+ * `undefined` otherwise — because a declared-but-undescribed tag still marks
3336
+ * its service as one that must not borrow another tag's wording. When several
3337
+ * tags normalize to the same service name, the first described one wins.
3338
+ *
3339
+ * `parseSpec` ignored `tags` entirely before descriptions were read, so a
3340
+ * document with a malformed value — `tags` written as an object, a `null`
3341
+ * entry, a numeric `name` — used to parse and must keep parsing. Anything
3342
+ * that cannot be matched is skipped rather than thrown on.
3343
+ */
3344
+ function indexDocumentTags(documentTags) {
3345
+ const index = /* @__PURE__ */ new Map();
3346
+ if (!Array.isArray(documentTags)) return index;
3347
+ for (const tag of documentTags) {
3348
+ if (typeof tag !== "object" || tag === null) continue;
3349
+ const { name, description } = tag;
3350
+ if (typeof name !== "string" || name === "") continue;
3351
+ const serviceName = toPascalCase(name);
3352
+ if (index.get(serviceName) !== void 0) continue;
3353
+ const described = typeof description === "string" && description.trim() !== "";
3354
+ index.set(serviceName, described ? description : void 0);
3355
+ }
3356
+ return index;
3357
+ }
3358
+ /**
3359
+ * Pick a service's description from the document's tags.
3360
+ *
3361
+ * A service whose own name is a declared tag is described by that tag alone:
3362
+ * its description when it has one, nothing otherwise. Another tag's wording is
3363
+ * never borrowed for it — operations tagged `api_keys` that the path-prefix
3364
+ * split merges into `UserManagement` must not label that service "API keys".
3365
+ *
3366
+ * A service whose own name is not declared is one the split carved out of a
3367
+ * tag (e.g. `OrganizationDomains` from operations tagged `organizations`); it
3368
+ * inherits the description of the first described tag its operations carry.
3369
+ * Path-named services whose operations have no tag carry nothing and stay
3370
+ * undescribed.
3371
+ */
3372
+ function resolveServiceDescription(serviceName, operations, tagServiceByOperation, declaredTags) {
3373
+ if (declaredTags.has(serviceName)) return declaredTags.get(serviceName);
3374
+ for (const op of operations) {
3375
+ const tagService = tagServiceByOperation.get(op);
3376
+ if (tagService === void 0) continue;
3377
+ const description = declaredTags.get(tagService);
3378
+ if (description !== void 0) return description;
3379
+ }
3380
+ }
1850
3381
  function inferServiceName(path, tag) {
1851
3382
  if (tag) return toPascalCase(tag);
1852
3383
  const first = path.split("/").filter(Boolean)[0] ?? "Default";
@@ -1865,6 +3396,24 @@ function inferServiceName(path, tag) {
1865
3396
  function normalizeOperationIdForNaming(operationId) {
1866
3397
  return operationId.replace(/\[\d+\]\s*$/, "");
1867
3398
  }
3399
+ /**
3400
+ * The inline request body or response model name of an operation that has an
3401
+ * `operationId`. The single source of that name: extractRequestBody and
3402
+ * deriveResponseName use it to name, resolvePathModelBaseNames to reserve.
3403
+ */
3404
+ function operationIdModelName(operationId, suffix) {
3405
+ return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(operationId)) + suffix);
3406
+ }
3407
+ /**
3408
+ * The stem every model derived from an `operationId` starts with: its
3409
+ * `Request` and `Response` models, a list's `Item`, nested inline models under
3410
+ * those (`…ResponseOwner`) and envelope companions (`…ListMetadata`). Every
3411
+ * step of cleanSchemaName is a global replace or acts on the leading word, so
3412
+ * this is a true prefix of each of those names. Used to reserve them all.
3413
+ */
3414
+ function operationIdModelBase(operationId) {
3415
+ return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(operationId)));
3416
+ }
1868
3417
  function inferOperationName(method, path, operationId, operationIdTransform) {
1869
3418
  if (operationId) {
1870
3419
  const normalized = normalizeOperationIdForNaming(operationId);
@@ -2052,8 +3601,9 @@ function extractBodyParameterGroups(op, requestBody, operationContext, component
2052
3601
  }
2053
3602
  return groups.length > 0 ? groups : void 0;
2054
3603
  }
2055
- function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies) {
2056
- const opLabel = op.operationId ?? `${method.toUpperCase()} ${path}`;
3604
+ function buildOperation(method, path, op, pathLevelParams, pathModelBaseName, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies, componentExamples, componentHeaders, headerModelRegistry = /* @__PURE__ */ new Map()) {
3605
+ const opLabel = operationLabel(op, path, method);
3606
+ const operationName = inferOperationName(method, path, op.operationId, operationIdTransform);
2057
3607
  const requestBodyObject = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
2058
3608
  const paramsByIdentity = /* @__PURE__ */ new Map();
2059
3609
  for (const raw of [...pathLevelParams, ...op.parameters ?? []]) {
@@ -2070,14 +3620,20 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2070
3620
  }
2071
3621
  const wireParams = bodyOwnedNames.size > 0 ? allParams.filter((p) => !(p.in === "query" && bodyOwnedNames.has(p.name))) : allParams;
2072
3622
  const opContext = serviceName;
2073
- const pathParams = extractParams(wireParams, "path", opContext, componentSchemas);
2074
- const queryParams = extractParams(wireParams, "query", opContext, componentSchemas);
2075
- const headerParams = extractParams(wireParams, "header", opContext, componentSchemas).filter((p) => p.name.toLowerCase() !== "idempotency-key");
2076
- const cookieParams = extractParams(wireParams, "cookie", opContext, componentSchemas);
2077
- const bodyOwnedQueryParams = bodyOwnedNames.size > 0 ? extractParams(allParams.filter((p) => p.in === "query" && bodyOwnedNames.has(p.name)), "query", opContext, componentSchemas) : [];
3623
+ const pathParams = extractParams(wireParams, "path", opContext, componentSchemas, componentExamples, opLabel);
3624
+ const queryParams = extractParams(wireParams, "query", opContext, componentSchemas, componentExamples, opLabel);
3625
+ const headerParams = extractParams(wireParams, "header", opContext, componentSchemas, componentExamples, opLabel).filter((p) => p.name.toLowerCase() !== "idempotency-key");
3626
+ const cookieParams = extractParams(wireParams, "cookie", opContext, componentSchemas, componentExamples, opLabel);
3627
+ const bodyOwnedQueryParams = bodyOwnedNames.size > 0 ? extractParams(allParams.filter((p) => p.in === "query" && bodyOwnedNames.has(p.name)), "query", opContext, componentSchemas, componentExamples, opLabel) : [];
2078
3628
  const reqBodyModels = [];
2079
- const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(requestBodyObject, op, reqBodyModels);
2080
- const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method, componentSchemas);
3629
+ const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(requestBodyObject, op, pathModelBaseName, reqBodyModels, componentSchemas);
3630
+ const { response, responseHeaders, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType, envelopeFields, envelopeCompanions } = extractResponses(op.responses, op, pathModelBaseName, path, method, componentSchemas, {
3631
+ componentHeaders,
3632
+ componentSchemas,
3633
+ serviceName: opContext,
3634
+ operationQualifier: toPascalCase(operationName),
3635
+ registry: headerModelRegistry
3636
+ });
2081
3637
  inlineModels.push(...reqBodyModels);
2082
3638
  const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
2083
3639
  let pagination;
@@ -2088,10 +3644,11 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2088
3644
  param: paginationFromParams?.param ?? "after",
2089
3645
  limitParam: paginationFromParams?.limitParam,
2090
3646
  dataPath: responseDataPath ?? paginationFromParams?.dataPath,
2091
- itemType
3647
+ itemType,
3648
+ envelopeFields
2092
3649
  };
2093
3650
  } else if (paginationFromParams) pagination = paginationFromParams;
2094
- const security = extractOperationSecurity(op.security);
3651
+ const security = extractSecurityRequirements(op.security, opLabel);
2095
3652
  const queryParamGroups = extractParameterGroups(op, [
2096
3653
  ...pathParams,
2097
3654
  ...queryParams,
@@ -2100,10 +3657,12 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2100
3657
  ], opLabel);
2101
3658
  const bodyParamGroups = extractBodyParameterGroups(op, requestBodyObject, opLabel, componentSchemas);
2102
3659
  const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
3660
+ const summary = presentText(op.summary);
2103
3661
  return {
2104
3662
  operation: {
2105
- name: inferOperationName(method, path, op.operationId, operationIdTransform),
2106
- description: buildDescription(op.summary, op.description),
3663
+ name: operationName,
3664
+ ...summary !== void 0 ? { summary } : {},
3665
+ description: buildDescription(summary, op.description),
2107
3666
  httpMethod: method,
2108
3667
  path,
2109
3668
  pathParams,
@@ -2114,6 +3673,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2114
3673
  requestBody,
2115
3674
  requestBodyEncoding,
2116
3675
  response,
3676
+ ...responseHeaders && { responseHeaders },
2117
3677
  successResponses,
2118
3678
  errors,
2119
3679
  pagination,
@@ -2123,31 +3683,49 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2123
3683
  security,
2124
3684
  parameterGroups
2125
3685
  },
2126
- inlineModels
3686
+ inlineModels,
3687
+ envelopeCompanions
2127
3688
  };
2128
3689
  }
2129
3690
  /**
2130
- * Extract per-operation security requirements from an OpenAPI security directive.
2131
- * Returns undefined when the operation uses the spec-level default security.
2132
- *
2133
- * OpenAPI security format: `security: [{ schemeName: [scope1, scope2] }]`
3691
+ * Combine an operation's `summary` and `description` into one doc text.
3692
+ * Empty or whitespace-only text counts as absent, so `summary: "List pets"`
3693
+ * with `description: ""` yields `"List pets"` rather than `""`. Text that has
3694
+ * content is kept verbatim, surrounding whitespace included.
2134
3695
  */
2135
- function extractOperationSecurity(security) {
2136
- if (!security || security.length === 0) return void 0;
2137
- const requirements = [];
2138
- for (const entry of security) for (const [schemeName, scopes] of Object.entries(entry)) requirements.push({
2139
- schemeName,
2140
- scopes: scopes ?? []
2141
- });
2142
- return requirements.length > 0 ? requirements : void 0;
2143
- }
2144
3696
  function buildDescription(summary, description) {
2145
- if (summary && description && summary !== description) return `${summary}\n\n${description}`;
2146
- return description ?? summary;
3697
+ const presentSummary = presentText(summary);
3698
+ const presentDescription = presentText(description);
3699
+ if (presentSummary && presentDescription && presentSummary !== presentDescription) return `${presentSummary}\n\n${presentDescription}`;
3700
+ return presentDescription ?? presentSummary;
2147
3701
  }
2148
- function extractParams(params, location, operationContext, componentSchemas) {
3702
+ /** The text itself when it has non-whitespace content, otherwise `undefined`. */
3703
+ function presentText(text) {
3704
+ return typeof text === "string" && text.trim() !== "" ? text : void 0;
3705
+ }
3706
+ function extractParams(params, location, operationContext, componentSchemas, componentExamples, opLabel) {
2149
3707
  return params.filter((p) => p.in === location).map((p) => {
2150
- const refTarget = resolveParamSchemaRef(p.schema, componentSchemas);
3708
+ const refTarget = resolveComponentSchemaRef(p.schema, componentSchemas);
3709
+ const { example, examples } = resolveExamples([
3710
+ {
3711
+ example: p.example,
3712
+ examples: exampleObjectValues(p.examples, componentExamples, `parameter "${p.name}" in ${opLabel ?? location}`)
3713
+ },
3714
+ {
3715
+ example: p.schema?.example,
3716
+ examples: nonEmptyExamples(p.schema?.examples)
3717
+ },
3718
+ {
3719
+ example: refTarget?.example,
3720
+ examples: nonEmptyExamples(refTarget?.examples)
3721
+ }
3722
+ ]);
3723
+ const own = p.schema ? extractConstraints(p.schema) : void 0;
3724
+ const inherited = refTarget ? extractConstraints(refTarget) : void 0;
3725
+ const constraints = own && inherited ? {
3726
+ ...inherited,
3727
+ ...own
3728
+ } : own ?? inherited;
2151
3729
  return {
2152
3730
  name: p.name,
2153
3731
  type: p.schema ? schemaToTypeRef(p.schema, p.name, operationContext ? toPascalCase(operationContext) : void 0) : {
@@ -2155,32 +3733,74 @@ function extractParams(params, location, operationContext, componentSchemas) {
2155
3733
  type: "string"
2156
3734
  },
2157
3735
  required: p.required ?? false,
2158
- description: p.description,
3736
+ description: firstDescription(p.description, p.schema?.description, refTarget?.description),
2159
3737
  deprecated: p.deprecated || p.schema?.deprecated || refTarget?.deprecated || void 0,
2160
3738
  default: p.schema?.default ?? refTarget?.default,
2161
- example: p.example ?? p.schema?.example ?? refTarget?.example,
3739
+ example,
3740
+ ...examples ? { examples } : {},
2162
3741
  style: p.style,
2163
- explode: p.explode
3742
+ explode: p.explode,
3743
+ ...constraints ? { constraints } : {}
2164
3744
  };
2165
3745
  });
2166
3746
  }
2167
3747
  /**
2168
- * If a parameter's schema is a `$ref` to `#/components/schemas/X`, return the
2169
- * referenced component schema. Used to surface schema-level metadata (default,
2170
- * example, deprecated) that wouldn't otherwise be visible at the param level.
2171
- *
2172
- * Refs are not pre-resolved by the bundler (`dereference: false` in refs.ts),
2173
- * so without this, a param like `schema: { $ref: '.../PaginationOrder' }`
2174
- * silently loses the target's `default: desc` and the SDK stops emitting the
2175
- * spec-mandated default value in generated method signatures.
3748
+ * First non-empty description in precedence order. A parameter that declares
3749
+ * `description: ""` is treated as undocumented so its schema's text still
3750
+ * surfaces, and the result is `undefined` rather than `""` when nothing in
3751
+ * the chain is documented.
2176
3752
  */
2177
- function resolveParamSchemaRef(schema, componentSchemas) {
2178
- if (!schema || !componentSchemas) return void 0;
2179
- const ref = schema.$ref;
2180
- if (!ref) return void 0;
2181
- const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
2182
- if (!match) return void 0;
2183
- return componentSchemas[match[1]];
3753
+ function firstDescription(...candidates) {
3754
+ return candidates.find((candidate) => typeof candidate === "string" && candidate.length > 0);
3755
+ }
3756
+ /**
3757
+ * Flatten a parameter's `examples` map of Example Objects to the inline
3758
+ * values it carries, in document order. A `$ref` entry is resolved against
3759
+ * `components.examples` first (see `resolveExampleRef`). Entries that point
3760
+ * elsewhere (`externalValue`) or have no `value` are skipped; `null` is a
3761
+ * legitimate value and kept. Returns undefined when nothing remains so the
3762
+ * caller can fall through to the schema's `examples`.
3763
+ */
3764
+ function exampleObjectValues(examples, componentExamples, context) {
3765
+ if (!examples || typeof examples !== "object" || Array.isArray(examples)) return void 0;
3766
+ const values = [];
3767
+ for (const raw of Object.values(examples)) {
3768
+ const entry = resolveExampleRef(raw, componentExamples, context);
3769
+ if (!entry || typeof entry !== "object") continue;
3770
+ if (entry.externalValue !== void 0 || entry.value === void 0) continue;
3771
+ values.push(entry.value);
3772
+ }
3773
+ return values.length > 0 ? values : void 0;
3774
+ }
3775
+ /**
3776
+ * Follow an Example Object's `$ref` into `components.examples`, including a
3777
+ * component that is itself a `$ref`, with a cycle guard. The bundler keeps
3778
+ * refs (`dereference: false`), so without this every `$ref` entry would be
3779
+ * dropped and a map made only of refs would silently yield nothing — the
3780
+ * parameter would then take its `examples` from the schema instead of the
3781
+ * values the document points at. A ref that can't be resolved is skipped
3782
+ * with a warning rather than failing the parse: a missing example is a doc
3783
+ * gap, not a broken API surface.
3784
+ */
3785
+ function resolveExampleRef(raw, componentExamples, context) {
3786
+ const seenRefs = /* @__PURE__ */ new Set();
3787
+ let current = raw;
3788
+ while (current && typeof current === "object" && current.$ref !== void 0) {
3789
+ const ref = current.$ref;
3790
+ if (seenRefs.has(ref)) {
3791
+ console.warn(`[oagen] Warning: Circular example $ref chain starting at "${ref}" on ${context}; skipping`);
3792
+ return;
3793
+ }
3794
+ seenRefs.add(ref);
3795
+ const refName = /^#\/components\/examples\/(.+)$/.exec(ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
3796
+ const resolved = refName ? componentExamples?.[refName] : void 0;
3797
+ if (!resolved) {
3798
+ console.warn(`[oagen] Warning: Unresolved example $ref "${ref}" on ${context}; skipping`);
3799
+ return;
3800
+ }
3801
+ current = resolved;
3802
+ }
3803
+ return current;
2184
3804
  }
2185
3805
  /**
2186
3806
  * Collect the property schemas of every `oneOf` branch reachable from a request
@@ -2232,7 +3852,7 @@ function resolveSchemaRefObject(schema, componentSchemas) {
2232
3852
  return componentSchemas[match[1]];
2233
3853
  }
2234
3854
  /** HTTP methods whose request body is the primary parameter carrier. */
2235
- const BODY_CARRYING_METHODS = new Set([
3855
+ const BODY_CARRYING_METHODS = /* @__PURE__ */ new Set([
2236
3856
  "post",
2237
3857
  "put",
2238
3858
  "patch"
@@ -2295,7 +3915,7 @@ function collectBodyPropertyNames(schema, componentSchemas, seen = /* @__PURE__
2295
3915
  const ref = schema.$ref;
2296
3916
  if (ref) {
2297
3917
  if (seen.has(ref)) return /* @__PURE__ */ new Set();
2298
- return collectBodyPropertyNames(resolveSchemaRefObject(schema, componentSchemas), componentSchemas, new Set([...seen, ref]));
3918
+ return collectBodyPropertyNames(resolveSchemaRefObject(schema, componentSchemas), componentSchemas, /* @__PURE__ */ new Set([...seen, ref]));
2299
3919
  }
2300
3920
  const names = new Set(Object.keys(schema.properties ?? {}));
2301
3921
  for (const sub of schema.allOf ?? []) for (const name of collectBodyPropertyNames(sub, componentSchemas, seen)) names.add(name);
@@ -2311,7 +3931,7 @@ function collectBodyPropertyNames(schema, componentSchemas, seen = /* @__PURE__
2311
3931
  }
2312
3932
  return names;
2313
3933
  }
2314
- function extractRequestBody(body, op, inlineModels) {
3934
+ function extractRequestBody(body, op, pathModelBaseName, inlineModels, componentSchemas) {
2315
3935
  if (!body?.content) return {};
2316
3936
  const selected = selectRequestBodyContent(body);
2317
3937
  const encoding = selected.encoding;
@@ -2334,24 +3954,26 @@ function extractRequestBody(body, op, inlineModels) {
2334
3954
  };
2335
3955
  return {};
2336
3956
  }
2337
- const contextName = cleanSchemaName(op?.operationId ? toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Request" : "RequestBody");
3957
+ const contextName = op?.operationId ? operationIdModelName(op.operationId, "Request") : pathModelBaseName + "Request";
2338
3958
  if (schema.oneOf && schema.discriminator) {
2339
3959
  const merged = flattenDiscriminatedUnionBody(schema);
2340
3960
  if (merged) schema = merged;
2341
3961
  }
2342
3962
  if (schema.properties && (schema.type === "object" || !schema.type)) {
2343
3963
  const requiredSet = new Set(schema.required ?? []);
3964
+ const siblings = collectProperties(schema);
2344
3965
  const fields = [];
2345
3966
  for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
2346
3967
  if (!fieldSchema) continue;
2347
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet));
3968
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet, componentSchemas, siblings));
2348
3969
  }
2349
3970
  inlineModels.push({
2350
3971
  name: contextName,
2351
3972
  description: void 0,
2352
- fields
3973
+ fields,
3974
+ ...constraintsEntry(schema)
2353
3975
  });
2354
- extractInlineModelsFromProperties(schema, inlineModels, contextName);
3976
+ extractInlineModelsFromProperties(schema, inlineModels, contextName, componentSchemas);
2355
3977
  return {
2356
3978
  body: {
2357
3979
  kind: "model",
@@ -2362,23 +3984,29 @@ function extractRequestBody(body, op, inlineModels) {
2362
3984
  }
2363
3985
  if (schema.oneOf) {
2364
3986
  const variants = [];
3987
+ const inlineObjectVariants = schema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
3988
+ const contentNamed = [];
2365
3989
  for (const variant of schema.oneOf) if (variant.$ref) {
2366
3990
  const ref = schemaToTypeRef(variant, contextName);
2367
3991
  variants.push(ref);
2368
3992
  } else if (variant.type === "null") {} else if (variant.properties && (variant.type === "object" || !variant.type)) {
2369
- const variantName = deriveOneOfVariantName(variant, contextName, inlineModels);
3993
+ const named = deriveOneOfVariantName(variant, contextName, inlineModels, inlineObjectVariants, contentNamed, Boolean(op?.operationId));
3994
+ const variantName = named.name;
3995
+ if (named.fromContent) contentNamed.push(variantName);
2370
3996
  const requiredSet = new Set(variant.required ?? []);
3997
+ const siblings = collectProperties(variant);
2371
3998
  const fields = [];
2372
3999
  for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
2373
4000
  if (!fieldSchema) continue;
2374
- fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet));
4001
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet, componentSchemas, siblings));
2375
4002
  }
2376
4003
  inlineModels.push({
2377
4004
  name: variantName,
2378
4005
  description: variant.description,
2379
- fields
4006
+ fields,
4007
+ ...constraintsEntry(variant)
2380
4008
  });
2381
- extractInlineModelsFromProperties(variant, inlineModels, variantName);
4009
+ extractInlineModelsFromProperties(variant, inlineModels, variantName, componentSchemas);
2382
4010
  variants.push({
2383
4011
  kind: "model",
2384
4012
  name: variantName
@@ -2464,38 +4092,452 @@ function flattenDiscriminatedUnionBody(schema) {
2464
4092
  type: "object",
2465
4093
  properties,
2466
4094
  required: order.filter((name) => name === discProp || requiredCounts[name] === variantCount),
2467
- ...schema.description ? { description: schema.description } : {}
4095
+ ...schema.description ? { description: schema.description } : {},
4096
+ ...extractConstraints(schema)
2468
4097
  };
2469
4098
  }
2470
- /** Derive a unique variant name for a oneOf inline model. */
2471
- function deriveOneOfVariantName(variant, baseName, existingModels) {
4099
+ /**
4100
+ * Derive a unique variant name for a request-body `oneOf` inline model.
4101
+ *
4102
+ * A variant with a string-const property is named after it
4103
+ * (`RadarEmailChallengeCodeSessionAuthenticateRequest`). Any other variant is
4104
+ * named from its content by the same rule every inline union variant follows
4105
+ * (`nameInlineVariant`): the first const-less variant keeps the body's name
4106
+ * (or its title, when every inline variant of the union is titled), and each
4107
+ * later one takes its title, a distinguishing property
4108
+ * (`CreateThingRequestWithB`) or, when its property set repeats an earlier
4109
+ * variant's, the position counter (`CreateThingRequest2`). `siblings` are the
4110
+ * union's inline object variants and `contentNamed` the names its const-less
4111
+ * variants have taken so far; `fromContent` tells the caller to record this
4112
+ * one there. A clash with a model minted elsewhere still bumps the counter:
4113
+ * on the base for a content-named variant (`CreateThingRequest2`), on the
4114
+ * const-derived name for a const-named one (`FooCreateThingRequest2`) — so a
4115
+ * variant titled `Foo` and a later one pinning `kind: foo` in the same body
4116
+ * never share a model.
4117
+ *
4118
+ * `fromOperationId` says where `baseName` came from. An operationId-derived
4119
+ * base is already in cleanSchemaName form and its variants go through
4120
+ * resolveSchemaName, so they follow the same project-level naming policy as
4121
+ * component schemas. A path-derived base is used verbatim: it is deliberately
4122
+ * not cleaned (see derivePathModelBaseName) and resolvePathModelBaseNames
4123
+ * reserves names by that exact prefix, so cleaning a variant here would let it
4124
+ * slip past the reservation — `NotesPostRequest` became `NotePostRequest` and
4125
+ * collided with a component of that name.
4126
+ */
4127
+ function deriveOneOfVariantName(variant, baseName, existingModels, siblings, contentNamed, fromOperationId) {
4128
+ const finalize = (name) => fromOperationId ? resolveSchemaName(name) : name;
4129
+ const existingNames = new Set(existingModels.map((m) => m.name));
4130
+ const unique = (name, stem) => {
4131
+ let candidate = name;
4132
+ for (let suffix = 2; existingNames.has(candidate); suffix++) candidate = `${stem}${suffix}`;
4133
+ return candidate;
4134
+ };
2472
4135
  if (variant.properties) {
2473
- for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string") return resolveSchemaName(toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request");
4136
+ for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string") {
4137
+ const derivedName = finalize(toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request");
4138
+ return {
4139
+ name: unique(derivedName, derivedName),
4140
+ fromContent: false
4141
+ };
4142
+ }
2474
4143
  }
2475
- const existingNames = new Set(existingModels.map((m) => m.name));
2476
- const transformedBaseName = resolveSchemaName(baseName);
2477
- let name = transformedBaseName;
2478
- let suffix = 2;
2479
- while (existingNames.has(name)) {
2480
- name = `${transformedBaseName}${suffix}`;
2481
- suffix++;
4144
+ const finalBaseName = finalize(baseName);
4145
+ return {
4146
+ name: unique(nameInlineVariant(variant, siblings, finalBaseName, contentNamed, null), finalBaseName),
4147
+ fromContent: true
4148
+ };
4149
+ }
4150
+ /** Human-readable label for an operation in diagnostics: its operationId, else `METHOD /path`. */
4151
+ function operationLabel(op, path, method) {
4152
+ return op?.operationId ?? `${method.toUpperCase()} ${path}`;
4153
+ }
4154
+ /** Shape of a model, independent of field order: name, requiredness, and type of every field. */
4155
+ function modelFingerprint(model) {
4156
+ return model.fields.map((f) => `${f.name}${f.required ? "!" : "?"}:${stableStringify(f.type)}`).sort().join("|");
4157
+ }
4158
+ /** `JSON.stringify` with object keys sorted, so equal shapes serialize equally. */
4159
+ function stableStringify(value) {
4160
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
4161
+ if (value !== null && typeof value === "object") {
4162
+ const record = value;
4163
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
2482
4164
  }
2483
- return name;
4165
+ return JSON.stringify(value) ?? "undefined";
4166
+ }
4167
+ /**
4168
+ * Type a header's schema and materialize the inline models it names, under a
4169
+ * property name that does not collide with a different shape anywhere in the
4170
+ * spec.
4171
+ *
4172
+ * Header models are named from the service and header name alone, so an
4173
+ * inline `X-Meta` on a `Documents` operation is `DocumentsXMeta` — the same
4174
+ * name a component schema, or the same header on a 429 or on another
4175
+ * `Documents` operation, may already own with a different shape. Left alone,
4176
+ * the later merge keeps one definition for the name and every other
4177
+ * reference silently gets the wrong fields (a request body using the
4178
+ * component would even change type without any emitter reading headers).
4179
+ *
4180
+ * The first owner of a name keeps it: component models are seeded before any
4181
+ * operation, operations are visited in path order, and a single operation's
4182
+ * responses in ascending status-code order (integer-like keys iterate that
4183
+ * way), so the lowest code wins regardless of where it appears in the
4184
+ * document. A later header with a different shape — at any depth, a nested
4185
+ * object that differs counts — is re-derived under the first free candidate:
4186
+ *
4187
+ * 1. the bare header name `DocumentsXMeta`
4188
+ * 2. when the name is taken inside this operation, the status code
4189
+ * (`DocumentsXMeta429`); when taken by a component or another
4190
+ * operation, the operation (`DocumentsCreateDocumentXMeta`)
4191
+ * 3. the other of those two
4192
+ * 4. operation and status code `DocumentsCreateDocumentXMeta429`
4193
+ * 5. a numeric suffix `DocumentsXMeta2`, `DocumentsXMeta3`, ...
4194
+ *
4195
+ * Both the type and the models come from the same property name, so nested
4196
+ * models (`DocumentsXMeta429Origin`) line up by construction. A shape
4197
+ * identical to the name's current owner shares it and materializes nothing
4198
+ * new, which is also how a header reuses a matching component model.
4199
+ *
4200
+ * Treating the header as a one-property object reuses the field-level
4201
+ * extractor, whose names come from the same `qualifyInlineModelName(name,
4202
+ * parent)` rule `schemaToTypeRef` applies — the trick the inline request-body
4203
+ * path uses.
4204
+ */
4205
+ function typeHeaderSchema(schema, name, statusCode, ctx, models) {
4206
+ const parentName = ctx.serviceName ? toPascalCase(ctx.serviceName) : void 0;
4207
+ const byStatus = `${name}${statusCode}`;
4208
+ const byOperation = `${ctx.operationQualifier}${name}`;
4209
+ const byBoth = `${ctx.operationQualifier}${name}${statusCode}`;
4210
+ let candidates = [
4211
+ name,
4212
+ byStatus,
4213
+ byOperation,
4214
+ byBoth
4215
+ ];
4216
+ for (let attempt = 0;; attempt++) {
4217
+ const propertyName = attempt < candidates.length ? candidates[attempt] : `${name}${attempt - candidates.length + 2}`;
4218
+ const trial = [];
4219
+ extractInlineModelsFromProperties({ properties: { [propertyName]: schema } }, trial, parentName, ctx.componentSchemas);
4220
+ const fingerprints = trial.map((model) => [model, modelFingerprint(model)]);
4221
+ const collisions = fingerprints.flatMap(([model, fingerprint]) => {
4222
+ const registered = ctx.registry.get(model.name);
4223
+ return registered !== void 0 && registered.fingerprint !== fingerprint ? [registered] : [];
4224
+ });
4225
+ if (collisions.length > 0) {
4226
+ if (attempt === 0 && !collisions.every((c) => c.operation === ctx.operationQualifier)) candidates = [
4227
+ name,
4228
+ byOperation,
4229
+ byStatus,
4230
+ byBoth
4231
+ ];
4232
+ continue;
4233
+ }
4234
+ for (const [model, fingerprint] of fingerprints) {
4235
+ if (ctx.registry.has(model.name)) continue;
4236
+ ctx.registry.set(model.name, {
4237
+ fingerprint,
4238
+ operation: ctx.operationQualifier
4239
+ });
4240
+ models.push(model);
4241
+ }
4242
+ return schemaToTypeRef(schema, propertyName, parentName);
4243
+ }
4244
+ }
4245
+ /**
4246
+ * Read a Response Object's `headers` map into IR response headers, resolving
4247
+ * `components/headers` refs. Returns `undefined` rather than `[]` when nothing
4248
+ * survives so callers can leave the `headers` key off entirely.
4249
+ *
4250
+ * An object-shaped header schema (directly, inside an array, or as a oneOf
4251
+ * variant) becomes a `ModelRef`, so the model it names is appended to
4252
+ * `models` — otherwise the ref would dangle with nothing to emit.
4253
+ */
4254
+ function extractResponseHeaders(headers, operationLabel, statusCode, ctx, models) {
4255
+ if (!headers) return void 0;
4256
+ const result = [];
4257
+ for (const [name, raw] of Object.entries(headers)) {
4258
+ if (name.toLowerCase() === "content-type") continue;
4259
+ const header = resolveHeaderRef(raw, ctx.componentHeaders, name, operationLabel);
4260
+ if (!header) continue;
4261
+ const contentSchema = header.content ? Object.values(header.content)[0]?.schema : void 0;
4262
+ const schema = header.schema ?? contentSchema;
4263
+ const type = schema ? typeHeaderSchema(schema, name, statusCode, ctx, models) : {
4264
+ kind: "primitive",
4265
+ type: "string"
4266
+ };
4267
+ result.push({
4268
+ name,
4269
+ type,
4270
+ required: header.required ?? false,
4271
+ description: header.description,
4272
+ deprecated: header.deprecated || void 0
4273
+ });
4274
+ }
4275
+ return result.length > 0 ? result : void 0;
4276
+ }
4277
+ /**
4278
+ * Build the prefix (`NotesByIdPinPost`) that names both inline models — the
4279
+ * request body (`…Request`) and the response (`…Response`) — of an operation
4280
+ * that has no `operationId`, from the only thing that identifies it: its path
4281
+ * and method.
4282
+ *
4283
+ * Every non-parameter segment is PascalCased in order and every parameter
4284
+ * segment becomes `By` + PascalCase(param name); then the optional
4285
+ * disambiguator and the method are appended. The whole is normalized with
4286
+ * toPascalCase so it is a fixed point of that function: responses.ts re-cases
4287
+ * the response name in `deriveModelName`, and without this step `POST /a/b`
4288
+ * named its body `ABPostRequest` but its response `AbPostResponse`.
4289
+ *
4290
+ * GET /widgets → WidgetsGet
4291
+ * POST /notes → NotesPost
4292
+ * POST /notes/{id}/pin → NotesByIdPinPost
4293
+ * GET /users/{user_id}/sessions → UsersByUserIdSessionsGet
4294
+ *
4295
+ * This tells the common path shapes apart (list vs. item, sibling and nested
4296
+ * sub-resources) but is not unique by construction: `/foo-bar` and `/foo/bar`,
4297
+ * `/notes/by-id` and `/notes/{id}`, `/notes` and `/notes/` all flatten to the
4298
+ * same words. `resolvePathModelBaseNames` detects such collisions: the path
4299
+ * spelled most plainly keeps the plain name and every other one gets a
4300
+ * `disambiguator` derived from its raw path.
4301
+ *
4302
+ * A single-segment path keeps the name the old first-segment rule produced.
4303
+ * The result is deliberately not passed through cleanSchemaName (the old
4304
+ * fallback wasn't either): the segments are the resource names as the API
4305
+ * spells them.
4306
+ */
4307
+ function derivePathModelBaseName(path, method, disambiguator = "") {
4308
+ const parts = path.split("/").filter(Boolean).map((segment) => segment.startsWith("{") ? "By" + toPascalCase(segment.replace(/[{}]/g, "")) : toPascalCase(segment));
4309
+ const resource = parts.length > 0 ? parts.join("") : "Unknown";
4310
+ return toPascalCase([
4311
+ resource,
4312
+ disambiguator,
4313
+ method
4314
+ ].filter(Boolean).join("_"));
4315
+ }
4316
+ /**
4317
+ * Decide the path-derived model prefix of every operation that has no
4318
+ * `operationId`, keyed by `${method} ${path}`.
4319
+ *
4320
+ * A prefix collides when two distinct raw paths with the same method flatten
4321
+ * to it (see derivePathModelBaseName), or when a reserved name starts with
4322
+ * it. The reserved names are every resolved component model name and, for
4323
+ * every operation with an `operationId`, its model stem
4324
+ * (operationIdModelBase). A prefix is the stem of several emitted names —
4325
+ * `…Request`, `…Response`, a list's `…Item`, nested inline models such as
4326
+ * `…ResponseOwner`, envelope companions such as `…ListMetadata` — so the
4327
+ * check is on the stem, not on an enumeration of suffixes: an unnamed
4328
+ * `GET /audit/events` must not overwrite `components/schemas/AuditEventsGetItem`
4329
+ * any more than an unnamed `POST /audit/events` may overwrite the models of
4330
+ * a `POST /legacy-audit` whose operationId is `auditEventsPost`. Reserved
4331
+ * names never change.
4332
+ *
4333
+ * This is deliberately a superset: a component named `AuditEventsGetter`, or
4334
+ * an operationId `auditEventsGetSummary`, also pushes `GET /audit/events`
4335
+ * onto a hashed name although no emitted name would coincide. Determinism
4336
+ * and never sharing a name wrongly matter more than the rare extra hash, and
4337
+ * the reservation is unconditional — an inline body is never merged into a
4338
+ * same-named component even when the shapes agree — so the rule stays
4339
+ * simple and order-free. Every name derived from a path-based prefix uses it
4340
+ * verbatim (body `oneOf` variants included; see deriveOneOfVariantName), and
4341
+ * as a second line of defence the check also runs on the cleaned spellings
4342
+ * of the prefix (see prefixSpellings), so `GET /notes` yields to a component
4343
+ * `NoteGetItem` although its own item model is `NotesGetItem`. Names that
4344
+ * embed the prefix behind a const value — a body `oneOf` variant pinning
4345
+ * `kind: email` is `EmailTagsPostRequest`, a nested variant
4346
+ * `<Const><Parent><Field>` — are checked too, with each const the operation
4347
+ * can mint prepended to every spelling (see collectOperationConstPrefixes);
4348
+ * the hash then lands inside those names as well (`EmailTags151Dd5PostRequest`).
4349
+ *
4350
+ * These names are types SDK users read and import, so a collision costs the
4351
+ * plain name only where it must. Within a group of paths that flatten to one
4352
+ * prefix, the path spelled most plainly (see pathDeviation: no trailing
4353
+ * slash, `{id}` rather than a literal `by-id`) is canonical and keeps the
4354
+ * plain prefix; every other path gets a six-character disambiguator, the
4355
+ * start of the SHA-1 of its raw path, inserted before the method
4356
+ * (`POST /foo-bar` next to `POST /foo/bar` → `FooBar546F12Post`). When two
4357
+ * paths tie for plainest, or the prefix is reserved, nobody is canonical and
4358
+ * every path in the group is disambiguated. The outcome depends only on the
4359
+ * document's paths, operationIds and component names, never on their order,
4360
+ * and a path that stops colliding returns to its plain name.
4361
+ */
4362
+ function resolvePathModelBaseNames(paths, componentModels, componentRequestBodies) {
4363
+ const candidates = [];
4364
+ const rawPathsByPlainName = /* @__PURE__ */ new Map();
4365
+ const constPrefixesByPlainName = /* @__PURE__ */ new Map();
4366
+ const reservedNames = /* @__PURE__ */ new Set();
4367
+ for (const model of componentModels ?? []) reservedNames.add(model.name);
4368
+ for (const [path, pathItem] of Object.entries(paths)) for (const method of HTTP_METHODS) {
4369
+ const op = pathItem[method];
4370
+ if (!op) continue;
4371
+ if (op.operationId) {
4372
+ reservedNames.add(operationIdModelBase(op.operationId));
4373
+ continue;
4374
+ }
4375
+ const plain = derivePathModelBaseName(path, method);
4376
+ candidates.push({
4377
+ key: `${method} ${path}`,
4378
+ path,
4379
+ method,
4380
+ plain
4381
+ });
4382
+ const rawPaths = rawPathsByPlainName.get(plain) ?? /* @__PURE__ */ new Set();
4383
+ rawPaths.add(path);
4384
+ rawPathsByPlainName.set(plain, rawPaths);
4385
+ const constPrefixes = constPrefixesByPlainName.get(plain) ?? /* @__PURE__ */ new Set();
4386
+ for (const prefix of collectOperationConstPrefixes(op, componentRequestBodies, operationLabel(op, path, method))) constPrefixes.add(prefix);
4387
+ constPrefixesByPlainName.set(plain, constPrefixes);
4388
+ }
4389
+ const reservedList = [...reservedNames];
4390
+ const canonicalByPlainName = /* @__PURE__ */ new Map();
4391
+ for (const [plain, rawPaths] of rawPathsByPlainName) {
4392
+ const spellings = prefixSpellings(plain);
4393
+ const reserved = ["", ...constPrefixesByPlainName.get(plain) ?? []].flatMap((constPrefix) => spellings.map((spelling) => constPrefix + spelling)).some((stem) => reservedList.some((name) => name.startsWith(stem)));
4394
+ canonicalByPlainName.set(plain, reserved ? void 0 : canonicalPath(rawPaths));
4395
+ }
4396
+ const resolved = /* @__PURE__ */ new Map();
4397
+ for (const { key, path, method, plain } of candidates) {
4398
+ const keepsPlainName = canonicalByPlainName.get(plain) === path;
4399
+ resolved.set(key, keepsPlainName ? plain : derivePathModelBaseName(path, method, pathDisambiguator(path)));
4400
+ }
4401
+ return resolved;
4402
+ }
4403
+ /**
4404
+ * The one path in a group that is spelled most plainly by pathDeviation, or
4405
+ * undefined when two or more tie for it, so the choice never depends on the
4406
+ * order the paths were written in.
4407
+ */
4408
+ function canonicalPath(rawPaths) {
4409
+ let best;
4410
+ let bestScore;
4411
+ let tied = false;
4412
+ for (const path of rawPaths) {
4413
+ const score = pathDeviation(path);
4414
+ const cmp = bestScore ? compareDeviation(score, bestScore) : -1;
4415
+ if (cmp < 0) {
4416
+ best = path;
4417
+ bestScore = score;
4418
+ tied = false;
4419
+ } else if (cmp === 0) tied = true;
4420
+ }
4421
+ return tied ? void 0 : best;
4422
+ }
4423
+ /**
4424
+ * How far a raw path's spelling is from the plain prefix it flattens to:
4425
+ * `[trailing slash, literal segments with any character outside a-z0-9]`,
4426
+ * compared lexicographically. `/notes` scores [0, 0] and `/notes/` [1, 0];
4427
+ * `/notes/{id}` scores [0, 0] and `/notes/by-id` [0, 1].
4428
+ */
4429
+ function pathDeviation(rawPath) {
4430
+ return [rawPath.length > 1 && rawPath.endsWith("/") ? 1 : 0, rawPath.split("/").filter(Boolean).filter((segment) => !segment.startsWith("{") && /[^a-z0-9]/.test(segment)).length];
4431
+ }
4432
+ function compareDeviation(a, b) {
4433
+ return a[0] - b[0] || a[1] - b[1];
4434
+ }
4435
+ /** The first six hex characters of the SHA-1 of a raw path. */
4436
+ function pathDisambiguator(rawPath) {
4437
+ return createHash("sha1").update(rawPath).digest("hex").slice(0, 6);
4438
+ }
4439
+ /**
4440
+ * The spellings a path-based prefix could take if a name derived from it were
4441
+ * run through one of the parser's cleaning steps: the prefix itself (every
4442
+ * path-derived name uses it verbatim), cleanSchemaName's form (lead-word
4443
+ * singularization plus backend and ListItem-marker stripping),
4444
+ * resolveSchemaName's form (the same plus the project's schemaNameTransform),
4445
+ * and stripListItemMarkers alone, which nested-model and inline-enum
4446
+ * qualification apply to a parent name. Reserving by every spelling is the
4447
+ * second line of defence behind "use the base verbatim": a cleaning step added
4448
+ * later cannot let a derived name slip past the reservation. Empty spellings
4449
+ * are dropped so a degenerate prefix cannot match every reserved name.
4450
+ */
4451
+ function prefixSpellings(prefix) {
4452
+ const spellings = [
4453
+ prefix,
4454
+ cleanSchemaName(prefix),
4455
+ resolveSchemaName(prefix),
4456
+ stripListItemMarkers(prefix)
4457
+ ];
4458
+ return [...new Set(spellings)].filter((spelling) => spelling.length > 0);
4459
+ }
4460
+ /**
4461
+ * The PascalCase const values an operationId-less operation can prepend to a
4462
+ * name minted from its prefix, read from the schemas its request body and 2xx
4463
+ * responses declare inline. A body `oneOf` variant pinning `kind: email`
4464
+ * yields `EmailTagsPostRequest` (deriveOneOfVariantName); a nested
4465
+ * `oneOf`/`anyOf` object variant yields `<Const><Parent><Field>`
4466
+ * (nameOneOfVariant, nameVariantModel). Every string const or single-value
4467
+ * enum of every inline object variant is collected, top level and nested — a
4468
+ * superset of whichever property each of those picks as the discriminator, so
4469
+ * the reservation can never be narrower than the naming. `$ref` targets are
4470
+ * components with names of their own and are not followed. A request body
4471
+ * `$ref` that cannot be resolved is left for buildOperation to report.
4472
+ *
4473
+ * Names that do not embed the prefix at all — a response `oneOf` variant
4474
+ * named from its bare const (`Email`, see deriveModelName in responses.ts)
4475
+ * or a `title`-mapped variant — are outside what a disambiguator can
4476
+ * separate and are not reserved here.
4477
+ */
4478
+ function collectOperationConstPrefixes(op, componentRequestBodies, opLabel) {
4479
+ const prefixes = /* @__PURE__ */ new Set();
4480
+ let body;
4481
+ try {
4482
+ body = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
4483
+ } catch {
4484
+ body = void 0;
4485
+ }
4486
+ collectConstPrefixes(selectRequestBodyContent(body).schema, prefixes, 0);
4487
+ for (const [statusCode, response] of Object.entries(op.responses ?? {})) {
4488
+ const code = parseInt(statusCode, 10);
4489
+ if (code < 200 || code >= 300) continue;
4490
+ for (const media of Object.values(response.content ?? {})) collectConstPrefixes(media?.schema, prefixes, 0);
4491
+ }
4492
+ return prefixes;
4493
+ }
4494
+ function collectConstPrefixes(schema, out, depth) {
4495
+ if (!schema || schema.$ref || depth > 16) return;
4496
+ for (const variants of [schema.oneOf, schema.anyOf]) for (const variant of variants ?? []) {
4497
+ if (variant.$ref || !variant.properties) continue;
4498
+ for (const property of Object.values(variant.properties)) {
4499
+ const value = constStringOf(property);
4500
+ const prefix = value === null ? "" : toPascalCase(value);
4501
+ if (prefix) out.add(prefix);
4502
+ }
4503
+ }
4504
+ const children = [
4505
+ ...schema.oneOf ?? [],
4506
+ ...schema.anyOf ?? [],
4507
+ ...schema.allOf ?? [],
4508
+ schema.items,
4509
+ ...Object.values(schema.properties ?? {}),
4510
+ typeof schema.additionalProperties === "object" ? schema.additionalProperties : void 0
4511
+ ];
4512
+ for (const child of children) collectConstPrefixes(child, out, depth + 1);
2484
4513
  }
2485
- function deriveResponseName(op, path, method) {
2486
- if (op?.operationId) return cleanSchemaName(toPascalCase(normalizeOperationIdForNaming(op.operationId)) + "Response");
2487
- return toPascalCase(path.split("/").filter(Boolean)[0] ?? "Unknown") + toPascalCase(method) + "Response";
4514
+ /** A property's pinned string value: a string `const`, or a single-value string `enum`. */
4515
+ function constStringOf(property) {
4516
+ if (!property) return null;
4517
+ if (typeof property.const === "string") return property.const;
4518
+ if (Array.isArray(property.enum) && property.enum.length === 1 && typeof property.enum[0] === "string") return property.enum[0];
4519
+ return null;
4520
+ }
4521
+ function deriveResponseName(op, pathModelBaseName) {
4522
+ if (op?.operationId) return operationIdModelName(op.operationId, "Response");
4523
+ return pathModelBaseName + "Response";
2488
4524
  }
2489
- function extractResponses(responses, op, path, method, componentSchemas) {
4525
+ function extractResponses(responses, op, pathModelBaseName, path, method, componentSchemas, headerContext = {
4526
+ operationQualifier: "",
4527
+ registry: /* @__PURE__ */ new Map()
4528
+ }) {
2490
4529
  const errors = [];
2491
4530
  let response = {
2492
4531
  kind: "primitive",
2493
4532
  type: "string"
2494
4533
  };
4534
+ let responseHeaders;
2495
4535
  let inlineModels = [];
2496
4536
  let isPaginated = false;
2497
4537
  let dataPath;
2498
4538
  let itemType;
4539
+ let envelopeFields;
4540
+ let envelopeCompanions;
2499
4541
  if (!responses) return {
2500
4542
  response,
2501
4543
  errors,
@@ -2503,8 +4545,11 @@ function extractResponses(responses, op, path, method, componentSchemas) {
2503
4545
  isPaginated
2504
4546
  };
2505
4547
  const allSuccessResponses = [];
4548
+ const opLabel = operationLabel(op, path ?? "/", method ?? "get");
4549
+ const headerModels = [];
2506
4550
  for (const [statusCode, resp] of Object.entries(responses)) {
2507
4551
  const code = parseInt(statusCode, 10);
4552
+ const headers = code >= 200 ? extractResponseHeaders(resp.headers, opLabel, code, headerContext, headerModels) : void 0;
2508
4553
  if (code >= 200 && code < 300) {
2509
4554
  let extractedType = {
2510
4555
  kind: "primitive",
@@ -2512,62 +4557,75 @@ function extractResponses(responses, op, path, method, componentSchemas) {
2512
4557
  };
2513
4558
  const jsonContent = resp.content?.["application/json"];
2514
4559
  if (jsonContent?.schema) {
2515
- const contextName = deriveResponseName(op, path ?? "/", method ?? "get");
4560
+ const contextName = deriveResponseName(op, pathModelBaseName);
2516
4561
  const result = classifyAndExtractResponse(jsonContent.schema, contextName, componentSchemas);
2517
4562
  extractedType = result.response;
2518
4563
  inlineModels = result.inlineModels;
2519
4564
  isPaginated = result.isPaginated;
2520
4565
  dataPath = result.dataPath;
2521
4566
  itemType = result.itemType;
2522
- } else if (resp.content) if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) extractedType = {
2523
- kind: "primitive",
2524
- type: "string",
2525
- format: "binary"
2526
- };
2527
- else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) extractedType = {
2528
- kind: "primitive",
2529
- type: "string"
2530
- };
2531
- else {
2532
- const firstKey = Object.keys(resp.content)[0];
2533
- if (firstKey) {
2534
- const firstContent = resp.content[firstKey];
2535
- if (firstContent?.schema) extractedType = schemaToTypeRef(firstContent.schema, deriveResponseName(op, path ?? "/", method ?? "get"));
4567
+ envelopeFields = result.envelopeFields;
4568
+ envelopeCompanions = result.envelopeCompanions;
4569
+ } else if (resp.content) {
4570
+ if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) extractedType = {
4571
+ kind: "primitive",
4572
+ type: "string",
4573
+ format: "binary"
4574
+ };
4575
+ else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) extractedType = {
4576
+ kind: "primitive",
4577
+ type: "string"
4578
+ };
4579
+ else {
4580
+ const firstKey = Object.keys(resp.content)[0];
4581
+ if (firstKey) {
4582
+ const firstContent = resp.content[firstKey];
4583
+ if (firstContent?.schema) extractedType = schemaToTypeRef(firstContent.schema, deriveResponseName(op, pathModelBaseName));
4584
+ }
2536
4585
  }
2537
4586
  }
2538
4587
  allSuccessResponses.push({
2539
4588
  statusCode: code,
2540
- type: extractedType
4589
+ type: extractedType,
4590
+ ...headers && { headers }
2541
4591
  });
2542
4592
  } else if (code >= 300 && code < 400) allSuccessResponses.push({
2543
4593
  statusCode: code,
2544
4594
  type: {
2545
4595
  kind: "primitive",
2546
4596
  type: "unknown"
2547
- }
4597
+ },
4598
+ ...headers && { headers }
2548
4599
  });
2549
4600
  else if (code >= 400) {
2550
4601
  const jsonContent = resp.content?.["application/json"];
2551
4602
  const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
2552
4603
  errors.push({
2553
4604
  statusCode: code,
2554
- type
4605
+ type,
4606
+ ...headers && { headers }
2555
4607
  });
2556
4608
  }
2557
4609
  }
2558
4610
  if (allSuccessResponses.length > 0) {
2559
4611
  const sorted = [...allSuccessResponses].sort((a, b) => a.statusCode - b.statusCode);
2560
- response = (sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0]).type;
4612
+ const primary = sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0];
4613
+ response = primary.type;
4614
+ responseHeaders = primary.headers;
2561
4615
  }
2562
4616
  const successResponses = allSuccessResponses.length > 1 ? allSuccessResponses : void 0;
4617
+ if (headerModels.length > 0) inlineModels = [...inlineModels, ...headerModels];
2563
4618
  return {
2564
4619
  response,
4620
+ responseHeaders,
2565
4621
  successResponses,
2566
4622
  errors,
2567
4623
  inlineModels,
2568
4624
  isPaginated,
2569
4625
  dataPath,
2570
- itemType
4626
+ itemType,
4627
+ envelopeFields,
4628
+ envelopeCompanions
2571
4629
  };
2572
4630
  }
2573
4631
  //#endregion
@@ -2576,10 +4634,25 @@ function keepLargestByName(models) {
2576
4634
  const byName = /* @__PURE__ */ new Map();
2577
4635
  for (const model of models) {
2578
4636
  const existing = byName.get(model.name);
4637
+ if (existing && !haveSameFields(existing, model)) {
4638
+ const kept = model.fields.length === existing.fields.length ? "keeping the first definition" : "keeping the one with more fields";
4639
+ 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}`);
4640
+ }
2579
4641
  if (!existing || model.fields.length > existing.fields.length) byName.set(model.name, model);
2580
4642
  }
2581
4643
  return [...byName.values()];
2582
4644
  }
4645
+ /**
4646
+ * Same field names with the same types, in any order. Types are compared by
4647
+ * their JSON serialization: the parser builds a given shape's TypeRef with the
4648
+ * same key order every time, so equal shapes serialize identically.
4649
+ */
4650
+ function haveSameFields(a, b) {
4651
+ const signatures = (m) => new Set(m.fields.map((f) => `${f.name}:${JSON.stringify(f.type)}`));
4652
+ const aSignatures = signatures(a);
4653
+ const bSignatures = signatures(b);
4654
+ return aSignatures.size === bSignatures.size && [...aSignatures].every((s) => bSignatures.has(s));
4655
+ }
2583
4656
  function rewriteModelRef(ref, oldName, newName) {
2584
4657
  walkTypeRef(ref, { model: (r) => {
2585
4658
  if (r.name === oldName) r.name = newName;
@@ -2590,6 +4663,8 @@ function rewriteModelRefs(models, services, oldName, newName) {
2590
4663
  for (const service of services) for (const op of service.operations) {
2591
4664
  rewriteModelRef(op.response, oldName, newName);
2592
4665
  if (op.requestBody) rewriteModelRef(op.requestBody, oldName, newName);
4666
+ for (const field of op.pagination?.envelopeFields ?? []) rewriteModelRef(field.type, oldName, newName);
4667
+ for (const header of collectResponseHeaders(op)) rewriteModelRef(header.type, oldName, newName);
2593
4668
  }
2594
4669
  }
2595
4670
  function mergeInlineResponseModels(schemaModels, inlineModels) {
@@ -2612,15 +4687,25 @@ function mergeInlineResponseModels(schemaModels, inlineModels) {
2612
4687
  });
2613
4688
  return [...mergedSchemaModels, ...keepLargestByName(deduplicatedInlineModels)];
2614
4689
  }
2615
- function mergeFieldInlineModels(existingModels, fieldInlineModels) {
4690
+ /**
4691
+ * Fold the field extractor's inline models into the models gathered so far,
4692
+ * keeping the earlier definition on a name clash. `componentNames` tells a
4693
+ * clash with a real component schema apart from one between two inline
4694
+ * models, so the warning can say which happened and name both shapes.
4695
+ */
4696
+ function mergeFieldInlineModels(existingModels, fieldInlineModels, componentNames) {
2616
4697
  const modelNames = new Set(existingModels.map((m) => m.name));
2617
4698
  const modelsByName = new Map(existingModels.map((m) => [m.name, m]));
4699
+ const fieldList = (m) => m.fields.map((f) => f.name).join(", ") || "(no fields)";
2618
4700
  const deduplicatedFieldModels = fieldInlineModels.filter((model) => {
2619
4701
  if (!modelNames.has(model.name)) return true;
2620
4702
  const existing = modelsByName.get(model.name);
2621
4703
  const existingFieldNames = new Set(existing.fields.map((f) => f.name));
2622
4704
  const inlineFieldNames = new Set(model.fields.map((f) => f.name));
2623
- 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`);
4705
+ if (model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name))) {
4706
+ 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`);
4707
+ else console.warn(`[oagen] Warning: Inline model "${model.name}" has different fields than component schema — using component schema`);
4708
+ }
2624
4709
  return false;
2625
4710
  });
2626
4711
  return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
@@ -2652,7 +4737,7 @@ function collapseJsonSuffixModels(models, services) {
2652
4737
  if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
2653
4738
  if (!baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name))) continue;
2654
4739
  if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
2655
- if (isModelReferencedByOthers(model.name, normalizedModels, new Set([model.name, baseName]))) continue;
4740
+ if (isModelReferencedByOthers(model.name, normalizedModels, /* @__PURE__ */ new Set([model.name, baseName]))) continue;
2656
4741
  mergeCandidates.push({
2657
4742
  jsonModel: model,
2658
4743
  baseModel
@@ -2661,6 +4746,10 @@ function collapseJsonSuffixModels(models, services) {
2661
4746
  const toRemove = /* @__PURE__ */ new Set();
2662
4747
  for (const { jsonModel, baseModel } of mergeCandidates) {
2663
4748
  baseModel.fields = jsonModel.fields;
4749
+ if (jsonModel.constraints || baseModel.constraints) baseModel.constraints = {
4750
+ ...baseModel.constraints,
4751
+ ...jsonModel.constraints
4752
+ };
2664
4753
  toRemove.add(jsonModel.name);
2665
4754
  rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
2666
4755
  console.warn(`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`);
@@ -2696,17 +4785,82 @@ function collectInlineEnumsFromModels(models, enums) {
2696
4785
  }
2697
4786
  /**
2698
4787
  * Collect inline enum definitions from operation parameters (query, path, header)
2699
- * and promote them to top-level enums so emitters can generate typed enum files.
4788
+ * list-envelope companion fields, and response headers, and promote them to
4789
+ * top-level enums so emitters can generate typed enum files.
2700
4790
  */
2701
4791
  function collectInlineEnumsFromOperations(services, enums) {
2702
4792
  const enumNames = new Set(enums.map((e) => e.name));
2703
- for (const service of services) for (const op of service.operations) for (const param of [
2704
- ...op.pathParams,
2705
- ...op.queryParams,
2706
- ...op.bodyOwnedQueryParams ?? [],
2707
- ...op.headerParams,
2708
- ...op.cookieParams ?? []
2709
- ]) collectInlineEnumFromRef(param.type, enums, enumNames);
4793
+ for (const service of services) for (const op of service.operations) {
4794
+ for (const param of [
4795
+ ...op.pathParams,
4796
+ ...op.queryParams,
4797
+ ...op.bodyOwnedQueryParams ?? [],
4798
+ ...op.headerParams,
4799
+ ...op.cookieParams ?? []
4800
+ ]) collectInlineEnumFromRef(param.type, enums, enumNames);
4801
+ for (const field of op.pagination?.envelopeFields ?? []) collectInlineEnumFromRef(field.type, enums, enumNames);
4802
+ for (const { headers, qualifier } of responseHeaderGroups(op)) for (const header of headers) collectHeaderEnums(header, qualifier, enums, enumNames);
4803
+ }
4804
+ }
4805
+ /**
4806
+ * Header lists to walk, each with the status-code qualifier that keeps a
4807
+ * colliding enum name apart. When `successResponses` exists the primary list
4808
+ * is the same array as its entry there, so the entries are walked with their
4809
+ * own codes instead of `responseHeaders`; a lone 2xx carries no status code
4810
+ * in the IR, so its headers get no qualifier.
4811
+ */
4812
+ function responseHeaderGroups(op) {
4813
+ const groups = [];
4814
+ if (op.successResponses) {
4815
+ for (const entry of op.successResponses) if (entry.headers) groups.push({
4816
+ headers: entry.headers,
4817
+ qualifier: String(entry.statusCode)
4818
+ });
4819
+ } else if (op.responseHeaders) groups.push({ headers: op.responseHeaders });
4820
+ for (const error of op.errors) if (error.headers) groups.push({
4821
+ headers: error.headers,
4822
+ qualifier: String(error.statusCode)
4823
+ });
4824
+ return groups;
4825
+ }
4826
+ /**
4827
+ * Promote a header's inline enums. A header enum is named from the service
4828
+ * and header name alone, so `X-Policy` allowing `fast`/`slow` on a 200 and
4829
+ * `retry`/`stop` on a 429 would share one name and whichever was collected
4830
+ * first would silently win; a request parameter named like the header
4831
+ * collides the same way. When the name is already taken by a different value
4832
+ * set, the header's ref is renamed to a distinct name before it is promoted.
4833
+ * A taken name with the same values is shared, as before.
4834
+ */
4835
+ function collectHeaderEnums(header, qualifier, enums, enumNames) {
4836
+ walkTypeRef(header.type, { enum: (ref) => {
4837
+ if (!ref.values) return;
4838
+ const existing = enums.find((e) => e.name === ref.name);
4839
+ if (existing && !sameEnumValues(existing, ref.values)) ref.name = distinctEnumName(ref.name, ref.values, qualifier, enums);
4840
+ collectInlineEnumFromRef(ref, enums, enumNames);
4841
+ } });
4842
+ }
4843
+ /** Order-insensitive: a reordering of the same members is the same type. */
4844
+ function sameEnumValues(existing, values) {
4845
+ if (existing.values.length !== values.length) return false;
4846
+ const have = new Set(existing.values.map((v) => v.value));
4847
+ return values.every((v) => have.has(v));
4848
+ }
4849
+ /**
4850
+ * The first name not taken by a different value set: the base qualified by
4851
+ * the status code (`DocumentsXPolicy429`), then numeric suffixes
4852
+ * (`DocumentsXPolicy2`, `DocumentsXPolicy3`, ...). Deterministic for a given
4853
+ * spec, since it depends only on declaration order.
4854
+ */
4855
+ function distinctEnumName(base, values, qualifier, enums) {
4856
+ const isUsable = (name) => {
4857
+ const taken = enums.find((e) => e.name === name);
4858
+ return !taken || sameEnumValues(taken, values);
4859
+ };
4860
+ if (qualifier && isUsable(`${base}${qualifier}`)) return `${base}${qualifier}`;
4861
+ let suffix = 2;
4862
+ while (!isUsable(`${base}${suffix}`)) suffix++;
4863
+ return `${base}${suffix}`;
2710
4864
  }
2711
4865
  //#endregion
2712
4866
  //#region src/parser/parameter-group-names.ts
@@ -2985,6 +5139,8 @@ function validateModelRefs(spec) {
2985
5139
  ]) walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
2986
5140
  if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
2987
5141
  walkRef(op.response, `${service.name}.${op.name}.response`);
5142
+ for (const field of op.pagination?.envelopeFields ?? []) walkRef(field.type, `${service.name}.${op.name}.pagination.${field.name}`);
5143
+ for (const header of collectResponseHeaders(op)) walkRef(header.type, `${service.name}.${op.name}.headers.${header.name}`);
2988
5144
  }
2989
5145
  }
2990
5146
  //#endregion
@@ -2995,11 +5151,12 @@ async function parseSpec(specPath, options) {
2995
5151
  const version = spec.openapi ?? "";
2996
5152
  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}\`.`);
2997
5153
  const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
2998
- const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters, spec.components?.requestBodies);
5154
+ 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);
2999
5155
  const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
3000
5156
  const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
5157
+ const componentNames = new Set(Object.keys(spec.components?.schemas ?? {}).map((schemaName) => resolveSchemaName(schemaName)));
3001
5158
  clearSchemaNameTransform();
3002
- const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels), services);
5159
+ const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels, componentNames), services);
3003
5160
  applyFieldHints(finalModels, options?.fieldHints);
3004
5161
  collectInlineEnumsFromModels(finalModels, enums);
3005
5162
  collectInlineEnumsFromOperations(services, enums);
@@ -3018,6 +5175,7 @@ async function parseSpec(specPath, options) {
3018
5175
  models: finalModels,
3019
5176
  enums,
3020
5177
  auth,
5178
+ security: extractSecurityRequirements(spec.security, "the document"),
3021
5179
  sdk: defaultSdkBehavior()
3022
5180
  };
3023
5181
  assignParameterGroupWrapperNames(result);
@@ -3040,20 +5198,47 @@ function applyFieldHints(models, fieldHints) {
3040
5198
  }
3041
5199
  }
3042
5200
  }
3043
- /** Extract authentication schemes from OpenAPI securitySchemes. */
5201
+ /**
5202
+ * Extract authentication schemes from OpenAPI securitySchemes.
5203
+ *
5204
+ * Document order is preserved. Each entry carries its `securitySchemes` key as
5205
+ * `schemeName` so emitters can join the requirements under `ApiSpec.security`
5206
+ * and `Operation.security` back to it.
5207
+ * Schemes the IR cannot represent are omitted with a warning rather than silently.
5208
+ */
3044
5209
  function extractAuthSchemes(securitySchemes) {
3045
5210
  if (!securitySchemes) return void 0;
3046
5211
  const schemes = [];
3047
- for (const [, scheme] of Object.entries(securitySchemes)) if (scheme.type === "http" && scheme.scheme === "bearer") schemes.push({ kind: "bearer" });
3048
- else if (scheme.type === "apiKey" && scheme.in && scheme.name) schemes.push({
3049
- kind: "apiKey",
3050
- in: scheme.in,
3051
- name: scheme.name
3052
- });
3053
- else if (scheme.type === "oauth2" && scheme.flows) schemes.push({
3054
- kind: "oauth2",
3055
- flows: scheme.flows
3056
- });
5212
+ for (const [schemeName, scheme] of Object.entries(securitySchemes)) {
5213
+ const common = scheme.description !== void 0 ? {
5214
+ schemeName,
5215
+ description: scheme.description
5216
+ } : { schemeName };
5217
+ const httpScheme = scheme.type === "http" ? scheme.scheme?.toLowerCase() : void 0;
5218
+ if (httpScheme === "bearer") schemes.push({
5219
+ kind: "bearer",
5220
+ ...common
5221
+ });
5222
+ else if (httpScheme === "basic") schemes.push({
5223
+ kind: "basic",
5224
+ ...common
5225
+ });
5226
+ else if (scheme.type === "apiKey" && scheme.in && scheme.name) schemes.push({
5227
+ kind: "apiKey",
5228
+ ...common,
5229
+ in: scheme.in,
5230
+ name: scheme.name
5231
+ });
5232
+ else if (scheme.type === "oauth2" && scheme.flows) schemes.push({
5233
+ kind: "oauth2",
5234
+ ...common,
5235
+ flows: scheme.flows
5236
+ });
5237
+ else {
5238
+ const detail = scheme.type === "http" ? `type: http, scheme: ${scheme.scheme}` : `type: ${scheme.type}`;
5239
+ console.warn(`[oagen] Warning: Unsupported security scheme "${schemeName}" (${detail}) omitted from spec.auth`);
5240
+ }
5241
+ }
3057
5242
  return schemes.length > 0 ? schemes : void 0;
3058
5243
  }
3059
5244
  //#endregion
@@ -3147,29 +5332,47 @@ function createExampleBuilder(spec) {
3147
5332
  for (const m of spec.models) modelsByName.set(m.name, m);
3148
5333
  const enumsByName = /* @__PURE__ */ new Map();
3149
5334
  for (const e of spec.enums) enumsByName.set(e.name, e);
3150
- function buildForType(type, depth) {
5335
+ /**
5336
+ * `seed` distinguishes sibling items of a `uniqueItems` array: it is
5337
+ * appended to generated strings, added to generated numbers and alternates
5338
+ * generated booleans. `0` leaves the value as is.
5339
+ */
5340
+ function buildForType(type, depth, constraints, seed = 0) {
3151
5341
  if (depth > MAX_DEPTH) return null;
3152
5342
  switch (type.kind) {
3153
- case "primitive": return primitiveExample(type.type, type.format);
5343
+ case "primitive": return primitiveExample(type.type, type.format, constraints, seed);
3154
5344
  case "literal": return type.value;
3155
5345
  case "enum":
3156
5346
  if (type.values && type.values.length > 0) return type.values[0];
3157
5347
  return enumsByName.get(type.name)?.values[0]?.value ?? null;
3158
- case "array": return [buildForType(type.items, depth + 1)];
3159
- case "nullable": return buildForType(type.inner, depth);
5348
+ case "array": return arrayExample(type, depth, constraints);
5349
+ case "nullable": return buildForType(type.inner, depth, constraints, seed);
3160
5350
  case "union": {
3161
5351
  const first = type.variants[0];
3162
- return first ? buildForType(first, depth) : null;
3163
- }
3164
- case "map": {
3165
- const key = "key";
3166
- const value = buildForType(type.valueType, depth + 1);
3167
- return { [key]: value };
5352
+ return first ? buildForType(first, depth, constraints, seed) : null;
3168
5353
  }
5354
+ case "map": return mapExample(type, depth, constraints);
3169
5355
  case "model": return buildForModel(type.name, depth + 1);
3170
5356
  default: return null;
3171
5357
  }
3172
5358
  }
5359
+ function arrayExample(type, depth, constraints) {
5360
+ let count = countWithin(constraints?.minItems, constraints?.maxItems);
5361
+ const vary = constraints?.uniqueItems === true && count > 1;
5362
+ if (vary && isBooleanType(type.items)) count = Math.min(count, 2);
5363
+ const items = [];
5364
+ for (let i = 0; i < count; i++) items.push(buildForType(type.items, depth + 1, void 0, vary ? i : 0));
5365
+ return items;
5366
+ }
5367
+ function mapExample(type, depth, constraints) {
5368
+ const count = countWithin(constraints?.minProperties, constraints?.maxProperties);
5369
+ const result = {};
5370
+ for (let i = 0; i < count; i++) {
5371
+ const key = count === 1 ? "key" : `key${i + 1}`;
5372
+ result[key] = buildForType(type.valueType, depth + 1);
5373
+ }
5374
+ return result;
5375
+ }
3173
5376
  function buildForModel(nameOrModel, depth) {
3174
5377
  if (depth > MAX_DEPTH) return {};
3175
5378
  const model = typeof nameOrModel === "string" ? modelsByName.get(nameOrModel) : nameOrModel;
@@ -3184,29 +5387,159 @@ function createExampleBuilder(spec) {
3184
5387
  function buildForField(field, depth) {
3185
5388
  if (field.example !== void 0) return field.example;
3186
5389
  if (field.default !== void 0) return field.default;
3187
- return buildForType(field.type, depth);
5390
+ return buildForType(field.type, depth, field.constraints);
5391
+ }
5392
+ function buildForParameter(param) {
5393
+ if (param.example !== void 0) return param.example;
5394
+ if (param.default !== void 0) return param.default;
5395
+ return buildForType(param.type, 0, param.constraints);
3188
5396
  }
3189
5397
  return {
3190
- forType: (type) => buildForType(type, 0),
5398
+ forType: (type, constraints) => buildForType(type, 0, constraints),
3191
5399
  forField: (field) => buildForField(field, 0),
5400
+ forParameter: (param) => buildForParameter(param),
3192
5401
  forModel: (model) => buildForModel(model, 0)
3193
5402
  };
3194
5403
  }
3195
- function primitiveExample(type, format) {
5404
+ /** True for a boolean primitive, looking through `nullable`. */
5405
+ function isBooleanType(type) {
5406
+ if (type.kind === "nullable") return isBooleanType(type.inner);
5407
+ return type.kind === "primitive" && type.type === "boolean";
5408
+ }
5409
+ /**
5410
+ * `max(1, min)` capped at the size budget and then at `max`; an explicit
5411
+ * `max: 0` yields `0`.
5412
+ */
5413
+ function countWithin(min, max) {
5414
+ let count = Math.min(Math.max(1, min ?? 0), 8);
5415
+ if (max !== void 0) count = Math.min(count, max);
5416
+ return Math.max(0, Math.floor(count));
5417
+ }
5418
+ function primitiveExample(type, format, constraints, seed = 0) {
3196
5419
  switch (type) {
3197
- case "string":
3198
- if (format === "date-time") return "2026-01-15T12:00:00.000Z";
3199
- if (format === "date") return "2026-01-15";
3200
- if (format === "email") return "user@example.com";
3201
- if (format === "uri" || format === "url") return "https://example.com";
3202
- if (format === "uuid") return "00000000-0000-0000-0000-000000000000";
3203
- return "string_example";
3204
- case "integer":
3205
- case "number": return 1;
3206
- case "boolean": return true;
5420
+ case "string": return stringExample(format, constraints, seed);
5421
+ case "integer": return numberExample(true, constraints) + seed;
5422
+ case "number": return numberExample(false, constraints) + seed;
5423
+ case "boolean": return seed % 2 === 0;
3207
5424
  default: return null;
3208
5425
  }
3209
5426
  }
5427
+ const STRING_SAMPLE = "string_example";
5428
+ /**
5429
+ * Format-driven samples are returned untouched. The plain sample is padded
5430
+ * to `minLength` (within the size budget) and truncated to `maxLength`;
5431
+ * when both conflict, `maxLength` wins so the result at least fits. A
5432
+ * `pattern` is deliberately not honored.
5433
+ */
5434
+ function stringExample(format, constraints, seed = 0) {
5435
+ if (format === "date-time") return "2026-01-15T12:00:00.000Z";
5436
+ if (format === "date") return "2026-01-15";
5437
+ if (format === "email") return "user@example.com";
5438
+ if (format === "uri" || format === "url") return "https://example.com";
5439
+ if (format === "uuid") return "00000000-0000-0000-0000-000000000000";
5440
+ const suffix = seed > 0 ? String(seed) : "";
5441
+ let value = STRING_SAMPLE;
5442
+ const minLength = Math.min(constraints?.minLength ?? 0, 64);
5443
+ if (value.length + suffix.length < minLength) {
5444
+ const missing = minLength - value.length - suffix.length;
5445
+ value += STRING_SAMPLE.repeat(Math.ceil(missing / 14)).slice(0, missing);
5446
+ }
5447
+ const maxLength = constraints?.maxLength;
5448
+ if (maxLength !== void 0 && value.length + suffix.length > maxLength) value = value.slice(0, Math.max(0, maxLength - suffix.length));
5449
+ return (value + suffix).slice(0, maxLength ?? Infinity);
5450
+ }
5451
+ function edge(inclusive, exclusive, side) {
5452
+ if (inclusive === void 0 && exclusive === void 0) return void 0;
5453
+ if (inclusive === void 0) return {
5454
+ value: exclusive,
5455
+ exclusive: true
5456
+ };
5457
+ if (exclusive === void 0) return {
5458
+ value: inclusive,
5459
+ exclusive: false
5460
+ };
5461
+ return (side === "low" ? exclusive >= inclusive : exclusive <= inclusive) ? {
5462
+ value: exclusive,
5463
+ exclusive: true
5464
+ } : {
5465
+ value: inclusive,
5466
+ exclusive: false
5467
+ };
5468
+ }
5469
+ /**
5470
+ * A number inside the declared bounds and on the declared `multipleOf` —
5471
+ * see the `ExampleBuilder` doc for the anchor rules. Candidates are tried
5472
+ * in order and the first that satisfies every limit wins; only when none
5473
+ * does is the lower bound returned as is.
5474
+ */
5475
+ function numberExample(integer, constraints) {
5476
+ if (!constraints) return 1;
5477
+ const multipleOf = constraints.multipleOf !== void 0 && constraints.multipleOf > 0 ? constraints.multipleOf : void 0;
5478
+ const low = edge(constraints.minimum, constraints.exclusiveMinimum, "low");
5479
+ const high = edge(constraints.maximum, constraints.exclusiveMaximum, "high");
5480
+ const step = multipleOf ?? (integer ? 1 : low && high ? (high.value - low.value) / 2 : 1);
5481
+ const pastLow = () => {
5482
+ const l = low;
5483
+ if (multipleOf !== void 0) {
5484
+ const v = ceilToMultiple(l.value, multipleOf);
5485
+ return l.exclusive && v <= l.value ? trim(v + multipleOf) : v;
5486
+ }
5487
+ if (integer) return l.exclusive ? Math.floor(l.value) + 1 : Math.ceil(l.value);
5488
+ return l.exclusive ? trim(l.value + step) : l.value;
5489
+ };
5490
+ const belowHigh = () => {
5491
+ const h = high;
5492
+ if (multipleOf !== void 0) {
5493
+ const v = floorToMultiple(h.value, multipleOf);
5494
+ return h.exclusive && v >= h.value ? trim(v - multipleOf) : v;
5495
+ }
5496
+ if (integer) return h.exclusive ? Math.ceil(h.value) - 1 : Math.floor(h.value);
5497
+ return h.exclusive ? trim(h.value - step) : h.value;
5498
+ };
5499
+ const candidates = [];
5500
+ if (low && high) {
5501
+ if (integer) candidates.push(pastLow());
5502
+ else {
5503
+ const mid = (low.value + high.value) / 2;
5504
+ candidates.push(multipleOf !== void 0 ? roundToMultiple(mid, multipleOf) : trim(mid), pastLow(), belowHigh());
5505
+ }
5506
+ } else if (low) candidates.push(pastLow());
5507
+ else if (high) candidates.push(belowHigh());
5508
+ else candidates.push(multipleOf !== void 0 ? ceilToMultiple(1, multipleOf) : 1);
5509
+ for (const v of candidates) if (satisfies(v, constraints, integer, multipleOf)) return v;
5510
+ if (!low) return candidates[0];
5511
+ const lower = low.exclusive ? trim(low.value + step) : low.value;
5512
+ return integer ? Math.ceil(lower) : lower;
5513
+ }
5514
+ function satisfies(v, c, integer, multipleOf) {
5515
+ if (!Number.isFinite(v)) return false;
5516
+ if (integer && !Number.isInteger(v)) return false;
5517
+ if (c.minimum !== void 0 && v < c.minimum) return false;
5518
+ if (c.exclusiveMinimum !== void 0 && v <= c.exclusiveMinimum) return false;
5519
+ if (c.maximum !== void 0 && v > c.maximum) return false;
5520
+ if (c.exclusiveMaximum !== void 0 && v >= c.exclusiveMaximum) return false;
5521
+ if (multipleOf !== void 0) {
5522
+ const q = v / multipleOf;
5523
+ if (Math.abs(q - Math.round(q)) > 1e-9) return false;
5524
+ }
5525
+ return true;
5526
+ }
5527
+ /** Smallest multiple of `m` that is >= `value`. */
5528
+ function ceilToMultiple(value, m) {
5529
+ return trim(Math.ceil(value / m - 1e-9) * m);
5530
+ }
5531
+ /** Largest multiple of `m` that is <= `value`. */
5532
+ function floorToMultiple(value, m) {
5533
+ return trim(Math.floor(value / m + 1e-9) * m);
5534
+ }
5535
+ /** Nearest multiple of `m` to `value`. */
5536
+ function roundToMultiple(value, m) {
5537
+ return trim(Math.round(value / m) * m);
5538
+ }
5539
+ /** Trim floating-point noise (`0.30000000000000004` → `0.3`). */
5540
+ function trim(value) {
5541
+ return Number(value.toPrecision(12));
5542
+ }
3210
5543
  //#endregion
3211
5544
  //#region src/snippets/runner.ts
3212
5545
  /**
@@ -3355,14 +5688,12 @@ function findModel(ctx, name) {
3355
5688
  return ctx.spec.models.find((m) => m.name === name);
3356
5689
  }
3357
5690
  function exampleForParam(p, examples) {
3358
- if (p.example !== void 0) return p.example;
3359
- if (p.default !== void 0) return p.default;
3360
- return examples.forType(p.type);
5691
+ return examples.forParameter(p);
3361
5692
  }
3362
5693
  function exampleForField(f, examples) {
3363
5694
  return examples.forField(f);
3364
5695
  }
3365
5696
  //#endregion
3366
- export { ACRONYM_SET, CommandError, ConfigError, ConfigLoadError, ConfigVersionMismatchError, ExtractorError, InternalError, OagenError, RegistryError, SpecParseError, applyFileHeaders, assertNever, assignModelsToServices, buildDirectionIndex, buildEmitterContext, cleanSchemaName, collectEnumRefs, collectFieldDependencies, collectModelRefs, collectReferencedNames, collectRequestBodyModels, collectSnippetArgs, collectWrapperArgs, createExampleBuilder, defaultSdkBehavior, deriveMethodName, diffSpecs, directionOf, escapeBlockComment, findResolvedMethodCollisions, generate, generateAllFiles, generateFiles, getEmitter, hiddenParamSet, integrateGeneratedFiles, mapFilesForTargetIntegration, mapTypeRef, mergeSdkBehavior, parseSpec, planOperation, registerEmitter, resolveMountTarget, resolveOperations, runSnippetEmitters, sanitizeIdentifier, snippetResultsToFiles, stripBackendPrefixes, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toUpperSnakeCase, treeShakeFiles, validateModelHints, walkTypeRef };
5697
+ export { ACRONYM_SET, CommandError, ConfigError, ConfigLoadError, ConfigVersionMismatchError, ExtractorError, InternalError, OagenError, RegistryError, SpecParseError, applyFileHeaders, assertNever, assignModelsToServices, buildDirectionIndex, buildEmitterContext, cleanSchemaName, collectEnumRefs, collectFieldDependencies, collectModelRefs, collectReferencedNames, collectRequestBodyModels, collectResponseHeaders, collectSnippetArgs, collectWrapperArgs, createExampleBuilder, defaultSdkBehavior, deriveMethodName, diffSpecs, directionOf, escapeBlockComment, findResolvedMethodCollisions, generate, generateAllFiles, generateFiles, getEmitter, hiddenParamSet, integrateGeneratedFiles, mapFilesForTargetIntegration, mapTypeRef, mergeSdkBehavior, parseSpec, planOperation, registerEmitter, resolveMountTarget, resolveOperations, runSnippetEmitters, sanitizeIdentifier, snippetResultsToFiles, stripBackendPrefixes, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toUpperSnakeCase, treeShakeFiles, validateModelHints, walkTypeRef };
3367
5698
 
3368
5699
  //# sourceMappingURL=index.mjs.map