@workos/oagen 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/cli/index.d.mts +2 -0
  2. package/dist/cli/index.mjs +6233 -0
  3. package/dist/cli/index.mjs.map +1 -0
  4. package/dist/compat/{index.d.ts → index.d.mts} +43 -114
  5. package/dist/compat/index.d.mts.map +1 -0
  6. package/dist/compat/index.mjs +4562 -0
  7. package/dist/compat/index.mjs.map +1 -0
  8. package/dist/diff-Cn6S74vV.mjs +2432 -0
  9. package/dist/diff-Cn6S74vV.mjs.map +1 -0
  10. package/dist/differ-B6472Yef.mjs +1100 -0
  11. package/dist/differ-B6472Yef.mjs.map +1 -0
  12. package/dist/index.d.mts +324 -0
  13. package/dist/index.d.mts.map +1 -0
  14. package/dist/index.mjs +1553 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/scripts/smoke/baseline.d.mts +1 -0
  17. package/dist/scripts/smoke/baseline.mjs +1951 -0
  18. package/dist/scripts/smoke/baseline.mjs.map +1 -0
  19. package/dist/scripts/smoke/sdk-test.d.mts +1 -0
  20. package/dist/scripts/smoke/sdk-test.mjs +111 -0
  21. package/dist/scripts/smoke/sdk-test.mjs.map +1 -0
  22. package/dist/smoke.d.mts +331 -0
  23. package/dist/smoke.d.mts.map +1 -0
  24. package/dist/smoke.mjs +2119 -0
  25. package/dist/smoke.mjs.map +1 -0
  26. package/dist/tree-sitter-DFRXfWg9.mjs +394 -0
  27. package/dist/tree-sitter-DFRXfWg9.mjs.map +1 -0
  28. package/dist/types-O25qZxMl.d.mts +397 -0
  29. package/dist/types-O25qZxMl.d.mts.map +1 -0
  30. package/dist/verify/index.d.mts +82 -0
  31. package/dist/verify/index.d.mts.map +1 -0
  32. package/dist/verify/index.mjs +327 -0
  33. package/dist/verify/index.mjs.map +1 -0
  34. package/package.json +19 -16
  35. package/dist/chunk-2JXFTDTO.js +0 -1669
  36. package/dist/chunk-CIX5NOBF.js +0 -263
  37. package/dist/chunk-IJ5E5AE7.js +0 -1040
  38. package/dist/cli/index.js +0 -5322
  39. package/dist/compat/index.js +0 -4491
  40. package/dist/index.d.ts +0 -277
  41. package/dist/index.js +0 -1565
  42. package/dist/scripts/smoke/baseline.js +0 -1813
  43. package/dist/scripts/smoke/sdk-test.js +0 -93
  44. package/dist/smoke.js +0 -2025
  45. package/dist/types-B5t9O9Wm.d.ts +0 -367
  46. package/dist/verify/index.d.ts +0 -76
  47. package/dist/verify/index.js +0 -336
@@ -0,0 +1,1951 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { BaseResolver, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
6
+ import "dotenv/config";
7
+ //#region src/errors.ts
8
+ /**
9
+ * Structured error hierarchy for oagen.
10
+ *
11
+ * Every error carries a human-readable `hint` that suggests a recovery action.
12
+ * The hint is appended to the `message` so it appears in stack traces and logs.
13
+ */
14
+ /** Base class for all oagen errors. */
15
+ var OagenError = class extends Error {
16
+ hint;
17
+ constructor(message, hint) {
18
+ const fullMessage = hint ? `${message}\nHint: ${hint}` : message;
19
+ super(fullMessage);
20
+ this.name = "OagenError";
21
+ this.hint = hint;
22
+ }
23
+ };
24
+ /** Thrown when an OpenAPI spec cannot be parsed or has an unsupported version. */
25
+ var SpecParseError = class extends OagenError {
26
+ constructor(message, hint) {
27
+ super(message, hint);
28
+ this.name = "SpecParseError";
29
+ }
30
+ };
31
+ //#endregion
32
+ //#region src/parser/refs.ts
33
+ async function loadAndBundleSpec(specPath) {
34
+ const absolutePath = resolve(specPath);
35
+ const document = makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href);
36
+ const config = await createConfig({});
37
+ const resolver = new BaseResolver();
38
+ const result = await bundleDocument({
39
+ document,
40
+ config: config.styleguide,
41
+ externalRefResolver: resolver,
42
+ dereference: false
43
+ });
44
+ if (result.problems.some((p) => p.severity === "error")) throw new SpecParseError(`Failed to parse spec: ${result.problems.filter((p) => p.severity === "error").map((p) => p.message).join("\n")}`, `Check the OpenAPI spec at "${absolutePath}" for syntax errors. Run a linter such as \`npx @redocly/cli lint ${specPath}\` to identify issues.`);
45
+ return {
46
+ parsed: result.bundle.parsed,
47
+ specPath: absolutePath
48
+ };
49
+ }
50
+ //#endregion
51
+ //#region src/ir/types.ts
52
+ /**
53
+ * Exhaustive check helper for TypeRef switches.
54
+ * If a new kind is added to TypeRef, any switch that doesn't handle it
55
+ * will fail to compile because `never` won't accept the unhandled variant.
56
+ *
57
+ * Usage:
58
+ * switch (ref.kind) {
59
+ * case 'primitive': ...
60
+ * case 'array': ...
61
+ * // If you forget 'literal', TypeScript errors here:
62
+ * default: assertNever(ref);
63
+ * }
64
+ */
65
+ function assertNever(x) {
66
+ const kind = x.kind;
67
+ const err = /* @__PURE__ */ new Error(`Unexpected TypeRef kind: ${kind}\nHint: If you added a new TypeRef variant, handle it in every switch/case that calls assertNever.`);
68
+ err.name = "InternalError";
69
+ throw err;
70
+ }
71
+ /**
72
+ * Generic depth-first walker for TypeRef trees.
73
+ * Handles recursion into array/nullable/union/map children and provides
74
+ * an exhaustive `assertNever` check so callers don't need to repeat the
75
+ * switch boilerplate. Supply callbacks only for the leaf kinds you care about.
76
+ */
77
+ function walkTypeRef(ref, visitor) {
78
+ switch (ref.kind) {
79
+ case "model":
80
+ visitor.model?.(ref);
81
+ break;
82
+ case "enum":
83
+ visitor.enum?.(ref);
84
+ break;
85
+ case "array":
86
+ walkTypeRef(ref.items, visitor);
87
+ break;
88
+ case "nullable":
89
+ walkTypeRef(ref.inner, visitor);
90
+ break;
91
+ case "union":
92
+ for (const v of ref.variants) walkTypeRef(v, visitor);
93
+ break;
94
+ case "map":
95
+ if (ref.keyType) walkTypeRef(ref.keyType, visitor);
96
+ walkTypeRef(ref.valueType, visitor);
97
+ break;
98
+ case "literal":
99
+ visitor.literal?.(ref);
100
+ break;
101
+ case "primitive":
102
+ visitor.primitive?.(ref);
103
+ break;
104
+ default: assertNever(ref);
105
+ }
106
+ }
107
+ //#endregion
108
+ //#region src/utils/naming.ts
109
+ /**
110
+ * Known compound tokens that the regex-based splitter over-splits.
111
+ * Each entry is [lowercase-word-sequence, canonical-form].
112
+ * Sorted longest-first so greedy matching works correctly.
113
+ */
114
+ const COMPOUND_WORDS = [[[
115
+ "m",
116
+ "2",
117
+ "m"
118
+ ], "M2M"], [["o", "auth"], "OAuth"]];
119
+ /**
120
+ * Recombine adjacent words that form a known compound token.
121
+ */
122
+ function recombineCompounds(words) {
123
+ const result = [];
124
+ let i = 0;
125
+ while (i < words.length) {
126
+ let matched = false;
127
+ for (const [pattern, canonical] of COMPOUND_WORDS) if (i + pattern.length <= words.length) {
128
+ if (pattern.every((p, j) => words[i + j].toLowerCase() === p)) {
129
+ result.push(canonical);
130
+ i += pattern.length;
131
+ matched = true;
132
+ break;
133
+ }
134
+ }
135
+ if (!matched) {
136
+ result.push(words[i]);
137
+ i++;
138
+ }
139
+ }
140
+ return result;
141
+ }
142
+ /**
143
+ * Split a string into words, handling:
144
+ * - camelCase / PascalCase boundaries
145
+ * - snake_case / kebab-case separators
146
+ * - Consecutive capitals (e.g., "HTTPClient" → ["HTTP", "Client"])
147
+ * - Numbers as word boundaries (e.g., "OAuth2Token" → ["OAuth", "2", "Token"])
148
+ * - Known compounds are recombined (e.g., "M2M" stays as one word)
149
+ */
150
+ function splitWords(s) {
151
+ if (!s) return [];
152
+ return recombineCompounds(s.replace(/[^a-zA-Z0-9_\-\s.]/g, "_").replace(/([a-z])([A-Z])/g, "$1\0$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1\0$2").replace(/([a-zA-Z])(\d)/g, "$1\0$2").replace(/(\d)([a-zA-Z])/g, "$1\0$2").split(/[\0_\-\s.]+/).filter((w) => w.length > 0));
153
+ }
154
+ const ACRONYM_SET = new Set([
155
+ "SSO",
156
+ "FGA",
157
+ "SAML",
158
+ "SCIM",
159
+ "JWT",
160
+ "HMAC",
161
+ "M2M"
162
+ ]);
163
+ function toPascalCase(s, acronyms) {
164
+ const merged = acronyms ? new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
165
+ return splitWords(s).map((w) => {
166
+ const upper = w.toUpperCase();
167
+ if (merged.has(upper)) return upper;
168
+ if (upper === "OAUTH") return "OAuth";
169
+ return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
170
+ }).join("");
171
+ }
172
+ function toCamelCase(s, acronyms) {
173
+ const merged = acronyms ? new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
174
+ const words = splitWords(s);
175
+ if (words.length === 0) return "";
176
+ return words.map((w, i) => {
177
+ if (i === 0) return w.toLowerCase();
178
+ const upper = w.toUpperCase();
179
+ if (merged.has(upper)) return upper;
180
+ if (upper === "OAUTH") return "OAuth";
181
+ return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
182
+ }).join("");
183
+ }
184
+ function toSnakeCase(s) {
185
+ return splitWords(s).map((w) => w.toLowerCase()).join("_");
186
+ }
187
+ function toUpperSnakeCase(s) {
188
+ return splitWords(s).map((w) => w.toUpperCase()).join("_");
189
+ }
190
+ const BACKEND_SUFFIXES = [
191
+ "Dto",
192
+ "DTO",
193
+ "Controller"
194
+ ];
195
+ function stripBackendSuffixes(name) {
196
+ for (const suffix of BACKEND_SUFFIXES) if (name.endsWith(suffix) && name.length > suffix.length) return name.slice(0, -suffix.length);
197
+ return name;
198
+ }
199
+ function stripBackendPrefixes(name) {
200
+ return name.replace(/Userland/g, "").replace(/Controller/g, "");
201
+ }
202
+ /**
203
+ * Remove ListItem / ByExternalId / ByResourceId / ForResource markers from PascalCase names.
204
+ * E.g. "DirectoriesListItemState" → "DirectoriesState"
205
+ */
206
+ function stripListItemMarkers(name) {
207
+ return name.replace(/ListItem/g, "").replace(/ByExternalId/g, "").replace(/ByResourceId/g, "").replace(/ForResource/g, "");
208
+ }
209
+ /** Words that look plural but must NOT be singularized. */
210
+ const SINGULAR_SAFE_LIST = new Set([
211
+ "Status",
212
+ "Address",
213
+ "Access",
214
+ "Process",
215
+ "Progress",
216
+ "Success",
217
+ "Radius",
218
+ "Canvas",
219
+ "Alias",
220
+ "Basis",
221
+ "Bus"
222
+ ]);
223
+ /**
224
+ * Conservative singularize for the leading PascalCase word.
225
+ * Only applied to the first word of a PascalCase name (the resource word).
226
+ * - `ies` → `y` (e.g. Directories → Directory)
227
+ * - trailing `s` for words >4 chars (e.g. Organizations → Organization)
228
+ * Safe-listed words are never singularized.
229
+ */
230
+ function singularize(word) {
231
+ if (SINGULAR_SAFE_LIST.has(word)) return word;
232
+ if (word.endsWith("ies") && word.length > 4) return word.slice(0, -3) + "y";
233
+ if (word.endsWith("ses") && word.length > 4) return word.slice(0, -2);
234
+ if (word.endsWith("s") && !word.endsWith("ss") && word.length > 4) return word.slice(0, -1);
235
+ return word;
236
+ }
237
+ function cleanSchemaName(name) {
238
+ let result = stripBackendPrefixes(stripBackendSuffixes(name));
239
+ result = stripListItemMarkers(result);
240
+ const match = result.match(/^([A-Z][a-z]*)/);
241
+ if (match) {
242
+ const leadWord = match[1];
243
+ const singular = singularize(leadWord);
244
+ if (singular !== leadWord) result = singular + result.slice(leadWord.length);
245
+ }
246
+ return result;
247
+ }
248
+ //#endregion
249
+ //#region src/parser/inline-models.ts
250
+ /**
251
+ * Qualify an inline model name with the parent schema name.
252
+ * If `parentName` is provided and the field name doesn't already start with
253
+ * the parent, the result is `${parentName}${PascalField}` with the trailing
254
+ * word singularized (e.g., Connection + Domains → ConnectionDomain).
255
+ */
256
+ function qualifyInlineModelName(baseName, parentName) {
257
+ if (!parentName) return baseName;
258
+ const cleanParent = stripListItemMarkers(parentName);
259
+ if (baseName.startsWith(cleanParent)) return baseName;
260
+ const trailingMatch = baseName.match(/^(.*?)([A-Z][a-z]*)$/);
261
+ if (trailingMatch) {
262
+ const [, prefix, trailingWord] = trailingMatch;
263
+ return `${cleanParent}${prefix}${singularize(trailingWord)}`;
264
+ }
265
+ return `${cleanParent}${baseName}`;
266
+ }
267
+ /**
268
+ * Walk all component schemas and extract inline Model definitions for fields
269
+ * that are objects with properties (or arrays of such objects).
270
+ * These correspond to the ModelRef entries created by schemaToTypeRef.
271
+ */
272
+ function extractInlineModelsFromSchemas(schemas) {
273
+ if (!schemas) return [];
274
+ const inlineModels = [];
275
+ for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, cleanSchemaName(toPascalCase(schemaName)));
276
+ return inlineModels;
277
+ }
278
+ function extractInlineModelsFromProperties(schema, results, parentName) {
279
+ const properties = schema.properties ?? {};
280
+ const allOfSchemas = schema.allOf ?? [];
281
+ for (const sub of allOfSchemas) if (sub.properties) extractInlineModelsFromProperties(sub, results, parentName);
282
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
283
+ if (!fieldSchema) continue;
284
+ if (fieldSchema.properties && (fieldSchema.type === "object" || !fieldSchema.type)) {
285
+ const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
286
+ results.push(buildInlineModel(modelName, fieldSchema));
287
+ extractInlineModelsFromProperties(fieldSchema, results, modelName);
288
+ }
289
+ if (fieldSchema.type === "array" && fieldSchema.items) {
290
+ const items = fieldSchema.items;
291
+ if (items.properties && (items.type === "object" || !items.type)) {
292
+ const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
293
+ results.push(buildInlineModel(modelName, items));
294
+ extractInlineModelsFromProperties(items, results, modelName);
295
+ }
296
+ }
297
+ if (fieldSchema.oneOf) {
298
+ const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
299
+ if (objectVariant) {
300
+ const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
301
+ if (!new Set(results.map((r) => r.name)).has(modelName)) {
302
+ results.push(buildInlineModel(modelName, objectVariant));
303
+ extractInlineModelsFromProperties(objectVariant, results, modelName);
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
309
+ function buildInlineModel(name, schema) {
310
+ const requiredSet = new Set(schema.required ?? []);
311
+ const fields = [];
312
+ for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
313
+ if (!fieldSchema) continue;
314
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
315
+ }
316
+ return {
317
+ name,
318
+ description: schema.description,
319
+ fields
320
+ };
321
+ }
322
+ //#endregion
323
+ //#region src/parser/schemas.ts
324
+ function extractSchemas(schemas) {
325
+ const enums = [];
326
+ if (!schemas) return {
327
+ models: [],
328
+ enums
329
+ };
330
+ const modelsByCleanName = /* @__PURE__ */ new Map();
331
+ for (const [name, schema] of Object.entries(schemas)) {
332
+ const pascalName = cleanSchemaName(toPascalCase(name));
333
+ if (schema.enum) enums.push(extractEnum(pascalName, schema));
334
+ else {
335
+ const model = extractModel(pascalName, schema, schemas);
336
+ const existing = modelsByCleanName.get(pascalName);
337
+ if (existing) {
338
+ if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
339
+ } else modelsByCleanName.set(pascalName, model);
340
+ }
341
+ }
342
+ const models = [...modelsByCleanName.values()];
343
+ const modelNames = new Set(models.map((m) => m.name));
344
+ const inlineQueue = [...models];
345
+ while (inlineQueue.length > 0) {
346
+ const model = inlineQueue.pop();
347
+ for (const field of model.fields) collectNestedInlineModels(field.type, field.name, model.name, schemas ?? {}, models, modelNames, inlineQueue);
348
+ }
349
+ for (const model of models) collectInlineEnums(model.fields, enums);
350
+ return {
351
+ models,
352
+ enums
353
+ };
354
+ }
355
+ /**
356
+ * Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
357
+ * Shared by both schemas.ts (field-level) and parse.ts (model-level) collection passes.
358
+ */
359
+ function collectInlineEnumFromRef(ref, enums, seen) {
360
+ walkTypeRef(ref, { enum: (r) => {
361
+ if (r.values && !seen.has(r.name)) {
362
+ seen.add(r.name);
363
+ enums.push({
364
+ name: r.name,
365
+ values: r.values.map((v) => ({
366
+ name: toUpperSnakeCase(String(v)),
367
+ value: v,
368
+ description: void 0
369
+ }))
370
+ });
371
+ }
372
+ } });
373
+ }
374
+ /**
375
+ * Walk model fields and extract inline enum refs as top-level enum definitions.
376
+ * This ensures type alias files are generated for inline enums.
377
+ */
378
+ function collectInlineEnums(fields, enums) {
379
+ const seen = new Set(enums.map((e) => e.name));
380
+ for (const field of fields) collectInlineEnumFromRef(field.type, enums, seen);
381
+ }
382
+ /**
383
+ * Walk a TypeRef tree and extract inline models for any model refs that point
384
+ * to schemas with inline object properties (not in components/schemas).
385
+ * This handles nested objects like `totp: { oneOf: [{ type: object, properties: {...} }] }`
386
+ * that schemaToTypeRef turns into model refs but aren't extracted from components.
387
+ */
388
+ function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
389
+ walkTypeRef(ref, { model: (modelRef) => {
390
+ if (modelNames.has(modelRef.name)) return;
391
+ if (Object.keys(schemas).some((k) => cleanSchemaName(toPascalCase(k)) === modelRef.name)) return;
392
+ const parentSchema = findSchemaByName(parentModelName, schemas);
393
+ if (!parentSchema) return;
394
+ const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
395
+ if (!fieldSchema) return;
396
+ const extracted = extractNestedSchema(modelRef.name, fieldSchema);
397
+ for (const m of extracted) if (!modelNames.has(m.name)) {
398
+ models.push(m);
399
+ modelNames.add(m.name);
400
+ queue.push(m);
401
+ }
402
+ } });
403
+ }
404
+ /** Find a component schema by PascalCase name. */
405
+ function findSchemaByName(pascalName, schemas) {
406
+ for (const [k, v] of Object.entries(schemas)) if (cleanSchemaName(toPascalCase(k)) === pascalName) return v;
407
+ return null;
408
+ }
409
+ /** Walk into a schema to find a field's sub-schema, resolving allOf. */
410
+ function findNestedFieldSchema(fieldName, parentSchema, schemas) {
411
+ if (parentSchema.properties?.[fieldName]) return parentSchema.properties[fieldName];
412
+ if (parentSchema.allOf) for (const sub of parentSchema.allOf) {
413
+ let resolved = sub;
414
+ if (sub.$ref) {
415
+ const segments = sub.$ref.split("/");
416
+ const refName = segments[segments.length - 1];
417
+ if (refName && schemas[refName]) resolved = schemas[refName];
418
+ }
419
+ if (resolved.properties?.[fieldName]) return resolved.properties[fieldName];
420
+ }
421
+ return null;
422
+ }
423
+ /** Extract a model (and nested models) from an inline field schema. */
424
+ function extractNestedSchema(name, schema) {
425
+ if (schema.oneOf) {
426
+ const models = [];
427
+ for (const variant of schema.oneOf) {
428
+ if (variant.$ref) continue;
429
+ if (variant.properties && (variant.type === "object" || !variant.type)) {
430
+ const requiredSet = new Set(variant.required ?? []);
431
+ const fields = [];
432
+ for (const [fn, fs] of Object.entries(variant.properties)) {
433
+ if (!fs) continue;
434
+ fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
435
+ }
436
+ const variantName = models.length === 0 ? name : `${name}${models.length + 1}`;
437
+ models.push({
438
+ name: variantName,
439
+ description: variant.description,
440
+ fields
441
+ });
442
+ }
443
+ }
444
+ return models;
445
+ }
446
+ if (schema.properties && (schema.type === "object" || !schema.type)) {
447
+ const requiredSet = new Set(schema.required ?? []);
448
+ const fields = [];
449
+ for (const [fn, fs] of Object.entries(schema.properties)) {
450
+ if (!fs) continue;
451
+ fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
452
+ }
453
+ return [{
454
+ name,
455
+ description: schema.description,
456
+ fields
457
+ }];
458
+ }
459
+ if (schema.type === "array" && schema.items?.properties) {
460
+ const requiredSet = new Set(schema.items.required ?? []);
461
+ const fields = [];
462
+ for (const [fn, fs] of Object.entries(schema.items.properties)) {
463
+ if (!fs) continue;
464
+ fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
465
+ }
466
+ return [{
467
+ name,
468
+ description: schema.items.description,
469
+ fields
470
+ }];
471
+ }
472
+ return [];
473
+ }
474
+ /** Build a single Field from a schema property entry. Shared across all extraction sites. */
475
+ function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet) {
476
+ return {
477
+ name: fieldName,
478
+ type: schemaToTypeRef(fieldSchema, fieldName, contextName),
479
+ required: requiredSet.has(fieldName),
480
+ description: fieldSchema.description,
481
+ readOnly: fieldSchema.readOnly || void 0,
482
+ writeOnly: fieldSchema.writeOnly || void 0,
483
+ deprecated: fieldSchema.deprecated || void 0,
484
+ default: fieldSchema.default,
485
+ example: fieldSchema.example
486
+ };
487
+ }
488
+ function extractEnum(name, schema) {
489
+ return {
490
+ name,
491
+ values: (schema.enum ?? []).map((v) => ({
492
+ name: toUpperSnakeCase(String(v)),
493
+ value: typeof v === "number" ? v : String(v),
494
+ description: void 0
495
+ }))
496
+ };
497
+ }
498
+ function extractModel(name, schema, schemas) {
499
+ if (schema.allOf) return extractAllOfModel(name, schema, schemas);
500
+ const requiredSet = new Set(schema.required ?? []);
501
+ const fields = [];
502
+ for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
503
+ if (!fieldSchema) continue;
504
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
505
+ }
506
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object" && schema.properties) {
507
+ if (Object.keys(schema.additionalProperties).length > 0) {
508
+ const valueType = schemaToTypeRef(schema.additionalProperties, "additionalProperties", name);
509
+ fields.push({
510
+ name: "additionalProperties",
511
+ type: {
512
+ kind: "map",
513
+ valueType
514
+ },
515
+ required: false,
516
+ description: "Additional properties not captured by named fields"
517
+ });
518
+ }
519
+ }
520
+ return {
521
+ name,
522
+ description: schema.description,
523
+ fields
524
+ };
525
+ }
526
+ function extractAllOfModel(name, schema, schemas) {
527
+ const fields = [];
528
+ const requiredSet = /* @__PURE__ */ new Set();
529
+ for (const subSchema of schema.allOf ?? []) {
530
+ let resolved = subSchema;
531
+ if (subSchema.$ref && schemas) {
532
+ const segments = subSchema.$ref.split("/");
533
+ const refName = segments[segments.length - 1];
534
+ if (refName && schemas[refName]) resolved = schemas[refName];
535
+ }
536
+ if (resolved.allOf) {
537
+ const nested = extractAllOfModel(name, resolved, schemas);
538
+ for (const f of nested.fields) {
539
+ fields.push({
540
+ ...f,
541
+ required: false
542
+ });
543
+ if (f.required) requiredSet.add(f.name);
544
+ }
545
+ } else {
546
+ if (resolved.required) for (const r of resolved.required) requiredSet.add(r);
547
+ if (resolved.properties) {
548
+ const emptyRequired = /* @__PURE__ */ new Set();
549
+ for (const [fieldName, fieldSchema] of Object.entries(resolved.properties)) {
550
+ if (!fieldSchema) continue;
551
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
552
+ }
553
+ }
554
+ }
555
+ }
556
+ if (schema.required) for (const r of schema.required) requiredSet.add(r);
557
+ for (const f of fields) f.required = requiredSet.has(f.name);
558
+ return {
559
+ name,
560
+ description: schema.description,
561
+ fields
562
+ };
563
+ }
564
+ function schemaToTypeRef(schema, contextName, parentModelName) {
565
+ if (schema.$ref) {
566
+ const segments = schema.$ref.split("/");
567
+ const rawName = segments[segments.length - 1];
568
+ return {
569
+ kind: "model",
570
+ name: cleanSchemaName(toPascalCase(rawName))
571
+ };
572
+ }
573
+ if (Array.isArray(schema.type)) {
574
+ const nonNullTypes = schema.type.filter((t) => t !== "null");
575
+ if (nonNullTypes.length === 0) return {
576
+ kind: "nullable",
577
+ inner: {
578
+ kind: "primitive",
579
+ type: "unknown"
580
+ }
581
+ };
582
+ if (schema.type.includes("null") && nonNullTypes.length === 1) return {
583
+ kind: "nullable",
584
+ inner: schemaToTypeRef({
585
+ ...schema,
586
+ type: nonNullTypes[0],
587
+ nullable: false
588
+ }, contextName)
589
+ };
590
+ if (nonNullTypes.length > 1) {
591
+ const ref = {
592
+ kind: "union",
593
+ variants: nonNullTypes.map((t) => schemaToTypeRef({
594
+ ...schema,
595
+ type: t
596
+ }, contextName))
597
+ };
598
+ if (schema.type.includes("null")) return {
599
+ kind: "nullable",
600
+ inner: ref
601
+ };
602
+ return ref;
603
+ }
604
+ }
605
+ if (schema.nullable && schema.type) return {
606
+ kind: "nullable",
607
+ inner: schemaToTypeRef({
608
+ ...schema,
609
+ nullable: false
610
+ }, contextName)
611
+ };
612
+ if (schema.allOf) {
613
+ const refItem = schema.allOf.find((s) => s.$ref);
614
+ if (refItem) {
615
+ if (schema.allOf.some((s) => !s.$ref && (s.properties || s.type === "object"))) return {
616
+ kind: "model",
617
+ name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
618
+ };
619
+ return schemaToTypeRef(refItem, contextName, parentModelName);
620
+ }
621
+ if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName);
622
+ if (schema.allOf.some((s) => s.properties)) return {
623
+ kind: "model",
624
+ name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
625
+ };
626
+ }
627
+ if (schema.oneOf || schema.anyOf) {
628
+ const compositionKind = schema.oneOf ? "oneOf" : "anyOf";
629
+ const rawVariants = schema.oneOf ?? schema.anyOf ?? [];
630
+ const nullVariant = rawVariants.find((v) => v.type === "null" || Array.isArray(v.type) && v.type.length === 1 && v.type[0] === "null");
631
+ const nonNullVariants = rawVariants.filter((v) => v !== nullVariant);
632
+ if (nullVariant && nonNullVariants.length === 1) return {
633
+ kind: "nullable",
634
+ inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
635
+ };
636
+ const variants = rawVariants.filter((v) => v.type !== "null").map((v) => schemaToTypeRef(v, contextName, parentModelName));
637
+ const hasNull = !!nullVariant;
638
+ const union = {
639
+ kind: "union",
640
+ variants,
641
+ compositionKind,
642
+ ...schema.discriminator ? { discriminator: {
643
+ property: schema.discriminator.propertyName,
644
+ mapping: Object.fromEntries(Object.entries(schema.discriminator.mapping ?? {}).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]))
645
+ } } : {}
646
+ };
647
+ return hasNull ? {
648
+ kind: "nullable",
649
+ inner: union
650
+ } : union;
651
+ }
652
+ if (schema.const !== void 0) {
653
+ if (typeof schema.const === "string" || typeof schema.const === "number" || typeof schema.const === "boolean") return {
654
+ kind: "literal",
655
+ value: schema.const
656
+ };
657
+ if (schema.const === null) return {
658
+ kind: "nullable",
659
+ inner: {
660
+ kind: "primitive",
661
+ type: "unknown"
662
+ }
663
+ };
664
+ if (typeof schema.const === "object") {
665
+ if (Array.isArray(schema.const)) return {
666
+ kind: "array",
667
+ items: {
668
+ kind: "primitive",
669
+ type: "unknown"
670
+ }
671
+ };
672
+ return {
673
+ kind: "map",
674
+ valueType: {
675
+ kind: "primitive",
676
+ type: "unknown"
677
+ }
678
+ };
679
+ }
680
+ }
681
+ if (schema.enum && schema.enum.length === 1) {
682
+ const v = schema.enum[0];
683
+ return {
684
+ kind: "literal",
685
+ value: typeof v === "number" ? v : String(v)
686
+ };
687
+ }
688
+ if (schema.enum) {
689
+ const baseName = toPascalCase(contextName ?? "UnknownEnum");
690
+ const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
691
+ return {
692
+ kind: "enum",
693
+ name: cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName,
694
+ values: schema.enum.map((v) => typeof v === "number" ? v : String(v))
695
+ };
696
+ }
697
+ if (schema.type === "array" && schema.items) return {
698
+ kind: "array",
699
+ items: schemaToTypeRef(schema.items, contextName, parentModelName)
700
+ };
701
+ if (!schema.type && schema.items) return {
702
+ kind: "array",
703
+ items: schemaToTypeRef(schema.items, contextName, parentModelName)
704
+ };
705
+ if (schema.type === "object" && schema.properties) return {
706
+ kind: "model",
707
+ name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
708
+ };
709
+ if (schema.type === "object" && !schema.properties) {
710
+ let valueType = {
711
+ kind: "primitive",
712
+ type: "unknown"
713
+ };
714
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
715
+ if (Object.keys(schema.additionalProperties).length > 0) valueType = schemaToTypeRef(schema.additionalProperties, contextName);
716
+ } else if (schema.patternProperties) {
717
+ const patterns = Object.values(schema.patternProperties);
718
+ if (patterns.length > 0) valueType = schemaToTypeRef(patterns[0], contextName);
719
+ }
720
+ return {
721
+ kind: "map",
722
+ valueType
723
+ };
724
+ }
725
+ const primitiveMap = {
726
+ string: "string",
727
+ integer: "integer",
728
+ number: "number",
729
+ boolean: "boolean"
730
+ };
731
+ const typeStr = Array.isArray(schema.type) ? schema.type[0] : schema.type;
732
+ if (typeStr && primitiveMap[typeStr]) return {
733
+ kind: "primitive",
734
+ type: primitiveMap[typeStr],
735
+ ...schema.format ? { format: schema.format } : {}
736
+ };
737
+ if (!schema.type && schema.properties) return {
738
+ kind: "model",
739
+ name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
740
+ };
741
+ if (!schema.type && !schema.$ref && !schema.oneOf && !schema.anyOf && !schema.allOf && !schema.enum && !schema.properties && !schema.items) return {
742
+ kind: "primitive",
743
+ type: "unknown"
744
+ };
745
+ if (contextName) console.warn(`[oagen] Warning: Unknown schema shape treated as unknown (context: ${contextName})`);
746
+ return {
747
+ kind: "primitive",
748
+ type: "unknown"
749
+ };
750
+ }
751
+ //#endregion
752
+ //#region src/parser/pagination.ts
753
+ const CURSOR_PARAMS = [
754
+ "cursor",
755
+ "after",
756
+ "before",
757
+ "starting_after",
758
+ "ending_before",
759
+ "page_token",
760
+ "next_token"
761
+ ];
762
+ const OFFSET_PARAMS = [
763
+ "offset",
764
+ "page",
765
+ "page_number",
766
+ "skip"
767
+ ];
768
+ const LIMIT_PARAMS = [
769
+ "limit",
770
+ "page_size",
771
+ "per_page",
772
+ "size",
773
+ "count"
774
+ ];
775
+ /**
776
+ * Detect if an operation uses pagination and return structured metadata
777
+ * for auto-paging iterator generation.
778
+ *
779
+ * Supports two strategies:
780
+ * 1. **Cursor-based**: query params include a cursor-like parameter
781
+ * 2. **Offset-based**: query params include both an offset-like and limit-like parameter
782
+ *
783
+ * Returns null if no pagination pattern is detected.
784
+ */
785
+ function detectPagination(response, queryParams, dataPath) {
786
+ const cursorParam = queryParams.find((p) => CURSOR_PARAMS.includes(p.name));
787
+ if (cursorParam) {
788
+ const itemType = response.kind === "array" ? response.items : response;
789
+ return {
790
+ strategy: "cursor",
791
+ param: cursorParam.name,
792
+ dataPath,
793
+ itemType
794
+ };
795
+ }
796
+ const offsetParam = queryParams.find((p) => OFFSET_PARAMS.includes(p.name));
797
+ const limitParam = queryParams.find((p) => LIMIT_PARAMS.includes(p.name));
798
+ if (offsetParam && limitParam) {
799
+ const itemType = response.kind === "array" ? response.items : response;
800
+ return {
801
+ strategy: "offset",
802
+ param: offsetParam.name,
803
+ limitParam: limitParam.name,
804
+ dataPath,
805
+ itemType
806
+ };
807
+ }
808
+ return null;
809
+ }
810
+ //#endregion
811
+ //#region src/parser/responses.ts
812
+ function classifyAndExtractResponse(schema, contextName) {
813
+ if (schema.$ref) return {
814
+ response: schemaToTypeRef(schema, contextName),
815
+ inlineModels: [],
816
+ isPaginated: false
817
+ };
818
+ const envelope = detectListEnvelope(schema);
819
+ if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath);
820
+ if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName);
821
+ return extractDirectResource(schema, contextName);
822
+ }
823
+ const KNOWN_DATA_PATHS = new Set([
824
+ "data",
825
+ "items",
826
+ "results",
827
+ "records",
828
+ "entries",
829
+ "values",
830
+ "nodes",
831
+ "edges"
832
+ ]);
833
+ const PAGINATION_METADATA_PATTERNS = [
834
+ "metadata",
835
+ "pagination",
836
+ "cursor",
837
+ "has_more",
838
+ "page_info",
839
+ "total",
840
+ "next_page",
841
+ "previous_page",
842
+ "offset"
843
+ ];
844
+ function detectListEnvelope(schema) {
845
+ const propSources = [];
846
+ if (schema.allOf) {
847
+ for (const sub of schema.allOf) if (sub.properties) propSources.push(sub.properties);
848
+ }
849
+ if (schema.properties) propSources.push(schema.properties);
850
+ if (propSources.length === 0) return {
851
+ isEnvelope: false,
852
+ dataPath: null
853
+ };
854
+ const mergedProps = {};
855
+ for (const source of propSources) Object.assign(mergedProps, source);
856
+ const arrayProps = [];
857
+ const nonArrayKeys = [];
858
+ for (const [key, propSchema] of Object.entries(mergedProps)) {
859
+ if (!propSchema) continue;
860
+ if (propSchema.type === "array") arrayProps.push(key);
861
+ else nonArrayKeys.push(key);
862
+ }
863
+ if (arrayProps.length === 1 && nonArrayKeys.length >= 1) {
864
+ const arrayPropName = arrayProps[0];
865
+ if (KNOWN_DATA_PATHS.has(arrayPropName)) return {
866
+ isEnvelope: true,
867
+ dataPath: arrayPropName
868
+ };
869
+ if (nonArrayKeys.some((key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern)))) return {
870
+ isEnvelope: true,
871
+ dataPath: arrayPropName
872
+ };
873
+ }
874
+ return {
875
+ isEnvelope: false,
876
+ dataPath: null
877
+ };
878
+ }
879
+ function extractListResponse(schema, contextName, dataPath) {
880
+ let itemTypeRef = {
881
+ kind: "primitive",
882
+ type: "string"
883
+ };
884
+ const inlineModels = [];
885
+ const propSources = [];
886
+ if (schema.allOf) {
887
+ for (const sub of schema.allOf) if (sub.properties) propSources.push(sub.properties);
888
+ }
889
+ if (schema.properties) propSources.push(schema.properties);
890
+ for (const props of propSources) {
891
+ const dataProp = props[dataPath];
892
+ if (!dataProp) continue;
893
+ if (dataProp.type === "array" && dataProp.items) {
894
+ const items = dataProp.items;
895
+ if (items.$ref) itemTypeRef = schemaToTypeRef(items, contextName);
896
+ else if (items.type === "object" && items.properties) {
897
+ const itemName = contextName.replace(/Response$/, "") + "Item";
898
+ itemTypeRef = {
899
+ kind: "model",
900
+ name: itemName
901
+ };
902
+ inlineModels.push(...extractInlineModel(itemName, items));
903
+ }
904
+ }
905
+ }
906
+ return {
907
+ response: {
908
+ kind: "array",
909
+ items: itemTypeRef
910
+ },
911
+ inlineModels,
912
+ isPaginated: true,
913
+ dataPath,
914
+ itemType: itemTypeRef
915
+ };
916
+ }
917
+ function hasDiscriminantConstField(schema) {
918
+ if (!schema.properties) return null;
919
+ for (const name of ["object", "type"]) {
920
+ const field = schema.properties[name];
921
+ if (!field) continue;
922
+ if (field.const !== void 0 && typeof field.const === "string") return {
923
+ property: name,
924
+ value: field.const
925
+ };
926
+ if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return {
927
+ property: name,
928
+ value: field.enum[0]
929
+ };
930
+ }
931
+ for (const [name, field] of Object.entries(schema.properties)) {
932
+ if (!field) continue;
933
+ if (field.const !== void 0 && typeof field.const === "string") return {
934
+ property: name,
935
+ value: field.const
936
+ };
937
+ if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return {
938
+ property: name,
939
+ value: field.enum[0]
940
+ };
941
+ }
942
+ return null;
943
+ }
944
+ function isSingleResourceWrapper(schema) {
945
+ if (schema.type !== "object") return false;
946
+ if (!schema.properties) return false;
947
+ let wrapperKey;
948
+ if (schema.required && schema.required.length === 1) wrapperKey = schema.required[0];
949
+ else {
950
+ const propKeys = Object.keys(schema.properties);
951
+ if (propKeys.length === 1) wrapperKey = propKeys[0];
952
+ }
953
+ if (!wrapperKey) return false;
954
+ const propSchema = schema.properties[wrapperKey];
955
+ if (!propSchema) return false;
956
+ if (propSchema.type === "object" && hasDiscriminantConstField(propSchema) !== null) return true;
957
+ if (propSchema.oneOf) return propSchema.oneOf.some((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
958
+ return false;
959
+ }
960
+ function extractWrappedResource(schema, contextName) {
961
+ const props = schema.properties;
962
+ const propSchema = props[schema.required && schema.required.length === 1 ? schema.required[0] : Object.keys(props)[0]];
963
+ const inlineModels = [];
964
+ const resourceName = contextName.replace(/Response$/, "");
965
+ if (propSchema.oneOf) {
966
+ const objectVariant = propSchema.oneOf.find((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
967
+ if (objectVariant) {
968
+ const modelName = deriveModelName(objectVariant, resourceName);
969
+ inlineModels.push(...extractInlineModel(modelName, objectVariant));
970
+ const hasNullVariant = propSchema.oneOf.some((v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null"));
971
+ const modelRef = {
972
+ kind: "model",
973
+ name: modelName
974
+ };
975
+ return {
976
+ response: hasNullVariant ? {
977
+ kind: "nullable",
978
+ inner: modelRef
979
+ } : modelRef,
980
+ inlineModels,
981
+ isPaginated: false
982
+ };
983
+ }
984
+ }
985
+ const modelName = deriveModelName(propSchema, resourceName);
986
+ inlineModels.push(...extractInlineModel(modelName, propSchema));
987
+ return {
988
+ response: {
989
+ kind: "model",
990
+ name: modelName
991
+ },
992
+ inlineModels,
993
+ isPaginated: false
994
+ };
995
+ }
996
+ function extractDirectResource(schema, contextName) {
997
+ const inlineModels = [];
998
+ if (schema.properties && (schema.type === "object" || !schema.type)) {
999
+ const modelName = deriveModelName(schema, contextName);
1000
+ inlineModels.push(...extractInlineModel(modelName, schema));
1001
+ return {
1002
+ response: {
1003
+ kind: "model",
1004
+ name: modelName
1005
+ },
1006
+ inlineModels,
1007
+ isPaginated: false
1008
+ };
1009
+ }
1010
+ if (schema.allOf) {
1011
+ const mergedProperties = {};
1012
+ const mergedRequired = [];
1013
+ for (const sub of schema.allOf) {
1014
+ if (sub.properties) Object.assign(mergedProperties, sub.properties);
1015
+ if (sub.required) mergedRequired.push(...sub.required);
1016
+ }
1017
+ if (Object.keys(mergedProperties).length > 0) {
1018
+ const merged = {
1019
+ type: "object",
1020
+ properties: mergedProperties,
1021
+ required: mergedRequired
1022
+ };
1023
+ const modelName = deriveModelName(merged, contextName);
1024
+ inlineModels.push(...extractInlineModel(modelName, merged));
1025
+ return {
1026
+ response: {
1027
+ kind: "model",
1028
+ name: modelName
1029
+ },
1030
+ inlineModels,
1031
+ isPaginated: false
1032
+ };
1033
+ }
1034
+ }
1035
+ if (schema.oneOf) {
1036
+ for (const variant of schema.oneOf) if (variant.properties && (variant.type === "object" || !variant.type)) {
1037
+ const modelName = deriveModelName(variant, contextName);
1038
+ if (!new Set(inlineModels.map((m) => m.name)).has(modelName)) inlineModels.push(...extractInlineModel(modelName, variant));
1039
+ }
1040
+ }
1041
+ if (schema.type === "array" && schema.items) {
1042
+ const items = schema.items;
1043
+ if (items.$ref) return {
1044
+ response: {
1045
+ kind: "array",
1046
+ items: schemaToTypeRef(items, contextName)
1047
+ },
1048
+ inlineModels,
1049
+ isPaginated: false
1050
+ };
1051
+ if (items.properties && (items.type === "object" || !items.type)) {
1052
+ const itemName = contextName.replace(/Response$/, "") + "Item";
1053
+ inlineModels.push(...extractInlineModel(itemName, items));
1054
+ return {
1055
+ response: {
1056
+ kind: "array",
1057
+ items: {
1058
+ kind: "model",
1059
+ name: itemName
1060
+ }
1061
+ },
1062
+ inlineModels,
1063
+ isPaginated: false
1064
+ };
1065
+ }
1066
+ }
1067
+ return {
1068
+ response: schemaToTypeRef(schema, contextName),
1069
+ inlineModels,
1070
+ isPaginated: false
1071
+ };
1072
+ }
1073
+ function deriveModelName(schema, fallback) {
1074
+ if (!schema.properties) return toPascalCase(fallback);
1075
+ for (const name of ["object", "type"]) {
1076
+ const field = schema.properties[name];
1077
+ if (field && field.const && typeof field.const === "string") return toPascalCase(field.const);
1078
+ }
1079
+ for (const [, field] of Object.entries(schema.properties)) if (field && field.const && typeof field.const === "string") return toPascalCase(field.const);
1080
+ return toPascalCase(fallback);
1081
+ }
1082
+ /**
1083
+ * Qualify a nested inline model name with its parent to avoid generic names.
1084
+ * e.g., parent="Connection" + field="domains" → "ConnectionDomain" (singularized)
1085
+ * Only qualifies when the field name alone would be too generic.
1086
+ */
1087
+ function qualifyNestedName(parentName, fieldName) {
1088
+ const pascalField = toPascalCase(fieldName);
1089
+ if (pascalField.startsWith(parentName)) return pascalField;
1090
+ const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
1091
+ const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
1092
+ if (match && match[2]) {
1093
+ const trailing = singularize(match[2]);
1094
+ return match[1] + trailing;
1095
+ }
1096
+ return qualified;
1097
+ }
1098
+ function extractInlineModel(name, schema) {
1099
+ const requiredSet = new Set(schema.required ?? []);
1100
+ const fields = [];
1101
+ const properties = schema.properties ?? {};
1102
+ const nestedModels = [];
1103
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1104
+ if (!fieldSchema) continue;
1105
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
1106
+ if (fieldSchema.type === "object" && fieldSchema.properties) {
1107
+ const nestedName = qualifyNestedName(name, fieldName);
1108
+ nestedModels.push(...extractInlineModel(nestedName, fieldSchema));
1109
+ }
1110
+ if (fieldSchema.type === "array" && fieldSchema.items) {
1111
+ const items = fieldSchema.items;
1112
+ if (items.type === "object" && items.properties) {
1113
+ const nestedName = qualifyNestedName(name, fieldName);
1114
+ nestedModels.push(...extractInlineModel(nestedName, items));
1115
+ }
1116
+ }
1117
+ if (fieldSchema.allOf) {
1118
+ const mergedProperties = {};
1119
+ const mergedRequired = [];
1120
+ for (const sub of fieldSchema.allOf) {
1121
+ if (sub.properties) Object.assign(mergedProperties, sub.properties);
1122
+ if (sub.required) mergedRequired.push(...sub.required);
1123
+ }
1124
+ if (Object.keys(mergedProperties).length > 0) {
1125
+ const nestedName = qualifyNestedName(name, fieldName);
1126
+ const merged = {
1127
+ type: "object",
1128
+ properties: mergedProperties,
1129
+ required: mergedRequired
1130
+ };
1131
+ if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, merged));
1132
+ }
1133
+ }
1134
+ if (fieldSchema.oneOf) {
1135
+ const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
1136
+ if (objectVariant) {
1137
+ const nestedName = qualifyNestedName(name, fieldName);
1138
+ if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, objectVariant));
1139
+ }
1140
+ }
1141
+ }
1142
+ return [{
1143
+ name,
1144
+ description: schema.description,
1145
+ fields
1146
+ }, ...nestedModels];
1147
+ }
1148
+ //#endregion
1149
+ //#region src/parser/operations.ts
1150
+ const HTTP_METHODS = [
1151
+ "get",
1152
+ "post",
1153
+ "put",
1154
+ "patch",
1155
+ "delete",
1156
+ "head",
1157
+ "options",
1158
+ "trace"
1159
+ ];
1160
+ function extractOperations(paths, operationIdTransform) {
1161
+ if (!paths) return {
1162
+ services: [],
1163
+ inlineModels: []
1164
+ };
1165
+ const serviceMap = /* @__PURE__ */ new Map();
1166
+ const inlineModels = [];
1167
+ for (const [path, pathItem] of Object.entries(paths)) {
1168
+ const pathLevelParams = pathItem.parameters ?? [];
1169
+ for (const method of HTTP_METHODS) {
1170
+ const op = pathItem[method];
1171
+ if (!op) continue;
1172
+ const serviceName = inferServiceName(path, op.tags?.[0]);
1173
+ const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform);
1174
+ inlineModels.push(...opModels);
1175
+ const ops = serviceMap.get(serviceName) ?? [];
1176
+ ops.push(operation);
1177
+ serviceMap.set(serviceName, ops);
1178
+ }
1179
+ }
1180
+ for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
1181
+ return {
1182
+ services: Array.from(serviceMap.entries()).map(([name, operations]) => ({
1183
+ name,
1184
+ operations
1185
+ })),
1186
+ inlineModels
1187
+ };
1188
+ }
1189
+ function inferServiceName(path, tag) {
1190
+ if (tag) return toPascalCase(tag);
1191
+ const first = path.split("/").filter(Boolean)[0] ?? "Default";
1192
+ if (first.startsWith("{")) return "Default";
1193
+ return toPascalCase(first);
1194
+ }
1195
+ function inferOperationName(method, path, operationId, operationIdTransform) {
1196
+ if (operationId) {
1197
+ if (operationIdTransform) return operationIdTransform(operationId);
1198
+ return toCamelCase(operationId);
1199
+ }
1200
+ const verb = inferVerb(method, path);
1201
+ const resource = inferResourceFromPath(path);
1202
+ return resource ? toCamelCase(`${verb}_${resource}`) : verb;
1203
+ }
1204
+ /**
1205
+ * Infer the CRUD verb from HTTP method and path shape.
1206
+ */
1207
+ function inferVerb(method, path) {
1208
+ const segments = path.split("/").filter(Boolean);
1209
+ const hasTrailingParam = segments.length > 0 && segments[segments.length - 1].startsWith("{");
1210
+ switch (method) {
1211
+ case "get": return hasTrailingParam ? "get" : "list";
1212
+ case "post": return "create";
1213
+ case "put":
1214
+ case "patch": return "update";
1215
+ case "delete": return "delete";
1216
+ case "head": return "check";
1217
+ case "options": return "options";
1218
+ case "trace": return "trace";
1219
+ default: return method;
1220
+ }
1221
+ }
1222
+ /**
1223
+ * Extract a resource noun from the path for operation naming.
1224
+ * Uses the last non-param segment (the resource being operated on).
1225
+ *
1226
+ * Examples:
1227
+ * /user_management/users → "Users"
1228
+ * /user_management/users/{id} → "User" (singular for item operations)
1229
+ * /user_management/users/{id}/auth_factors → "AuthFactors"
1230
+ * /organizations/{id}/api_keys → "ApiKeys"
1231
+ * /sso/authorize → "Authorize" (action endpoint)
1232
+ */
1233
+ function inferResourceFromPath(path) {
1234
+ const segments = path.split("/").filter(Boolean);
1235
+ if (segments.length <= 1) return null;
1236
+ let resourceSegment = null;
1237
+ for (let i = segments.length - 1; i >= 0; i--) if (!segments[i].startsWith("{")) {
1238
+ resourceSegment = segments[i];
1239
+ break;
1240
+ }
1241
+ if (!resourceSegment) return null;
1242
+ if (resourceSegment === segments[0]) return null;
1243
+ return toPascalCase(resourceSegment);
1244
+ }
1245
+ /**
1246
+ * Detect and resolve name collisions within a service's operations.
1247
+ *
1248
+ * When multiple operations share the same name (e.g., several "list" from
1249
+ * different sub-resources), disambiguate by appending the resource noun
1250
+ * from the path. Only renames operations on DIFFERENT paths — same-name
1251
+ * operations on the same path (e.g., PUT + PATCH both "update") are left as-is
1252
+ * since they represent the same logical resource.
1253
+ */
1254
+ function disambiguateOperationNames(operations) {
1255
+ const byName = /* @__PURE__ */ new Map();
1256
+ for (const op of operations) {
1257
+ const group = byName.get(op.name) ?? [];
1258
+ group.push(op);
1259
+ byName.set(op.name, group);
1260
+ }
1261
+ for (const [name, group] of byName) {
1262
+ if (group.length <= 1) continue;
1263
+ if (new Set(group.map((op) => op.path)).size <= 1) continue;
1264
+ for (const op of group) {
1265
+ const resource = inferResourceFromPath(op.path);
1266
+ if (resource) op.name = toCamelCase(`${name}_${resource}`);
1267
+ }
1268
+ const renamed = /* @__PURE__ */ new Map();
1269
+ for (const op of group) {
1270
+ const rGroup = renamed.get(op.name) ?? [];
1271
+ rGroup.push(op);
1272
+ renamed.set(op.name, rGroup);
1273
+ }
1274
+ for (const [, rGroup] of renamed) {
1275
+ if (rGroup.length <= 1) continue;
1276
+ for (const op of rGroup) {
1277
+ const deeper = inferDeeperContext(op.path);
1278
+ if (deeper) op.name = toCamelCase(`${op.name}_${deeper}`);
1279
+ }
1280
+ }
1281
+ }
1282
+ }
1283
+ /**
1284
+ * Extract a deeper disambiguation context from the path when the last
1285
+ * segment isn't unique enough. Uses the second-to-last non-param segment.
1286
+ *
1287
+ * /users/{id}/auth_factors → already captured as "AuthFactors"
1288
+ * /users/external_id/{external_id} → "ExternalId" (special path, not a sub-resource)
1289
+ */
1290
+ function inferDeeperContext(path) {
1291
+ const nonParams = path.split("/").filter(Boolean).filter((s) => !s.startsWith("{"));
1292
+ if (nonParams.length >= 3) return toPascalCase(nonParams.slice(1, -1).join("_"));
1293
+ return null;
1294
+ }
1295
+ function buildOperation(method, path, op, pathLevelParams, operationIdTransform) {
1296
+ const allParams = [...pathLevelParams, ...op.parameters ?? []];
1297
+ const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
1298
+ const pathParams = extractParams(allParams, "path");
1299
+ const queryParams = extractParams(allParams, "query");
1300
+ const headerParams = extractParams(allParams, "header").filter((p) => p.name.toLowerCase() !== "idempotency-key");
1301
+ const cookieParams = extractParams(allParams, "cookie");
1302
+ const reqBodyModels = [];
1303
+ const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
1304
+ const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method);
1305
+ inlineModels.push(...reqBodyModels);
1306
+ const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
1307
+ let pagination;
1308
+ if (isPaginated) {
1309
+ const itemType = responseItemType ?? (response.kind === "array" ? response.items : response);
1310
+ pagination = {
1311
+ strategy: paginationFromParams?.strategy ?? "cursor",
1312
+ param: paginationFromParams?.param ?? "after",
1313
+ limitParam: paginationFromParams?.limitParam,
1314
+ dataPath: responseDataPath ?? paginationFromParams?.dataPath,
1315
+ itemType
1316
+ };
1317
+ } else if (paginationFromParams) pagination = paginationFromParams;
1318
+ return {
1319
+ operation: {
1320
+ name: inferOperationName(method, path, op.operationId, operationIdTransform),
1321
+ description: buildDescription(op.summary, op.description),
1322
+ httpMethod: method,
1323
+ path,
1324
+ pathParams,
1325
+ queryParams,
1326
+ headerParams,
1327
+ cookieParams: cookieParams.length > 0 ? cookieParams : void 0,
1328
+ requestBody,
1329
+ requestBodyEncoding,
1330
+ response,
1331
+ successResponses,
1332
+ errors,
1333
+ pagination,
1334
+ injectIdempotencyKey: hasIdempotencyHeader,
1335
+ deprecated: op.deprecated || void 0,
1336
+ async: op["x-oagen-async"]
1337
+ },
1338
+ inlineModels
1339
+ };
1340
+ }
1341
+ function buildDescription(summary, description) {
1342
+ if (summary && description && summary !== description) return `${summary}\n\n${description}`;
1343
+ return description ?? summary;
1344
+ }
1345
+ function extractParams(params, location) {
1346
+ return params.filter((p) => p.in === location).map((p) => ({
1347
+ name: p.name,
1348
+ type: p.schema ? schemaToTypeRef(p.schema, p.name) : {
1349
+ kind: "primitive",
1350
+ type: "string"
1351
+ },
1352
+ required: p.required ?? false,
1353
+ description: p.description,
1354
+ deprecated: p.deprecated || p.schema?.deprecated || void 0,
1355
+ default: p.schema?.default,
1356
+ example: p.example ?? p.schema?.example
1357
+ }));
1358
+ }
1359
+ function extractRequestBody(body, op, inlineModels) {
1360
+ if (!body?.content) return {};
1361
+ let encoding = "json";
1362
+ let schema;
1363
+ if (body.content["application/json"]?.schema) {
1364
+ encoding = "json";
1365
+ schema = body.content["application/json"].schema;
1366
+ } else if (body.content["multipart/form-data"]?.schema) {
1367
+ encoding = "form-data";
1368
+ schema = body.content["multipart/form-data"].schema;
1369
+ } else if (body.content["application/x-www-form-urlencoded"]?.schema) {
1370
+ encoding = "form-urlencoded";
1371
+ schema = body.content["application/x-www-form-urlencoded"].schema;
1372
+ } else if (body.content["application/octet-stream"]) {
1373
+ encoding = "binary";
1374
+ schema = body.content["application/octet-stream"].schema;
1375
+ } else if (body.content["text/plain"]) {
1376
+ encoding = "text";
1377
+ schema = body.content["text/plain"].schema;
1378
+ }
1379
+ if (!schema) {
1380
+ if (encoding === "binary") return {
1381
+ body: {
1382
+ kind: "primitive",
1383
+ type: "string",
1384
+ format: "binary"
1385
+ },
1386
+ encoding
1387
+ };
1388
+ if (encoding === "text") return {
1389
+ body: {
1390
+ kind: "primitive",
1391
+ type: "string"
1392
+ },
1393
+ encoding
1394
+ };
1395
+ return {};
1396
+ }
1397
+ const contextName = cleanSchemaName(op?.operationId ? toPascalCase(op.operationId) + "Request" : "RequestBody");
1398
+ if (schema.properties && (schema.type === "object" || !schema.type)) {
1399
+ const requiredSet = new Set(schema.required ?? []);
1400
+ const fields = [];
1401
+ for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
1402
+ if (!fieldSchema) continue;
1403
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet));
1404
+ }
1405
+ inlineModels.push({
1406
+ name: contextName,
1407
+ description: void 0,
1408
+ fields
1409
+ });
1410
+ return {
1411
+ body: {
1412
+ kind: "model",
1413
+ name: contextName
1414
+ },
1415
+ encoding
1416
+ };
1417
+ }
1418
+ if (schema.oneOf) {
1419
+ const variants = [];
1420
+ for (const variant of schema.oneOf) if (variant.$ref) {
1421
+ const ref = schemaToTypeRef(variant, contextName);
1422
+ variants.push(ref);
1423
+ } else if (variant.type === "null") {} else if (variant.properties && (variant.type === "object" || !variant.type)) {
1424
+ const variantName = deriveOneOfVariantName(variant, contextName, inlineModels);
1425
+ const requiredSet = new Set(variant.required ?? []);
1426
+ const fields = [];
1427
+ for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
1428
+ if (!fieldSchema) continue;
1429
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet));
1430
+ }
1431
+ inlineModels.push({
1432
+ name: variantName,
1433
+ description: variant.description,
1434
+ fields
1435
+ });
1436
+ variants.push({
1437
+ kind: "model",
1438
+ name: variantName
1439
+ });
1440
+ } else variants.push(schemaToTypeRef(variant, contextName));
1441
+ const hasNull = schema.oneOf.some((v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null"));
1442
+ const union = {
1443
+ kind: "union",
1444
+ variants,
1445
+ ...schema.discriminator ? { discriminator: {
1446
+ property: schema.discriminator.propertyName,
1447
+ mapping: schema.discriminator.mapping ?? {}
1448
+ } } : {}
1449
+ };
1450
+ return {
1451
+ body: hasNull ? {
1452
+ kind: "nullable",
1453
+ inner: union
1454
+ } : union,
1455
+ encoding
1456
+ };
1457
+ }
1458
+ return {
1459
+ body: schemaToTypeRef(schema, contextName),
1460
+ encoding
1461
+ };
1462
+ }
1463
+ /** Derive a unique variant name for a oneOf inline model. */
1464
+ function deriveOneOfVariantName(variant, baseName, existingModels) {
1465
+ if (variant.properties) {
1466
+ for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string") return toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request";
1467
+ }
1468
+ const existingNames = new Set(existingModels.map((m) => m.name));
1469
+ let name = baseName;
1470
+ let suffix = 2;
1471
+ while (existingNames.has(name)) {
1472
+ name = `${baseName}${suffix}`;
1473
+ suffix++;
1474
+ }
1475
+ return name;
1476
+ }
1477
+ function deriveResponseName(op, path, method) {
1478
+ if (op?.operationId) return cleanSchemaName(toPascalCase(op.operationId) + "Response");
1479
+ return toPascalCase(path.split("/").filter(Boolean)[0] ?? "Unknown") + toPascalCase(method) + "Response";
1480
+ }
1481
+ function extractResponses(responses, op, path, method) {
1482
+ const errors = [];
1483
+ let response = {
1484
+ kind: "primitive",
1485
+ type: "string"
1486
+ };
1487
+ let inlineModels = [];
1488
+ let isPaginated = false;
1489
+ let dataPath;
1490
+ let itemType;
1491
+ if (!responses) return {
1492
+ response,
1493
+ errors,
1494
+ inlineModels,
1495
+ isPaginated
1496
+ };
1497
+ const allSuccessResponses = [];
1498
+ for (const [statusCode, resp] of Object.entries(responses)) {
1499
+ const code = parseInt(statusCode, 10);
1500
+ if (code >= 200 && code < 300) {
1501
+ let extractedType = {
1502
+ kind: "primitive",
1503
+ type: "unknown"
1504
+ };
1505
+ const jsonContent = resp.content?.["application/json"];
1506
+ if (jsonContent?.schema) {
1507
+ const contextName = deriveResponseName(op, path ?? "/", method ?? "get");
1508
+ const result = classifyAndExtractResponse(jsonContent.schema, contextName);
1509
+ extractedType = result.response;
1510
+ inlineModels = result.inlineModels;
1511
+ isPaginated = result.isPaginated;
1512
+ dataPath = result.dataPath;
1513
+ itemType = result.itemType;
1514
+ } else if (resp.content) if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) extractedType = {
1515
+ kind: "primitive",
1516
+ type: "string",
1517
+ format: "binary"
1518
+ };
1519
+ else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) extractedType = {
1520
+ kind: "primitive",
1521
+ type: "string"
1522
+ };
1523
+ else {
1524
+ const firstKey = Object.keys(resp.content)[0];
1525
+ if (firstKey) {
1526
+ const firstContent = resp.content[firstKey];
1527
+ if (firstContent?.schema) extractedType = schemaToTypeRef(firstContent.schema, deriveResponseName(op, path ?? "/", method ?? "get"));
1528
+ }
1529
+ }
1530
+ allSuccessResponses.push({
1531
+ statusCode: code,
1532
+ type: extractedType
1533
+ });
1534
+ } else if (code >= 400) {
1535
+ const jsonContent = resp.content?.["application/json"];
1536
+ const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
1537
+ errors.push({
1538
+ statusCode: code,
1539
+ type
1540
+ });
1541
+ }
1542
+ }
1543
+ if (allSuccessResponses.length > 0) {
1544
+ const sorted = [...allSuccessResponses].sort((a, b) => a.statusCode - b.statusCode);
1545
+ response = (sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0]).type;
1546
+ }
1547
+ const successResponses = allSuccessResponses.length > 1 ? allSuccessResponses : void 0;
1548
+ return {
1549
+ response,
1550
+ successResponses,
1551
+ errors,
1552
+ inlineModels,
1553
+ isPaginated,
1554
+ dataPath,
1555
+ itemType
1556
+ };
1557
+ }
1558
+ //#endregion
1559
+ //#region src/parser/normalize-inline-models.ts
1560
+ function keepLargestByName(models) {
1561
+ const byName = /* @__PURE__ */ new Map();
1562
+ for (const model of models) {
1563
+ const existing = byName.get(model.name);
1564
+ if (!existing || model.fields.length > existing.fields.length) byName.set(model.name, model);
1565
+ }
1566
+ return [...byName.values()];
1567
+ }
1568
+ function rewriteModelRef(ref, oldName, newName) {
1569
+ walkTypeRef(ref, { model: (r) => {
1570
+ if (r.name === oldName) r.name = newName;
1571
+ } });
1572
+ }
1573
+ function rewriteModelRefs(models, services, oldName, newName) {
1574
+ for (const model of models) for (const field of model.fields) rewriteModelRef(field.type, oldName, newName);
1575
+ for (const service of services) for (const op of service.operations) {
1576
+ rewriteModelRef(op.response, oldName, newName);
1577
+ if (op.requestBody) rewriteModelRef(op.requestBody, oldName, newName);
1578
+ }
1579
+ }
1580
+ function mergeInlineResponseModels(schemaModels, inlineModels) {
1581
+ const mergedSchemaModels = [...schemaModels];
1582
+ const schemaModelNames = new Set(mergedSchemaModels.map((m) => m.name));
1583
+ const schemaModelsByName = new Map(mergedSchemaModels.map((m) => [m.name, m]));
1584
+ const deduplicatedInlineModels = inlineModels.filter((model) => {
1585
+ if (!schemaModelNames.has(model.name)) return true;
1586
+ const existing = schemaModelsByName.get(model.name);
1587
+ const existingFieldNames = new Set(existing.fields.map((f) => f.name));
1588
+ const inlineFieldNames = new Set(model.fields.map((f) => f.name));
1589
+ const hasDifference = model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name));
1590
+ if (hasDifference && model.fields.length > existing.fields.length) {
1591
+ const idx = mergedSchemaModels.indexOf(existing);
1592
+ if (idx !== -1) mergedSchemaModels[idx] = model;
1593
+ schemaModelsByName.set(model.name, model);
1594
+ console.warn(`[oagen] Warning: Inline model "${model.name}" has more fields than component schema (${model.fields.length} vs ${existing.fields.length}) — using inline response model`);
1595
+ } else if (hasDifference) console.warn(`[oagen] Warning: Inline model "${model.name}" has different fields than component schema — using component schema`);
1596
+ return false;
1597
+ });
1598
+ return [...mergedSchemaModels, ...keepLargestByName(deduplicatedInlineModels)];
1599
+ }
1600
+ function mergeFieldInlineModels(existingModels, fieldInlineModels) {
1601
+ const modelNames = new Set(existingModels.map((m) => m.name));
1602
+ const modelsByName = new Map(existingModels.map((m) => [m.name, m]));
1603
+ const deduplicatedFieldModels = fieldInlineModels.filter((model) => {
1604
+ if (!modelNames.has(model.name)) return true;
1605
+ const existing = modelsByName.get(model.name);
1606
+ const existingFieldNames = new Set(existing.fields.map((f) => f.name));
1607
+ const inlineFieldNames = new Set(model.fields.map((f) => f.name));
1608
+ if (model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name))) console.warn(`[oagen] Warning: Inline model "${model.name}" has different fields than component schema — using component schema`);
1609
+ return false;
1610
+ });
1611
+ return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
1612
+ }
1613
+ function hasModelConstDiscriminant(model) {
1614
+ return model.fields.some((f) => (f.name === "object" || f.name === "type") && f.type.kind === "literal" && typeof f.type.value === "string");
1615
+ }
1616
+ function isModelReferencedByOthers(name, models, excludeNames) {
1617
+ for (const model of models) {
1618
+ if (excludeNames.has(model.name)) continue;
1619
+ for (const field of model.fields) {
1620
+ let found = false;
1621
+ walkTypeRef(field.type, { model: (r) => {
1622
+ if (r.name === name) found = true;
1623
+ } });
1624
+ if (found) return true;
1625
+ }
1626
+ }
1627
+ return false;
1628
+ }
1629
+ function collapseJsonSuffixModels(models, services) {
1630
+ const normalizedModels = [...models];
1631
+ const byName = new Map(normalizedModels.map((m) => [m.name, m]));
1632
+ const mergeCandidates = [];
1633
+ for (const model of normalizedModels) {
1634
+ if (!model.name.endsWith("Json")) continue;
1635
+ const baseName = model.name.slice(0, -4);
1636
+ const baseModel = byName.get(baseName);
1637
+ if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
1638
+ if (!baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name))) continue;
1639
+ if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
1640
+ if (isModelReferencedByOthers(model.name, normalizedModels, new Set([model.name, baseName]))) continue;
1641
+ mergeCandidates.push({
1642
+ jsonModel: model,
1643
+ baseModel
1644
+ });
1645
+ }
1646
+ const toRemove = /* @__PURE__ */ new Set();
1647
+ for (const { jsonModel, baseModel } of mergeCandidates) {
1648
+ baseModel.fields = jsonModel.fields;
1649
+ toRemove.add(jsonModel.name);
1650
+ rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
1651
+ console.warn(`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`);
1652
+ }
1653
+ return normalizedModels.filter((m) => !toRemove.has(m.name));
1654
+ }
1655
+ //#endregion
1656
+ //#region src/parser/collect-inline-enums.ts
1657
+ function collectInlineEnumsFromModels(models, enums) {
1658
+ const enumNames = new Set(enums.map((e) => e.name));
1659
+ for (const model of models) for (const field of model.fields) collectInlineEnumFromRef(field.type, enums, enumNames);
1660
+ }
1661
+ //#endregion
1662
+ //#region src/parser/normalize-model-refs.ts
1663
+ function validateModelRefs(spec) {
1664
+ const knownNames = /* @__PURE__ */ new Set();
1665
+ for (const m of spec.models) knownNames.add(m.name);
1666
+ for (const e of spec.enums) knownNames.add(e.name);
1667
+ function walkRef(ref, context) {
1668
+ walkTypeRef(ref, { model: (r) => {
1669
+ if (!knownNames.has(r.name)) console.warn(`[oagen] Warning: Unresolved model reference "${r.name}" (context: ${context})`);
1670
+ } });
1671
+ }
1672
+ for (const model of spec.models) for (const field of model.fields) walkRef(field.type, `${model.name}.${field.name}`);
1673
+ for (const service of spec.services) for (const op of service.operations) {
1674
+ for (const p of [
1675
+ ...op.pathParams,
1676
+ ...op.queryParams,
1677
+ ...op.headerParams
1678
+ ]) walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
1679
+ if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
1680
+ walkRef(op.response, `${service.name}.${op.name}.response`);
1681
+ }
1682
+ }
1683
+ //#endregion
1684
+ //#region src/parser/parse.ts
1685
+ async function parseSpec(specPath, options) {
1686
+ const { parsed } = await loadAndBundleSpec(specPath);
1687
+ const spec = parsed;
1688
+ const version = spec.openapi ?? "";
1689
+ 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}\`.`);
1690
+ const { models, enums } = extractSchemas(spec.components?.schemas);
1691
+ const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform);
1692
+ const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(mergeInlineResponseModels(models, inlineModels), extractInlineModelsFromSchemas(spec.components?.schemas)), services);
1693
+ collectInlineEnumsFromModels(finalModels, enums);
1694
+ const auth = extractAuthSchemes(spec.components?.securitySchemes);
1695
+ const serverEntries = (spec.servers ?? []).map((s) => ({
1696
+ url: s.url ?? "",
1697
+ description: s.description
1698
+ })).filter((s) => s.url);
1699
+ const result = {
1700
+ name: spec.info?.title ?? "Unknown API",
1701
+ version: spec.info?.version ?? "0.0.0",
1702
+ description: spec.info?.description,
1703
+ baseUrl: serverEntries[0]?.url ?? "",
1704
+ servers: serverEntries.length > 0 ? serverEntries : void 0,
1705
+ services,
1706
+ models: finalModels,
1707
+ enums,
1708
+ auth
1709
+ };
1710
+ validateModelRefs(result);
1711
+ return result;
1712
+ }
1713
+ /** Extract authentication schemes from OpenAPI securitySchemes. */
1714
+ function extractAuthSchemes(securitySchemes) {
1715
+ if (!securitySchemes) return void 0;
1716
+ const schemes = [];
1717
+ for (const [, scheme] of Object.entries(securitySchemes)) if (scheme.type === "http" && scheme.scheme === "bearer") schemes.push({ kind: "bearer" });
1718
+ else if (scheme.type === "apiKey" && scheme.in && scheme.name) schemes.push({
1719
+ kind: "apiKey",
1720
+ in: scheme.in,
1721
+ name: scheme.name
1722
+ });
1723
+ else if (scheme.type === "oauth2" && scheme.flows) schemes.push({
1724
+ kind: "oauth2",
1725
+ flows: scheme.flows
1726
+ });
1727
+ return schemes.length > 0 ? schemes : void 0;
1728
+ }
1729
+ //#endregion
1730
+ //#region scripts/smoke/shared.ts
1731
+ /** Operations that require complex preconditions and can't be auto-tested */
1732
+ let SKIP_OPERATIONS = /* @__PURE__ */ new Set();
1733
+ /** Services to skip entirely */
1734
+ let SKIP_SERVICES = /* @__PURE__ */ new Set();
1735
+ /**
1736
+ * Services that produce IDs other services depend on should run first.
1737
+ * Lower number = runs earlier. Unlisted services default to 50.
1738
+ */
1739
+ let SERVICE_PRIORITY = {};
1740
+ /** Load smoke test configuration from a JSON file */
1741
+ function loadSmokeConfig(configPath) {
1742
+ if (!configPath) return;
1743
+ const raw = readFileSync(configPath, "utf-8");
1744
+ const config = JSON.parse(raw);
1745
+ if (config.skipOperations) SKIP_OPERATIONS = new Set(config.skipOperations);
1746
+ if (config.skipServices) SKIP_SERVICES = new Set(config.skipServices);
1747
+ if (config.servicePriority) SERVICE_PRIORITY = config.servicePriority;
1748
+ if (config.servicePropertyMap) config.servicePropertyMap;
1749
+ }
1750
+ /**
1751
+ * Assign a numeric sort key to an operation so that ID-producing operations
1752
+ * (parameterless creates and lists) run before ID-consuming ones.
1753
+ */
1754
+ function operationSortKey(op) {
1755
+ const hasParams = op.pathParams.length > 0;
1756
+ const method = op.httpMethod;
1757
+ if (method === "post" && !hasParams) return 0;
1758
+ if (method === "get" && !hasParams && op.pagination) return 1;
1759
+ if (method === "get" && !hasParams) return 2;
1760
+ if (method === "post" && hasParams) return 3;
1761
+ if (method === "get" && hasParams && op.pagination) return 4;
1762
+ if (method === "get" && hasParams) return 5;
1763
+ if (method === "put" || method === "patch") return 6;
1764
+ if (method === "delete") return 7;
1765
+ return 99;
1766
+ }
1767
+ function planOperations(spec) {
1768
+ const groups = [];
1769
+ for (const service of spec.services) {
1770
+ if (SKIP_SERVICES.has(service.name)) continue;
1771
+ const ops = [];
1772
+ for (const op of service.operations) {
1773
+ if (SKIP_OPERATIONS.has(op.name)) continue;
1774
+ ops.push({
1775
+ service: service.name,
1776
+ operation: op
1777
+ });
1778
+ }
1779
+ if (ops.length === 0) continue;
1780
+ ops.sort((a, b) => {
1781
+ return operationSortKey(a.operation) - operationSortKey(b.operation);
1782
+ });
1783
+ groups.push({
1784
+ service: service.name,
1785
+ operations: ops
1786
+ });
1787
+ }
1788
+ groups.sort((a, b) => {
1789
+ return (SERVICE_PRIORITY[a.service] ?? 50) - (SERVICE_PRIORITY[b.service] ?? 50);
1790
+ });
1791
+ return groups;
1792
+ }
1793
+ function generateFixtureValue(typeRef, fieldName, spec) {
1794
+ switch (typeRef.kind) {
1795
+ case "primitive": return generatePrimitiveFixture(typeRef.type, typeRef.format, fieldName);
1796
+ case "array": return [generateFixtureValue(typeRef.items, fieldName, spec)];
1797
+ case "model": {
1798
+ const model = spec.models.find((m) => m.name === typeRef.name);
1799
+ if (model) {
1800
+ const obj = {};
1801
+ for (const field of model.fields) if (field.required) obj[toSnakeCase(field.name)] = generateFixtureValue(field.type, field.name, spec);
1802
+ return obj;
1803
+ }
1804
+ return {};
1805
+ }
1806
+ case "enum": return spec.enums.find((en) => en.name === typeRef.name)?.values[0]?.value ?? "unknown";
1807
+ case "nullable": return generateFixtureValue(typeRef.inner, fieldName, spec);
1808
+ case "union":
1809
+ if (typeRef.variants.length > 0) return generateFixtureValue(typeRef.variants[0], fieldName, spec);
1810
+ return null;
1811
+ }
1812
+ }
1813
+ function generatePrimitiveFixture(type, format, fieldName) {
1814
+ if (type === "string") {
1815
+ if (format === "uuid") return "550e8400-e29b-41d4-a716-446655440000";
1816
+ if (format === "date") return "2024-01-01";
1817
+ if (format === "date-time") return "2024-01-01T00:00:00Z";
1818
+ if (format === "email") return "test@example.com";
1819
+ if (format === "uri" || format === "url") return "https://example.com";
1820
+ return `test_${toSnakeCase(fieldName)}`;
1821
+ }
1822
+ if (type === "integer") return 1;
1823
+ if (type === "number") return 1;
1824
+ if (type === "boolean") return true;
1825
+ return null;
1826
+ }
1827
+ /** Generate a snake_case request body payload from an operation's requestBody type */
1828
+ function generatePayload(op, spec) {
1829
+ if (!op.requestBody) return null;
1830
+ const bodyType = op.requestBody;
1831
+ if (bodyType.kind === "model") {
1832
+ const model = spec.models.find((m) => m.name === bodyType.name);
1833
+ if (!model) return {};
1834
+ const payload = {};
1835
+ for (const field of model.fields) {
1836
+ if (!field.required) continue;
1837
+ const key = toSnakeCase(field.name);
1838
+ let value = generateFixtureValue(field.type, field.name, spec);
1839
+ if ((key === "name" || key === "slug") && typeof value === "string") value = `smoke-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1840
+ payload[key] = value;
1841
+ }
1842
+ return payload;
1843
+ }
1844
+ return {};
1845
+ }
1846
+ /** Generate query params for an operation (required params only, plus limit=1 for lists) */
1847
+ function generateQueryParams(op, spec) {
1848
+ const params = {};
1849
+ if (op.pagination) params["limit"] = "1";
1850
+ for (const p of op.queryParams) {
1851
+ if (!p.required) continue;
1852
+ const value = generateFixtureValue(p.type, p.name, spec);
1853
+ params[toSnakeCase(p.name)] = String(value);
1854
+ }
1855
+ const sorted = {};
1856
+ for (const key of Object.keys(params).sort()) sorted[key] = params[key];
1857
+ return sorted;
1858
+ }
1859
+ function resolvePath(op, pathParams) {
1860
+ let path = op.path;
1861
+ for (const [name, value] of Object.entries(pathParams)) path = path.replace(`{${name}}`, value);
1862
+ return path;
1863
+ }
1864
+ function parseCliArgs() {
1865
+ const args = process.argv.slice(2);
1866
+ const specIdx = args.indexOf("--spec");
1867
+ const spec = specIdx !== -1 && args[specIdx + 1] ? args[specIdx + 1] : process.env.OPENAPI_SPEC_PATH;
1868
+ if (!spec) {
1869
+ console.error("OpenAPI spec path is required. Set OPENAPI_SPEC_PATH env var or pass --spec <path>.");
1870
+ process.exit(1);
1871
+ }
1872
+ const sdkPathIdx = args.indexOf("--sdk-path");
1873
+ const sdkPath = sdkPathIdx !== -1 && args[sdkPathIdx + 1] ? args[sdkPathIdx + 1] : void 0;
1874
+ const configIdx = args.indexOf("--smoke-config");
1875
+ return {
1876
+ spec,
1877
+ sdkPath,
1878
+ smokeConfig: configIdx !== -1 && args[configIdx + 1] ? args[configIdx + 1] : process.env.SMOKE_CONFIG
1879
+ };
1880
+ }
1881
+ //#endregion
1882
+ //#region scripts/smoke/baseline.ts
1883
+ /**
1884
+ * Spec-only baseline generator.
1885
+ *
1886
+ * Generates expected request structure from the OpenAPI spec alone — no live
1887
+ * API needed. Produces a SmokeResults file that the diff tool can compare
1888
+ * against SDK smoke test output for offline CRITICAL-level verification.
1889
+ *
1890
+ * Usage:
1891
+ * OPENAPI_SPEC_PATH=path/to/spec.yaml npm run smoke:baseline
1892
+ */
1893
+ async function main() {
1894
+ const { spec: specPath, smokeConfig } = parseCliArgs();
1895
+ loadSmokeConfig(smokeConfig);
1896
+ console.log("Parsing spec...");
1897
+ const spec = await parseSpec(specPath);
1898
+ console.log(`Spec: ${spec.name} v${spec.version}`);
1899
+ const groups = planOperations(spec);
1900
+ const exchanges = [];
1901
+ for (const group of groups) for (const planned of group.operations) {
1902
+ const op = planned.operation;
1903
+ const operationId = `${group.service}.${op.name}`;
1904
+ const placeholderParams = {};
1905
+ for (const p of op.pathParams) placeholderParams[p.name] = "<ID>";
1906
+ const path = resolvePath(op, placeholderParams);
1907
+ const queryParams = generateQueryParams(op, spec);
1908
+ const body = generatePayload(op, spec);
1909
+ const status = {
1910
+ post: 201,
1911
+ get: 200,
1912
+ put: 200,
1913
+ patch: 200,
1914
+ delete: 204
1915
+ }[op.httpMethod] ?? 200;
1916
+ exchanges.push({
1917
+ operationId,
1918
+ service: group.service,
1919
+ operationName: op.name,
1920
+ request: {
1921
+ method: op.httpMethod.toUpperCase(),
1922
+ path,
1923
+ queryParams,
1924
+ body
1925
+ },
1926
+ response: {
1927
+ status,
1928
+ body: null
1929
+ },
1930
+ outcome: "success",
1931
+ durationMs: 0
1932
+ });
1933
+ }
1934
+ const results = {
1935
+ source: "spec-baseline",
1936
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1937
+ specVersion: spec.version,
1938
+ exchanges
1939
+ };
1940
+ writeFileSync("smoke-results-spec-baseline.json", JSON.stringify(results, null, 2));
1941
+ console.log(`\nGenerated ${exchanges.length} operations from spec`);
1942
+ console.log("Written to smoke-results-spec-baseline.json");
1943
+ }
1944
+ main().catch((err) => {
1945
+ console.error("Baseline generation failed:", err);
1946
+ process.exit(1);
1947
+ });
1948
+ //#endregion
1949
+ export {};
1950
+
1951
+ //# sourceMappingURL=baseline.mjs.map