@workos/oagen 0.30.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -12
- package/dist/cli/index.d.mts +1 -1
- package/dist/cli/index.mjs +3003 -664
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +85 -36
- package/dist/compat/index.d.mts.map +1 -1
- package/dist/compat/index.mjs +33 -23
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-DMFiDAI6.mjs → diff-CAsypxXV.mjs} +109 -89
- package/dist/{diff-DMFiDAI6.mjs.map → diff-CAsypxXV.mjs.map} +1 -1
- package/dist/differ-B0DSuKUZ.d.mts +72 -0
- package/dist/differ-B0DSuKUZ.d.mts.map +1 -0
- package/dist/index.d.mts +129 -61
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2992 -563
- package/dist/index.mjs.map +1 -1
- package/dist/{overlay-BhXruu32.mjs → overlay-Dj3WWlNP.mjs} +45 -38
- package/dist/overlay-Dj3WWlNP.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.d.mts +1 -1
- package/dist/scripts/smoke/baseline.mjs +2848 -537
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/scripts/smoke/sdk-test.d.mts +1 -1
- package/dist/scripts/smoke/sdk-test.mjs +4 -2
- package/dist/scripts/smoke/sdk-test.mjs.map +1 -1
- package/dist/smoke.d.mts +259 -36
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +2848 -537
- package/dist/smoke.mjs.map +1 -1
- package/dist/{tree-sitter-C6HB4OrC.mjs → tree-sitter-C7Hk-NDu.mjs} +36 -7
- package/dist/tree-sitter-C7Hk-NDu.mjs.map +1 -0
- package/dist/{types-yrBidOg3.d.mts → types-I7W7CNl1.d.mts} +227 -9
- package/dist/types-I7W7CNl1.d.mts.map +1 -0
- package/dist/verify/index.d.mts +7 -8
- package/dist/verify/index.d.mts.map +1 -1
- package/dist/verify/index.mjs +24 -14
- package/dist/verify/index.mjs.map +1 -1
- package/docs/architecture/emitter-contract.md +1 -0
- package/docs/architecture/ir-types.md +175 -17
- package/docs/cli.md +13 -13
- package/package.json +3 -3
- package/skills/check-emitter-parity/SKILL.md +15 -1
- package/dist/differ-DHldxZMr.d.mts +0 -123
- package/dist/differ-DHldxZMr.d.mts.map +0 -1
- package/dist/overlay-BhXruu32.mjs.map +0 -1
- package/dist/tree-sitter-C6HB4OrC.mjs.map +0 -1
- package/dist/types-yrBidOg3.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { A as
|
|
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-
|
|
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
|
|
148
|
-
config
|
|
152
|
+
document,
|
|
153
|
+
config,
|
|
149
154
|
types: Oas3_1Types,
|
|
150
|
-
externalRefResolver:
|
|
155
|
+
externalRefResolver: resolver,
|
|
151
156
|
dereference: false
|
|
152
157
|
});
|
|
153
|
-
if (result.problems.some((p) => p.severity === "error"))
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
209
|
-
const
|
|
210
|
-
|
|
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(
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
222
|
-
const emittedNames = [];
|
|
606
|
+
const variantNames = nameInlineVariants(fieldSchema, modelName, reservedFor(fieldName));
|
|
223
607
|
for (const variant of inlineObjectVariants) {
|
|
224
|
-
const variantName =
|
|
225
|
-
|
|
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
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
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
|
|
283
|
-
const properties =
|
|
635
|
+
function mergeAugmentation(schema, componentSchemas) {
|
|
636
|
+
const properties = [];
|
|
637
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
284
638
|
const required = [];
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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)
|
|
671
|
+
};
|
|
672
|
+
}
|
|
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 } : {}
|
|
298
698
|
};
|
|
299
699
|
}
|
|
300
|
-
|
|
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
|
-
/**
|
|
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)
|
|
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
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
for (const [,
|
|
351
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
469
|
-
|
|
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
|
|
1265
|
+
const variantNames = nameInlineVariants(schema, name, reserved);
|
|
473
1266
|
for (const variant of schema.oneOf) {
|
|
474
|
-
|
|
475
|
-
if (
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
* The
|
|
535
|
-
*
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
|
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
|
-
* `
|
|
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
|
-
/**
|
|
584
|
-
|
|
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
|
|
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
|
|
632
|
-
*
|
|
633
|
-
*
|
|
634
|
-
*
|
|
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
|
|
1687
|
+
function titleMappedDiscriminatorProperty(variants) {
|
|
644
1688
|
if (variants.length < 2) return null;
|
|
645
|
-
|
|
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
|
-
|
|
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`
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
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,
|
|
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
|
|
711
|
-
|
|
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
|
|
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)
|
|
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: [
|
|
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)
|
|
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) =>
|
|
2160
|
+
const inlineObjectVariants = fieldSchema.oneOf.filter((v) => isInlineObjectSchema(v));
|
|
1056
2161
|
if (inlineObjectVariants.length > 0) {
|
|
1057
|
-
const
|
|
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 =
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
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))
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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(
|
|
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,
|
|
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
|
|
1185
|
-
if (
|
|
1186
|
-
|
|
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,
|
|
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
|
|
1201
|
-
if (
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
2379
|
+
const enumRef = {
|
|
1260
2380
|
kind: "enum",
|
|
1261
2381
|
name: activeSchemaNameTransform ? activeSchemaNameTransform(qualifiedName) : qualifiedName,
|
|
1262
|
-
values:
|
|
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 {
|
|
2386
|
+
if ((schema.type === "array" || !schema.type) && schema.items) return {
|
|
1266
2387
|
kind: "array",
|
|
1267
|
-
items: schemaToTypeRef(schema.items, contextName, parentModelName)
|
|
1268
|
-
};
|
|
1269
|
-
if (!schema.type && schema.items) return {
|
|
1270
|
-
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
|
-
|
|
1283
|
-
|
|
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
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
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
|
-
*
|
|
1680
|
-
*
|
|
1681
|
-
*
|
|
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
|
|
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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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;
|
|
1736
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}`;
|
|
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
|
|
@@ -1767,6 +3198,60 @@ function resolveParameterRef(raw, componentParameters, operationContext) {
|
|
|
1767
3198
|
}
|
|
1768
3199
|
return current;
|
|
1769
3200
|
}
|
|
3201
|
+
/**
|
|
3202
|
+
* Resolve a raw request body to a concrete Request Body Object, following
|
|
3203
|
+
* `$ref` chains into `components/requestBodies` with a cycle guard. Throws on
|
|
3204
|
+
* an unresolvable ref so a malformed spec fails at parse time instead of
|
|
3205
|
+
* silently losing the body — and with it the body-ownership rule that keeps
|
|
3206
|
+
* duplicated secrets out of the query string.
|
|
3207
|
+
*/
|
|
3208
|
+
function resolveRequestBodyRef(raw, componentRequestBodies, operationContext) {
|
|
3209
|
+
if (!raw) return void 0;
|
|
3210
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
3211
|
+
let current = raw;
|
|
3212
|
+
while ("$ref" in current) {
|
|
3213
|
+
if (seenRefs.has(current.$ref)) throw new Error(`Circular request body $ref chain starting at "${current.$ref}" in ${operationContext}.`);
|
|
3214
|
+
seenRefs.add(current.$ref);
|
|
3215
|
+
const refName = /^#\/components\/requestBodies\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
|
|
3216
|
+
const resolved = refName ? componentRequestBodies?.[refName] : void 0;
|
|
3217
|
+
if (!resolved) throw new Error(`Unresolved request body $ref "${current.$ref}" in ${operationContext}: no matching entry under components/requestBodies.`);
|
|
3218
|
+
current = resolved;
|
|
3219
|
+
}
|
|
3220
|
+
return current;
|
|
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
|
+
}
|
|
1770
3255
|
const HTTP_METHODS = [
|
|
1771
3256
|
"get",
|
|
1772
3257
|
"post",
|
|
@@ -1777,26 +3262,34 @@ const HTTP_METHODS = [
|
|
|
1777
3262
|
"options",
|
|
1778
3263
|
"trace"
|
|
1779
3264
|
];
|
|
1780
|
-
function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters) {
|
|
3265
|
+
function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters, componentRequestBodies, componentModels, documentTags, componentExamples, componentHeaders) {
|
|
1781
3266
|
if (!paths) return {
|
|
1782
3267
|
services: [],
|
|
1783
3268
|
inlineModels: []
|
|
1784
3269
|
};
|
|
3270
|
+
const pathModelBaseNames = resolvePathModelBaseNames(paths, componentModels, componentRequestBodies);
|
|
1785
3271
|
const serviceMap = /* @__PURE__ */ new Map();
|
|
1786
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) }]));
|
|
1787
3276
|
for (const [path, pathItem] of Object.entries(paths)) {
|
|
1788
3277
|
const pathLevelParams = pathItem.parameters ?? [];
|
|
1789
3278
|
for (const method of HTTP_METHODS) {
|
|
1790
3279
|
const op = pathItem[method];
|
|
1791
3280
|
if (!op) continue;
|
|
1792
|
-
const
|
|
1793
|
-
const
|
|
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);
|
|
1794
3284
|
inlineModels.push(...opModels);
|
|
3285
|
+
if (opCompanions) envelopeCompanions.push(...opCompanions);
|
|
3286
|
+
if (tag) tagServiceByOperation.set(operation, serviceName);
|
|
1795
3287
|
const ops = serviceMap.get(serviceName) ?? [];
|
|
1796
3288
|
ops.push(operation);
|
|
1797
3289
|
serviceMap.set(serviceName, ops);
|
|
1798
3290
|
}
|
|
1799
3291
|
}
|
|
3292
|
+
inlineModels.push(...resolveEnvelopeCompanionNames(envelopeCompanions, componentModels));
|
|
1800
3293
|
for (const [serviceName, operations] of serviceMap) {
|
|
1801
3294
|
const pathGroups = /* @__PURE__ */ new Map();
|
|
1802
3295
|
for (const op of operations) {
|
|
@@ -1818,14 +3311,73 @@ function extractOperations(paths, operationIdTransform, componentSchemas, compon
|
|
|
1818
3311
|
}
|
|
1819
3312
|
}
|
|
1820
3313
|
for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
|
|
3314
|
+
const declaredTags = indexDocumentTags(documentTags);
|
|
1821
3315
|
return {
|
|
1822
|
-
services: Array.from(serviceMap.entries()).map(([name, operations]) =>
|
|
1823
|
-
name,
|
|
1824
|
-
|
|
1825
|
-
|
|
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
|
+
}),
|
|
1826
3327
|
inlineModels
|
|
1827
3328
|
};
|
|
1828
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
|
+
}
|
|
1829
3381
|
function inferServiceName(path, tag) {
|
|
1830
3382
|
if (tag) return toPascalCase(tag);
|
|
1831
3383
|
const first = path.split("/").filter(Boolean)[0] ?? "Default";
|
|
@@ -1844,6 +3396,24 @@ function inferServiceName(path, tag) {
|
|
|
1844
3396
|
function normalizeOperationIdForNaming(operationId) {
|
|
1845
3397
|
return operationId.replace(/\[\d+\]\s*$/, "");
|
|
1846
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
|
+
}
|
|
1847
3417
|
function inferOperationName(method, path, operationId, operationIdTransform) {
|
|
1848
3418
|
if (operationId) {
|
|
1849
3419
|
const normalized = normalizeOperationIdForNaming(operationId);
|
|
@@ -1988,10 +3558,10 @@ function extractParameterGroups(op, allIRParams, operationContext) {
|
|
|
1988
3558
|
* the request body. Emitters use the group's presence to generate sum-type
|
|
1989
3559
|
* interfaces and custom JSON marshalling instead of flat optional fields.
|
|
1990
3560
|
*/
|
|
1991
|
-
function extractBodyParameterGroups(op, operationContext, componentSchemas) {
|
|
3561
|
+
function extractBodyParameterGroups(op, requestBody, operationContext, componentSchemas) {
|
|
1992
3562
|
const raw = op["x-mutually-exclusive-body-groups"];
|
|
1993
3563
|
if (!raw || typeof raw !== "object") return void 0;
|
|
1994
|
-
const bodySchema =
|
|
3564
|
+
const bodySchema = requestBody?.content?.["application/json"]?.schema;
|
|
1995
3565
|
const variantFieldSchemas = collectVariantFieldSchemas(bodySchema, componentSchemas);
|
|
1996
3566
|
const groups = [];
|
|
1997
3567
|
for (const [groupName, groupDef] of Object.entries(raw)) {
|
|
@@ -2031,8 +3601,10 @@ function extractBodyParameterGroups(op, operationContext, componentSchemas) {
|
|
|
2031
3601
|
}
|
|
2032
3602
|
return groups.length > 0 ? groups : void 0;
|
|
2033
3603
|
}
|
|
2034
|
-
function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters) {
|
|
2035
|
-
const opLabel = op
|
|
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);
|
|
3607
|
+
const requestBodyObject = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
|
|
2036
3608
|
const paramsByIdentity = /* @__PURE__ */ new Map();
|
|
2037
3609
|
for (const raw of [...pathLevelParams, ...op.parameters ?? []]) {
|
|
2038
3610
|
const param = resolveParameterRef(raw, componentParameters, opLabel);
|
|
@@ -2040,14 +3612,28 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
2040
3612
|
}
|
|
2041
3613
|
const allParams = [...paramsByIdentity.values()];
|
|
2042
3614
|
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
3615
|
+
const groupedQueryNames = collectParameterGroupMemberNames(op);
|
|
3616
|
+
const bodyOwnedNames = /* @__PURE__ */ new Set();
|
|
3617
|
+
if (BODY_CARRYING_METHODS.has(method)) {
|
|
3618
|
+
const bodySchema = selectRequestBodyContent(requestBodyObject).schema;
|
|
3619
|
+
for (const name of collectBodyPropertyNames(bodySchema, componentSchemas)) if (!groupedQueryNames.has(name)) bodyOwnedNames.add(name);
|
|
3620
|
+
}
|
|
3621
|
+
const wireParams = bodyOwnedNames.size > 0 ? allParams.filter((p) => !(p.in === "query" && bodyOwnedNames.has(p.name))) : allParams;
|
|
2043
3622
|
const opContext = serviceName;
|
|
2044
|
-
const pathParams = extractParams(
|
|
2045
|
-
const queryParams = extractParams(
|
|
2046
|
-
const headerParams = extractParams(
|
|
2047
|
-
const cookieParams = extractParams(
|
|
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) : [];
|
|
2048
3628
|
const reqBodyModels = [];
|
|
2049
|
-
const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(
|
|
2050
|
-
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
|
+
});
|
|
2051
3637
|
inlineModels.push(...reqBodyModels);
|
|
2052
3638
|
const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
|
|
2053
3639
|
let pagination;
|
|
@@ -2058,31 +3644,36 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
2058
3644
|
param: paginationFromParams?.param ?? "after",
|
|
2059
3645
|
limitParam: paginationFromParams?.limitParam,
|
|
2060
3646
|
dataPath: responseDataPath ?? paginationFromParams?.dataPath,
|
|
2061
|
-
itemType
|
|
3647
|
+
itemType,
|
|
3648
|
+
envelopeFields
|
|
2062
3649
|
};
|
|
2063
3650
|
} else if (paginationFromParams) pagination = paginationFromParams;
|
|
2064
|
-
const security =
|
|
3651
|
+
const security = extractSecurityRequirements(op.security, opLabel);
|
|
2065
3652
|
const queryParamGroups = extractParameterGroups(op, [
|
|
2066
3653
|
...pathParams,
|
|
2067
3654
|
...queryParams,
|
|
2068
3655
|
...headerParams,
|
|
2069
3656
|
...cookieParams
|
|
2070
3657
|
], opLabel);
|
|
2071
|
-
const bodyParamGroups = extractBodyParameterGroups(op, opLabel, componentSchemas);
|
|
3658
|
+
const bodyParamGroups = extractBodyParameterGroups(op, requestBodyObject, opLabel, componentSchemas);
|
|
2072
3659
|
const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
|
|
3660
|
+
const summary = presentText(op.summary);
|
|
2073
3661
|
return {
|
|
2074
3662
|
operation: {
|
|
2075
|
-
name:
|
|
2076
|
-
|
|
3663
|
+
name: operationName,
|
|
3664
|
+
...summary !== void 0 ? { summary } : {},
|
|
3665
|
+
description: buildDescription(summary, op.description),
|
|
2077
3666
|
httpMethod: method,
|
|
2078
3667
|
path,
|
|
2079
3668
|
pathParams,
|
|
2080
3669
|
queryParams,
|
|
3670
|
+
bodyOwnedQueryParams: bodyOwnedQueryParams.length > 0 ? bodyOwnedQueryParams : void 0,
|
|
2081
3671
|
headerParams,
|
|
2082
3672
|
cookieParams: cookieParams.length > 0 ? cookieParams : void 0,
|
|
2083
3673
|
requestBody,
|
|
2084
3674
|
requestBodyEncoding,
|
|
2085
3675
|
response,
|
|
3676
|
+
...responseHeaders && { responseHeaders },
|
|
2086
3677
|
successResponses,
|
|
2087
3678
|
errors,
|
|
2088
3679
|
pagination,
|
|
@@ -2092,31 +3683,49 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
|
|
|
2092
3683
|
security,
|
|
2093
3684
|
parameterGroups
|
|
2094
3685
|
},
|
|
2095
|
-
inlineModels
|
|
3686
|
+
inlineModels,
|
|
3687
|
+
envelopeCompanions
|
|
2096
3688
|
};
|
|
2097
3689
|
}
|
|
2098
3690
|
/**
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2102
|
-
*
|
|
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.
|
|
2103
3695
|
*/
|
|
2104
|
-
function extractOperationSecurity(security) {
|
|
2105
|
-
if (!security || security.length === 0) return void 0;
|
|
2106
|
-
const requirements = [];
|
|
2107
|
-
for (const entry of security) for (const [schemeName, scopes] of Object.entries(entry)) requirements.push({
|
|
2108
|
-
schemeName,
|
|
2109
|
-
scopes: scopes ?? []
|
|
2110
|
-
});
|
|
2111
|
-
return requirements.length > 0 ? requirements : void 0;
|
|
2112
|
-
}
|
|
2113
3696
|
function buildDescription(summary, description) {
|
|
2114
|
-
|
|
2115
|
-
|
|
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;
|
|
3701
|
+
}
|
|
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;
|
|
2116
3705
|
}
|
|
2117
|
-
function extractParams(params, location, operationContext, componentSchemas) {
|
|
3706
|
+
function extractParams(params, location, operationContext, componentSchemas, componentExamples, opLabel) {
|
|
2118
3707
|
return params.filter((p) => p.in === location).map((p) => {
|
|
2119
|
-
const refTarget =
|
|
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;
|
|
2120
3729
|
return {
|
|
2121
3730
|
name: p.name,
|
|
2122
3731
|
type: p.schema ? schemaToTypeRef(p.schema, p.name, operationContext ? toPascalCase(operationContext) : void 0) : {
|
|
@@ -2124,32 +3733,74 @@ function extractParams(params, location, operationContext, componentSchemas) {
|
|
|
2124
3733
|
type: "string"
|
|
2125
3734
|
},
|
|
2126
3735
|
required: p.required ?? false,
|
|
2127
|
-
description: p.description,
|
|
3736
|
+
description: firstDescription(p.description, p.schema?.description, refTarget?.description),
|
|
2128
3737
|
deprecated: p.deprecated || p.schema?.deprecated || refTarget?.deprecated || void 0,
|
|
2129
3738
|
default: p.schema?.default ?? refTarget?.default,
|
|
2130
|
-
example
|
|
3739
|
+
example,
|
|
3740
|
+
...examples ? { examples } : {},
|
|
2131
3741
|
style: p.style,
|
|
2132
|
-
explode: p.explode
|
|
3742
|
+
explode: p.explode,
|
|
3743
|
+
...constraints ? { constraints } : {}
|
|
2133
3744
|
};
|
|
2134
3745
|
});
|
|
2135
3746
|
}
|
|
2136
3747
|
/**
|
|
2137
|
-
*
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2140
|
-
*
|
|
2141
|
-
* Refs are not pre-resolved by the bundler (`dereference: false` in refs.ts),
|
|
2142
|
-
* so without this, a param like `schema: { $ref: '.../PaginationOrder' }`
|
|
2143
|
-
* silently loses the target's `default: desc` and the SDK stops emitting the
|
|
2144
|
-
* 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.
|
|
2145
3752
|
*/
|
|
2146
|
-
function
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
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;
|
|
2153
3804
|
}
|
|
2154
3805
|
/**
|
|
2155
3806
|
* Collect the property schemas of every `oneOf` branch reachable from a request
|
|
@@ -2200,26 +3851,91 @@ function resolveSchemaRefObject(schema, componentSchemas) {
|
|
|
2200
3851
|
if (!match) return void 0;
|
|
2201
3852
|
return componentSchemas[match[1]];
|
|
2202
3853
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
3854
|
+
/** HTTP methods whose request body is the primary parameter carrier. */
|
|
3855
|
+
const BODY_CARRYING_METHODS = /* @__PURE__ */ new Set([
|
|
3856
|
+
"post",
|
|
3857
|
+
"put",
|
|
3858
|
+
"patch"
|
|
3859
|
+
]);
|
|
3860
|
+
/**
|
|
3861
|
+
* Pick the request body's schema and wire encoding from its content map, in
|
|
3862
|
+
* priority order. JSON wins over form encodings; binary and text bodies may
|
|
3863
|
+
* legitimately carry no schema at all.
|
|
3864
|
+
*/
|
|
3865
|
+
function selectRequestBodyContent(body) {
|
|
3866
|
+
const content = body?.content;
|
|
3867
|
+
if (!content) return { encoding: "json" };
|
|
3868
|
+
if (content["application/json"]?.schema) return {
|
|
3869
|
+
encoding: "json",
|
|
3870
|
+
schema: content["application/json"].schema
|
|
3871
|
+
};
|
|
3872
|
+
if (content["multipart/form-data"]?.schema) return {
|
|
3873
|
+
encoding: "form-data",
|
|
3874
|
+
schema: content["multipart/form-data"].schema
|
|
3875
|
+
};
|
|
3876
|
+
if (content["application/x-www-form-urlencoded"]?.schema) return {
|
|
3877
|
+
encoding: "form-urlencoded",
|
|
3878
|
+
schema: content["application/x-www-form-urlencoded"].schema
|
|
3879
|
+
};
|
|
3880
|
+
if (content["application/octet-stream"]) return {
|
|
3881
|
+
encoding: "binary",
|
|
3882
|
+
schema: content["application/octet-stream"].schema
|
|
3883
|
+
};
|
|
3884
|
+
if (content["text/plain"]) return {
|
|
3885
|
+
encoding: "text",
|
|
3886
|
+
schema: content["text/plain"].schema
|
|
3887
|
+
};
|
|
3888
|
+
return { encoding: "json" };
|
|
3889
|
+
}
|
|
3890
|
+
/** Parameter names referenced by any `x-mutually-exclusive-parameter-groups` variant. */
|
|
3891
|
+
function collectParameterGroupMemberNames(op) {
|
|
3892
|
+
const names = /* @__PURE__ */ new Set();
|
|
3893
|
+
const raw = op["x-mutually-exclusive-parameter-groups"];
|
|
3894
|
+
if (!raw || typeof raw !== "object") return names;
|
|
3895
|
+
for (const groupDef of Object.values(raw)) {
|
|
3896
|
+
const variants = groupDef && typeof groupDef === "object" ? groupDef.variants : void 0;
|
|
3897
|
+
if (!variants || typeof variants !== "object") continue;
|
|
3898
|
+
for (const members of Object.values(variants)) {
|
|
3899
|
+
if (!Array.isArray(members)) continue;
|
|
3900
|
+
for (const member of members) if (typeof member === "string") names.add(member);
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
return names;
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* Every property name a request body is guaranteed to accept, following
|
|
3907
|
+
* `$ref`s and descending through composition. `allOf` members all apply, so
|
|
3908
|
+
* their properties union. `oneOf` / `anyOf` alternatives are choices, so only
|
|
3909
|
+
* a property every non-null alternative declares counts — otherwise a caller
|
|
3910
|
+
* on another branch would have no slot for the value in either the body or
|
|
3911
|
+
* the query. Used to decide which `in: query` declarations are body-owned.
|
|
3912
|
+
*/
|
|
3913
|
+
function collectBodyPropertyNames(schema, componentSchemas, seen = /* @__PURE__ */ new Set()) {
|
|
3914
|
+
if (!schema) return /* @__PURE__ */ new Set();
|
|
3915
|
+
const ref = schema.$ref;
|
|
3916
|
+
if (ref) {
|
|
3917
|
+
if (seen.has(ref)) return /* @__PURE__ */ new Set();
|
|
3918
|
+
return collectBodyPropertyNames(resolveSchemaRefObject(schema, componentSchemas), componentSchemas, /* @__PURE__ */ new Set([...seen, ref]));
|
|
3919
|
+
}
|
|
3920
|
+
const names = new Set(Object.keys(schema.properties ?? {}));
|
|
3921
|
+
for (const sub of schema.allOf ?? []) for (const name of collectBodyPropertyNames(sub, componentSchemas, seen)) names.add(name);
|
|
3922
|
+
for (const alternatives of [schema.oneOf, schema.anyOf]) {
|
|
3923
|
+
const branches = (alternatives ?? []).filter((alt) => alt.type !== "null");
|
|
3924
|
+
if (branches.length === 0) continue;
|
|
3925
|
+
let common;
|
|
3926
|
+
for (const branch of branches) {
|
|
3927
|
+
const branchNames = collectBodyPropertyNames(branch, componentSchemas, seen);
|
|
3928
|
+
common = common ? new Set([...common].filter((name) => branchNames.has(name))) : branchNames;
|
|
3929
|
+
}
|
|
3930
|
+
for (const name of common ?? []) names.add(name);
|
|
2222
3931
|
}
|
|
3932
|
+
return names;
|
|
3933
|
+
}
|
|
3934
|
+
function extractRequestBody(body, op, pathModelBaseName, inlineModels, componentSchemas) {
|
|
3935
|
+
if (!body?.content) return {};
|
|
3936
|
+
const selected = selectRequestBodyContent(body);
|
|
3937
|
+
const encoding = selected.encoding;
|
|
3938
|
+
let schema = selected.schema;
|
|
2223
3939
|
if (!schema) {
|
|
2224
3940
|
if (encoding === "binary") return {
|
|
2225
3941
|
body: {
|
|
@@ -2238,24 +3954,26 @@ function extractRequestBody(body, op, inlineModels) {
|
|
|
2238
3954
|
};
|
|
2239
3955
|
return {};
|
|
2240
3956
|
}
|
|
2241
|
-
const contextName =
|
|
3957
|
+
const contextName = op?.operationId ? operationIdModelName(op.operationId, "Request") : pathModelBaseName + "Request";
|
|
2242
3958
|
if (schema.oneOf && schema.discriminator) {
|
|
2243
3959
|
const merged = flattenDiscriminatedUnionBody(schema);
|
|
2244
3960
|
if (merged) schema = merged;
|
|
2245
3961
|
}
|
|
2246
3962
|
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
2247
3963
|
const requiredSet = new Set(schema.required ?? []);
|
|
3964
|
+
const siblings = collectProperties(schema);
|
|
2248
3965
|
const fields = [];
|
|
2249
3966
|
for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
|
|
2250
3967
|
if (!fieldSchema) continue;
|
|
2251
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet));
|
|
3968
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet, componentSchemas, siblings));
|
|
2252
3969
|
}
|
|
2253
3970
|
inlineModels.push({
|
|
2254
3971
|
name: contextName,
|
|
2255
3972
|
description: void 0,
|
|
2256
|
-
fields
|
|
3973
|
+
fields,
|
|
3974
|
+
...constraintsEntry(schema)
|
|
2257
3975
|
});
|
|
2258
|
-
extractInlineModelsFromProperties(schema, inlineModels, contextName);
|
|
3976
|
+
extractInlineModelsFromProperties(schema, inlineModels, contextName, componentSchemas);
|
|
2259
3977
|
return {
|
|
2260
3978
|
body: {
|
|
2261
3979
|
kind: "model",
|
|
@@ -2266,23 +3984,29 @@ function extractRequestBody(body, op, inlineModels) {
|
|
|
2266
3984
|
}
|
|
2267
3985
|
if (schema.oneOf) {
|
|
2268
3986
|
const variants = [];
|
|
3987
|
+
const inlineObjectVariants = schema.oneOf.filter((v) => !v.$ref && v.properties && (v.type === "object" || !v.type));
|
|
3988
|
+
const contentNamed = [];
|
|
2269
3989
|
for (const variant of schema.oneOf) if (variant.$ref) {
|
|
2270
3990
|
const ref = schemaToTypeRef(variant, contextName);
|
|
2271
3991
|
variants.push(ref);
|
|
2272
3992
|
} else if (variant.type === "null") {} else if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
2273
|
-
const
|
|
3993
|
+
const named = deriveOneOfVariantName(variant, contextName, inlineModels, inlineObjectVariants, contentNamed, Boolean(op?.operationId));
|
|
3994
|
+
const variantName = named.name;
|
|
3995
|
+
if (named.fromContent) contentNamed.push(variantName);
|
|
2274
3996
|
const requiredSet = new Set(variant.required ?? []);
|
|
3997
|
+
const siblings = collectProperties(variant);
|
|
2275
3998
|
const fields = [];
|
|
2276
3999
|
for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
|
|
2277
4000
|
if (!fieldSchema) continue;
|
|
2278
|
-
fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet));
|
|
4001
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet, componentSchemas, siblings));
|
|
2279
4002
|
}
|
|
2280
4003
|
inlineModels.push({
|
|
2281
4004
|
name: variantName,
|
|
2282
4005
|
description: variant.description,
|
|
2283
|
-
fields
|
|
4006
|
+
fields,
|
|
4007
|
+
...constraintsEntry(variant)
|
|
2284
4008
|
});
|
|
2285
|
-
extractInlineModelsFromProperties(variant, inlineModels, variantName);
|
|
4009
|
+
extractInlineModelsFromProperties(variant, inlineModels, variantName, componentSchemas);
|
|
2286
4010
|
variants.push({
|
|
2287
4011
|
kind: "model",
|
|
2288
4012
|
name: variantName
|
|
@@ -2368,38 +4092,452 @@ function flattenDiscriminatedUnionBody(schema) {
|
|
|
2368
4092
|
type: "object",
|
|
2369
4093
|
properties,
|
|
2370
4094
|
required: order.filter((name) => name === discProp || requiredCounts[name] === variantCount),
|
|
2371
|
-
...schema.description ? { description: schema.description } : {}
|
|
4095
|
+
...schema.description ? { description: schema.description } : {},
|
|
4096
|
+
...extractConstraints(schema)
|
|
2372
4097
|
};
|
|
2373
4098
|
}
|
|
2374
|
-
/**
|
|
2375
|
-
|
|
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
|
+
};
|
|
2376
4135
|
if (variant.properties) {
|
|
2377
|
-
for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string")
|
|
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
|
+
}
|
|
2378
4143
|
}
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
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(",")}}`;
|
|
2386
4164
|
}
|
|
2387
|
-
return
|
|
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);
|
|
4513
|
+
}
|
|
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;
|
|
2388
4520
|
}
|
|
2389
|
-
function deriveResponseName(op,
|
|
2390
|
-
if (op?.operationId) return
|
|
2391
|
-
return
|
|
4521
|
+
function deriveResponseName(op, pathModelBaseName) {
|
|
4522
|
+
if (op?.operationId) return operationIdModelName(op.operationId, "Response");
|
|
4523
|
+
return pathModelBaseName + "Response";
|
|
2392
4524
|
}
|
|
2393
|
-
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
|
+
}) {
|
|
2394
4529
|
const errors = [];
|
|
2395
4530
|
let response = {
|
|
2396
4531
|
kind: "primitive",
|
|
2397
4532
|
type: "string"
|
|
2398
4533
|
};
|
|
4534
|
+
let responseHeaders;
|
|
2399
4535
|
let inlineModels = [];
|
|
2400
4536
|
let isPaginated = false;
|
|
2401
4537
|
let dataPath;
|
|
2402
4538
|
let itemType;
|
|
4539
|
+
let envelopeFields;
|
|
4540
|
+
let envelopeCompanions;
|
|
2403
4541
|
if (!responses) return {
|
|
2404
4542
|
response,
|
|
2405
4543
|
errors,
|
|
@@ -2407,8 +4545,11 @@ function extractResponses(responses, op, path, method, componentSchemas) {
|
|
|
2407
4545
|
isPaginated
|
|
2408
4546
|
};
|
|
2409
4547
|
const allSuccessResponses = [];
|
|
4548
|
+
const opLabel = operationLabel(op, path ?? "/", method ?? "get");
|
|
4549
|
+
const headerModels = [];
|
|
2410
4550
|
for (const [statusCode, resp] of Object.entries(responses)) {
|
|
2411
4551
|
const code = parseInt(statusCode, 10);
|
|
4552
|
+
const headers = code >= 200 ? extractResponseHeaders(resp.headers, opLabel, code, headerContext, headerModels) : void 0;
|
|
2412
4553
|
if (code >= 200 && code < 300) {
|
|
2413
4554
|
let extractedType = {
|
|
2414
4555
|
kind: "primitive",
|
|
@@ -2416,62 +4557,75 @@ function extractResponses(responses, op, path, method, componentSchemas) {
|
|
|
2416
4557
|
};
|
|
2417
4558
|
const jsonContent = resp.content?.["application/json"];
|
|
2418
4559
|
if (jsonContent?.schema) {
|
|
2419
|
-
const contextName = deriveResponseName(op,
|
|
4560
|
+
const contextName = deriveResponseName(op, pathModelBaseName);
|
|
2420
4561
|
const result = classifyAndExtractResponse(jsonContent.schema, contextName, componentSchemas);
|
|
2421
4562
|
extractedType = result.response;
|
|
2422
4563
|
inlineModels = result.inlineModels;
|
|
2423
4564
|
isPaginated = result.isPaginated;
|
|
2424
4565
|
dataPath = result.dataPath;
|
|
2425
4566
|
itemType = result.itemType;
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
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
|
+
}
|
|
2440
4585
|
}
|
|
2441
4586
|
}
|
|
2442
4587
|
allSuccessResponses.push({
|
|
2443
4588
|
statusCode: code,
|
|
2444
|
-
type: extractedType
|
|
4589
|
+
type: extractedType,
|
|
4590
|
+
...headers && { headers }
|
|
2445
4591
|
});
|
|
2446
4592
|
} else if (code >= 300 && code < 400) allSuccessResponses.push({
|
|
2447
4593
|
statusCode: code,
|
|
2448
4594
|
type: {
|
|
2449
4595
|
kind: "primitive",
|
|
2450
4596
|
type: "unknown"
|
|
2451
|
-
}
|
|
4597
|
+
},
|
|
4598
|
+
...headers && { headers }
|
|
2452
4599
|
});
|
|
2453
4600
|
else if (code >= 400) {
|
|
2454
4601
|
const jsonContent = resp.content?.["application/json"];
|
|
2455
4602
|
const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
|
|
2456
4603
|
errors.push({
|
|
2457
4604
|
statusCode: code,
|
|
2458
|
-
type
|
|
4605
|
+
type,
|
|
4606
|
+
...headers && { headers }
|
|
2459
4607
|
});
|
|
2460
4608
|
}
|
|
2461
4609
|
}
|
|
2462
4610
|
if (allSuccessResponses.length > 0) {
|
|
2463
4611
|
const sorted = [...allSuccessResponses].sort((a, b) => a.statusCode - b.statusCode);
|
|
2464
|
-
|
|
4612
|
+
const primary = sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0];
|
|
4613
|
+
response = primary.type;
|
|
4614
|
+
responseHeaders = primary.headers;
|
|
2465
4615
|
}
|
|
2466
4616
|
const successResponses = allSuccessResponses.length > 1 ? allSuccessResponses : void 0;
|
|
4617
|
+
if (headerModels.length > 0) inlineModels = [...inlineModels, ...headerModels];
|
|
2467
4618
|
return {
|
|
2468
4619
|
response,
|
|
4620
|
+
responseHeaders,
|
|
2469
4621
|
successResponses,
|
|
2470
4622
|
errors,
|
|
2471
4623
|
inlineModels,
|
|
2472
4624
|
isPaginated,
|
|
2473
4625
|
dataPath,
|
|
2474
|
-
itemType
|
|
4626
|
+
itemType,
|
|
4627
|
+
envelopeFields,
|
|
4628
|
+
envelopeCompanions
|
|
2475
4629
|
};
|
|
2476
4630
|
}
|
|
2477
4631
|
//#endregion
|
|
@@ -2480,10 +4634,25 @@ function keepLargestByName(models) {
|
|
|
2480
4634
|
const byName = /* @__PURE__ */ new Map();
|
|
2481
4635
|
for (const model of models) {
|
|
2482
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
|
+
}
|
|
2483
4641
|
if (!existing || model.fields.length > existing.fields.length) byName.set(model.name, model);
|
|
2484
4642
|
}
|
|
2485
4643
|
return [...byName.values()];
|
|
2486
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
|
+
}
|
|
2487
4656
|
function rewriteModelRef(ref, oldName, newName) {
|
|
2488
4657
|
walkTypeRef(ref, { model: (r) => {
|
|
2489
4658
|
if (r.name === oldName) r.name = newName;
|
|
@@ -2494,6 +4663,8 @@ function rewriteModelRefs(models, services, oldName, newName) {
|
|
|
2494
4663
|
for (const service of services) for (const op of service.operations) {
|
|
2495
4664
|
rewriteModelRef(op.response, oldName, newName);
|
|
2496
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);
|
|
2497
4668
|
}
|
|
2498
4669
|
}
|
|
2499
4670
|
function mergeInlineResponseModels(schemaModels, inlineModels) {
|
|
@@ -2516,15 +4687,25 @@ function mergeInlineResponseModels(schemaModels, inlineModels) {
|
|
|
2516
4687
|
});
|
|
2517
4688
|
return [...mergedSchemaModels, ...keepLargestByName(deduplicatedInlineModels)];
|
|
2518
4689
|
}
|
|
2519
|
-
|
|
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) {
|
|
2520
4697
|
const modelNames = new Set(existingModels.map((m) => m.name));
|
|
2521
4698
|
const modelsByName = new Map(existingModels.map((m) => [m.name, m]));
|
|
4699
|
+
const fieldList = (m) => m.fields.map((f) => f.name).join(", ") || "(no fields)";
|
|
2522
4700
|
const deduplicatedFieldModels = fieldInlineModels.filter((model) => {
|
|
2523
4701
|
if (!modelNames.has(model.name)) return true;
|
|
2524
4702
|
const existing = modelsByName.get(model.name);
|
|
2525
4703
|
const existingFieldNames = new Set(existing.fields.map((f) => f.name));
|
|
2526
4704
|
const inlineFieldNames = new Set(model.fields.map((f) => f.name));
|
|
2527
|
-
if (model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name)))
|
|
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
|
+
}
|
|
2528
4709
|
return false;
|
|
2529
4710
|
});
|
|
2530
4711
|
return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
|
|
@@ -2556,7 +4737,7 @@ function collapseJsonSuffixModels(models, services) {
|
|
|
2556
4737
|
if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
|
|
2557
4738
|
if (!baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name))) continue;
|
|
2558
4739
|
if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
|
|
2559
|
-
if (isModelReferencedByOthers(model.name, normalizedModels, new Set([model.name, baseName]))) continue;
|
|
4740
|
+
if (isModelReferencedByOthers(model.name, normalizedModels, /* @__PURE__ */ new Set([model.name, baseName]))) continue;
|
|
2560
4741
|
mergeCandidates.push({
|
|
2561
4742
|
jsonModel: model,
|
|
2562
4743
|
baseModel
|
|
@@ -2565,6 +4746,10 @@ function collapseJsonSuffixModels(models, services) {
|
|
|
2565
4746
|
const toRemove = /* @__PURE__ */ new Set();
|
|
2566
4747
|
for (const { jsonModel, baseModel } of mergeCandidates) {
|
|
2567
4748
|
baseModel.fields = jsonModel.fields;
|
|
4749
|
+
if (jsonModel.constraints || baseModel.constraints) baseModel.constraints = {
|
|
4750
|
+
...baseModel.constraints,
|
|
4751
|
+
...jsonModel.constraints
|
|
4752
|
+
};
|
|
2568
4753
|
toRemove.add(jsonModel.name);
|
|
2569
4754
|
rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
|
|
2570
4755
|
console.warn(`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`);
|
|
@@ -2600,16 +4785,82 @@ function collectInlineEnumsFromModels(models, enums) {
|
|
|
2600
4785
|
}
|
|
2601
4786
|
/**
|
|
2602
4787
|
* Collect inline enum definitions from operation parameters (query, path, header)
|
|
2603
|
-
*
|
|
4788
|
+
* list-envelope companion fields, and response headers, and promote them to
|
|
4789
|
+
* top-level enums so emitters can generate typed enum files.
|
|
2604
4790
|
*/
|
|
2605
4791
|
function collectInlineEnumsFromOperations(services, enums) {
|
|
2606
4792
|
const enumNames = new Set(enums.map((e) => e.name));
|
|
2607
|
-
for (const service of services) for (const op of service.operations)
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
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}`;
|
|
2613
4864
|
}
|
|
2614
4865
|
//#endregion
|
|
2615
4866
|
//#region src/parser/parameter-group-names.ts
|
|
@@ -2883,10 +5134,13 @@ function validateModelRefs(spec) {
|
|
|
2883
5134
|
for (const p of [
|
|
2884
5135
|
...op.pathParams,
|
|
2885
5136
|
...op.queryParams,
|
|
5137
|
+
...op.bodyOwnedQueryParams ?? [],
|
|
2886
5138
|
...op.headerParams
|
|
2887
5139
|
]) walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
|
|
2888
5140
|
if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
|
|
2889
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}`);
|
|
2890
5144
|
}
|
|
2891
5145
|
}
|
|
2892
5146
|
//#endregion
|
|
@@ -2897,11 +5151,12 @@ async function parseSpec(specPath, options) {
|
|
|
2897
5151
|
const version = spec.openapi ?? "";
|
|
2898
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}\`.`);
|
|
2899
5153
|
const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
|
|
2900
|
-
const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters);
|
|
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);
|
|
2901
5155
|
const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
|
|
2902
5156
|
const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
|
|
5157
|
+
const componentNames = new Set(Object.keys(spec.components?.schemas ?? {}).map((schemaName) => resolveSchemaName(schemaName)));
|
|
2903
5158
|
clearSchemaNameTransform();
|
|
2904
|
-
const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels), services);
|
|
5159
|
+
const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(responseNormalizedModels, fieldInlineModels, componentNames), services);
|
|
2905
5160
|
applyFieldHints(finalModels, options?.fieldHints);
|
|
2906
5161
|
collectInlineEnumsFromModels(finalModels, enums);
|
|
2907
5162
|
collectInlineEnumsFromOperations(services, enums);
|
|
@@ -2920,6 +5175,7 @@ async function parseSpec(specPath, options) {
|
|
|
2920
5175
|
models: finalModels,
|
|
2921
5176
|
enums,
|
|
2922
5177
|
auth,
|
|
5178
|
+
security: extractSecurityRequirements(spec.security, "the document"),
|
|
2923
5179
|
sdk: defaultSdkBehavior()
|
|
2924
5180
|
};
|
|
2925
5181
|
assignParameterGroupWrapperNames(result);
|
|
@@ -2942,20 +5198,47 @@ function applyFieldHints(models, fieldHints) {
|
|
|
2942
5198
|
}
|
|
2943
5199
|
}
|
|
2944
5200
|
}
|
|
2945
|
-
/**
|
|
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
|
+
*/
|
|
2946
5209
|
function extractAuthSchemes(securitySchemes) {
|
|
2947
5210
|
if (!securitySchemes) return void 0;
|
|
2948
5211
|
const schemes = [];
|
|
2949
|
-
for (const [, scheme] of Object.entries(securitySchemes))
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
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
|
+
}
|
|
2959
5242
|
return schemes.length > 0 ? schemes : void 0;
|
|
2960
5243
|
}
|
|
2961
5244
|
//#endregion
|
|
@@ -3049,29 +5332,47 @@ function createExampleBuilder(spec) {
|
|
|
3049
5332
|
for (const m of spec.models) modelsByName.set(m.name, m);
|
|
3050
5333
|
const enumsByName = /* @__PURE__ */ new Map();
|
|
3051
5334
|
for (const e of spec.enums) enumsByName.set(e.name, e);
|
|
3052
|
-
|
|
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) {
|
|
3053
5341
|
if (depth > MAX_DEPTH) return null;
|
|
3054
5342
|
switch (type.kind) {
|
|
3055
|
-
case "primitive": return primitiveExample(type.type, type.format);
|
|
5343
|
+
case "primitive": return primitiveExample(type.type, type.format, constraints, seed);
|
|
3056
5344
|
case "literal": return type.value;
|
|
3057
5345
|
case "enum":
|
|
3058
5346
|
if (type.values && type.values.length > 0) return type.values[0];
|
|
3059
5347
|
return enumsByName.get(type.name)?.values[0]?.value ?? null;
|
|
3060
|
-
case "array": return
|
|
3061
|
-
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);
|
|
3062
5350
|
case "union": {
|
|
3063
5351
|
const first = type.variants[0];
|
|
3064
|
-
return first ? buildForType(first, depth) : null;
|
|
3065
|
-
}
|
|
3066
|
-
case "map": {
|
|
3067
|
-
const key = "key";
|
|
3068
|
-
const value = buildForType(type.valueType, depth + 1);
|
|
3069
|
-
return { [key]: value };
|
|
5352
|
+
return first ? buildForType(first, depth, constraints, seed) : null;
|
|
3070
5353
|
}
|
|
5354
|
+
case "map": return mapExample(type, depth, constraints);
|
|
3071
5355
|
case "model": return buildForModel(type.name, depth + 1);
|
|
3072
5356
|
default: return null;
|
|
3073
5357
|
}
|
|
3074
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
|
+
}
|
|
3075
5376
|
function buildForModel(nameOrModel, depth) {
|
|
3076
5377
|
if (depth > MAX_DEPTH) return {};
|
|
3077
5378
|
const model = typeof nameOrModel === "string" ? modelsByName.get(nameOrModel) : nameOrModel;
|
|
@@ -3086,29 +5387,159 @@ function createExampleBuilder(spec) {
|
|
|
3086
5387
|
function buildForField(field, depth) {
|
|
3087
5388
|
if (field.example !== void 0) return field.example;
|
|
3088
5389
|
if (field.default !== void 0) return field.default;
|
|
3089
|
-
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);
|
|
3090
5396
|
}
|
|
3091
5397
|
return {
|
|
3092
|
-
forType: (type) => buildForType(type, 0),
|
|
5398
|
+
forType: (type, constraints) => buildForType(type, 0, constraints),
|
|
3093
5399
|
forField: (field) => buildForField(field, 0),
|
|
5400
|
+
forParameter: (param) => buildForParameter(param),
|
|
3094
5401
|
forModel: (model) => buildForModel(model, 0)
|
|
3095
5402
|
};
|
|
3096
5403
|
}
|
|
3097
|
-
|
|
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) {
|
|
3098
5419
|
switch (type) {
|
|
3099
|
-
case "string":
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
if (format === "uri" || format === "url") return "https://example.com";
|
|
3104
|
-
if (format === "uuid") return "00000000-0000-0000-0000-000000000000";
|
|
3105
|
-
return "string_example";
|
|
3106
|
-
case "integer":
|
|
3107
|
-
case "number": return 1;
|
|
3108
|
-
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;
|
|
3109
5424
|
default: return null;
|
|
3110
5425
|
}
|
|
3111
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
|
+
}
|
|
3112
5543
|
//#endregion
|
|
3113
5544
|
//#region src/snippets/runner.ts
|
|
3114
5545
|
/**
|
|
@@ -3257,14 +5688,12 @@ function findModel(ctx, name) {
|
|
|
3257
5688
|
return ctx.spec.models.find((m) => m.name === name);
|
|
3258
5689
|
}
|
|
3259
5690
|
function exampleForParam(p, examples) {
|
|
3260
|
-
|
|
3261
|
-
if (p.default !== void 0) return p.default;
|
|
3262
|
-
return examples.forType(p.type);
|
|
5691
|
+
return examples.forParameter(p);
|
|
3263
5692
|
}
|
|
3264
5693
|
function exampleForField(f, examples) {
|
|
3265
5694
|
return examples.forField(f);
|
|
3266
5695
|
}
|
|
3267
5696
|
//#endregion
|
|
3268
|
-
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 };
|
|
3269
5698
|
|
|
3270
5699
|
//# sourceMappingURL=index.mjs.map
|