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