@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.
- package/dist/cli/index.d.mts +2 -0
- package/dist/cli/index.mjs +6233 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/compat/{index.d.ts → index.d.mts} +43 -114
- package/dist/compat/index.d.mts.map +1 -0
- package/dist/compat/index.mjs +4562 -0
- package/dist/compat/index.mjs.map +1 -0
- package/dist/diff-Cn6S74vV.mjs +2432 -0
- package/dist/diff-Cn6S74vV.mjs.map +1 -0
- package/dist/differ-B6472Yef.mjs +1100 -0
- package/dist/differ-B6472Yef.mjs.map +1 -0
- package/dist/index.d.mts +324 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1553 -0
- package/dist/index.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.d.mts +1 -0
- package/dist/scripts/smoke/baseline.mjs +1951 -0
- package/dist/scripts/smoke/baseline.mjs.map +1 -0
- package/dist/scripts/smoke/sdk-test.d.mts +1 -0
- package/dist/scripts/smoke/sdk-test.mjs +111 -0
- package/dist/scripts/smoke/sdk-test.mjs.map +1 -0
- package/dist/smoke.d.mts +331 -0
- package/dist/smoke.d.mts.map +1 -0
- package/dist/smoke.mjs +2119 -0
- package/dist/smoke.mjs.map +1 -0
- package/dist/tree-sitter-DFRXfWg9.mjs +394 -0
- package/dist/tree-sitter-DFRXfWg9.mjs.map +1 -0
- package/dist/types-O25qZxMl.d.mts +397 -0
- package/dist/types-O25qZxMl.d.mts.map +1 -0
- package/dist/verify/index.d.mts +82 -0
- package/dist/verify/index.d.mts.map +1 -0
- package/dist/verify/index.mjs +327 -0
- package/dist/verify/index.mjs.map +1 -0
- package/package.json +19 -16
- package/dist/chunk-2JXFTDTO.js +0 -1669
- package/dist/chunk-CIX5NOBF.js +0 -263
- package/dist/chunk-IJ5E5AE7.js +0 -1040
- package/dist/cli/index.js +0 -5322
- package/dist/compat/index.js +0 -4491
- package/dist/index.d.ts +0 -277
- package/dist/index.js +0 -1565
- package/dist/scripts/smoke/baseline.js +0 -1813
- package/dist/scripts/smoke/sdk-test.js +0 -93
- package/dist/smoke.js +0 -2025
- package/dist/types-B5t9O9Wm.d.ts +0 -367
- package/dist/verify/index.d.ts +0 -76
- package/dist/verify/index.js +0 -336
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1553 @@
|
|
|
1
|
+
import { A as walkTypeRef, C as assertNever, D as collectModelRefs, E as collectFieldDependencies, O as collectRequestBodyModels, S as SpecParseError, T as collectEnumRefs, _ as ConfigVersionMismatchError, a as singularize, b as OagenError, c as stripListItemMarkers, d as toPascalCase, f as toSnakeCase, g as ConfigLoadError, h as ConfigError, k as mapTypeRef, l as toCamelCase, m as CommandError, n as ACRONYM_SET, p as toUpperSnakeCase, r as cleanSchemaName, s as stripBackendPrefixes, u as toKebabCase, v as ExtractorError, w as assignModelsToServices, x as RegistryError, y as InternalError } from "./tree-sitter-DFRXfWg9.mjs";
|
|
2
|
+
import { a as integrateGeneratedFiles, c as applyFileHeaders, d as generateAllFiles, f as generateFiles, i as generate, l as buildEmitterContext, n as getEmitter, o as mapFilesForTargetIntegration, r as registerEmitter, s as treeShakeFiles, t as diffSpecs, u as collectReferencedNames } from "./diff-Cn6S74vV.mjs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { BaseResolver, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
|
|
7
|
+
//#region src/parser/refs.ts
|
|
8
|
+
async function loadAndBundleSpec(specPath) {
|
|
9
|
+
const absolutePath = resolve(specPath);
|
|
10
|
+
const document = makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href);
|
|
11
|
+
const config = await createConfig({});
|
|
12
|
+
const resolver = new BaseResolver();
|
|
13
|
+
const result = await bundleDocument({
|
|
14
|
+
document,
|
|
15
|
+
config: config.styleguide,
|
|
16
|
+
externalRefResolver: resolver,
|
|
17
|
+
dereference: false
|
|
18
|
+
});
|
|
19
|
+
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.`);
|
|
20
|
+
return {
|
|
21
|
+
parsed: result.bundle.parsed,
|
|
22
|
+
specPath: absolutePath
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/parser/inline-models.ts
|
|
27
|
+
/**
|
|
28
|
+
* Qualify an inline model name with the parent schema name.
|
|
29
|
+
* If `parentName` is provided and the field name doesn't already start with
|
|
30
|
+
* the parent, the result is `${parentName}${PascalField}` with the trailing
|
|
31
|
+
* word singularized (e.g., Connection + Domains → ConnectionDomain).
|
|
32
|
+
*/
|
|
33
|
+
function qualifyInlineModelName(baseName, parentName) {
|
|
34
|
+
if (!parentName) return baseName;
|
|
35
|
+
const cleanParent = stripListItemMarkers(parentName);
|
|
36
|
+
if (baseName.startsWith(cleanParent)) return baseName;
|
|
37
|
+
const trailingMatch = baseName.match(/^(.*?)([A-Z][a-z]*)$/);
|
|
38
|
+
if (trailingMatch) {
|
|
39
|
+
const [, prefix, trailingWord] = trailingMatch;
|
|
40
|
+
return `${cleanParent}${prefix}${singularize(trailingWord)}`;
|
|
41
|
+
}
|
|
42
|
+
return `${cleanParent}${baseName}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Walk all component schemas and extract inline Model definitions for fields
|
|
46
|
+
* that are objects with properties (or arrays of such objects).
|
|
47
|
+
* These correspond to the ModelRef entries created by schemaToTypeRef.
|
|
48
|
+
*/
|
|
49
|
+
function extractInlineModelsFromSchemas(schemas) {
|
|
50
|
+
if (!schemas) return [];
|
|
51
|
+
const inlineModels = [];
|
|
52
|
+
for (const [schemaName, schema] of Object.entries(schemas)) extractInlineModelsFromProperties(schema, inlineModels, cleanSchemaName(toPascalCase(schemaName)));
|
|
53
|
+
return inlineModels;
|
|
54
|
+
}
|
|
55
|
+
function extractInlineModelsFromProperties(schema, results, parentName) {
|
|
56
|
+
const properties = schema.properties ?? {};
|
|
57
|
+
const allOfSchemas = schema.allOf ?? [];
|
|
58
|
+
for (const sub of allOfSchemas) if (sub.properties) extractInlineModelsFromProperties(sub, results, parentName);
|
|
59
|
+
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
|
60
|
+
if (!fieldSchema) continue;
|
|
61
|
+
if (fieldSchema.properties && (fieldSchema.type === "object" || !fieldSchema.type)) {
|
|
62
|
+
const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
|
|
63
|
+
results.push(buildInlineModel(modelName, fieldSchema));
|
|
64
|
+
extractInlineModelsFromProperties(fieldSchema, results, modelName);
|
|
65
|
+
}
|
|
66
|
+
if (fieldSchema.type === "array" && fieldSchema.items) {
|
|
67
|
+
const items = fieldSchema.items;
|
|
68
|
+
if (items.properties && (items.type === "object" || !items.type)) {
|
|
69
|
+
const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
|
|
70
|
+
results.push(buildInlineModel(modelName, items));
|
|
71
|
+
extractInlineModelsFromProperties(items, results, modelName);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (fieldSchema.oneOf) {
|
|
75
|
+
const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
|
|
76
|
+
if (objectVariant) {
|
|
77
|
+
const modelName = qualifyInlineModelName(toPascalCase(fieldName), parentName);
|
|
78
|
+
if (!new Set(results.map((r) => r.name)).has(modelName)) {
|
|
79
|
+
results.push(buildInlineModel(modelName, objectVariant));
|
|
80
|
+
extractInlineModelsFromProperties(objectVariant, results, modelName);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function buildInlineModel(name, schema) {
|
|
87
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
88
|
+
const fields = [];
|
|
89
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
|
|
90
|
+
if (!fieldSchema) continue;
|
|
91
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
name,
|
|
95
|
+
description: schema.description,
|
|
96
|
+
fields
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/parser/schemas.ts
|
|
101
|
+
function extractSchemas(schemas) {
|
|
102
|
+
const enums = [];
|
|
103
|
+
if (!schemas) return {
|
|
104
|
+
models: [],
|
|
105
|
+
enums
|
|
106
|
+
};
|
|
107
|
+
const modelsByCleanName = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
109
|
+
const pascalName = cleanSchemaName(toPascalCase(name));
|
|
110
|
+
if (schema.enum) enums.push(extractEnum(pascalName, schema));
|
|
111
|
+
else {
|
|
112
|
+
const model = extractModel(pascalName, schema, schemas);
|
|
113
|
+
const existing = modelsByCleanName.get(pascalName);
|
|
114
|
+
if (existing) {
|
|
115
|
+
if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
|
|
116
|
+
} else modelsByCleanName.set(pascalName, model);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const models = [...modelsByCleanName.values()];
|
|
120
|
+
const modelNames = new Set(models.map((m) => m.name));
|
|
121
|
+
const inlineQueue = [...models];
|
|
122
|
+
while (inlineQueue.length > 0) {
|
|
123
|
+
const model = inlineQueue.pop();
|
|
124
|
+
for (const field of model.fields) collectNestedInlineModels(field.type, field.name, model.name, schemas ?? {}, models, modelNames, inlineQueue);
|
|
125
|
+
}
|
|
126
|
+
for (const model of models) collectInlineEnums(model.fields, enums);
|
|
127
|
+
return {
|
|
128
|
+
models,
|
|
129
|
+
enums
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
|
|
134
|
+
* Shared by both schemas.ts (field-level) and parse.ts (model-level) collection passes.
|
|
135
|
+
*/
|
|
136
|
+
function collectInlineEnumFromRef(ref, enums, seen) {
|
|
137
|
+
walkTypeRef(ref, { enum: (r) => {
|
|
138
|
+
if (r.values && !seen.has(r.name)) {
|
|
139
|
+
seen.add(r.name);
|
|
140
|
+
enums.push({
|
|
141
|
+
name: r.name,
|
|
142
|
+
values: r.values.map((v) => ({
|
|
143
|
+
name: toUpperSnakeCase(String(v)),
|
|
144
|
+
value: v,
|
|
145
|
+
description: void 0
|
|
146
|
+
}))
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
} });
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Walk model fields and extract inline enum refs as top-level enum definitions.
|
|
153
|
+
* This ensures type alias files are generated for inline enums.
|
|
154
|
+
*/
|
|
155
|
+
function collectInlineEnums(fields, enums) {
|
|
156
|
+
const seen = new Set(enums.map((e) => e.name));
|
|
157
|
+
for (const field of fields) collectInlineEnumFromRef(field.type, enums, seen);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Walk a TypeRef tree and extract inline models for any model refs that point
|
|
161
|
+
* to schemas with inline object properties (not in components/schemas).
|
|
162
|
+
* This handles nested objects like `totp: { oneOf: [{ type: object, properties: {...} }] }`
|
|
163
|
+
* that schemaToTypeRef turns into model refs but aren't extracted from components.
|
|
164
|
+
*/
|
|
165
|
+
function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
|
|
166
|
+
walkTypeRef(ref, { model: (modelRef) => {
|
|
167
|
+
if (modelNames.has(modelRef.name)) return;
|
|
168
|
+
if (Object.keys(schemas).some((k) => cleanSchemaName(toPascalCase(k)) === modelRef.name)) return;
|
|
169
|
+
const parentSchema = findSchemaByName(parentModelName, schemas);
|
|
170
|
+
if (!parentSchema) return;
|
|
171
|
+
const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
|
|
172
|
+
if (!fieldSchema) return;
|
|
173
|
+
const extracted = extractNestedSchema(modelRef.name, fieldSchema);
|
|
174
|
+
for (const m of extracted) if (!modelNames.has(m.name)) {
|
|
175
|
+
models.push(m);
|
|
176
|
+
modelNames.add(m.name);
|
|
177
|
+
queue.push(m);
|
|
178
|
+
}
|
|
179
|
+
} });
|
|
180
|
+
}
|
|
181
|
+
/** Find a component schema by PascalCase name. */
|
|
182
|
+
function findSchemaByName(pascalName, schemas) {
|
|
183
|
+
for (const [k, v] of Object.entries(schemas)) if (cleanSchemaName(toPascalCase(k)) === pascalName) return v;
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
/** Walk into a schema to find a field's sub-schema, resolving allOf. */
|
|
187
|
+
function findNestedFieldSchema(fieldName, parentSchema, schemas) {
|
|
188
|
+
if (parentSchema.properties?.[fieldName]) return parentSchema.properties[fieldName];
|
|
189
|
+
if (parentSchema.allOf) for (const sub of parentSchema.allOf) {
|
|
190
|
+
let resolved = sub;
|
|
191
|
+
if (sub.$ref) {
|
|
192
|
+
const segments = sub.$ref.split("/");
|
|
193
|
+
const refName = segments[segments.length - 1];
|
|
194
|
+
if (refName && schemas[refName]) resolved = schemas[refName];
|
|
195
|
+
}
|
|
196
|
+
if (resolved.properties?.[fieldName]) return resolved.properties[fieldName];
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
/** Extract a model (and nested models) from an inline field schema. */
|
|
201
|
+
function extractNestedSchema(name, schema) {
|
|
202
|
+
if (schema.oneOf) {
|
|
203
|
+
const models = [];
|
|
204
|
+
for (const variant of schema.oneOf) {
|
|
205
|
+
if (variant.$ref) continue;
|
|
206
|
+
if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
207
|
+
const requiredSet = new Set(variant.required ?? []);
|
|
208
|
+
const fields = [];
|
|
209
|
+
for (const [fn, fs] of Object.entries(variant.properties)) {
|
|
210
|
+
if (!fs) continue;
|
|
211
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
212
|
+
}
|
|
213
|
+
const variantName = models.length === 0 ? name : `${name}${models.length + 1}`;
|
|
214
|
+
models.push({
|
|
215
|
+
name: variantName,
|
|
216
|
+
description: variant.description,
|
|
217
|
+
fields
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return models;
|
|
222
|
+
}
|
|
223
|
+
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
224
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
225
|
+
const fields = [];
|
|
226
|
+
for (const [fn, fs] of Object.entries(schema.properties)) {
|
|
227
|
+
if (!fs) continue;
|
|
228
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
229
|
+
}
|
|
230
|
+
return [{
|
|
231
|
+
name,
|
|
232
|
+
description: schema.description,
|
|
233
|
+
fields
|
|
234
|
+
}];
|
|
235
|
+
}
|
|
236
|
+
if (schema.type === "array" && schema.items?.properties) {
|
|
237
|
+
const requiredSet = new Set(schema.items.required ?? []);
|
|
238
|
+
const fields = [];
|
|
239
|
+
for (const [fn, fs] of Object.entries(schema.items.properties)) {
|
|
240
|
+
if (!fs) continue;
|
|
241
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
242
|
+
}
|
|
243
|
+
return [{
|
|
244
|
+
name,
|
|
245
|
+
description: schema.items.description,
|
|
246
|
+
fields
|
|
247
|
+
}];
|
|
248
|
+
}
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
/** Build a single Field from a schema property entry. Shared across all extraction sites. */
|
|
252
|
+
function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet) {
|
|
253
|
+
return {
|
|
254
|
+
name: fieldName,
|
|
255
|
+
type: schemaToTypeRef(fieldSchema, fieldName, contextName),
|
|
256
|
+
required: requiredSet.has(fieldName),
|
|
257
|
+
description: fieldSchema.description,
|
|
258
|
+
readOnly: fieldSchema.readOnly || void 0,
|
|
259
|
+
writeOnly: fieldSchema.writeOnly || void 0,
|
|
260
|
+
deprecated: fieldSchema.deprecated || void 0,
|
|
261
|
+
default: fieldSchema.default,
|
|
262
|
+
example: fieldSchema.example
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function extractEnum(name, schema) {
|
|
266
|
+
return {
|
|
267
|
+
name,
|
|
268
|
+
values: (schema.enum ?? []).map((v) => ({
|
|
269
|
+
name: toUpperSnakeCase(String(v)),
|
|
270
|
+
value: typeof v === "number" ? v : String(v),
|
|
271
|
+
description: void 0
|
|
272
|
+
}))
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function extractModel(name, schema, schemas) {
|
|
276
|
+
if (schema.allOf) return extractAllOfModel(name, schema, schemas);
|
|
277
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
278
|
+
const fields = [];
|
|
279
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
|
|
280
|
+
if (!fieldSchema) continue;
|
|
281
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
282
|
+
}
|
|
283
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object" && schema.properties) {
|
|
284
|
+
if (Object.keys(schema.additionalProperties).length > 0) {
|
|
285
|
+
const valueType = schemaToTypeRef(schema.additionalProperties, "additionalProperties", name);
|
|
286
|
+
fields.push({
|
|
287
|
+
name: "additionalProperties",
|
|
288
|
+
type: {
|
|
289
|
+
kind: "map",
|
|
290
|
+
valueType
|
|
291
|
+
},
|
|
292
|
+
required: false,
|
|
293
|
+
description: "Additional properties not captured by named fields"
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
name,
|
|
299
|
+
description: schema.description,
|
|
300
|
+
fields
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function extractAllOfModel(name, schema, schemas) {
|
|
304
|
+
const fields = [];
|
|
305
|
+
const requiredSet = /* @__PURE__ */ new Set();
|
|
306
|
+
for (const subSchema of schema.allOf ?? []) {
|
|
307
|
+
let resolved = subSchema;
|
|
308
|
+
if (subSchema.$ref && schemas) {
|
|
309
|
+
const segments = subSchema.$ref.split("/");
|
|
310
|
+
const refName = segments[segments.length - 1];
|
|
311
|
+
if (refName && schemas[refName]) resolved = schemas[refName];
|
|
312
|
+
}
|
|
313
|
+
if (resolved.allOf) {
|
|
314
|
+
const nested = extractAllOfModel(name, resolved, schemas);
|
|
315
|
+
for (const f of nested.fields) {
|
|
316
|
+
fields.push({
|
|
317
|
+
...f,
|
|
318
|
+
required: false
|
|
319
|
+
});
|
|
320
|
+
if (f.required) requiredSet.add(f.name);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
if (resolved.required) for (const r of resolved.required) requiredSet.add(r);
|
|
324
|
+
if (resolved.properties) {
|
|
325
|
+
const emptyRequired = /* @__PURE__ */ new Set();
|
|
326
|
+
for (const [fieldName, fieldSchema] of Object.entries(resolved.properties)) {
|
|
327
|
+
if (!fieldSchema) continue;
|
|
328
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (schema.required) for (const r of schema.required) requiredSet.add(r);
|
|
334
|
+
for (const f of fields) f.required = requiredSet.has(f.name);
|
|
335
|
+
return {
|
|
336
|
+
name,
|
|
337
|
+
description: schema.description,
|
|
338
|
+
fields
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
342
|
+
if (schema.$ref) {
|
|
343
|
+
const segments = schema.$ref.split("/");
|
|
344
|
+
const rawName = segments[segments.length - 1];
|
|
345
|
+
return {
|
|
346
|
+
kind: "model",
|
|
347
|
+
name: cleanSchemaName(toPascalCase(rawName))
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (Array.isArray(schema.type)) {
|
|
351
|
+
const nonNullTypes = schema.type.filter((t) => t !== "null");
|
|
352
|
+
if (nonNullTypes.length === 0) return {
|
|
353
|
+
kind: "nullable",
|
|
354
|
+
inner: {
|
|
355
|
+
kind: "primitive",
|
|
356
|
+
type: "unknown"
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
if (schema.type.includes("null") && nonNullTypes.length === 1) return {
|
|
360
|
+
kind: "nullable",
|
|
361
|
+
inner: schemaToTypeRef({
|
|
362
|
+
...schema,
|
|
363
|
+
type: nonNullTypes[0],
|
|
364
|
+
nullable: false
|
|
365
|
+
}, contextName)
|
|
366
|
+
};
|
|
367
|
+
if (nonNullTypes.length > 1) {
|
|
368
|
+
const ref = {
|
|
369
|
+
kind: "union",
|
|
370
|
+
variants: nonNullTypes.map((t) => schemaToTypeRef({
|
|
371
|
+
...schema,
|
|
372
|
+
type: t
|
|
373
|
+
}, contextName))
|
|
374
|
+
};
|
|
375
|
+
if (schema.type.includes("null")) return {
|
|
376
|
+
kind: "nullable",
|
|
377
|
+
inner: ref
|
|
378
|
+
};
|
|
379
|
+
return ref;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (schema.nullable && schema.type) return {
|
|
383
|
+
kind: "nullable",
|
|
384
|
+
inner: schemaToTypeRef({
|
|
385
|
+
...schema,
|
|
386
|
+
nullable: false
|
|
387
|
+
}, contextName)
|
|
388
|
+
};
|
|
389
|
+
if (schema.allOf) {
|
|
390
|
+
const refItem = schema.allOf.find((s) => s.$ref);
|
|
391
|
+
if (refItem) {
|
|
392
|
+
if (schema.allOf.some((s) => !s.$ref && (s.properties || s.type === "object"))) return {
|
|
393
|
+
kind: "model",
|
|
394
|
+
name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
|
|
395
|
+
};
|
|
396
|
+
return schemaToTypeRef(refItem, contextName, parentModelName);
|
|
397
|
+
}
|
|
398
|
+
if (schema.allOf.length === 1) return schemaToTypeRef(schema.allOf[0], contextName, parentModelName);
|
|
399
|
+
if (schema.allOf.some((s) => s.properties)) return {
|
|
400
|
+
kind: "model",
|
|
401
|
+
name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (schema.oneOf || schema.anyOf) {
|
|
405
|
+
const compositionKind = schema.oneOf ? "oneOf" : "anyOf";
|
|
406
|
+
const rawVariants = schema.oneOf ?? schema.anyOf ?? [];
|
|
407
|
+
const nullVariant = rawVariants.find((v) => v.type === "null" || Array.isArray(v.type) && v.type.length === 1 && v.type[0] === "null");
|
|
408
|
+
const nonNullVariants = rawVariants.filter((v) => v !== nullVariant);
|
|
409
|
+
if (nullVariant && nonNullVariants.length === 1) return {
|
|
410
|
+
kind: "nullable",
|
|
411
|
+
inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
|
|
412
|
+
};
|
|
413
|
+
const variants = rawVariants.filter((v) => v.type !== "null").map((v) => schemaToTypeRef(v, contextName, parentModelName));
|
|
414
|
+
const hasNull = !!nullVariant;
|
|
415
|
+
const union = {
|
|
416
|
+
kind: "union",
|
|
417
|
+
variants,
|
|
418
|
+
compositionKind,
|
|
419
|
+
...schema.discriminator ? { discriminator: {
|
|
420
|
+
property: schema.discriminator.propertyName,
|
|
421
|
+
mapping: Object.fromEntries(Object.entries(schema.discriminator.mapping ?? {}).map(([k, v]) => [k, v.replace(/^#\/components\/schemas\//, "")]))
|
|
422
|
+
} } : {}
|
|
423
|
+
};
|
|
424
|
+
return hasNull ? {
|
|
425
|
+
kind: "nullable",
|
|
426
|
+
inner: union
|
|
427
|
+
} : union;
|
|
428
|
+
}
|
|
429
|
+
if (schema.const !== void 0) {
|
|
430
|
+
if (typeof schema.const === "string" || typeof schema.const === "number" || typeof schema.const === "boolean") return {
|
|
431
|
+
kind: "literal",
|
|
432
|
+
value: schema.const
|
|
433
|
+
};
|
|
434
|
+
if (schema.const === null) return {
|
|
435
|
+
kind: "nullable",
|
|
436
|
+
inner: {
|
|
437
|
+
kind: "primitive",
|
|
438
|
+
type: "unknown"
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
if (typeof schema.const === "object") {
|
|
442
|
+
if (Array.isArray(schema.const)) return {
|
|
443
|
+
kind: "array",
|
|
444
|
+
items: {
|
|
445
|
+
kind: "primitive",
|
|
446
|
+
type: "unknown"
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
return {
|
|
450
|
+
kind: "map",
|
|
451
|
+
valueType: {
|
|
452
|
+
kind: "primitive",
|
|
453
|
+
type: "unknown"
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (schema.enum && schema.enum.length === 1) {
|
|
459
|
+
const v = schema.enum[0];
|
|
460
|
+
return {
|
|
461
|
+
kind: "literal",
|
|
462
|
+
value: typeof v === "number" ? v : String(v)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (schema.enum) {
|
|
466
|
+
const baseName = toPascalCase(contextName ?? "UnknownEnum");
|
|
467
|
+
const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
|
|
468
|
+
return {
|
|
469
|
+
kind: "enum",
|
|
470
|
+
name: cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName,
|
|
471
|
+
values: schema.enum.map((v) => typeof v === "number" ? v : String(v))
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (schema.type === "array" && schema.items) return {
|
|
475
|
+
kind: "array",
|
|
476
|
+
items: schemaToTypeRef(schema.items, contextName, parentModelName)
|
|
477
|
+
};
|
|
478
|
+
if (!schema.type && schema.items) return {
|
|
479
|
+
kind: "array",
|
|
480
|
+
items: schemaToTypeRef(schema.items, contextName, parentModelName)
|
|
481
|
+
};
|
|
482
|
+
if (schema.type === "object" && schema.properties) return {
|
|
483
|
+
kind: "model",
|
|
484
|
+
name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
|
|
485
|
+
};
|
|
486
|
+
if (schema.type === "object" && !schema.properties) {
|
|
487
|
+
let valueType = {
|
|
488
|
+
kind: "primitive",
|
|
489
|
+
type: "unknown"
|
|
490
|
+
};
|
|
491
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
492
|
+
if (Object.keys(schema.additionalProperties).length > 0) valueType = schemaToTypeRef(schema.additionalProperties, contextName);
|
|
493
|
+
} else if (schema.patternProperties) {
|
|
494
|
+
const patterns = Object.values(schema.patternProperties);
|
|
495
|
+
if (patterns.length > 0) valueType = schemaToTypeRef(patterns[0], contextName);
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
kind: "map",
|
|
499
|
+
valueType
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const primitiveMap = {
|
|
503
|
+
string: "string",
|
|
504
|
+
integer: "integer",
|
|
505
|
+
number: "number",
|
|
506
|
+
boolean: "boolean"
|
|
507
|
+
};
|
|
508
|
+
const typeStr = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
509
|
+
if (typeStr && primitiveMap[typeStr]) return {
|
|
510
|
+
kind: "primitive",
|
|
511
|
+
type: primitiveMap[typeStr],
|
|
512
|
+
...schema.format ? { format: schema.format } : {}
|
|
513
|
+
};
|
|
514
|
+
if (!schema.type && schema.properties) return {
|
|
515
|
+
kind: "model",
|
|
516
|
+
name: qualifyInlineModelName(toPascalCase(contextName ?? "UnknownModel"), parentModelName)
|
|
517
|
+
};
|
|
518
|
+
if (!schema.type && !schema.$ref && !schema.oneOf && !schema.anyOf && !schema.allOf && !schema.enum && !schema.properties && !schema.items) return {
|
|
519
|
+
kind: "primitive",
|
|
520
|
+
type: "unknown"
|
|
521
|
+
};
|
|
522
|
+
if (contextName) console.warn(`[oagen] Warning: Unknown schema shape treated as unknown (context: ${contextName})`);
|
|
523
|
+
return {
|
|
524
|
+
kind: "primitive",
|
|
525
|
+
type: "unknown"
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/parser/pagination.ts
|
|
530
|
+
const CURSOR_PARAMS = [
|
|
531
|
+
"cursor",
|
|
532
|
+
"after",
|
|
533
|
+
"before",
|
|
534
|
+
"starting_after",
|
|
535
|
+
"ending_before",
|
|
536
|
+
"page_token",
|
|
537
|
+
"next_token"
|
|
538
|
+
];
|
|
539
|
+
const OFFSET_PARAMS = [
|
|
540
|
+
"offset",
|
|
541
|
+
"page",
|
|
542
|
+
"page_number",
|
|
543
|
+
"skip"
|
|
544
|
+
];
|
|
545
|
+
const LIMIT_PARAMS = [
|
|
546
|
+
"limit",
|
|
547
|
+
"page_size",
|
|
548
|
+
"per_page",
|
|
549
|
+
"size",
|
|
550
|
+
"count"
|
|
551
|
+
];
|
|
552
|
+
/**
|
|
553
|
+
* Detect if an operation uses pagination and return structured metadata
|
|
554
|
+
* for auto-paging iterator generation.
|
|
555
|
+
*
|
|
556
|
+
* Supports two strategies:
|
|
557
|
+
* 1. **Cursor-based**: query params include a cursor-like parameter
|
|
558
|
+
* 2. **Offset-based**: query params include both an offset-like and limit-like parameter
|
|
559
|
+
*
|
|
560
|
+
* Returns null if no pagination pattern is detected.
|
|
561
|
+
*/
|
|
562
|
+
function detectPagination(response, queryParams, dataPath) {
|
|
563
|
+
const cursorParam = queryParams.find((p) => CURSOR_PARAMS.includes(p.name));
|
|
564
|
+
if (cursorParam) {
|
|
565
|
+
const itemType = response.kind === "array" ? response.items : response;
|
|
566
|
+
return {
|
|
567
|
+
strategy: "cursor",
|
|
568
|
+
param: cursorParam.name,
|
|
569
|
+
dataPath,
|
|
570
|
+
itemType
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const offsetParam = queryParams.find((p) => OFFSET_PARAMS.includes(p.name));
|
|
574
|
+
const limitParam = queryParams.find((p) => LIMIT_PARAMS.includes(p.name));
|
|
575
|
+
if (offsetParam && limitParam) {
|
|
576
|
+
const itemType = response.kind === "array" ? response.items : response;
|
|
577
|
+
return {
|
|
578
|
+
strategy: "offset",
|
|
579
|
+
param: offsetParam.name,
|
|
580
|
+
limitParam: limitParam.name,
|
|
581
|
+
dataPath,
|
|
582
|
+
itemType
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/parser/responses.ts
|
|
589
|
+
function classifyAndExtractResponse(schema, contextName) {
|
|
590
|
+
if (schema.$ref) return {
|
|
591
|
+
response: schemaToTypeRef(schema, contextName),
|
|
592
|
+
inlineModels: [],
|
|
593
|
+
isPaginated: false
|
|
594
|
+
};
|
|
595
|
+
const envelope = detectListEnvelope(schema);
|
|
596
|
+
if (envelope.isEnvelope) return extractListResponse(schema, contextName, envelope.dataPath);
|
|
597
|
+
if (isSingleResourceWrapper(schema)) return extractWrappedResource(schema, contextName);
|
|
598
|
+
return extractDirectResource(schema, contextName);
|
|
599
|
+
}
|
|
600
|
+
const KNOWN_DATA_PATHS = new Set([
|
|
601
|
+
"data",
|
|
602
|
+
"items",
|
|
603
|
+
"results",
|
|
604
|
+
"records",
|
|
605
|
+
"entries",
|
|
606
|
+
"values",
|
|
607
|
+
"nodes",
|
|
608
|
+
"edges"
|
|
609
|
+
]);
|
|
610
|
+
const PAGINATION_METADATA_PATTERNS = [
|
|
611
|
+
"metadata",
|
|
612
|
+
"pagination",
|
|
613
|
+
"cursor",
|
|
614
|
+
"has_more",
|
|
615
|
+
"page_info",
|
|
616
|
+
"total",
|
|
617
|
+
"next_page",
|
|
618
|
+
"previous_page",
|
|
619
|
+
"offset"
|
|
620
|
+
];
|
|
621
|
+
function detectListEnvelope(schema) {
|
|
622
|
+
const propSources = [];
|
|
623
|
+
if (schema.allOf) {
|
|
624
|
+
for (const sub of schema.allOf) if (sub.properties) propSources.push(sub.properties);
|
|
625
|
+
}
|
|
626
|
+
if (schema.properties) propSources.push(schema.properties);
|
|
627
|
+
if (propSources.length === 0) return {
|
|
628
|
+
isEnvelope: false,
|
|
629
|
+
dataPath: null
|
|
630
|
+
};
|
|
631
|
+
const mergedProps = {};
|
|
632
|
+
for (const source of propSources) Object.assign(mergedProps, source);
|
|
633
|
+
const arrayProps = [];
|
|
634
|
+
const nonArrayKeys = [];
|
|
635
|
+
for (const [key, propSchema] of Object.entries(mergedProps)) {
|
|
636
|
+
if (!propSchema) continue;
|
|
637
|
+
if (propSchema.type === "array") arrayProps.push(key);
|
|
638
|
+
else nonArrayKeys.push(key);
|
|
639
|
+
}
|
|
640
|
+
if (arrayProps.length === 1 && nonArrayKeys.length >= 1) {
|
|
641
|
+
const arrayPropName = arrayProps[0];
|
|
642
|
+
if (KNOWN_DATA_PATHS.has(arrayPropName)) return {
|
|
643
|
+
isEnvelope: true,
|
|
644
|
+
dataPath: arrayPropName
|
|
645
|
+
};
|
|
646
|
+
if (nonArrayKeys.some((key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern)))) return {
|
|
647
|
+
isEnvelope: true,
|
|
648
|
+
dataPath: arrayPropName
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
isEnvelope: false,
|
|
653
|
+
dataPath: null
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function extractListResponse(schema, contextName, dataPath) {
|
|
657
|
+
let itemTypeRef = {
|
|
658
|
+
kind: "primitive",
|
|
659
|
+
type: "string"
|
|
660
|
+
};
|
|
661
|
+
const inlineModels = [];
|
|
662
|
+
const propSources = [];
|
|
663
|
+
if (schema.allOf) {
|
|
664
|
+
for (const sub of schema.allOf) if (sub.properties) propSources.push(sub.properties);
|
|
665
|
+
}
|
|
666
|
+
if (schema.properties) propSources.push(schema.properties);
|
|
667
|
+
for (const props of propSources) {
|
|
668
|
+
const dataProp = props[dataPath];
|
|
669
|
+
if (!dataProp) continue;
|
|
670
|
+
if (dataProp.type === "array" && dataProp.items) {
|
|
671
|
+
const items = dataProp.items;
|
|
672
|
+
if (items.$ref) itemTypeRef = schemaToTypeRef(items, contextName);
|
|
673
|
+
else if (items.type === "object" && items.properties) {
|
|
674
|
+
const itemName = contextName.replace(/Response$/, "") + "Item";
|
|
675
|
+
itemTypeRef = {
|
|
676
|
+
kind: "model",
|
|
677
|
+
name: itemName
|
|
678
|
+
};
|
|
679
|
+
inlineModels.push(...extractInlineModel(itemName, items));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
response: {
|
|
685
|
+
kind: "array",
|
|
686
|
+
items: itemTypeRef
|
|
687
|
+
},
|
|
688
|
+
inlineModels,
|
|
689
|
+
isPaginated: true,
|
|
690
|
+
dataPath,
|
|
691
|
+
itemType: itemTypeRef
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function hasDiscriminantConstField(schema) {
|
|
695
|
+
if (!schema.properties) return null;
|
|
696
|
+
for (const name of ["object", "type"]) {
|
|
697
|
+
const field = schema.properties[name];
|
|
698
|
+
if (!field) continue;
|
|
699
|
+
if (field.const !== void 0 && typeof field.const === "string") return {
|
|
700
|
+
property: name,
|
|
701
|
+
value: field.const
|
|
702
|
+
};
|
|
703
|
+
if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return {
|
|
704
|
+
property: name,
|
|
705
|
+
value: field.enum[0]
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
for (const [name, field] of Object.entries(schema.properties)) {
|
|
709
|
+
if (!field) continue;
|
|
710
|
+
if (field.const !== void 0 && typeof field.const === "string") return {
|
|
711
|
+
property: name,
|
|
712
|
+
value: field.const
|
|
713
|
+
};
|
|
714
|
+
if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return {
|
|
715
|
+
property: name,
|
|
716
|
+
value: field.enum[0]
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
function isSingleResourceWrapper(schema) {
|
|
722
|
+
if (schema.type !== "object") return false;
|
|
723
|
+
if (!schema.properties) return false;
|
|
724
|
+
let wrapperKey;
|
|
725
|
+
if (schema.required && schema.required.length === 1) wrapperKey = schema.required[0];
|
|
726
|
+
else {
|
|
727
|
+
const propKeys = Object.keys(schema.properties);
|
|
728
|
+
if (propKeys.length === 1) wrapperKey = propKeys[0];
|
|
729
|
+
}
|
|
730
|
+
if (!wrapperKey) return false;
|
|
731
|
+
const propSchema = schema.properties[wrapperKey];
|
|
732
|
+
if (!propSchema) return false;
|
|
733
|
+
if (propSchema.type === "object" && hasDiscriminantConstField(propSchema) !== null) return true;
|
|
734
|
+
if (propSchema.oneOf) return propSchema.oneOf.some((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
function extractWrappedResource(schema, contextName) {
|
|
738
|
+
const props = schema.properties;
|
|
739
|
+
const propSchema = props[schema.required && schema.required.length === 1 ? schema.required[0] : Object.keys(props)[0]];
|
|
740
|
+
const inlineModels = [];
|
|
741
|
+
const resourceName = contextName.replace(/Response$/, "");
|
|
742
|
+
if (propSchema.oneOf) {
|
|
743
|
+
const objectVariant = propSchema.oneOf.find((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
|
|
744
|
+
if (objectVariant) {
|
|
745
|
+
const modelName = deriveModelName(objectVariant, resourceName);
|
|
746
|
+
inlineModels.push(...extractInlineModel(modelName, objectVariant));
|
|
747
|
+
const hasNullVariant = propSchema.oneOf.some((v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null"));
|
|
748
|
+
const modelRef = {
|
|
749
|
+
kind: "model",
|
|
750
|
+
name: modelName
|
|
751
|
+
};
|
|
752
|
+
return {
|
|
753
|
+
response: hasNullVariant ? {
|
|
754
|
+
kind: "nullable",
|
|
755
|
+
inner: modelRef
|
|
756
|
+
} : modelRef,
|
|
757
|
+
inlineModels,
|
|
758
|
+
isPaginated: false
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const modelName = deriveModelName(propSchema, resourceName);
|
|
763
|
+
inlineModels.push(...extractInlineModel(modelName, propSchema));
|
|
764
|
+
return {
|
|
765
|
+
response: {
|
|
766
|
+
kind: "model",
|
|
767
|
+
name: modelName
|
|
768
|
+
},
|
|
769
|
+
inlineModels,
|
|
770
|
+
isPaginated: false
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function extractDirectResource(schema, contextName) {
|
|
774
|
+
const inlineModels = [];
|
|
775
|
+
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
776
|
+
const modelName = deriveModelName(schema, contextName);
|
|
777
|
+
inlineModels.push(...extractInlineModel(modelName, schema));
|
|
778
|
+
return {
|
|
779
|
+
response: {
|
|
780
|
+
kind: "model",
|
|
781
|
+
name: modelName
|
|
782
|
+
},
|
|
783
|
+
inlineModels,
|
|
784
|
+
isPaginated: false
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
if (schema.allOf) {
|
|
788
|
+
const mergedProperties = {};
|
|
789
|
+
const mergedRequired = [];
|
|
790
|
+
for (const sub of schema.allOf) {
|
|
791
|
+
if (sub.properties) Object.assign(mergedProperties, sub.properties);
|
|
792
|
+
if (sub.required) mergedRequired.push(...sub.required);
|
|
793
|
+
}
|
|
794
|
+
if (Object.keys(mergedProperties).length > 0) {
|
|
795
|
+
const merged = {
|
|
796
|
+
type: "object",
|
|
797
|
+
properties: mergedProperties,
|
|
798
|
+
required: mergedRequired
|
|
799
|
+
};
|
|
800
|
+
const modelName = deriveModelName(merged, contextName);
|
|
801
|
+
inlineModels.push(...extractInlineModel(modelName, merged));
|
|
802
|
+
return {
|
|
803
|
+
response: {
|
|
804
|
+
kind: "model",
|
|
805
|
+
name: modelName
|
|
806
|
+
},
|
|
807
|
+
inlineModels,
|
|
808
|
+
isPaginated: false
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (schema.oneOf) {
|
|
813
|
+
for (const variant of schema.oneOf) if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
814
|
+
const modelName = deriveModelName(variant, contextName);
|
|
815
|
+
if (!new Set(inlineModels.map((m) => m.name)).has(modelName)) inlineModels.push(...extractInlineModel(modelName, variant));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (schema.type === "array" && schema.items) {
|
|
819
|
+
const items = schema.items;
|
|
820
|
+
if (items.$ref) return {
|
|
821
|
+
response: {
|
|
822
|
+
kind: "array",
|
|
823
|
+
items: schemaToTypeRef(items, contextName)
|
|
824
|
+
},
|
|
825
|
+
inlineModels,
|
|
826
|
+
isPaginated: false
|
|
827
|
+
};
|
|
828
|
+
if (items.properties && (items.type === "object" || !items.type)) {
|
|
829
|
+
const itemName = contextName.replace(/Response$/, "") + "Item";
|
|
830
|
+
inlineModels.push(...extractInlineModel(itemName, items));
|
|
831
|
+
return {
|
|
832
|
+
response: {
|
|
833
|
+
kind: "array",
|
|
834
|
+
items: {
|
|
835
|
+
kind: "model",
|
|
836
|
+
name: itemName
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
inlineModels,
|
|
840
|
+
isPaginated: false
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return {
|
|
845
|
+
response: schemaToTypeRef(schema, contextName),
|
|
846
|
+
inlineModels,
|
|
847
|
+
isPaginated: false
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function deriveModelName(schema, fallback) {
|
|
851
|
+
if (!schema.properties) return toPascalCase(fallback);
|
|
852
|
+
for (const name of ["object", "type"]) {
|
|
853
|
+
const field = schema.properties[name];
|
|
854
|
+
if (field && field.const && typeof field.const === "string") return toPascalCase(field.const);
|
|
855
|
+
}
|
|
856
|
+
for (const [, field] of Object.entries(schema.properties)) if (field && field.const && typeof field.const === "string") return toPascalCase(field.const);
|
|
857
|
+
return toPascalCase(fallback);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Qualify a nested inline model name with its parent to avoid generic names.
|
|
861
|
+
* e.g., parent="Connection" + field="domains" → "ConnectionDomain" (singularized)
|
|
862
|
+
* Only qualifies when the field name alone would be too generic.
|
|
863
|
+
*/
|
|
864
|
+
function qualifyNestedName(parentName, fieldName) {
|
|
865
|
+
const pascalField = toPascalCase(fieldName);
|
|
866
|
+
if (pascalField.startsWith(parentName)) return pascalField;
|
|
867
|
+
const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
|
|
868
|
+
const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
|
|
869
|
+
if (match && match[2]) {
|
|
870
|
+
const trailing = singularize(match[2]);
|
|
871
|
+
return match[1] + trailing;
|
|
872
|
+
}
|
|
873
|
+
return qualified;
|
|
874
|
+
}
|
|
875
|
+
function extractInlineModel(name, schema) {
|
|
876
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
877
|
+
const fields = [];
|
|
878
|
+
const properties = schema.properties ?? {};
|
|
879
|
+
const nestedModels = [];
|
|
880
|
+
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
|
881
|
+
if (!fieldSchema) continue;
|
|
882
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
883
|
+
if (fieldSchema.type === "object" && fieldSchema.properties) {
|
|
884
|
+
const nestedName = qualifyNestedName(name, fieldName);
|
|
885
|
+
nestedModels.push(...extractInlineModel(nestedName, fieldSchema));
|
|
886
|
+
}
|
|
887
|
+
if (fieldSchema.type === "array" && fieldSchema.items) {
|
|
888
|
+
const items = fieldSchema.items;
|
|
889
|
+
if (items.type === "object" && items.properties) {
|
|
890
|
+
const nestedName = qualifyNestedName(name, fieldName);
|
|
891
|
+
nestedModels.push(...extractInlineModel(nestedName, items));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (fieldSchema.allOf) {
|
|
895
|
+
const mergedProperties = {};
|
|
896
|
+
const mergedRequired = [];
|
|
897
|
+
for (const sub of fieldSchema.allOf) {
|
|
898
|
+
if (sub.properties) Object.assign(mergedProperties, sub.properties);
|
|
899
|
+
if (sub.required) mergedRequired.push(...sub.required);
|
|
900
|
+
}
|
|
901
|
+
if (Object.keys(mergedProperties).length > 0) {
|
|
902
|
+
const nestedName = qualifyNestedName(name, fieldName);
|
|
903
|
+
const merged = {
|
|
904
|
+
type: "object",
|
|
905
|
+
properties: mergedProperties,
|
|
906
|
+
required: mergedRequired
|
|
907
|
+
};
|
|
908
|
+
if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, merged));
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (fieldSchema.oneOf) {
|
|
912
|
+
const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
|
|
913
|
+
if (objectVariant) {
|
|
914
|
+
const nestedName = qualifyNestedName(name, fieldName);
|
|
915
|
+
if (!new Set(nestedModels.map((m) => m.name)).has(nestedName)) nestedModels.push(...extractInlineModel(nestedName, objectVariant));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return [{
|
|
920
|
+
name,
|
|
921
|
+
description: schema.description,
|
|
922
|
+
fields
|
|
923
|
+
}, ...nestedModels];
|
|
924
|
+
}
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/parser/operations.ts
|
|
927
|
+
const HTTP_METHODS = [
|
|
928
|
+
"get",
|
|
929
|
+
"post",
|
|
930
|
+
"put",
|
|
931
|
+
"patch",
|
|
932
|
+
"delete",
|
|
933
|
+
"head",
|
|
934
|
+
"options",
|
|
935
|
+
"trace"
|
|
936
|
+
];
|
|
937
|
+
function extractOperations(paths, operationIdTransform) {
|
|
938
|
+
if (!paths) return {
|
|
939
|
+
services: [],
|
|
940
|
+
inlineModels: []
|
|
941
|
+
};
|
|
942
|
+
const serviceMap = /* @__PURE__ */ new Map();
|
|
943
|
+
const inlineModels = [];
|
|
944
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
945
|
+
const pathLevelParams = pathItem.parameters ?? [];
|
|
946
|
+
for (const method of HTTP_METHODS) {
|
|
947
|
+
const op = pathItem[method];
|
|
948
|
+
if (!op) continue;
|
|
949
|
+
const serviceName = inferServiceName(path, op.tags?.[0]);
|
|
950
|
+
const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform);
|
|
951
|
+
inlineModels.push(...opModels);
|
|
952
|
+
const ops = serviceMap.get(serviceName) ?? [];
|
|
953
|
+
ops.push(operation);
|
|
954
|
+
serviceMap.set(serviceName, ops);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
|
|
958
|
+
return {
|
|
959
|
+
services: Array.from(serviceMap.entries()).map(([name, operations]) => ({
|
|
960
|
+
name,
|
|
961
|
+
operations
|
|
962
|
+
})),
|
|
963
|
+
inlineModels
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function inferServiceName(path, tag) {
|
|
967
|
+
if (tag) return toPascalCase(tag);
|
|
968
|
+
const first = path.split("/").filter(Boolean)[0] ?? "Default";
|
|
969
|
+
if (first.startsWith("{")) return "Default";
|
|
970
|
+
return toPascalCase(first);
|
|
971
|
+
}
|
|
972
|
+
function inferOperationName(method, path, operationId, operationIdTransform) {
|
|
973
|
+
if (operationId) {
|
|
974
|
+
if (operationIdTransform) return operationIdTransform(operationId);
|
|
975
|
+
return toCamelCase(operationId);
|
|
976
|
+
}
|
|
977
|
+
const verb = inferVerb(method, path);
|
|
978
|
+
const resource = inferResourceFromPath(path);
|
|
979
|
+
return resource ? toCamelCase(`${verb}_${resource}`) : verb;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Infer the CRUD verb from HTTP method and path shape.
|
|
983
|
+
*/
|
|
984
|
+
function inferVerb(method, path) {
|
|
985
|
+
const segments = path.split("/").filter(Boolean);
|
|
986
|
+
const hasTrailingParam = segments.length > 0 && segments[segments.length - 1].startsWith("{");
|
|
987
|
+
switch (method) {
|
|
988
|
+
case "get": return hasTrailingParam ? "get" : "list";
|
|
989
|
+
case "post": return "create";
|
|
990
|
+
case "put":
|
|
991
|
+
case "patch": return "update";
|
|
992
|
+
case "delete": return "delete";
|
|
993
|
+
case "head": return "check";
|
|
994
|
+
case "options": return "options";
|
|
995
|
+
case "trace": return "trace";
|
|
996
|
+
default: return method;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Extract a resource noun from the path for operation naming.
|
|
1001
|
+
* Uses the last non-param segment (the resource being operated on).
|
|
1002
|
+
*
|
|
1003
|
+
* Examples:
|
|
1004
|
+
* /user_management/users → "Users"
|
|
1005
|
+
* /user_management/users/{id} → "User" (singular for item operations)
|
|
1006
|
+
* /user_management/users/{id}/auth_factors → "AuthFactors"
|
|
1007
|
+
* /organizations/{id}/api_keys → "ApiKeys"
|
|
1008
|
+
* /sso/authorize → "Authorize" (action endpoint)
|
|
1009
|
+
*/
|
|
1010
|
+
function inferResourceFromPath(path) {
|
|
1011
|
+
const segments = path.split("/").filter(Boolean);
|
|
1012
|
+
if (segments.length <= 1) return null;
|
|
1013
|
+
let resourceSegment = null;
|
|
1014
|
+
for (let i = segments.length - 1; i >= 0; i--) if (!segments[i].startsWith("{")) {
|
|
1015
|
+
resourceSegment = segments[i];
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
if (!resourceSegment) return null;
|
|
1019
|
+
if (resourceSegment === segments[0]) return null;
|
|
1020
|
+
return toPascalCase(resourceSegment);
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Detect and resolve name collisions within a service's operations.
|
|
1024
|
+
*
|
|
1025
|
+
* When multiple operations share the same name (e.g., several "list" from
|
|
1026
|
+
* different sub-resources), disambiguate by appending the resource noun
|
|
1027
|
+
* from the path. Only renames operations on DIFFERENT paths — same-name
|
|
1028
|
+
* operations on the same path (e.g., PUT + PATCH both "update") are left as-is
|
|
1029
|
+
* since they represent the same logical resource.
|
|
1030
|
+
*/
|
|
1031
|
+
function disambiguateOperationNames(operations) {
|
|
1032
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1033
|
+
for (const op of operations) {
|
|
1034
|
+
const group = byName.get(op.name) ?? [];
|
|
1035
|
+
group.push(op);
|
|
1036
|
+
byName.set(op.name, group);
|
|
1037
|
+
}
|
|
1038
|
+
for (const [name, group] of byName) {
|
|
1039
|
+
if (group.length <= 1) continue;
|
|
1040
|
+
if (new Set(group.map((op) => op.path)).size <= 1) continue;
|
|
1041
|
+
for (const op of group) {
|
|
1042
|
+
const resource = inferResourceFromPath(op.path);
|
|
1043
|
+
if (resource) op.name = toCamelCase(`${name}_${resource}`);
|
|
1044
|
+
}
|
|
1045
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
1046
|
+
for (const op of group) {
|
|
1047
|
+
const rGroup = renamed.get(op.name) ?? [];
|
|
1048
|
+
rGroup.push(op);
|
|
1049
|
+
renamed.set(op.name, rGroup);
|
|
1050
|
+
}
|
|
1051
|
+
for (const [, rGroup] of renamed) {
|
|
1052
|
+
if (rGroup.length <= 1) continue;
|
|
1053
|
+
for (const op of rGroup) {
|
|
1054
|
+
const deeper = inferDeeperContext(op.path);
|
|
1055
|
+
if (deeper) op.name = toCamelCase(`${op.name}_${deeper}`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Extract a deeper disambiguation context from the path when the last
|
|
1062
|
+
* segment isn't unique enough. Uses the second-to-last non-param segment.
|
|
1063
|
+
*
|
|
1064
|
+
* /users/{id}/auth_factors → already captured as "AuthFactors"
|
|
1065
|
+
* /users/external_id/{external_id} → "ExternalId" (special path, not a sub-resource)
|
|
1066
|
+
*/
|
|
1067
|
+
function inferDeeperContext(path) {
|
|
1068
|
+
const nonParams = path.split("/").filter(Boolean).filter((s) => !s.startsWith("{"));
|
|
1069
|
+
if (nonParams.length >= 3) return toPascalCase(nonParams.slice(1, -1).join("_"));
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
function buildOperation(method, path, op, pathLevelParams, operationIdTransform) {
|
|
1073
|
+
const allParams = [...pathLevelParams, ...op.parameters ?? []];
|
|
1074
|
+
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
1075
|
+
const pathParams = extractParams(allParams, "path");
|
|
1076
|
+
const queryParams = extractParams(allParams, "query");
|
|
1077
|
+
const headerParams = extractParams(allParams, "header").filter((p) => p.name.toLowerCase() !== "idempotency-key");
|
|
1078
|
+
const cookieParams = extractParams(allParams, "cookie");
|
|
1079
|
+
const reqBodyModels = [];
|
|
1080
|
+
const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
|
|
1081
|
+
const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method);
|
|
1082
|
+
inlineModels.push(...reqBodyModels);
|
|
1083
|
+
const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
|
|
1084
|
+
let pagination;
|
|
1085
|
+
if (isPaginated) {
|
|
1086
|
+
const itemType = responseItemType ?? (response.kind === "array" ? response.items : response);
|
|
1087
|
+
pagination = {
|
|
1088
|
+
strategy: paginationFromParams?.strategy ?? "cursor",
|
|
1089
|
+
param: paginationFromParams?.param ?? "after",
|
|
1090
|
+
limitParam: paginationFromParams?.limitParam,
|
|
1091
|
+
dataPath: responseDataPath ?? paginationFromParams?.dataPath,
|
|
1092
|
+
itemType
|
|
1093
|
+
};
|
|
1094
|
+
} else if (paginationFromParams) pagination = paginationFromParams;
|
|
1095
|
+
return {
|
|
1096
|
+
operation: {
|
|
1097
|
+
name: inferOperationName(method, path, op.operationId, operationIdTransform),
|
|
1098
|
+
description: buildDescription(op.summary, op.description),
|
|
1099
|
+
httpMethod: method,
|
|
1100
|
+
path,
|
|
1101
|
+
pathParams,
|
|
1102
|
+
queryParams,
|
|
1103
|
+
headerParams,
|
|
1104
|
+
cookieParams: cookieParams.length > 0 ? cookieParams : void 0,
|
|
1105
|
+
requestBody,
|
|
1106
|
+
requestBodyEncoding,
|
|
1107
|
+
response,
|
|
1108
|
+
successResponses,
|
|
1109
|
+
errors,
|
|
1110
|
+
pagination,
|
|
1111
|
+
injectIdempotencyKey: hasIdempotencyHeader,
|
|
1112
|
+
deprecated: op.deprecated || void 0,
|
|
1113
|
+
async: op["x-oagen-async"]
|
|
1114
|
+
},
|
|
1115
|
+
inlineModels
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function buildDescription(summary, description) {
|
|
1119
|
+
if (summary && description && summary !== description) return `${summary}\n\n${description}`;
|
|
1120
|
+
return description ?? summary;
|
|
1121
|
+
}
|
|
1122
|
+
function extractParams(params, location) {
|
|
1123
|
+
return params.filter((p) => p.in === location).map((p) => ({
|
|
1124
|
+
name: p.name,
|
|
1125
|
+
type: p.schema ? schemaToTypeRef(p.schema, p.name) : {
|
|
1126
|
+
kind: "primitive",
|
|
1127
|
+
type: "string"
|
|
1128
|
+
},
|
|
1129
|
+
required: p.required ?? false,
|
|
1130
|
+
description: p.description,
|
|
1131
|
+
deprecated: p.deprecated || p.schema?.deprecated || void 0,
|
|
1132
|
+
default: p.schema?.default,
|
|
1133
|
+
example: p.example ?? p.schema?.example
|
|
1134
|
+
}));
|
|
1135
|
+
}
|
|
1136
|
+
function extractRequestBody(body, op, inlineModels) {
|
|
1137
|
+
if (!body?.content) return {};
|
|
1138
|
+
let encoding = "json";
|
|
1139
|
+
let schema;
|
|
1140
|
+
if (body.content["application/json"]?.schema) {
|
|
1141
|
+
encoding = "json";
|
|
1142
|
+
schema = body.content["application/json"].schema;
|
|
1143
|
+
} else if (body.content["multipart/form-data"]?.schema) {
|
|
1144
|
+
encoding = "form-data";
|
|
1145
|
+
schema = body.content["multipart/form-data"].schema;
|
|
1146
|
+
} else if (body.content["application/x-www-form-urlencoded"]?.schema) {
|
|
1147
|
+
encoding = "form-urlencoded";
|
|
1148
|
+
schema = body.content["application/x-www-form-urlencoded"].schema;
|
|
1149
|
+
} else if (body.content["application/octet-stream"]) {
|
|
1150
|
+
encoding = "binary";
|
|
1151
|
+
schema = body.content["application/octet-stream"].schema;
|
|
1152
|
+
} else if (body.content["text/plain"]) {
|
|
1153
|
+
encoding = "text";
|
|
1154
|
+
schema = body.content["text/plain"].schema;
|
|
1155
|
+
}
|
|
1156
|
+
if (!schema) {
|
|
1157
|
+
if (encoding === "binary") return {
|
|
1158
|
+
body: {
|
|
1159
|
+
kind: "primitive",
|
|
1160
|
+
type: "string",
|
|
1161
|
+
format: "binary"
|
|
1162
|
+
},
|
|
1163
|
+
encoding
|
|
1164
|
+
};
|
|
1165
|
+
if (encoding === "text") return {
|
|
1166
|
+
body: {
|
|
1167
|
+
kind: "primitive",
|
|
1168
|
+
type: "string"
|
|
1169
|
+
},
|
|
1170
|
+
encoding
|
|
1171
|
+
};
|
|
1172
|
+
return {};
|
|
1173
|
+
}
|
|
1174
|
+
const contextName = cleanSchemaName(op?.operationId ? toPascalCase(op.operationId) + "Request" : "RequestBody");
|
|
1175
|
+
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
1176
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
1177
|
+
const fields = [];
|
|
1178
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
|
|
1179
|
+
if (!fieldSchema) continue;
|
|
1180
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet));
|
|
1181
|
+
}
|
|
1182
|
+
inlineModels.push({
|
|
1183
|
+
name: contextName,
|
|
1184
|
+
description: void 0,
|
|
1185
|
+
fields
|
|
1186
|
+
});
|
|
1187
|
+
return {
|
|
1188
|
+
body: {
|
|
1189
|
+
kind: "model",
|
|
1190
|
+
name: contextName
|
|
1191
|
+
},
|
|
1192
|
+
encoding
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
if (schema.oneOf) {
|
|
1196
|
+
const variants = [];
|
|
1197
|
+
for (const variant of schema.oneOf) if (variant.$ref) {
|
|
1198
|
+
const ref = schemaToTypeRef(variant, contextName);
|
|
1199
|
+
variants.push(ref);
|
|
1200
|
+
} else if (variant.type === "null") {} else if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
1201
|
+
const variantName = deriveOneOfVariantName(variant, contextName, inlineModels);
|
|
1202
|
+
const requiredSet = new Set(variant.required ?? []);
|
|
1203
|
+
const fields = [];
|
|
1204
|
+
for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
|
|
1205
|
+
if (!fieldSchema) continue;
|
|
1206
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet));
|
|
1207
|
+
}
|
|
1208
|
+
inlineModels.push({
|
|
1209
|
+
name: variantName,
|
|
1210
|
+
description: variant.description,
|
|
1211
|
+
fields
|
|
1212
|
+
});
|
|
1213
|
+
variants.push({
|
|
1214
|
+
kind: "model",
|
|
1215
|
+
name: variantName
|
|
1216
|
+
});
|
|
1217
|
+
} else variants.push(schemaToTypeRef(variant, contextName));
|
|
1218
|
+
const hasNull = schema.oneOf.some((v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null"));
|
|
1219
|
+
const union = {
|
|
1220
|
+
kind: "union",
|
|
1221
|
+
variants,
|
|
1222
|
+
...schema.discriminator ? { discriminator: {
|
|
1223
|
+
property: schema.discriminator.propertyName,
|
|
1224
|
+
mapping: schema.discriminator.mapping ?? {}
|
|
1225
|
+
} } : {}
|
|
1226
|
+
};
|
|
1227
|
+
return {
|
|
1228
|
+
body: hasNull ? {
|
|
1229
|
+
kind: "nullable",
|
|
1230
|
+
inner: union
|
|
1231
|
+
} : union,
|
|
1232
|
+
encoding
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
body: schemaToTypeRef(schema, contextName),
|
|
1237
|
+
encoding
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
/** Derive a unique variant name for a oneOf inline model. */
|
|
1241
|
+
function deriveOneOfVariantName(variant, baseName, existingModels) {
|
|
1242
|
+
if (variant.properties) {
|
|
1243
|
+
for (const [, propSchema] of Object.entries(variant.properties)) if (propSchema?.const && typeof propSchema.const === "string") return toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request";
|
|
1244
|
+
}
|
|
1245
|
+
const existingNames = new Set(existingModels.map((m) => m.name));
|
|
1246
|
+
let name = baseName;
|
|
1247
|
+
let suffix = 2;
|
|
1248
|
+
while (existingNames.has(name)) {
|
|
1249
|
+
name = `${baseName}${suffix}`;
|
|
1250
|
+
suffix++;
|
|
1251
|
+
}
|
|
1252
|
+
return name;
|
|
1253
|
+
}
|
|
1254
|
+
function deriveResponseName(op, path, method) {
|
|
1255
|
+
if (op?.operationId) return cleanSchemaName(toPascalCase(op.operationId) + "Response");
|
|
1256
|
+
return toPascalCase(path.split("/").filter(Boolean)[0] ?? "Unknown") + toPascalCase(method) + "Response";
|
|
1257
|
+
}
|
|
1258
|
+
function extractResponses(responses, op, path, method) {
|
|
1259
|
+
const errors = [];
|
|
1260
|
+
let response = {
|
|
1261
|
+
kind: "primitive",
|
|
1262
|
+
type: "string"
|
|
1263
|
+
};
|
|
1264
|
+
let inlineModels = [];
|
|
1265
|
+
let isPaginated = false;
|
|
1266
|
+
let dataPath;
|
|
1267
|
+
let itemType;
|
|
1268
|
+
if (!responses) return {
|
|
1269
|
+
response,
|
|
1270
|
+
errors,
|
|
1271
|
+
inlineModels,
|
|
1272
|
+
isPaginated
|
|
1273
|
+
};
|
|
1274
|
+
const allSuccessResponses = [];
|
|
1275
|
+
for (const [statusCode, resp] of Object.entries(responses)) {
|
|
1276
|
+
const code = parseInt(statusCode, 10);
|
|
1277
|
+
if (code >= 200 && code < 300) {
|
|
1278
|
+
let extractedType = {
|
|
1279
|
+
kind: "primitive",
|
|
1280
|
+
type: "unknown"
|
|
1281
|
+
};
|
|
1282
|
+
const jsonContent = resp.content?.["application/json"];
|
|
1283
|
+
if (jsonContent?.schema) {
|
|
1284
|
+
const contextName = deriveResponseName(op, path ?? "/", method ?? "get");
|
|
1285
|
+
const result = classifyAndExtractResponse(jsonContent.schema, contextName);
|
|
1286
|
+
extractedType = result.response;
|
|
1287
|
+
inlineModels = result.inlineModels;
|
|
1288
|
+
isPaginated = result.isPaginated;
|
|
1289
|
+
dataPath = result.dataPath;
|
|
1290
|
+
itemType = result.itemType;
|
|
1291
|
+
} else if (resp.content) if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) extractedType = {
|
|
1292
|
+
kind: "primitive",
|
|
1293
|
+
type: "string",
|
|
1294
|
+
format: "binary"
|
|
1295
|
+
};
|
|
1296
|
+
else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) extractedType = {
|
|
1297
|
+
kind: "primitive",
|
|
1298
|
+
type: "string"
|
|
1299
|
+
};
|
|
1300
|
+
else {
|
|
1301
|
+
const firstKey = Object.keys(resp.content)[0];
|
|
1302
|
+
if (firstKey) {
|
|
1303
|
+
const firstContent = resp.content[firstKey];
|
|
1304
|
+
if (firstContent?.schema) extractedType = schemaToTypeRef(firstContent.schema, deriveResponseName(op, path ?? "/", method ?? "get"));
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
allSuccessResponses.push({
|
|
1308
|
+
statusCode: code,
|
|
1309
|
+
type: extractedType
|
|
1310
|
+
});
|
|
1311
|
+
} else if (code >= 400) {
|
|
1312
|
+
const jsonContent = resp.content?.["application/json"];
|
|
1313
|
+
const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
|
|
1314
|
+
errors.push({
|
|
1315
|
+
statusCode: code,
|
|
1316
|
+
type
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
if (allSuccessResponses.length > 0) {
|
|
1321
|
+
const sorted = [...allSuccessResponses].sort((a, b) => a.statusCode - b.statusCode);
|
|
1322
|
+
response = (sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0]).type;
|
|
1323
|
+
}
|
|
1324
|
+
const successResponses = allSuccessResponses.length > 1 ? allSuccessResponses : void 0;
|
|
1325
|
+
return {
|
|
1326
|
+
response,
|
|
1327
|
+
successResponses,
|
|
1328
|
+
errors,
|
|
1329
|
+
inlineModels,
|
|
1330
|
+
isPaginated,
|
|
1331
|
+
dataPath,
|
|
1332
|
+
itemType
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/parser/normalize-inline-models.ts
|
|
1337
|
+
function keepLargestByName(models) {
|
|
1338
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1339
|
+
for (const model of models) {
|
|
1340
|
+
const existing = byName.get(model.name);
|
|
1341
|
+
if (!existing || model.fields.length > existing.fields.length) byName.set(model.name, model);
|
|
1342
|
+
}
|
|
1343
|
+
return [...byName.values()];
|
|
1344
|
+
}
|
|
1345
|
+
function rewriteModelRef(ref, oldName, newName) {
|
|
1346
|
+
walkTypeRef(ref, { model: (r) => {
|
|
1347
|
+
if (r.name === oldName) r.name = newName;
|
|
1348
|
+
} });
|
|
1349
|
+
}
|
|
1350
|
+
function rewriteModelRefs(models, services, oldName, newName) {
|
|
1351
|
+
for (const model of models) for (const field of model.fields) rewriteModelRef(field.type, oldName, newName);
|
|
1352
|
+
for (const service of services) for (const op of service.operations) {
|
|
1353
|
+
rewriteModelRef(op.response, oldName, newName);
|
|
1354
|
+
if (op.requestBody) rewriteModelRef(op.requestBody, oldName, newName);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function mergeInlineResponseModels(schemaModels, inlineModels) {
|
|
1358
|
+
const mergedSchemaModels = [...schemaModels];
|
|
1359
|
+
const schemaModelNames = new Set(mergedSchemaModels.map((m) => m.name));
|
|
1360
|
+
const schemaModelsByName = new Map(mergedSchemaModels.map((m) => [m.name, m]));
|
|
1361
|
+
const deduplicatedInlineModels = inlineModels.filter((model) => {
|
|
1362
|
+
if (!schemaModelNames.has(model.name)) return true;
|
|
1363
|
+
const existing = schemaModelsByName.get(model.name);
|
|
1364
|
+
const existingFieldNames = new Set(existing.fields.map((f) => f.name));
|
|
1365
|
+
const inlineFieldNames = new Set(model.fields.map((f) => f.name));
|
|
1366
|
+
const hasDifference = model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name));
|
|
1367
|
+
if (hasDifference && model.fields.length > existing.fields.length) {
|
|
1368
|
+
const idx = mergedSchemaModels.indexOf(existing);
|
|
1369
|
+
if (idx !== -1) mergedSchemaModels[idx] = model;
|
|
1370
|
+
schemaModelsByName.set(model.name, model);
|
|
1371
|
+
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`);
|
|
1372
|
+
} else if (hasDifference) console.warn(`[oagen] Warning: Inline model "${model.name}" has different fields than component schema — using component schema`);
|
|
1373
|
+
return false;
|
|
1374
|
+
});
|
|
1375
|
+
return [...mergedSchemaModels, ...keepLargestByName(deduplicatedInlineModels)];
|
|
1376
|
+
}
|
|
1377
|
+
function mergeFieldInlineModels(existingModels, fieldInlineModels) {
|
|
1378
|
+
const modelNames = new Set(existingModels.map((m) => m.name));
|
|
1379
|
+
const modelsByName = new Map(existingModels.map((m) => [m.name, m]));
|
|
1380
|
+
const deduplicatedFieldModels = fieldInlineModels.filter((model) => {
|
|
1381
|
+
if (!modelNames.has(model.name)) return true;
|
|
1382
|
+
const existing = modelsByName.get(model.name);
|
|
1383
|
+
const existingFieldNames = new Set(existing.fields.map((f) => f.name));
|
|
1384
|
+
const inlineFieldNames = new Set(model.fields.map((f) => f.name));
|
|
1385
|
+
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`);
|
|
1386
|
+
return false;
|
|
1387
|
+
});
|
|
1388
|
+
return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
|
|
1389
|
+
}
|
|
1390
|
+
function hasModelConstDiscriminant(model) {
|
|
1391
|
+
return model.fields.some((f) => (f.name === "object" || f.name === "type") && f.type.kind === "literal" && typeof f.type.value === "string");
|
|
1392
|
+
}
|
|
1393
|
+
function isModelReferencedByOthers(name, models, excludeNames) {
|
|
1394
|
+
for (const model of models) {
|
|
1395
|
+
if (excludeNames.has(model.name)) continue;
|
|
1396
|
+
for (const field of model.fields) {
|
|
1397
|
+
let found = false;
|
|
1398
|
+
walkTypeRef(field.type, { model: (r) => {
|
|
1399
|
+
if (r.name === name) found = true;
|
|
1400
|
+
} });
|
|
1401
|
+
if (found) return true;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
function collapseJsonSuffixModels(models, services) {
|
|
1407
|
+
const normalizedModels = [...models];
|
|
1408
|
+
const byName = new Map(normalizedModels.map((m) => [m.name, m]));
|
|
1409
|
+
const mergeCandidates = [];
|
|
1410
|
+
for (const model of normalizedModels) {
|
|
1411
|
+
if (!model.name.endsWith("Json")) continue;
|
|
1412
|
+
const baseName = model.name.slice(0, -4);
|
|
1413
|
+
const baseModel = byName.get(baseName);
|
|
1414
|
+
if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
|
|
1415
|
+
if (!baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name))) continue;
|
|
1416
|
+
if (hasModelConstDiscriminant(model) && hasModelConstDiscriminant(baseModel)) continue;
|
|
1417
|
+
if (isModelReferencedByOthers(model.name, normalizedModels, new Set([model.name, baseName]))) continue;
|
|
1418
|
+
mergeCandidates.push({
|
|
1419
|
+
jsonModel: model,
|
|
1420
|
+
baseModel
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
1424
|
+
for (const { jsonModel, baseModel } of mergeCandidates) {
|
|
1425
|
+
baseModel.fields = jsonModel.fields;
|
|
1426
|
+
toRemove.add(jsonModel.name);
|
|
1427
|
+
rewriteModelRefs(normalizedModels, services, jsonModel.name, baseModel.name);
|
|
1428
|
+
console.warn(`[oagen] Merged "${jsonModel.name}" into "${baseModel.name}" (${jsonModel.fields.length} fields, superset)`);
|
|
1429
|
+
}
|
|
1430
|
+
return normalizedModels.filter((m) => !toRemove.has(m.name));
|
|
1431
|
+
}
|
|
1432
|
+
//#endregion
|
|
1433
|
+
//#region src/parser/collect-inline-enums.ts
|
|
1434
|
+
function collectInlineEnumsFromModels(models, enums) {
|
|
1435
|
+
const enumNames = new Set(enums.map((e) => e.name));
|
|
1436
|
+
for (const model of models) for (const field of model.fields) collectInlineEnumFromRef(field.type, enums, enumNames);
|
|
1437
|
+
}
|
|
1438
|
+
//#endregion
|
|
1439
|
+
//#region src/parser/normalize-model-refs.ts
|
|
1440
|
+
function validateModelRefs(spec) {
|
|
1441
|
+
const knownNames = /* @__PURE__ */ new Set();
|
|
1442
|
+
for (const m of spec.models) knownNames.add(m.name);
|
|
1443
|
+
for (const e of spec.enums) knownNames.add(e.name);
|
|
1444
|
+
function walkRef(ref, context) {
|
|
1445
|
+
walkTypeRef(ref, { model: (r) => {
|
|
1446
|
+
if (!knownNames.has(r.name)) console.warn(`[oagen] Warning: Unresolved model reference "${r.name}" (context: ${context})`);
|
|
1447
|
+
} });
|
|
1448
|
+
}
|
|
1449
|
+
for (const model of spec.models) for (const field of model.fields) walkRef(field.type, `${model.name}.${field.name}`);
|
|
1450
|
+
for (const service of spec.services) for (const op of service.operations) {
|
|
1451
|
+
for (const p of [
|
|
1452
|
+
...op.pathParams,
|
|
1453
|
+
...op.queryParams,
|
|
1454
|
+
...op.headerParams
|
|
1455
|
+
]) walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
|
|
1456
|
+
if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
|
|
1457
|
+
walkRef(op.response, `${service.name}.${op.name}.response`);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
//#endregion
|
|
1461
|
+
//#region src/parser/parse.ts
|
|
1462
|
+
async function parseSpec(specPath, options) {
|
|
1463
|
+
const { parsed } = await loadAndBundleSpec(specPath);
|
|
1464
|
+
const spec = parsed;
|
|
1465
|
+
const version = spec.openapi ?? "";
|
|
1466
|
+
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}\`.`);
|
|
1467
|
+
const { models, enums } = extractSchemas(spec.components?.schemas);
|
|
1468
|
+
const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform);
|
|
1469
|
+
const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(mergeInlineResponseModels(models, inlineModels), extractInlineModelsFromSchemas(spec.components?.schemas)), services);
|
|
1470
|
+
collectInlineEnumsFromModels(finalModels, enums);
|
|
1471
|
+
const auth = extractAuthSchemes(spec.components?.securitySchemes);
|
|
1472
|
+
const serverEntries = (spec.servers ?? []).map((s) => ({
|
|
1473
|
+
url: s.url ?? "",
|
|
1474
|
+
description: s.description
|
|
1475
|
+
})).filter((s) => s.url);
|
|
1476
|
+
const result = {
|
|
1477
|
+
name: spec.info?.title ?? "Unknown API",
|
|
1478
|
+
version: spec.info?.version ?? "0.0.0",
|
|
1479
|
+
description: spec.info?.description,
|
|
1480
|
+
baseUrl: serverEntries[0]?.url ?? "",
|
|
1481
|
+
servers: serverEntries.length > 0 ? serverEntries : void 0,
|
|
1482
|
+
services,
|
|
1483
|
+
models: finalModels,
|
|
1484
|
+
enums,
|
|
1485
|
+
auth
|
|
1486
|
+
};
|
|
1487
|
+
validateModelRefs(result);
|
|
1488
|
+
return result;
|
|
1489
|
+
}
|
|
1490
|
+
/** Extract authentication schemes from OpenAPI securitySchemes. */
|
|
1491
|
+
function extractAuthSchemes(securitySchemes) {
|
|
1492
|
+
if (!securitySchemes) return void 0;
|
|
1493
|
+
const schemes = [];
|
|
1494
|
+
for (const [, scheme] of Object.entries(securitySchemes)) if (scheme.type === "http" && scheme.scheme === "bearer") schemes.push({ kind: "bearer" });
|
|
1495
|
+
else if (scheme.type === "apiKey" && scheme.in && scheme.name) schemes.push({
|
|
1496
|
+
kind: "apiKey",
|
|
1497
|
+
in: scheme.in,
|
|
1498
|
+
name: scheme.name
|
|
1499
|
+
});
|
|
1500
|
+
else if (scheme.type === "oauth2" && scheme.flows) schemes.push({
|
|
1501
|
+
kind: "oauth2",
|
|
1502
|
+
flows: scheme.flows
|
|
1503
|
+
});
|
|
1504
|
+
return schemes.length > 0 ? schemes : void 0;
|
|
1505
|
+
}
|
|
1506
|
+
//#endregion
|
|
1507
|
+
//#region src/engine/operation-plan.ts
|
|
1508
|
+
function planOperation(op) {
|
|
1509
|
+
const isDelete = op.httpMethod === "delete";
|
|
1510
|
+
const hasBody = !!op.requestBody;
|
|
1511
|
+
const isIdempotentPost = op.injectIdempotencyKey && op.httpMethod === "post";
|
|
1512
|
+
const hasQueryParams = op.queryParams.length > 0;
|
|
1513
|
+
const pathParamsInOptions = op.pathParams.length > 1 || op.pathParams.length > 0 && (hasBody || hasQueryParams);
|
|
1514
|
+
const isPaginated = op.pagination !== void 0;
|
|
1515
|
+
const responseModelName = resolveResponseModelName(op);
|
|
1516
|
+
return {
|
|
1517
|
+
operation: op,
|
|
1518
|
+
isDelete,
|
|
1519
|
+
hasBody,
|
|
1520
|
+
isIdempotentPost,
|
|
1521
|
+
pathParamsInOptions,
|
|
1522
|
+
isPaginated,
|
|
1523
|
+
responseModelName,
|
|
1524
|
+
paginatedItemModelName: isPaginated && op.pagination?.itemType.kind === "model" ? op.pagination.itemType.name : null,
|
|
1525
|
+
isModelResponse: responseModelName !== null,
|
|
1526
|
+
hasQueryParams,
|
|
1527
|
+
isAsync: op.async ?? true
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function resolveResponseModelName(op) {
|
|
1531
|
+
if (op.httpMethod === "delete") return null;
|
|
1532
|
+
return extractModelName(op.response);
|
|
1533
|
+
}
|
|
1534
|
+
function extractModelName(ref) {
|
|
1535
|
+
switch (ref.kind) {
|
|
1536
|
+
case "model": return ref.name;
|
|
1537
|
+
case "array": return ref.items.kind === "model" ? ref.items.name : null;
|
|
1538
|
+
case "nullable": return extractModelName(ref.inner);
|
|
1539
|
+
case "union": {
|
|
1540
|
+
const firstModel = ref.variants.find((v) => v.kind === "model");
|
|
1541
|
+
return firstModel && firstModel.kind === "model" ? firstModel.name : null;
|
|
1542
|
+
}
|
|
1543
|
+
case "map":
|
|
1544
|
+
case "enum":
|
|
1545
|
+
case "primitive":
|
|
1546
|
+
case "literal": return null;
|
|
1547
|
+
default: return assertNever(ref);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
//#endregion
|
|
1551
|
+
export { ACRONYM_SET, CommandError, ConfigError, ConfigLoadError, ConfigVersionMismatchError, ExtractorError, InternalError, OagenError, RegistryError, SpecParseError, applyFileHeaders, assertNever, assignModelsToServices, buildEmitterContext, cleanSchemaName, collectEnumRefs, collectFieldDependencies, collectModelRefs, collectReferencedNames, collectRequestBodyModels, diffSpecs, generate, generateAllFiles, generateFiles, getEmitter, integrateGeneratedFiles, mapFilesForTargetIntegration, mapTypeRef, parseSpec, planOperation, registerEmitter, stripBackendPrefixes, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toUpperSnakeCase, treeShakeFiles, walkTypeRef };
|
|
1552
|
+
|
|
1553
|
+
//# sourceMappingURL=index.mjs.map
|