@workos/oagen 0.0.1
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/.claude-plugin/plugin.json +12 -0
- package/LICENSE.txt +21 -0
- package/README.md +197 -0
- package/dist/chunk-CIX5NOBF.js +263 -0
- package/dist/chunk-IJ5E5AE7.js +1040 -0
- package/dist/chunk-P5XUDUGY.js +1748 -0
- package/dist/cli/index.js +5603 -0
- package/dist/compat/index.d.ts +195 -0
- package/dist/compat/index.js +4453 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +1733 -0
- package/dist/scripts/smoke/baseline.js +1751 -0
- package/dist/scripts/smoke/sdk-test.js +93 -0
- package/dist/smoke.js +1963 -0
- package/dist/types-B5t9O9Wm.d.ts +367 -0
- package/dist/verify/index.d.ts +76 -0
- package/dist/verify/index.js +336 -0
- package/docs/advanced/index.md +11 -0
- package/docs/advanced/oagen-ignore.md +144 -0
- package/docs/agents/architecture.md +53 -0
- package/docs/agents/emitters.md +69 -0
- package/docs/agents/testing.md +60 -0
- package/docs/architecture/dependency-layers.md +75 -0
- package/docs/architecture/emitter-contract.md +160 -0
- package/docs/architecture/extractor-contract.md +227 -0
- package/docs/architecture/ir-types.md +275 -0
- package/docs/architecture/non-additive-changes.md +76 -0
- package/docs/architecture/pipeline.md +110 -0
- package/docs/architecture/workflows.md +215 -0
- package/docs/cli.md +156 -0
- package/docs/contributor/index.md +10 -0
- package/docs/contributor/public-api.md +59 -0
- package/docs/contributor/versioning.md +48 -0
- package/docs/core/index.md +12 -0
- package/docs/core/minimal-emitter.md +134 -0
- package/docs/core/quickstart.md +142 -0
- package/docs/diagrams/generate-sdk-workflow.md +50 -0
- package/docs/index.md +40 -0
- package/examples/reference-emitter/README.md +41 -0
- package/examples/reference-emitter/oagen.config.ts +7 -0
- package/examples/reference-emitter/spec/github-subset.yml +400 -0
- package/examples/reference-emitter/src/emitter.ts +52 -0
- package/examples/reference-emitter/src/generators/client.ts +37 -0
- package/examples/reference-emitter/src/generators/config.ts +19 -0
- package/examples/reference-emitter/src/generators/enums.ts +16 -0
- package/examples/reference-emitter/src/generators/errors.ts +39 -0
- package/examples/reference-emitter/src/generators/models.ts +29 -0
- package/examples/reference-emitter/src/generators/resources.ts +60 -0
- package/examples/reference-emitter/src/generators/tests.ts +7 -0
- package/examples/reference-emitter/src/index.ts +1 -0
- package/examples/reference-emitter/src/naming.ts +21 -0
- package/examples/reference-emitter/src/type-mapper.ts +39 -0
- package/examples/reference-emitter/tsconfig.json +19 -0
- package/package.json +102 -0
- package/skills/generate-emitter/SKILL.md +393 -0
- package/skills/generate-emitter/references/common-pitfalls.md +23 -0
- package/skills/generate-emitter/references/generator-guide.md +201 -0
- package/skills/generate-emitter/references/project-scaffold.md +138 -0
- package/skills/generate-extractor/SKILL.md +200 -0
- package/skills/generate-sdk/SKILL.md +155 -0
- package/skills/generate-smoke-test/SKILL.md +185 -0
- package/skills/integrate/SKILL.md +108 -0
- package/skills/verify-compat/SKILL.md +151 -0
- package/skills/verify-smoke-test/SKILL.md +176 -0
|
@@ -0,0 +1,1751 @@
|
|
|
1
|
+
// scripts/smoke/baseline.ts
|
|
2
|
+
import { writeFileSync } from "fs";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var OagenError = class extends Error {
|
|
6
|
+
hint;
|
|
7
|
+
constructor(message, hint) {
|
|
8
|
+
const fullMessage = hint ? `${message}
|
|
9
|
+
Hint: ${hint}` : message;
|
|
10
|
+
super(fullMessage);
|
|
11
|
+
this.name = "OagenError";
|
|
12
|
+
this.hint = hint;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var SpecParseError = class extends OagenError {
|
|
16
|
+
constructor(message, hint) {
|
|
17
|
+
super(message, hint);
|
|
18
|
+
this.name = "SpecParseError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/parser/refs.ts
|
|
23
|
+
import { readFile } from "fs/promises";
|
|
24
|
+
import { resolve } from "path";
|
|
25
|
+
import { pathToFileURL } from "url";
|
|
26
|
+
import { makeDocumentFromString, bundleDocument, createConfig, BaseResolver } from "@redocly/openapi-core";
|
|
27
|
+
async function loadAndBundleSpec(specPath) {
|
|
28
|
+
const absolutePath = resolve(specPath);
|
|
29
|
+
const content = await readFile(absolutePath, "utf-8");
|
|
30
|
+
const document = makeDocumentFromString(content, pathToFileURL(absolutePath).href);
|
|
31
|
+
const config = await createConfig({});
|
|
32
|
+
const resolver = new BaseResolver();
|
|
33
|
+
const result = await bundleDocument({
|
|
34
|
+
document,
|
|
35
|
+
config: config.styleguide,
|
|
36
|
+
externalRefResolver: resolver,
|
|
37
|
+
dereference: false
|
|
38
|
+
});
|
|
39
|
+
if (result.problems.some((p) => p.severity === "error")) {
|
|
40
|
+
const errors = result.problems.filter((p) => p.severity === "error").map((p) => p.message).join("\n");
|
|
41
|
+
throw new SpecParseError(
|
|
42
|
+
`Failed to parse spec: ${errors}`,
|
|
43
|
+
`Check the OpenAPI spec at "${absolutePath}" for syntax errors. Run a linter such as \`npx @redocly/cli lint ${specPath}\` to identify issues.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
parsed: result.bundle.parsed,
|
|
48
|
+
specPath: absolutePath
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/ir/types.ts
|
|
53
|
+
function assertNever(x) {
|
|
54
|
+
const kind = x.kind;
|
|
55
|
+
const err = new Error(
|
|
56
|
+
`Unexpected TypeRef kind: ${kind}
|
|
57
|
+
Hint: If you added a new TypeRef variant, handle it in every switch/case that calls assertNever.`
|
|
58
|
+
);
|
|
59
|
+
err.name = "InternalError";
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
function walkTypeRef(ref, visitor) {
|
|
63
|
+
switch (ref.kind) {
|
|
64
|
+
case "model":
|
|
65
|
+
visitor.model?.(ref);
|
|
66
|
+
break;
|
|
67
|
+
case "enum":
|
|
68
|
+
visitor.enum?.(ref);
|
|
69
|
+
break;
|
|
70
|
+
case "array":
|
|
71
|
+
walkTypeRef(ref.items, visitor);
|
|
72
|
+
break;
|
|
73
|
+
case "nullable":
|
|
74
|
+
walkTypeRef(ref.inner, visitor);
|
|
75
|
+
break;
|
|
76
|
+
case "union":
|
|
77
|
+
for (const v of ref.variants) walkTypeRef(v, visitor);
|
|
78
|
+
break;
|
|
79
|
+
case "map":
|
|
80
|
+
if (ref.keyType) walkTypeRef(ref.keyType, visitor);
|
|
81
|
+
walkTypeRef(ref.valueType, visitor);
|
|
82
|
+
break;
|
|
83
|
+
case "literal":
|
|
84
|
+
visitor.literal?.(ref);
|
|
85
|
+
break;
|
|
86
|
+
case "primitive":
|
|
87
|
+
visitor.primitive?.(ref);
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
assertNever(ref);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/utils/naming.ts
|
|
95
|
+
function splitWords(s) {
|
|
96
|
+
if (!s) return [];
|
|
97
|
+
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);
|
|
98
|
+
}
|
|
99
|
+
var ACRONYM_SET = /* @__PURE__ */ new Set(["SSO", "FGA", "SAML", "SCIM", "JWT", "HMAC"]);
|
|
100
|
+
function toPascalCase(s, acronyms) {
|
|
101
|
+
const merged = acronyms ? /* @__PURE__ */ new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
|
|
102
|
+
return splitWords(s).map((w) => {
|
|
103
|
+
const upper = w.toUpperCase();
|
|
104
|
+
if (merged.has(upper)) return upper;
|
|
105
|
+
if (upper === "OAUTH") return "OAuth";
|
|
106
|
+
return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
|
|
107
|
+
}).join("");
|
|
108
|
+
}
|
|
109
|
+
function toCamelCase(s, acronyms) {
|
|
110
|
+
const merged = acronyms ? /* @__PURE__ */ new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
|
|
111
|
+
const words = splitWords(s);
|
|
112
|
+
if (words.length === 0) return "";
|
|
113
|
+
return words.map((w, i) => {
|
|
114
|
+
if (i === 0) return w.toLowerCase();
|
|
115
|
+
const upper = w.toUpperCase();
|
|
116
|
+
if (merged.has(upper)) return upper;
|
|
117
|
+
if (upper === "OAUTH") return "OAuth";
|
|
118
|
+
return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
|
|
119
|
+
}).join("");
|
|
120
|
+
}
|
|
121
|
+
function toSnakeCase(s) {
|
|
122
|
+
return splitWords(s).map((w) => w.toLowerCase()).join("_");
|
|
123
|
+
}
|
|
124
|
+
function toUpperSnakeCase(s) {
|
|
125
|
+
return splitWords(s).map((w) => w.toUpperCase()).join("_");
|
|
126
|
+
}
|
|
127
|
+
var BACKEND_SUFFIXES = ["Dto", "DTO", "Controller"];
|
|
128
|
+
function stripBackendSuffixes(name) {
|
|
129
|
+
for (const suffix of BACKEND_SUFFIXES) {
|
|
130
|
+
if (name.endsWith(suffix) && name.length > suffix.length) {
|
|
131
|
+
return name.slice(0, -suffix.length);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return name;
|
|
135
|
+
}
|
|
136
|
+
function stripBackendPrefixes(name) {
|
|
137
|
+
return name.replace(/Userland/g, "").replace(/Controller/g, "");
|
|
138
|
+
}
|
|
139
|
+
function stripListItemMarkers(name) {
|
|
140
|
+
return name.replace(/ListItem/g, "").replace(/ByExternalId/g, "").replace(/ByResourceId/g, "").replace(/ForResource/g, "");
|
|
141
|
+
}
|
|
142
|
+
var SINGULAR_SAFE_LIST = /* @__PURE__ */ new Set([
|
|
143
|
+
"Status",
|
|
144
|
+
"Address",
|
|
145
|
+
"Access",
|
|
146
|
+
"Process",
|
|
147
|
+
"Progress",
|
|
148
|
+
"Success",
|
|
149
|
+
"Radius",
|
|
150
|
+
"Canvas",
|
|
151
|
+
"Alias",
|
|
152
|
+
"Basis",
|
|
153
|
+
"Bus"
|
|
154
|
+
]);
|
|
155
|
+
function singularize(word) {
|
|
156
|
+
if (SINGULAR_SAFE_LIST.has(word)) return word;
|
|
157
|
+
if (word.endsWith("ies") && word.length > 4) {
|
|
158
|
+
return word.slice(0, -3) + "y";
|
|
159
|
+
}
|
|
160
|
+
if (word.endsWith("ses") && word.length > 4) {
|
|
161
|
+
return word.slice(0, -2);
|
|
162
|
+
}
|
|
163
|
+
if (word.endsWith("s") && !word.endsWith("ss") && word.length > 4) {
|
|
164
|
+
return word.slice(0, -1);
|
|
165
|
+
}
|
|
166
|
+
return word;
|
|
167
|
+
}
|
|
168
|
+
function cleanSchemaName(name) {
|
|
169
|
+
let result = stripBackendPrefixes(stripBackendSuffixes(name));
|
|
170
|
+
result = stripListItemMarkers(result);
|
|
171
|
+
const match = result.match(/^([A-Z][a-z]*)/);
|
|
172
|
+
if (match) {
|
|
173
|
+
const leadWord = match[1];
|
|
174
|
+
const singular = singularize(leadWord);
|
|
175
|
+
if (singular !== leadWord) {
|
|
176
|
+
result = singular + result.slice(leadWord.length);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/parser/inline-models.ts
|
|
183
|
+
function qualifyInlineModelName(baseName, parentName) {
|
|
184
|
+
if (!parentName) return baseName;
|
|
185
|
+
const cleanParent = stripListItemMarkers(parentName);
|
|
186
|
+
if (baseName.startsWith(cleanParent)) return baseName;
|
|
187
|
+
const trailingMatch = baseName.match(/^(.*?)([A-Z][a-z]*)$/);
|
|
188
|
+
if (trailingMatch) {
|
|
189
|
+
const [, prefix, trailingWord] = trailingMatch;
|
|
190
|
+
const singular = singularize(trailingWord);
|
|
191
|
+
return `${cleanParent}${prefix}${singular}`;
|
|
192
|
+
}
|
|
193
|
+
return `${cleanParent}${baseName}`;
|
|
194
|
+
}
|
|
195
|
+
function extractInlineModelsFromSchemas(schemas) {
|
|
196
|
+
if (!schemas) return [];
|
|
197
|
+
const inlineModels = [];
|
|
198
|
+
for (const [schemaName, schema] of Object.entries(schemas)) {
|
|
199
|
+
const parentName = cleanSchemaName(toPascalCase(schemaName));
|
|
200
|
+
extractInlineModelsFromProperties(schema, inlineModels, parentName);
|
|
201
|
+
}
|
|
202
|
+
return inlineModels;
|
|
203
|
+
}
|
|
204
|
+
function extractInlineModelsFromProperties(schema, results, parentName) {
|
|
205
|
+
const properties = schema.properties ?? {};
|
|
206
|
+
const allOfSchemas = schema.allOf ?? [];
|
|
207
|
+
for (const sub of allOfSchemas) {
|
|
208
|
+
if (sub.properties) {
|
|
209
|
+
extractInlineModelsFromProperties(sub, results, parentName);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
|
213
|
+
if (!fieldSchema) continue;
|
|
214
|
+
if (fieldSchema.properties && (fieldSchema.type === "object" || !fieldSchema.type)) {
|
|
215
|
+
const baseName = toPascalCase(fieldName);
|
|
216
|
+
const modelName = qualifyInlineModelName(baseName, parentName);
|
|
217
|
+
results.push(buildInlineModel(modelName, fieldSchema));
|
|
218
|
+
extractInlineModelsFromProperties(fieldSchema, results, modelName);
|
|
219
|
+
}
|
|
220
|
+
if (fieldSchema.type === "array" && fieldSchema.items) {
|
|
221
|
+
const items = fieldSchema.items;
|
|
222
|
+
if (items.properties && (items.type === "object" || !items.type)) {
|
|
223
|
+
const baseName = toPascalCase(fieldName);
|
|
224
|
+
const modelName = qualifyInlineModelName(baseName, parentName);
|
|
225
|
+
results.push(buildInlineModel(modelName, items));
|
|
226
|
+
extractInlineModelsFromProperties(items, results, modelName);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (fieldSchema.oneOf) {
|
|
230
|
+
const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
|
|
231
|
+
if (objectVariant) {
|
|
232
|
+
const baseName = toPascalCase(fieldName);
|
|
233
|
+
const modelName = qualifyInlineModelName(baseName, parentName);
|
|
234
|
+
const existingNames = new Set(results.map((r) => r.name));
|
|
235
|
+
if (!existingNames.has(modelName)) {
|
|
236
|
+
results.push(buildInlineModel(modelName, objectVariant));
|
|
237
|
+
extractInlineModelsFromProperties(objectVariant, results, modelName);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function buildInlineModel(name, schema) {
|
|
244
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
245
|
+
const fields = [];
|
|
246
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
|
|
247
|
+
if (!fieldSchema) continue;
|
|
248
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
249
|
+
}
|
|
250
|
+
return { name, description: schema.description, fields };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/parser/schemas.ts
|
|
254
|
+
function extractSchemas(schemas) {
|
|
255
|
+
const models = [];
|
|
256
|
+
const enums = [];
|
|
257
|
+
if (!schemas) return { models, enums };
|
|
258
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
259
|
+
const pascalName = cleanSchemaName(toPascalCase(name));
|
|
260
|
+
if (schema.enum) {
|
|
261
|
+
enums.push(extractEnum(pascalName, schema));
|
|
262
|
+
} else {
|
|
263
|
+
models.push(extractModel(pascalName, schema, schemas));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const modelNames = new Set(models.map((m) => m.name));
|
|
267
|
+
const inlineQueue = [...models];
|
|
268
|
+
while (inlineQueue.length > 0) {
|
|
269
|
+
const model = inlineQueue.pop();
|
|
270
|
+
for (const field of model.fields) {
|
|
271
|
+
collectNestedInlineModels(field.type, field.name, model.name, schemas ?? {}, models, modelNames, inlineQueue);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const model of models) {
|
|
275
|
+
collectInlineEnums(model.fields, enums);
|
|
276
|
+
}
|
|
277
|
+
return { models, enums };
|
|
278
|
+
}
|
|
279
|
+
function collectInlineEnumFromRef(ref, enums, seen) {
|
|
280
|
+
walkTypeRef(ref, {
|
|
281
|
+
enum: (r) => {
|
|
282
|
+
if (r.values && !seen.has(r.name)) {
|
|
283
|
+
seen.add(r.name);
|
|
284
|
+
enums.push({
|
|
285
|
+
name: r.name,
|
|
286
|
+
values: r.values.map((v) => ({
|
|
287
|
+
name: toUpperSnakeCase(String(v)),
|
|
288
|
+
value: v,
|
|
289
|
+
description: void 0
|
|
290
|
+
}))
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function collectInlineEnums(fields, enums) {
|
|
297
|
+
const seen = new Set(enums.map((e) => e.name));
|
|
298
|
+
for (const field of fields) {
|
|
299
|
+
collectInlineEnumFromRef(field.type, enums, seen);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
|
|
303
|
+
walkTypeRef(ref, {
|
|
304
|
+
model: (modelRef) => {
|
|
305
|
+
if (modelNames.has(modelRef.name)) return;
|
|
306
|
+
const isComponent = Object.keys(schemas).some((k) => cleanSchemaName(toPascalCase(k)) === modelRef.name);
|
|
307
|
+
if (isComponent) return;
|
|
308
|
+
const parentSchema = findSchemaByName(parentModelName, schemas);
|
|
309
|
+
if (!parentSchema) return;
|
|
310
|
+
const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
|
|
311
|
+
if (!fieldSchema) return;
|
|
312
|
+
const extracted = extractNestedSchema(modelRef.name, fieldSchema);
|
|
313
|
+
for (const m of extracted) {
|
|
314
|
+
if (!modelNames.has(m.name)) {
|
|
315
|
+
models.push(m);
|
|
316
|
+
modelNames.add(m.name);
|
|
317
|
+
queue.push(m);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function findSchemaByName(pascalName, schemas) {
|
|
324
|
+
for (const [k, v] of Object.entries(schemas)) {
|
|
325
|
+
if (cleanSchemaName(toPascalCase(k)) === pascalName) return v;
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
function findNestedFieldSchema(fieldName, parentSchema, schemas) {
|
|
330
|
+
if (parentSchema.properties?.[fieldName]) {
|
|
331
|
+
return parentSchema.properties[fieldName];
|
|
332
|
+
}
|
|
333
|
+
if (parentSchema.allOf) {
|
|
334
|
+
for (const sub of parentSchema.allOf) {
|
|
335
|
+
let resolved = sub;
|
|
336
|
+
if (sub.$ref) {
|
|
337
|
+
const segments = sub.$ref.split("/");
|
|
338
|
+
const refName = segments[segments.length - 1];
|
|
339
|
+
if (refName && schemas[refName]) resolved = schemas[refName];
|
|
340
|
+
}
|
|
341
|
+
if (resolved.properties?.[fieldName]) {
|
|
342
|
+
return resolved.properties[fieldName];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
function extractNestedSchema(name, schema) {
|
|
349
|
+
if (schema.oneOf) {
|
|
350
|
+
const models = [];
|
|
351
|
+
for (const variant of schema.oneOf) {
|
|
352
|
+
if (variant.$ref) continue;
|
|
353
|
+
if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
354
|
+
const requiredSet = new Set(variant.required ?? []);
|
|
355
|
+
const fields = [];
|
|
356
|
+
for (const [fn, fs] of Object.entries(variant.properties)) {
|
|
357
|
+
if (!fs) continue;
|
|
358
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
359
|
+
}
|
|
360
|
+
const variantName = models.length === 0 ? name : `${name}${models.length + 1}`;
|
|
361
|
+
models.push({ name: variantName, description: variant.description, fields });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return models;
|
|
365
|
+
}
|
|
366
|
+
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
367
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
368
|
+
const fields = [];
|
|
369
|
+
for (const [fn, fs] of Object.entries(schema.properties)) {
|
|
370
|
+
if (!fs) continue;
|
|
371
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
372
|
+
}
|
|
373
|
+
return [{ name, description: schema.description, fields }];
|
|
374
|
+
}
|
|
375
|
+
if (schema.type === "array" && schema.items?.properties) {
|
|
376
|
+
const requiredSet = new Set(schema.items.required ?? []);
|
|
377
|
+
const fields = [];
|
|
378
|
+
for (const [fn, fs] of Object.entries(schema.items.properties)) {
|
|
379
|
+
if (!fs) continue;
|
|
380
|
+
fields.push(buildFieldFromSchema(fn, fs, name, requiredSet));
|
|
381
|
+
}
|
|
382
|
+
return [{ name, description: schema.items.description, fields }];
|
|
383
|
+
}
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
function buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet) {
|
|
387
|
+
return {
|
|
388
|
+
name: fieldName,
|
|
389
|
+
type: schemaToTypeRef(fieldSchema, fieldName, contextName),
|
|
390
|
+
required: requiredSet.has(fieldName),
|
|
391
|
+
description: fieldSchema.description,
|
|
392
|
+
readOnly: fieldSchema.readOnly || void 0,
|
|
393
|
+
writeOnly: fieldSchema.writeOnly || void 0,
|
|
394
|
+
deprecated: fieldSchema.deprecated || void 0,
|
|
395
|
+
default: fieldSchema.default
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function extractEnum(name, schema) {
|
|
399
|
+
const values = (schema.enum ?? []).map((v) => ({
|
|
400
|
+
name: toUpperSnakeCase(String(v)),
|
|
401
|
+
value: typeof v === "number" ? v : String(v),
|
|
402
|
+
description: void 0
|
|
403
|
+
}));
|
|
404
|
+
return { name, values };
|
|
405
|
+
}
|
|
406
|
+
function extractModel(name, schema, schemas) {
|
|
407
|
+
if (schema.allOf) {
|
|
408
|
+
return extractAllOfModel(name, schema, schemas);
|
|
409
|
+
}
|
|
410
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
411
|
+
const fields = [];
|
|
412
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
|
|
413
|
+
if (!fieldSchema) continue;
|
|
414
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
415
|
+
}
|
|
416
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object" && schema.properties) {
|
|
417
|
+
const apKeys = Object.keys(schema.additionalProperties);
|
|
418
|
+
if (apKeys.length > 0) {
|
|
419
|
+
const valueType = schemaToTypeRef(schema.additionalProperties, "additionalProperties", name);
|
|
420
|
+
fields.push({
|
|
421
|
+
name: "additionalProperties",
|
|
422
|
+
type: { kind: "map", valueType },
|
|
423
|
+
required: false,
|
|
424
|
+
description: "Additional properties not captured by named fields"
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { name, description: schema.description, fields };
|
|
429
|
+
}
|
|
430
|
+
function extractAllOfModel(name, schema, schemas) {
|
|
431
|
+
const fields = [];
|
|
432
|
+
const requiredSet = /* @__PURE__ */ new Set();
|
|
433
|
+
for (const subSchema of schema.allOf ?? []) {
|
|
434
|
+
let resolved = subSchema;
|
|
435
|
+
if (subSchema.$ref && schemas) {
|
|
436
|
+
const segments = subSchema.$ref.split("/");
|
|
437
|
+
const refName = segments[segments.length - 1];
|
|
438
|
+
if (refName && schemas[refName]) {
|
|
439
|
+
resolved = schemas[refName];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (resolved.allOf) {
|
|
443
|
+
const nested = extractAllOfModel(name, resolved, schemas);
|
|
444
|
+
for (const f of nested.fields) {
|
|
445
|
+
fields.push({ ...f, required: false });
|
|
446
|
+
if (f.required) requiredSet.add(f.name);
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
if (resolved.required) {
|
|
450
|
+
for (const r of resolved.required) requiredSet.add(r);
|
|
451
|
+
}
|
|
452
|
+
if (resolved.properties) {
|
|
453
|
+
const emptyRequired = /* @__PURE__ */ new Set();
|
|
454
|
+
for (const [fieldName, fieldSchema] of Object.entries(resolved.properties)) {
|
|
455
|
+
if (!fieldSchema) continue;
|
|
456
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, emptyRequired));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (schema.required) {
|
|
462
|
+
for (const r of schema.required) requiredSet.add(r);
|
|
463
|
+
}
|
|
464
|
+
for (const f of fields) {
|
|
465
|
+
f.required = requiredSet.has(f.name);
|
|
466
|
+
}
|
|
467
|
+
return { name, description: schema.description, fields };
|
|
468
|
+
}
|
|
469
|
+
function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
470
|
+
if (schema.$ref) {
|
|
471
|
+
const segments = schema.$ref.split("/");
|
|
472
|
+
const rawName = segments[segments.length - 1];
|
|
473
|
+
return { kind: "model", name: cleanSchemaName(toPascalCase(rawName)) };
|
|
474
|
+
}
|
|
475
|
+
if (Array.isArray(schema.type)) {
|
|
476
|
+
const nonNullTypes = schema.type.filter((t) => t !== "null");
|
|
477
|
+
if (nonNullTypes.length === 0) {
|
|
478
|
+
return { kind: "nullable", inner: { kind: "primitive", type: "unknown" } };
|
|
479
|
+
}
|
|
480
|
+
if (schema.type.includes("null") && nonNullTypes.length === 1) {
|
|
481
|
+
return {
|
|
482
|
+
kind: "nullable",
|
|
483
|
+
inner: schemaToTypeRef({ ...schema, type: nonNullTypes[0], nullable: false }, contextName)
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (nonNullTypes.length > 1) {
|
|
487
|
+
const variants = nonNullTypes.map((t) => schemaToTypeRef({ ...schema, type: t }, contextName));
|
|
488
|
+
const ref = { kind: "union", variants };
|
|
489
|
+
if (schema.type.includes("null")) {
|
|
490
|
+
return { kind: "nullable", inner: ref };
|
|
491
|
+
}
|
|
492
|
+
return ref;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (schema.nullable && schema.type) {
|
|
496
|
+
return {
|
|
497
|
+
kind: "nullable",
|
|
498
|
+
inner: schemaToTypeRef({ ...schema, nullable: false }, contextName)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (schema.allOf) {
|
|
502
|
+
const refItem = schema.allOf.find((s) => s.$ref);
|
|
503
|
+
if (refItem) {
|
|
504
|
+
const hasAugmentation = schema.allOf.some((s) => !s.$ref && (s.properties || s.type === "object"));
|
|
505
|
+
if (hasAugmentation) {
|
|
506
|
+
const baseName = toPascalCase(contextName ?? "UnknownModel");
|
|
507
|
+
return { kind: "model", name: qualifyInlineModelName(baseName, parentModelName) };
|
|
508
|
+
}
|
|
509
|
+
return schemaToTypeRef(refItem, contextName, parentModelName);
|
|
510
|
+
}
|
|
511
|
+
if (schema.allOf.length === 1) {
|
|
512
|
+
return schemaToTypeRef(schema.allOf[0], contextName, parentModelName);
|
|
513
|
+
}
|
|
514
|
+
const hasProperties = schema.allOf.some((s) => s.properties);
|
|
515
|
+
if (hasProperties) {
|
|
516
|
+
const baseName = toPascalCase(contextName ?? "UnknownModel");
|
|
517
|
+
return {
|
|
518
|
+
kind: "model",
|
|
519
|
+
name: qualifyInlineModelName(baseName, parentModelName)
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (schema.oneOf || schema.anyOf) {
|
|
524
|
+
const compositionKind = schema.oneOf ? "oneOf" : "anyOf";
|
|
525
|
+
const rawVariants = schema.oneOf ?? schema.anyOf ?? [];
|
|
526
|
+
const nullVariant = rawVariants.find(
|
|
527
|
+
(v) => v.type === "null" || Array.isArray(v.type) && v.type.length === 1 && v.type[0] === "null"
|
|
528
|
+
);
|
|
529
|
+
const nonNullVariants = rawVariants.filter((v) => v !== nullVariant);
|
|
530
|
+
if (nullVariant && nonNullVariants.length === 1) {
|
|
531
|
+
return {
|
|
532
|
+
kind: "nullable",
|
|
533
|
+
inner: schemaToTypeRef(nonNullVariants[0], contextName, parentModelName)
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const variants = rawVariants.filter((v) => v.type !== "null").map((v) => schemaToTypeRef(v, contextName, parentModelName));
|
|
537
|
+
const hasNull = !!nullVariant;
|
|
538
|
+
const union = {
|
|
539
|
+
kind: "union",
|
|
540
|
+
variants,
|
|
541
|
+
compositionKind,
|
|
542
|
+
...schema.discriminator ? {
|
|
543
|
+
discriminator: {
|
|
544
|
+
property: schema.discriminator.propertyName,
|
|
545
|
+
mapping: Object.fromEntries(
|
|
546
|
+
Object.entries(schema.discriminator.mapping ?? {}).map(([k, v]) => [
|
|
547
|
+
k,
|
|
548
|
+
v.replace(/^#\/components\/schemas\//, "")
|
|
549
|
+
])
|
|
550
|
+
)
|
|
551
|
+
}
|
|
552
|
+
} : {}
|
|
553
|
+
};
|
|
554
|
+
return hasNull ? { kind: "nullable", inner: union } : union;
|
|
555
|
+
}
|
|
556
|
+
if (schema.const !== void 0) {
|
|
557
|
+
if (typeof schema.const === "string" || typeof schema.const === "number" || typeof schema.const === "boolean") {
|
|
558
|
+
return { kind: "literal", value: schema.const };
|
|
559
|
+
}
|
|
560
|
+
if (schema.const === null) {
|
|
561
|
+
return { kind: "nullable", inner: { kind: "primitive", type: "unknown" } };
|
|
562
|
+
}
|
|
563
|
+
if (typeof schema.const === "object") {
|
|
564
|
+
if (Array.isArray(schema.const)) {
|
|
565
|
+
return { kind: "array", items: { kind: "primitive", type: "unknown" } };
|
|
566
|
+
}
|
|
567
|
+
return { kind: "map", valueType: { kind: "primitive", type: "unknown" } };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (schema.enum && schema.enum.length === 1) {
|
|
571
|
+
const v = schema.enum[0];
|
|
572
|
+
return { kind: "literal", value: typeof v === "number" ? v : String(v) };
|
|
573
|
+
}
|
|
574
|
+
if (schema.enum) {
|
|
575
|
+
const baseName = toPascalCase(contextName ?? "UnknownEnum");
|
|
576
|
+
const cleanParent = parentModelName ? stripListItemMarkers(parentModelName) : void 0;
|
|
577
|
+
const qualifiedName = cleanParent && !baseName.startsWith(cleanParent) ? `${cleanParent}${baseName}` : baseName;
|
|
578
|
+
return {
|
|
579
|
+
kind: "enum",
|
|
580
|
+
name: qualifiedName,
|
|
581
|
+
values: schema.enum.map((v) => typeof v === "number" ? v : String(v))
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
if (schema.type === "array" && schema.items) {
|
|
585
|
+
return {
|
|
586
|
+
kind: "array",
|
|
587
|
+
items: schemaToTypeRef(schema.items, contextName, parentModelName)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
if (!schema.type && schema.items) {
|
|
591
|
+
return {
|
|
592
|
+
kind: "array",
|
|
593
|
+
items: schemaToTypeRef(schema.items, contextName, parentModelName)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
if (schema.type === "object" && schema.properties) {
|
|
597
|
+
const baseName = toPascalCase(contextName ?? "UnknownModel");
|
|
598
|
+
return {
|
|
599
|
+
kind: "model",
|
|
600
|
+
name: qualifyInlineModelName(baseName, parentModelName)
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (schema.type === "object" && !schema.properties) {
|
|
604
|
+
let valueType = { kind: "primitive", type: "unknown" };
|
|
605
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
606
|
+
const apKeys = Object.keys(schema.additionalProperties);
|
|
607
|
+
if (apKeys.length > 0) {
|
|
608
|
+
valueType = schemaToTypeRef(schema.additionalProperties, contextName);
|
|
609
|
+
}
|
|
610
|
+
} else if (schema.patternProperties) {
|
|
611
|
+
const patterns = Object.values(schema.patternProperties);
|
|
612
|
+
if (patterns.length > 0) {
|
|
613
|
+
valueType = schemaToTypeRef(patterns[0], contextName);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
kind: "map",
|
|
618
|
+
valueType
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const primitiveMap = {
|
|
622
|
+
string: "string",
|
|
623
|
+
integer: "integer",
|
|
624
|
+
number: "number",
|
|
625
|
+
boolean: "boolean"
|
|
626
|
+
};
|
|
627
|
+
const typeStr = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
628
|
+
if (typeStr && primitiveMap[typeStr]) {
|
|
629
|
+
return {
|
|
630
|
+
kind: "primitive",
|
|
631
|
+
type: primitiveMap[typeStr],
|
|
632
|
+
...schema.format ? { format: schema.format } : {}
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (!schema.type && schema.properties) {
|
|
636
|
+
const baseName = toPascalCase(contextName ?? "UnknownModel");
|
|
637
|
+
return {
|
|
638
|
+
kind: "model",
|
|
639
|
+
name: qualifyInlineModelName(baseName, parentModelName)
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
if (!schema.type && !schema.$ref && !schema.oneOf && !schema.anyOf && !schema.allOf && !schema.enum && !schema.properties && !schema.items) {
|
|
643
|
+
return { kind: "primitive", type: "unknown" };
|
|
644
|
+
}
|
|
645
|
+
if (contextName) {
|
|
646
|
+
console.warn(`[oagen] Warning: Unknown schema shape treated as unknown (context: ${contextName})`);
|
|
647
|
+
}
|
|
648
|
+
return { kind: "primitive", type: "unknown" };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/parser/pagination.ts
|
|
652
|
+
var CURSOR_PARAMS = ["cursor", "after", "before", "starting_after", "ending_before", "page_token", "next_token"];
|
|
653
|
+
var OFFSET_PARAMS = ["offset", "page", "page_number", "skip"];
|
|
654
|
+
var LIMIT_PARAMS = ["limit", "page_size", "per_page", "size", "count"];
|
|
655
|
+
function detectPagination(response, queryParams, dataPath) {
|
|
656
|
+
const cursorParam = queryParams.find((p) => CURSOR_PARAMS.includes(p.name));
|
|
657
|
+
if (cursorParam) {
|
|
658
|
+
const itemType = response.kind === "array" ? response.items : response;
|
|
659
|
+
return {
|
|
660
|
+
strategy: "cursor",
|
|
661
|
+
param: cursorParam.name,
|
|
662
|
+
dataPath,
|
|
663
|
+
itemType
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const offsetParam = queryParams.find((p) => OFFSET_PARAMS.includes(p.name));
|
|
667
|
+
const limitParam = queryParams.find((p) => LIMIT_PARAMS.includes(p.name));
|
|
668
|
+
if (offsetParam && limitParam) {
|
|
669
|
+
const itemType = response.kind === "array" ? response.items : response;
|
|
670
|
+
return {
|
|
671
|
+
strategy: "offset",
|
|
672
|
+
param: offsetParam.name,
|
|
673
|
+
limitParam: limitParam.name,
|
|
674
|
+
dataPath,
|
|
675
|
+
itemType
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/parser/responses.ts
|
|
682
|
+
function classifyAndExtractResponse(schema, contextName) {
|
|
683
|
+
if (schema.$ref) {
|
|
684
|
+
return { response: schemaToTypeRef(schema, contextName), inlineModels: [], isPaginated: false };
|
|
685
|
+
}
|
|
686
|
+
const envelope = detectListEnvelope(schema);
|
|
687
|
+
if (envelope.isEnvelope) {
|
|
688
|
+
return extractListResponse(schema, contextName, envelope.dataPath);
|
|
689
|
+
}
|
|
690
|
+
if (isSingleResourceWrapper(schema)) {
|
|
691
|
+
return extractWrappedResource(schema, contextName);
|
|
692
|
+
}
|
|
693
|
+
return extractDirectResource(schema, contextName);
|
|
694
|
+
}
|
|
695
|
+
function detectListEnvelope(schema) {
|
|
696
|
+
const propSources = [];
|
|
697
|
+
if (schema.allOf) {
|
|
698
|
+
for (const sub of schema.allOf) {
|
|
699
|
+
if (sub.properties) propSources.push(sub.properties);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (schema.properties) {
|
|
703
|
+
propSources.push(schema.properties);
|
|
704
|
+
}
|
|
705
|
+
if (propSources.length === 0) return { isEnvelope: false, dataPath: null };
|
|
706
|
+
const mergedProps = {};
|
|
707
|
+
for (const source of propSources) {
|
|
708
|
+
Object.assign(mergedProps, source);
|
|
709
|
+
}
|
|
710
|
+
const arrayProps = [];
|
|
711
|
+
let nonArrayCount = 0;
|
|
712
|
+
for (const [key, propSchema] of Object.entries(mergedProps)) {
|
|
713
|
+
if (!propSchema) continue;
|
|
714
|
+
if (propSchema.type === "array") {
|
|
715
|
+
arrayProps.push(key);
|
|
716
|
+
} else {
|
|
717
|
+
nonArrayCount++;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (arrayProps.length === 1 && nonArrayCount >= 1) {
|
|
721
|
+
return { isEnvelope: true, dataPath: arrayProps[0] };
|
|
722
|
+
}
|
|
723
|
+
return { isEnvelope: false, dataPath: null };
|
|
724
|
+
}
|
|
725
|
+
function extractListResponse(schema, contextName, dataPath) {
|
|
726
|
+
let itemTypeRef = { kind: "primitive", type: "string" };
|
|
727
|
+
const inlineModels = [];
|
|
728
|
+
const propSources = [];
|
|
729
|
+
if (schema.allOf) {
|
|
730
|
+
for (const sub of schema.allOf) {
|
|
731
|
+
if (sub.properties) propSources.push(sub.properties);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (schema.properties) {
|
|
735
|
+
propSources.push(schema.properties);
|
|
736
|
+
}
|
|
737
|
+
for (const props of propSources) {
|
|
738
|
+
const dataProp = props[dataPath];
|
|
739
|
+
if (!dataProp) continue;
|
|
740
|
+
if (dataProp.type === "array" && dataProp.items) {
|
|
741
|
+
const items = dataProp.items;
|
|
742
|
+
if (items.$ref) {
|
|
743
|
+
itemTypeRef = schemaToTypeRef(items, contextName);
|
|
744
|
+
} else if (items.type === "object" && items.properties) {
|
|
745
|
+
const itemName = contextName.replace(/Response$/, "") + "Item";
|
|
746
|
+
itemTypeRef = { kind: "model", name: itemName };
|
|
747
|
+
inlineModels.push(...extractInlineModel(itemName, items));
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
response: { kind: "array", items: itemTypeRef },
|
|
753
|
+
inlineModels,
|
|
754
|
+
isPaginated: true,
|
|
755
|
+
dataPath,
|
|
756
|
+
itemType: itemTypeRef
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
function hasDiscriminantConstField(schema) {
|
|
760
|
+
if (!schema.properties) return null;
|
|
761
|
+
const preferred = ["object", "type"];
|
|
762
|
+
for (const name of preferred) {
|
|
763
|
+
const field = schema.properties[name];
|
|
764
|
+
if (!field) continue;
|
|
765
|
+
if (field.const !== void 0 && typeof field.const === "string") {
|
|
766
|
+
return { property: name, value: field.const };
|
|
767
|
+
}
|
|
768
|
+
if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") {
|
|
769
|
+
return { property: name, value: field.enum[0] };
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
for (const [name, field] of Object.entries(schema.properties)) {
|
|
773
|
+
if (!field) continue;
|
|
774
|
+
if (field.const !== void 0 && typeof field.const === "string") {
|
|
775
|
+
return { property: name, value: field.const };
|
|
776
|
+
}
|
|
777
|
+
if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") {
|
|
778
|
+
return { property: name, value: field.enum[0] };
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
function isSingleResourceWrapper(schema) {
|
|
784
|
+
if (schema.type !== "object") return false;
|
|
785
|
+
if (!schema.properties) return false;
|
|
786
|
+
let wrapperKey;
|
|
787
|
+
if (schema.required && schema.required.length === 1) {
|
|
788
|
+
wrapperKey = schema.required[0];
|
|
789
|
+
} else {
|
|
790
|
+
const propKeys = Object.keys(schema.properties);
|
|
791
|
+
if (propKeys.length === 1) {
|
|
792
|
+
wrapperKey = propKeys[0];
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (!wrapperKey) return false;
|
|
796
|
+
const propSchema = schema.properties[wrapperKey];
|
|
797
|
+
if (!propSchema) return false;
|
|
798
|
+
if (propSchema.type === "object" && hasDiscriminantConstField(propSchema) !== null) return true;
|
|
799
|
+
if (propSchema.oneOf) {
|
|
800
|
+
return propSchema.oneOf.some((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
function extractWrappedResource(schema, contextName) {
|
|
805
|
+
const props = schema.properties;
|
|
806
|
+
const wrapperKey = schema.required && schema.required.length === 1 ? schema.required[0] : Object.keys(props)[0];
|
|
807
|
+
const propSchema = props[wrapperKey];
|
|
808
|
+
const inlineModels = [];
|
|
809
|
+
const resourceName = contextName.replace(/Response$/, "");
|
|
810
|
+
if (propSchema.oneOf) {
|
|
811
|
+
const objectVariant = propSchema.oneOf.find((v) => v.type === "object" && hasDiscriminantConstField(v) !== null);
|
|
812
|
+
if (objectVariant) {
|
|
813
|
+
const modelName2 = deriveModelName(objectVariant, resourceName);
|
|
814
|
+
inlineModels.push(...extractInlineModel(modelName2, objectVariant));
|
|
815
|
+
const hasNullVariant = propSchema.oneOf.some(
|
|
816
|
+
(v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null")
|
|
817
|
+
);
|
|
818
|
+
const modelRef = { kind: "model", name: modelName2 };
|
|
819
|
+
return {
|
|
820
|
+
response: hasNullVariant ? { kind: "nullable", inner: modelRef } : modelRef,
|
|
821
|
+
inlineModels,
|
|
822
|
+
isPaginated: false
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const modelName = deriveModelName(propSchema, resourceName);
|
|
827
|
+
inlineModels.push(...extractInlineModel(modelName, propSchema));
|
|
828
|
+
return {
|
|
829
|
+
response: { kind: "model", name: modelName },
|
|
830
|
+
inlineModels,
|
|
831
|
+
isPaginated: false
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function extractDirectResource(schema, contextName) {
|
|
835
|
+
const inlineModels = [];
|
|
836
|
+
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
837
|
+
const modelName = deriveModelName(schema, contextName);
|
|
838
|
+
inlineModels.push(...extractInlineModel(modelName, schema));
|
|
839
|
+
return {
|
|
840
|
+
response: { kind: "model", name: modelName },
|
|
841
|
+
inlineModels,
|
|
842
|
+
isPaginated: false
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
if (schema.allOf) {
|
|
846
|
+
const mergedProperties = {};
|
|
847
|
+
const mergedRequired = [];
|
|
848
|
+
for (const sub of schema.allOf) {
|
|
849
|
+
if (sub.properties) {
|
|
850
|
+
Object.assign(mergedProperties, sub.properties);
|
|
851
|
+
}
|
|
852
|
+
if (sub.required) {
|
|
853
|
+
mergedRequired.push(...sub.required);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (Object.keys(mergedProperties).length > 0) {
|
|
857
|
+
const merged = { type: "object", properties: mergedProperties, required: mergedRequired };
|
|
858
|
+
const modelName = deriveModelName(merged, contextName);
|
|
859
|
+
inlineModels.push(...extractInlineModel(modelName, merged));
|
|
860
|
+
return {
|
|
861
|
+
response: { kind: "model", name: modelName },
|
|
862
|
+
inlineModels,
|
|
863
|
+
isPaginated: false
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (schema.oneOf) {
|
|
868
|
+
for (const variant of schema.oneOf) {
|
|
869
|
+
if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
870
|
+
const modelName = deriveModelName(variant, contextName);
|
|
871
|
+
const existingNames = new Set(inlineModels.map((m) => m.name));
|
|
872
|
+
if (!existingNames.has(modelName)) {
|
|
873
|
+
inlineModels.push(...extractInlineModel(modelName, variant));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (schema.type === "array" && schema.items) {
|
|
879
|
+
const items = schema.items;
|
|
880
|
+
if (items.$ref) {
|
|
881
|
+
return {
|
|
882
|
+
response: { kind: "array", items: schemaToTypeRef(items, contextName) },
|
|
883
|
+
inlineModels,
|
|
884
|
+
isPaginated: false
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
if (items.properties && (items.type === "object" || !items.type)) {
|
|
888
|
+
const itemName = contextName.replace(/Response$/, "") + "Item";
|
|
889
|
+
inlineModels.push(...extractInlineModel(itemName, items));
|
|
890
|
+
return {
|
|
891
|
+
response: { kind: "array", items: { kind: "model", name: itemName } },
|
|
892
|
+
inlineModels,
|
|
893
|
+
isPaginated: false
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
response: schemaToTypeRef(schema, contextName),
|
|
899
|
+
inlineModels,
|
|
900
|
+
isPaginated: false
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function deriveModelName(schema, fallback) {
|
|
904
|
+
if (!schema.properties) return toPascalCase(fallback);
|
|
905
|
+
const preferred = ["object", "type"];
|
|
906
|
+
for (const name of preferred) {
|
|
907
|
+
const field = schema.properties[name];
|
|
908
|
+
if (field && field.const && typeof field.const === "string") {
|
|
909
|
+
return toPascalCase(field.const);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
for (const [, field] of Object.entries(schema.properties)) {
|
|
913
|
+
if (field && field.const && typeof field.const === "string") {
|
|
914
|
+
return toPascalCase(field.const);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return toPascalCase(fallback);
|
|
918
|
+
}
|
|
919
|
+
function qualifyNestedName(parentName, fieldName) {
|
|
920
|
+
const pascalField = toPascalCase(fieldName);
|
|
921
|
+
if (pascalField.startsWith(parentName)) return pascalField;
|
|
922
|
+
const cleanParent = stripListItemMarkers(parentName);
|
|
923
|
+
const qualified = `${cleanParent}${pascalField}`;
|
|
924
|
+
const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
|
|
925
|
+
if (match && match[2]) {
|
|
926
|
+
const trailing = singularize(match[2]);
|
|
927
|
+
return match[1] + trailing;
|
|
928
|
+
}
|
|
929
|
+
return qualified;
|
|
930
|
+
}
|
|
931
|
+
function extractInlineModel(name, schema) {
|
|
932
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
933
|
+
const fields = [];
|
|
934
|
+
const properties = schema.properties ?? {};
|
|
935
|
+
const nestedModels = [];
|
|
936
|
+
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
|
937
|
+
if (!fieldSchema) continue;
|
|
938
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
939
|
+
if (fieldSchema.type === "object" && fieldSchema.properties) {
|
|
940
|
+
const nestedName = qualifyNestedName(name, fieldName);
|
|
941
|
+
nestedModels.push(...extractInlineModel(nestedName, fieldSchema));
|
|
942
|
+
}
|
|
943
|
+
if (fieldSchema.type === "array" && fieldSchema.items) {
|
|
944
|
+
const items = fieldSchema.items;
|
|
945
|
+
if (items.type === "object" && items.properties) {
|
|
946
|
+
const nestedName = qualifyNestedName(name, fieldName);
|
|
947
|
+
nestedModels.push(...extractInlineModel(nestedName, items));
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (fieldSchema.allOf) {
|
|
951
|
+
const mergedProperties = {};
|
|
952
|
+
const mergedRequired = [];
|
|
953
|
+
for (const sub of fieldSchema.allOf) {
|
|
954
|
+
if (sub.properties) Object.assign(mergedProperties, sub.properties);
|
|
955
|
+
if (sub.required) mergedRequired.push(...sub.required);
|
|
956
|
+
}
|
|
957
|
+
if (Object.keys(mergedProperties).length > 0) {
|
|
958
|
+
const nestedName = qualifyNestedName(name, fieldName);
|
|
959
|
+
const merged = { type: "object", properties: mergedProperties, required: mergedRequired };
|
|
960
|
+
const existingNames = new Set(nestedModels.map((m) => m.name));
|
|
961
|
+
if (!existingNames.has(nestedName)) {
|
|
962
|
+
nestedModels.push(...extractInlineModel(nestedName, merged));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (fieldSchema.oneOf) {
|
|
967
|
+
const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
|
|
968
|
+
if (objectVariant) {
|
|
969
|
+
const nestedName = qualifyNestedName(name, fieldName);
|
|
970
|
+
const existingNames = new Set(nestedModels.map((m) => m.name));
|
|
971
|
+
if (!existingNames.has(nestedName)) {
|
|
972
|
+
nestedModels.push(...extractInlineModel(nestedName, objectVariant));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return [{ name, description: schema.description, fields }, ...nestedModels];
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/parser/operations.ts
|
|
981
|
+
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
|
|
982
|
+
function extractOperations(paths, operationIdTransform) {
|
|
983
|
+
if (!paths) return { services: [], inlineModels: [] };
|
|
984
|
+
const serviceMap = /* @__PURE__ */ new Map();
|
|
985
|
+
const inlineModels = [];
|
|
986
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
987
|
+
const pathLevelParams = pathItem.parameters ?? [];
|
|
988
|
+
for (const method of HTTP_METHODS) {
|
|
989
|
+
const op = pathItem[method];
|
|
990
|
+
if (!op) continue;
|
|
991
|
+
const serviceName = inferServiceName(path, op.tags?.[0]);
|
|
992
|
+
const { operation, inlineModels: opModels } = buildOperation(
|
|
993
|
+
method,
|
|
994
|
+
path,
|
|
995
|
+
op,
|
|
996
|
+
pathLevelParams,
|
|
997
|
+
operationIdTransform
|
|
998
|
+
);
|
|
999
|
+
inlineModels.push(...opModels);
|
|
1000
|
+
const ops = serviceMap.get(serviceName) ?? [];
|
|
1001
|
+
ops.push(operation);
|
|
1002
|
+
serviceMap.set(serviceName, ops);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
for (const [, operations] of serviceMap) {
|
|
1006
|
+
disambiguateOperationNames(operations);
|
|
1007
|
+
}
|
|
1008
|
+
const services = Array.from(serviceMap.entries()).map(([name, operations]) => ({
|
|
1009
|
+
name,
|
|
1010
|
+
operations
|
|
1011
|
+
}));
|
|
1012
|
+
return { services, inlineModels };
|
|
1013
|
+
}
|
|
1014
|
+
function inferServiceName(path, tag) {
|
|
1015
|
+
if (tag) {
|
|
1016
|
+
return toPascalCase(tag);
|
|
1017
|
+
}
|
|
1018
|
+
const segments = path.split("/").filter(Boolean);
|
|
1019
|
+
const first = segments[0] ?? "Default";
|
|
1020
|
+
if (first.startsWith("{")) return "Default";
|
|
1021
|
+
return toPascalCase(first);
|
|
1022
|
+
}
|
|
1023
|
+
function inferOperationName(method, path, operationId, operationIdTransform) {
|
|
1024
|
+
if (operationId) {
|
|
1025
|
+
if (operationIdTransform) {
|
|
1026
|
+
return operationIdTransform(operationId);
|
|
1027
|
+
}
|
|
1028
|
+
return toCamelCase(operationId);
|
|
1029
|
+
}
|
|
1030
|
+
const verb = inferVerb(method, path);
|
|
1031
|
+
const resource = inferResourceFromPath(path);
|
|
1032
|
+
return resource ? toCamelCase(`${verb}_${resource}`) : verb;
|
|
1033
|
+
}
|
|
1034
|
+
function inferVerb(method, path) {
|
|
1035
|
+
const segments = path.split("/").filter(Boolean);
|
|
1036
|
+
const hasTrailingParam = segments.length > 0 && segments[segments.length - 1].startsWith("{");
|
|
1037
|
+
switch (method) {
|
|
1038
|
+
case "get":
|
|
1039
|
+
return hasTrailingParam ? "get" : "list";
|
|
1040
|
+
case "post":
|
|
1041
|
+
return "create";
|
|
1042
|
+
case "put":
|
|
1043
|
+
case "patch":
|
|
1044
|
+
return "update";
|
|
1045
|
+
case "delete":
|
|
1046
|
+
return "delete";
|
|
1047
|
+
case "head":
|
|
1048
|
+
return "check";
|
|
1049
|
+
case "options":
|
|
1050
|
+
return "options";
|
|
1051
|
+
case "trace":
|
|
1052
|
+
return "trace";
|
|
1053
|
+
default:
|
|
1054
|
+
return method;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
function inferResourceFromPath(path) {
|
|
1058
|
+
const segments = path.split("/").filter(Boolean);
|
|
1059
|
+
if (segments.length <= 1) return null;
|
|
1060
|
+
let resourceSegment = null;
|
|
1061
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1062
|
+
if (!segments[i].startsWith("{")) {
|
|
1063
|
+
resourceSegment = segments[i];
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (!resourceSegment) return null;
|
|
1068
|
+
if (resourceSegment === segments[0]) return null;
|
|
1069
|
+
return toPascalCase(resourceSegment);
|
|
1070
|
+
}
|
|
1071
|
+
function disambiguateOperationNames(operations) {
|
|
1072
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1073
|
+
for (const op of operations) {
|
|
1074
|
+
const group = byName.get(op.name) ?? [];
|
|
1075
|
+
group.push(op);
|
|
1076
|
+
byName.set(op.name, group);
|
|
1077
|
+
}
|
|
1078
|
+
for (const [name, group] of byName) {
|
|
1079
|
+
if (group.length <= 1) continue;
|
|
1080
|
+
const uniquePaths = new Set(group.map((op) => op.path));
|
|
1081
|
+
if (uniquePaths.size <= 1) continue;
|
|
1082
|
+
for (const op of group) {
|
|
1083
|
+
const resource = inferResourceFromPath(op.path);
|
|
1084
|
+
if (resource) {
|
|
1085
|
+
const newName = toCamelCase(`${name}_${resource}`);
|
|
1086
|
+
op.name = newName;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const renamed = /* @__PURE__ */ new Map();
|
|
1090
|
+
for (const op of group) {
|
|
1091
|
+
const rGroup = renamed.get(op.name) ?? [];
|
|
1092
|
+
rGroup.push(op);
|
|
1093
|
+
renamed.set(op.name, rGroup);
|
|
1094
|
+
}
|
|
1095
|
+
for (const [, rGroup] of renamed) {
|
|
1096
|
+
if (rGroup.length <= 1) continue;
|
|
1097
|
+
for (const op of rGroup) {
|
|
1098
|
+
const deeper = inferDeeperContext(op.path);
|
|
1099
|
+
if (deeper) {
|
|
1100
|
+
op.name = toCamelCase(`${op.name}_${deeper}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function inferDeeperContext(path) {
|
|
1107
|
+
const segments = path.split("/").filter(Boolean);
|
|
1108
|
+
const nonParams = segments.filter((s) => !s.startsWith("{"));
|
|
1109
|
+
if (nonParams.length >= 3) {
|
|
1110
|
+
return toPascalCase(nonParams.slice(1, -1).join("_"));
|
|
1111
|
+
}
|
|
1112
|
+
return null;
|
|
1113
|
+
}
|
|
1114
|
+
function buildOperation(method, path, op, pathLevelParams, operationIdTransform) {
|
|
1115
|
+
const allParams = [...pathLevelParams, ...op.parameters ?? []];
|
|
1116
|
+
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
1117
|
+
const pathParams = extractParams(allParams, "path");
|
|
1118
|
+
const queryParams = extractParams(allParams, "query");
|
|
1119
|
+
const headerParams = extractParams(allParams, "header").filter((p) => p.name.toLowerCase() !== "idempotency-key");
|
|
1120
|
+
const cookieParams = extractParams(allParams, "cookie");
|
|
1121
|
+
const reqBodyModels = [];
|
|
1122
|
+
const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
|
|
1123
|
+
const {
|
|
1124
|
+
response,
|
|
1125
|
+
successResponses,
|
|
1126
|
+
errors,
|
|
1127
|
+
inlineModels,
|
|
1128
|
+
isPaginated,
|
|
1129
|
+
dataPath: responseDataPath,
|
|
1130
|
+
itemType: responseItemType
|
|
1131
|
+
} = extractResponses(op.responses, op, path, method);
|
|
1132
|
+
inlineModels.push(...reqBodyModels);
|
|
1133
|
+
const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
|
|
1134
|
+
let pagination;
|
|
1135
|
+
if (isPaginated) {
|
|
1136
|
+
const itemType = responseItemType ?? (response.kind === "array" ? response.items : response);
|
|
1137
|
+
pagination = {
|
|
1138
|
+
strategy: paginationFromParams?.strategy ?? "cursor",
|
|
1139
|
+
param: paginationFromParams?.param ?? "after",
|
|
1140
|
+
limitParam: paginationFromParams?.limitParam,
|
|
1141
|
+
dataPath: responseDataPath ?? paginationFromParams?.dataPath,
|
|
1142
|
+
itemType
|
|
1143
|
+
};
|
|
1144
|
+
} else if (paginationFromParams) {
|
|
1145
|
+
pagination = paginationFromParams;
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
operation: {
|
|
1149
|
+
name: inferOperationName(method, path, op.operationId, operationIdTransform),
|
|
1150
|
+
description: buildDescription(op.summary, op.description),
|
|
1151
|
+
httpMethod: method,
|
|
1152
|
+
path,
|
|
1153
|
+
pathParams,
|
|
1154
|
+
queryParams,
|
|
1155
|
+
headerParams,
|
|
1156
|
+
cookieParams: cookieParams.length > 0 ? cookieParams : void 0,
|
|
1157
|
+
requestBody,
|
|
1158
|
+
requestBodyEncoding,
|
|
1159
|
+
response,
|
|
1160
|
+
successResponses,
|
|
1161
|
+
errors,
|
|
1162
|
+
pagination,
|
|
1163
|
+
injectIdempotencyKey: hasIdempotencyHeader,
|
|
1164
|
+
deprecated: op.deprecated || void 0,
|
|
1165
|
+
async: op["x-oagen-async"]
|
|
1166
|
+
},
|
|
1167
|
+
inlineModels
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function buildDescription(summary, description) {
|
|
1171
|
+
if (summary && description && summary !== description) {
|
|
1172
|
+
return `${summary}
|
|
1173
|
+
|
|
1174
|
+
${description}`;
|
|
1175
|
+
}
|
|
1176
|
+
return description ?? summary;
|
|
1177
|
+
}
|
|
1178
|
+
function extractParams(params, location) {
|
|
1179
|
+
return params.filter((p) => p.in === location).map((p) => ({
|
|
1180
|
+
name: p.name,
|
|
1181
|
+
type: p.schema ? schemaToTypeRef(p.schema, p.name) : { kind: "primitive", type: "string" },
|
|
1182
|
+
required: p.required ?? false,
|
|
1183
|
+
description: p.description,
|
|
1184
|
+
deprecated: p.deprecated || p.schema?.deprecated || void 0,
|
|
1185
|
+
default: p.schema?.default,
|
|
1186
|
+
example: p.example ?? p.schema?.example
|
|
1187
|
+
}));
|
|
1188
|
+
}
|
|
1189
|
+
function extractRequestBody(body, op, inlineModels) {
|
|
1190
|
+
if (!body?.content) return {};
|
|
1191
|
+
let encoding = "json";
|
|
1192
|
+
let schema;
|
|
1193
|
+
if (body.content["application/json"]?.schema) {
|
|
1194
|
+
encoding = "json";
|
|
1195
|
+
schema = body.content["application/json"].schema;
|
|
1196
|
+
} else if (body.content["multipart/form-data"]?.schema) {
|
|
1197
|
+
encoding = "form-data";
|
|
1198
|
+
schema = body.content["multipart/form-data"].schema;
|
|
1199
|
+
} else if (body.content["application/x-www-form-urlencoded"]?.schema) {
|
|
1200
|
+
encoding = "form-urlencoded";
|
|
1201
|
+
schema = body.content["application/x-www-form-urlencoded"].schema;
|
|
1202
|
+
} else if (body.content["application/octet-stream"]) {
|
|
1203
|
+
encoding = "binary";
|
|
1204
|
+
schema = body.content["application/octet-stream"].schema;
|
|
1205
|
+
} else if (body.content["text/plain"]) {
|
|
1206
|
+
encoding = "text";
|
|
1207
|
+
schema = body.content["text/plain"].schema;
|
|
1208
|
+
}
|
|
1209
|
+
if (!schema) {
|
|
1210
|
+
if (encoding === "binary") {
|
|
1211
|
+
return { body: { kind: "primitive", type: "string", format: "binary" }, encoding };
|
|
1212
|
+
}
|
|
1213
|
+
if (encoding === "text") {
|
|
1214
|
+
return { body: { kind: "primitive", type: "string" }, encoding };
|
|
1215
|
+
}
|
|
1216
|
+
return {};
|
|
1217
|
+
}
|
|
1218
|
+
const rawName = op?.operationId ? toPascalCase(op.operationId) + "Request" : "RequestBody";
|
|
1219
|
+
const contextName = cleanSchemaName(rawName);
|
|
1220
|
+
if (schema.properties && (schema.type === "object" || !schema.type)) {
|
|
1221
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
1222
|
+
const fields = [];
|
|
1223
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
|
|
1224
|
+
if (!fieldSchema) continue;
|
|
1225
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, contextName, requiredSet));
|
|
1226
|
+
}
|
|
1227
|
+
inlineModels.push({ name: contextName, description: void 0, fields });
|
|
1228
|
+
return { body: { kind: "model", name: contextName }, encoding };
|
|
1229
|
+
}
|
|
1230
|
+
if (schema.oneOf) {
|
|
1231
|
+
const variants = [];
|
|
1232
|
+
for (const variant of schema.oneOf) {
|
|
1233
|
+
if (variant.$ref) {
|
|
1234
|
+
const ref = schemaToTypeRef(variant, contextName);
|
|
1235
|
+
variants.push(ref);
|
|
1236
|
+
} else if (variant.type === "null") {
|
|
1237
|
+
} else if (variant.properties && (variant.type === "object" || !variant.type)) {
|
|
1238
|
+
const variantName = deriveOneOfVariantName(variant, contextName, inlineModels);
|
|
1239
|
+
const requiredSet = new Set(variant.required ?? []);
|
|
1240
|
+
const fields = [];
|
|
1241
|
+
for (const [fieldName, fieldSchema] of Object.entries(variant.properties)) {
|
|
1242
|
+
if (!fieldSchema) continue;
|
|
1243
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, variantName, requiredSet));
|
|
1244
|
+
}
|
|
1245
|
+
inlineModels.push({ name: variantName, description: variant.description, fields });
|
|
1246
|
+
variants.push({ kind: "model", name: variantName });
|
|
1247
|
+
} else {
|
|
1248
|
+
variants.push(schemaToTypeRef(variant, contextName));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const hasNull = schema.oneOf.some(
|
|
1252
|
+
(v) => v.type === "null" || Array.isArray(v.type) && v.type.includes("null")
|
|
1253
|
+
);
|
|
1254
|
+
const union = {
|
|
1255
|
+
kind: "union",
|
|
1256
|
+
variants,
|
|
1257
|
+
...schema.discriminator ? {
|
|
1258
|
+
discriminator: { property: schema.discriminator.propertyName, mapping: schema.discriminator.mapping ?? {} }
|
|
1259
|
+
} : {}
|
|
1260
|
+
};
|
|
1261
|
+
const body2 = hasNull ? { kind: "nullable", inner: union } : union;
|
|
1262
|
+
return { body: body2, encoding };
|
|
1263
|
+
}
|
|
1264
|
+
return { body: schemaToTypeRef(schema, contextName), encoding };
|
|
1265
|
+
}
|
|
1266
|
+
function deriveOneOfVariantName(variant, baseName, existingModels) {
|
|
1267
|
+
if (variant.properties) {
|
|
1268
|
+
for (const [, propSchema] of Object.entries(variant.properties)) {
|
|
1269
|
+
if (propSchema?.const && typeof propSchema.const === "string") {
|
|
1270
|
+
return toPascalCase(propSchema.const) + baseName.replace(/Request$/, "") + "Request";
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const existingNames = new Set(existingModels.map((m) => m.name));
|
|
1275
|
+
let name = baseName;
|
|
1276
|
+
let suffix = 2;
|
|
1277
|
+
while (existingNames.has(name)) {
|
|
1278
|
+
name = `${baseName}${suffix}`;
|
|
1279
|
+
suffix++;
|
|
1280
|
+
}
|
|
1281
|
+
return name;
|
|
1282
|
+
}
|
|
1283
|
+
function deriveResponseName(op, path, method) {
|
|
1284
|
+
if (op?.operationId) {
|
|
1285
|
+
return cleanSchemaName(toPascalCase(op.operationId) + "Response");
|
|
1286
|
+
}
|
|
1287
|
+
const segments = path.split("/").filter(Boolean);
|
|
1288
|
+
const resource = segments[0] ?? "Unknown";
|
|
1289
|
+
return toPascalCase(resource) + toPascalCase(method) + "Response";
|
|
1290
|
+
}
|
|
1291
|
+
function extractResponses(responses, op, path, method) {
|
|
1292
|
+
const errors = [];
|
|
1293
|
+
let response = { kind: "primitive", type: "string" };
|
|
1294
|
+
let inlineModels = [];
|
|
1295
|
+
let isPaginated = false;
|
|
1296
|
+
let dataPath;
|
|
1297
|
+
let itemType;
|
|
1298
|
+
if (!responses) return { response, errors, inlineModels, isPaginated };
|
|
1299
|
+
const allSuccessResponses = [];
|
|
1300
|
+
for (const [statusCode, resp] of Object.entries(responses)) {
|
|
1301
|
+
const code = parseInt(statusCode, 10);
|
|
1302
|
+
if (code >= 200 && code < 300) {
|
|
1303
|
+
let extractedType = { kind: "primitive", type: "unknown" };
|
|
1304
|
+
const jsonContent = resp.content?.["application/json"];
|
|
1305
|
+
if (jsonContent?.schema) {
|
|
1306
|
+
const contextName = deriveResponseName(op, path ?? "/", method ?? "get");
|
|
1307
|
+
const result = classifyAndExtractResponse(jsonContent.schema, contextName);
|
|
1308
|
+
extractedType = result.response;
|
|
1309
|
+
inlineModels = result.inlineModels;
|
|
1310
|
+
isPaginated = result.isPaginated;
|
|
1311
|
+
dataPath = result.dataPath;
|
|
1312
|
+
itemType = result.itemType;
|
|
1313
|
+
} else if (resp.content) {
|
|
1314
|
+
if (resp.content["application/octet-stream"] || resp.content["application/pdf"]) {
|
|
1315
|
+
extractedType = { kind: "primitive", type: "string", format: "binary" };
|
|
1316
|
+
} else if (resp.content["text/plain"] || resp.content["text/html"] || resp.content["text/xml"] || resp.content["application/xml"]) {
|
|
1317
|
+
extractedType = { kind: "primitive", type: "string" };
|
|
1318
|
+
} else {
|
|
1319
|
+
const firstKey = Object.keys(resp.content)[0];
|
|
1320
|
+
if (firstKey) {
|
|
1321
|
+
const firstContent = resp.content[firstKey];
|
|
1322
|
+
if (firstContent?.schema) {
|
|
1323
|
+
extractedType = schemaToTypeRef(
|
|
1324
|
+
firstContent.schema,
|
|
1325
|
+
deriveResponseName(op, path ?? "/", method ?? "get")
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
allSuccessResponses.push({ statusCode: code, type: extractedType });
|
|
1332
|
+
} else if (code >= 400) {
|
|
1333
|
+
const jsonContent = resp.content?.["application/json"];
|
|
1334
|
+
const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
|
|
1335
|
+
errors.push({ statusCode: code, type });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (allSuccessResponses.length > 0) {
|
|
1339
|
+
const sorted = [...allSuccessResponses].sort((a, b) => a.statusCode - b.statusCode);
|
|
1340
|
+
const primary = sorted.find((r) => r.type.kind !== "primitive" || r.type.type !== "unknown") ?? sorted[0];
|
|
1341
|
+
response = primary.type;
|
|
1342
|
+
}
|
|
1343
|
+
const successResponses = allSuccessResponses.length > 1 ? allSuccessResponses : void 0;
|
|
1344
|
+
return { response, successResponses, errors, inlineModels, isPaginated, dataPath, itemType };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/parser/normalize-inline-models.ts
|
|
1348
|
+
function keepLargestByName(models) {
|
|
1349
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1350
|
+
for (const model of models) {
|
|
1351
|
+
const existing = byName.get(model.name);
|
|
1352
|
+
if (!existing || model.fields.length > existing.fields.length) {
|
|
1353
|
+
byName.set(model.name, model);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return [...byName.values()];
|
|
1357
|
+
}
|
|
1358
|
+
function rewriteModelRefsInServices(services, oldName, newName) {
|
|
1359
|
+
for (const service of services) {
|
|
1360
|
+
for (const op of service.operations) {
|
|
1361
|
+
walkTypeRef(op.response, {
|
|
1362
|
+
model: (r) => {
|
|
1363
|
+
if (r.name === oldName) r.name = newName;
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
if (op.requestBody) {
|
|
1367
|
+
walkTypeRef(op.requestBody, {
|
|
1368
|
+
model: (r) => {
|
|
1369
|
+
if (r.name === oldName) r.name = newName;
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
function mergeInlineResponseModels(schemaModels, inlineModels) {
|
|
1377
|
+
const mergedSchemaModels = [...schemaModels];
|
|
1378
|
+
const schemaModelNames = new Set(mergedSchemaModels.map((m) => m.name));
|
|
1379
|
+
const schemaModelsByName = new Map(mergedSchemaModels.map((m) => [m.name, m]));
|
|
1380
|
+
const deduplicatedInlineModels = inlineModels.filter((model) => {
|
|
1381
|
+
if (!schemaModelNames.has(model.name)) return true;
|
|
1382
|
+
const existing = schemaModelsByName.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
|
+
const hasDifference = model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name));
|
|
1386
|
+
if (hasDifference && model.fields.length > existing.fields.length) {
|
|
1387
|
+
const idx = mergedSchemaModels.indexOf(existing);
|
|
1388
|
+
if (idx !== -1) mergedSchemaModels[idx] = model;
|
|
1389
|
+
schemaModelsByName.set(model.name, model);
|
|
1390
|
+
console.warn(
|
|
1391
|
+
`[oagen] Warning: Inline model "${model.name}" has more fields than component schema (${model.fields.length} vs ${existing.fields.length}) \u2014 using inline response model`
|
|
1392
|
+
);
|
|
1393
|
+
} else if (hasDifference) {
|
|
1394
|
+
console.warn(
|
|
1395
|
+
`[oagen] Warning: Inline model "${model.name}" has different fields than component schema \u2014 using component schema`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
return false;
|
|
1399
|
+
});
|
|
1400
|
+
return [...mergedSchemaModels, ...keepLargestByName(deduplicatedInlineModels)];
|
|
1401
|
+
}
|
|
1402
|
+
function mergeFieldInlineModels(existingModels, fieldInlineModels) {
|
|
1403
|
+
const modelNames = new Set(existingModels.map((m) => m.name));
|
|
1404
|
+
const modelsByName = new Map(existingModels.map((m) => [m.name, m]));
|
|
1405
|
+
const deduplicatedFieldModels = fieldInlineModels.filter((model) => {
|
|
1406
|
+
if (!modelNames.has(model.name)) return true;
|
|
1407
|
+
const existing = modelsByName.get(model.name);
|
|
1408
|
+
const existingFieldNames = new Set(existing.fields.map((f) => f.name));
|
|
1409
|
+
const inlineFieldNames = new Set(model.fields.map((f) => f.name));
|
|
1410
|
+
const hasDifference = model.fields.some((f) => !existingFieldNames.has(f.name)) || existing.fields.some((f) => !inlineFieldNames.has(f.name));
|
|
1411
|
+
if (hasDifference) {
|
|
1412
|
+
console.warn(
|
|
1413
|
+
`[oagen] Warning: Inline model "${model.name}" has different fields than component schema \u2014 using component schema`
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
return false;
|
|
1417
|
+
});
|
|
1418
|
+
return [...existingModels, ...keepLargestByName(deduplicatedFieldModels)];
|
|
1419
|
+
}
|
|
1420
|
+
function collapseJsonSuffixModels(models, services) {
|
|
1421
|
+
const normalizedModels = [...models];
|
|
1422
|
+
const byName = new Map(normalizedModels.map((m) => [m.name, m]));
|
|
1423
|
+
for (const model of normalizedModels) {
|
|
1424
|
+
if (!model.name.endsWith("Json")) continue;
|
|
1425
|
+
const baseName = model.name.slice(0, -4);
|
|
1426
|
+
const baseModel = byName.get(baseName);
|
|
1427
|
+
if (!baseModel || model.fields.length <= baseModel.fields.length) continue;
|
|
1428
|
+
const isSuperset = baseModel.fields.every((f) => model.fields.some((mf) => mf.name === f.name));
|
|
1429
|
+
if (!isSuperset) continue;
|
|
1430
|
+
baseModel.fields = model.fields;
|
|
1431
|
+
const jsonIdx = normalizedModels.indexOf(model);
|
|
1432
|
+
if (jsonIdx !== -1) normalizedModels.splice(jsonIdx, 1);
|
|
1433
|
+
rewriteModelRefsInServices(services, model.name, baseName);
|
|
1434
|
+
console.warn(`[oagen] Merged "${model.name}" into "${baseName}" (${model.fields.length} fields, superset)`);
|
|
1435
|
+
}
|
|
1436
|
+
return normalizedModels;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/parser/collect-inline-enums.ts
|
|
1440
|
+
function collectInlineEnumsFromModels(models, enums) {
|
|
1441
|
+
const enumNames = new Set(enums.map((e) => e.name));
|
|
1442
|
+
for (const model of models) {
|
|
1443
|
+
for (const field of model.fields) {
|
|
1444
|
+
collectInlineEnumFromRef(field.type, enums, enumNames);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/parser/normalize-model-refs.ts
|
|
1450
|
+
function validateModelRefs(spec) {
|
|
1451
|
+
const knownNames = /* @__PURE__ */ new Set();
|
|
1452
|
+
for (const m of spec.models) knownNames.add(m.name);
|
|
1453
|
+
for (const e of spec.enums) knownNames.add(e.name);
|
|
1454
|
+
function walkRef(ref, context) {
|
|
1455
|
+
walkTypeRef(ref, {
|
|
1456
|
+
model: (r) => {
|
|
1457
|
+
if (!knownNames.has(r.name)) {
|
|
1458
|
+
console.warn(`[oagen] Warning: Unresolved model reference "${r.name}" (context: ${context})`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
for (const model of spec.models) {
|
|
1464
|
+
for (const field of model.fields) {
|
|
1465
|
+
walkRef(field.type, `${model.name}.${field.name}`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
for (const service of spec.services) {
|
|
1469
|
+
for (const op of service.operations) {
|
|
1470
|
+
for (const p of [...op.pathParams, ...op.queryParams, ...op.headerParams]) {
|
|
1471
|
+
walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
|
|
1472
|
+
}
|
|
1473
|
+
if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
|
|
1474
|
+
walkRef(op.response, `${service.name}.${op.name}.response`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// src/parser/parse.ts
|
|
1480
|
+
async function parseSpec(specPath, options) {
|
|
1481
|
+
const { parsed } = await loadAndBundleSpec(specPath);
|
|
1482
|
+
const spec = parsed;
|
|
1483
|
+
const version = spec.openapi ?? "";
|
|
1484
|
+
if (!version.startsWith("3.")) {
|
|
1485
|
+
throw new SpecParseError(
|
|
1486
|
+
`Unsupported OpenAPI version: ${version}. oagen requires OpenAPI 3.x`,
|
|
1487
|
+
`Update the spec to OpenAPI 3.0 or 3.1. If you are using Swagger 2.x, convert it first with \`npx swagger2openapi ${specPath}\`.`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
const { models, enums } = extractSchemas(
|
|
1491
|
+
spec.components?.schemas
|
|
1492
|
+
);
|
|
1493
|
+
const { services, inlineModels } = extractOperations(
|
|
1494
|
+
spec.paths,
|
|
1495
|
+
options?.operationIdTransform
|
|
1496
|
+
);
|
|
1497
|
+
const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
|
|
1498
|
+
const jsonCollapsedModels = collapseJsonSuffixModels(responseNormalizedModels, services);
|
|
1499
|
+
const fieldInlineModels = extractInlineModelsFromSchemas(
|
|
1500
|
+
spec.components?.schemas
|
|
1501
|
+
);
|
|
1502
|
+
const finalModels = mergeFieldInlineModels(jsonCollapsedModels, fieldInlineModels);
|
|
1503
|
+
collectInlineEnumsFromModels(finalModels, enums);
|
|
1504
|
+
const auth = extractAuthSchemes(spec.components?.securitySchemes);
|
|
1505
|
+
const serverEntries = (spec.servers ?? []).map((s) => ({ url: s.url ?? "", description: s.description })).filter((s) => s.url);
|
|
1506
|
+
const result = {
|
|
1507
|
+
name: spec.info?.title ?? "Unknown API",
|
|
1508
|
+
version: spec.info?.version ?? "0.0.0",
|
|
1509
|
+
description: spec.info?.description,
|
|
1510
|
+
baseUrl: serverEntries[0]?.url ?? "",
|
|
1511
|
+
servers: serverEntries.length > 0 ? serverEntries : void 0,
|
|
1512
|
+
services,
|
|
1513
|
+
models: finalModels,
|
|
1514
|
+
enums,
|
|
1515
|
+
auth
|
|
1516
|
+
};
|
|
1517
|
+
validateModelRefs(result);
|
|
1518
|
+
return result;
|
|
1519
|
+
}
|
|
1520
|
+
function extractAuthSchemes(securitySchemes) {
|
|
1521
|
+
if (!securitySchemes) return void 0;
|
|
1522
|
+
const schemes = [];
|
|
1523
|
+
for (const [, scheme] of Object.entries(securitySchemes)) {
|
|
1524
|
+
if (scheme.type === "http" && scheme.scheme === "bearer") {
|
|
1525
|
+
schemes.push({ kind: "bearer" });
|
|
1526
|
+
} else if (scheme.type === "apiKey" && scheme.in && scheme.name) {
|
|
1527
|
+
schemes.push({ kind: "apiKey", in: scheme.in, name: scheme.name });
|
|
1528
|
+
} else if (scheme.type === "oauth2" && scheme.flows) {
|
|
1529
|
+
schemes.push({ kind: "oauth2", flows: scheme.flows });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return schemes.length > 0 ? schemes : void 0;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// scripts/smoke/shared.ts
|
|
1536
|
+
import "dotenv/config";
|
|
1537
|
+
import { readFileSync } from "fs";
|
|
1538
|
+
var SKIP_OPERATIONS = /* @__PURE__ */ new Set();
|
|
1539
|
+
var SKIP_SERVICES = /* @__PURE__ */ new Set();
|
|
1540
|
+
var SERVICE_PRIORITY = {};
|
|
1541
|
+
var SERVICE_PROPERTY_MAP = {};
|
|
1542
|
+
function loadSmokeConfig(configPath) {
|
|
1543
|
+
if (!configPath) return;
|
|
1544
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
1545
|
+
const config = JSON.parse(raw);
|
|
1546
|
+
if (config.skipOperations) SKIP_OPERATIONS = new Set(config.skipOperations);
|
|
1547
|
+
if (config.skipServices) SKIP_SERVICES = new Set(config.skipServices);
|
|
1548
|
+
if (config.servicePriority) SERVICE_PRIORITY = config.servicePriority;
|
|
1549
|
+
if (config.servicePropertyMap) SERVICE_PROPERTY_MAP = config.servicePropertyMap;
|
|
1550
|
+
}
|
|
1551
|
+
function operationSortKey(op) {
|
|
1552
|
+
const hasParams = op.pathParams.length > 0;
|
|
1553
|
+
const method = op.httpMethod;
|
|
1554
|
+
if (method === "post" && !hasParams) return 0;
|
|
1555
|
+
if (method === "get" && !hasParams && op.pagination) return 1;
|
|
1556
|
+
if (method === "get" && !hasParams) return 2;
|
|
1557
|
+
if (method === "post" && hasParams) return 3;
|
|
1558
|
+
if (method === "get" && hasParams && op.pagination) return 4;
|
|
1559
|
+
if (method === "get" && hasParams) return 5;
|
|
1560
|
+
if (method === "put" || method === "patch") return 6;
|
|
1561
|
+
if (method === "delete") return 7;
|
|
1562
|
+
return 99;
|
|
1563
|
+
}
|
|
1564
|
+
function planOperations(spec) {
|
|
1565
|
+
const groups = [];
|
|
1566
|
+
for (const service of spec.services) {
|
|
1567
|
+
if (SKIP_SERVICES.has(service.name)) continue;
|
|
1568
|
+
const ops = [];
|
|
1569
|
+
for (const op of service.operations) {
|
|
1570
|
+
if (SKIP_OPERATIONS.has(op.name)) continue;
|
|
1571
|
+
ops.push({ service: service.name, operation: op });
|
|
1572
|
+
}
|
|
1573
|
+
if (ops.length === 0) continue;
|
|
1574
|
+
ops.sort((a, b) => {
|
|
1575
|
+
return operationSortKey(a.operation) - operationSortKey(b.operation);
|
|
1576
|
+
});
|
|
1577
|
+
groups.push({ service: service.name, operations: ops });
|
|
1578
|
+
}
|
|
1579
|
+
groups.sort((a, b) => {
|
|
1580
|
+
const aPri = SERVICE_PRIORITY[a.service] ?? 50;
|
|
1581
|
+
const bPri = SERVICE_PRIORITY[b.service] ?? 50;
|
|
1582
|
+
return aPri - bPri;
|
|
1583
|
+
});
|
|
1584
|
+
return groups;
|
|
1585
|
+
}
|
|
1586
|
+
function generateFixtureValue(typeRef, fieldName, spec) {
|
|
1587
|
+
switch (typeRef.kind) {
|
|
1588
|
+
case "primitive":
|
|
1589
|
+
return generatePrimitiveFixture(typeRef.type, typeRef.format, fieldName);
|
|
1590
|
+
case "array":
|
|
1591
|
+
return [generateFixtureValue(typeRef.items, fieldName, spec)];
|
|
1592
|
+
case "model": {
|
|
1593
|
+
const model = spec.models.find((m) => m.name === typeRef.name);
|
|
1594
|
+
if (model) {
|
|
1595
|
+
const obj = {};
|
|
1596
|
+
for (const field of model.fields) {
|
|
1597
|
+
if (field.required) {
|
|
1598
|
+
obj[toSnakeCase(field.name)] = generateFixtureValue(field.type, field.name, spec);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
return obj;
|
|
1602
|
+
}
|
|
1603
|
+
return {};
|
|
1604
|
+
}
|
|
1605
|
+
case "enum": {
|
|
1606
|
+
const e = spec.enums.find((en) => en.name === typeRef.name);
|
|
1607
|
+
return e?.values[0]?.value ?? "unknown";
|
|
1608
|
+
}
|
|
1609
|
+
case "nullable":
|
|
1610
|
+
return generateFixtureValue(typeRef.inner, fieldName, spec);
|
|
1611
|
+
case "union":
|
|
1612
|
+
if (typeRef.variants.length > 0) {
|
|
1613
|
+
return generateFixtureValue(typeRef.variants[0], fieldName, spec);
|
|
1614
|
+
}
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function generatePrimitiveFixture(type, format, fieldName) {
|
|
1619
|
+
if (type === "string") {
|
|
1620
|
+
if (format === "uuid") return "550e8400-e29b-41d4-a716-446655440000";
|
|
1621
|
+
if (format === "date") return "2024-01-01";
|
|
1622
|
+
if (format === "date-time") return "2024-01-01T00:00:00Z";
|
|
1623
|
+
if (format === "email") return "test@example.com";
|
|
1624
|
+
if (format === "uri" || format === "url") return "https://example.com";
|
|
1625
|
+
return `test_${toSnakeCase(fieldName)}`;
|
|
1626
|
+
}
|
|
1627
|
+
if (type === "integer") return 1;
|
|
1628
|
+
if (type === "number") return 1;
|
|
1629
|
+
if (type === "boolean") return true;
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
function generatePayload(op, spec) {
|
|
1633
|
+
if (!op.requestBody) return null;
|
|
1634
|
+
const bodyType = op.requestBody;
|
|
1635
|
+
if (bodyType.kind === "model") {
|
|
1636
|
+
const model = spec.models.find((m) => m.name === bodyType.name);
|
|
1637
|
+
if (!model) return {};
|
|
1638
|
+
const payload = {};
|
|
1639
|
+
for (const field of model.fields) {
|
|
1640
|
+
if (!field.required) continue;
|
|
1641
|
+
const key = toSnakeCase(field.name);
|
|
1642
|
+
let value = generateFixtureValue(field.type, field.name, spec);
|
|
1643
|
+
if ((key === "name" || key === "slug") && typeof value === "string") {
|
|
1644
|
+
value = `smoke-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1645
|
+
}
|
|
1646
|
+
payload[key] = value;
|
|
1647
|
+
}
|
|
1648
|
+
return payload;
|
|
1649
|
+
}
|
|
1650
|
+
return {};
|
|
1651
|
+
}
|
|
1652
|
+
function generateQueryParams(op, spec) {
|
|
1653
|
+
const params = {};
|
|
1654
|
+
if (op.pagination) {
|
|
1655
|
+
params["limit"] = "1";
|
|
1656
|
+
}
|
|
1657
|
+
for (const p of op.queryParams) {
|
|
1658
|
+
if (!p.required) continue;
|
|
1659
|
+
const value = generateFixtureValue(p.type, p.name, spec);
|
|
1660
|
+
params[toSnakeCase(p.name)] = String(value);
|
|
1661
|
+
}
|
|
1662
|
+
const sorted = {};
|
|
1663
|
+
for (const key of Object.keys(params).sort()) {
|
|
1664
|
+
sorted[key] = params[key];
|
|
1665
|
+
}
|
|
1666
|
+
return sorted;
|
|
1667
|
+
}
|
|
1668
|
+
function resolvePath(op, pathParams) {
|
|
1669
|
+
let path = op.path;
|
|
1670
|
+
for (const [name, value] of Object.entries(pathParams)) {
|
|
1671
|
+
path = path.replace(`{${name}}`, value);
|
|
1672
|
+
}
|
|
1673
|
+
return path;
|
|
1674
|
+
}
|
|
1675
|
+
function parseCliArgs() {
|
|
1676
|
+
const args = process.argv.slice(2);
|
|
1677
|
+
const specIdx = args.indexOf("--spec");
|
|
1678
|
+
const spec = specIdx !== -1 && args[specIdx + 1] ? args[specIdx + 1] : process.env.OPENAPI_SPEC_PATH;
|
|
1679
|
+
if (!spec) {
|
|
1680
|
+
console.error("OpenAPI spec path is required. Set OPENAPI_SPEC_PATH env var or pass --spec <path>.");
|
|
1681
|
+
process.exit(1);
|
|
1682
|
+
}
|
|
1683
|
+
const sdkPathIdx = args.indexOf("--sdk-path");
|
|
1684
|
+
const sdkPath = sdkPathIdx !== -1 && args[sdkPathIdx + 1] ? args[sdkPathIdx + 1] : void 0;
|
|
1685
|
+
const configIdx = args.indexOf("--smoke-config");
|
|
1686
|
+
const smokeConfig = configIdx !== -1 && args[configIdx + 1] ? args[configIdx + 1] : process.env.SMOKE_CONFIG;
|
|
1687
|
+
return { spec, sdkPath, smokeConfig };
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// scripts/smoke/baseline.ts
|
|
1691
|
+
async function main() {
|
|
1692
|
+
const { spec: specPath, smokeConfig } = parseCliArgs();
|
|
1693
|
+
loadSmokeConfig(smokeConfig);
|
|
1694
|
+
console.log("Parsing spec...");
|
|
1695
|
+
const spec = await parseSpec(specPath);
|
|
1696
|
+
console.log(`Spec: ${spec.name} v${spec.version}`);
|
|
1697
|
+
const groups = planOperations(spec);
|
|
1698
|
+
const exchanges = [];
|
|
1699
|
+
for (const group of groups) {
|
|
1700
|
+
for (const planned of group.operations) {
|
|
1701
|
+
const op = planned.operation;
|
|
1702
|
+
const operationId = `${group.service}.${op.name}`;
|
|
1703
|
+
const placeholderParams = {};
|
|
1704
|
+
for (const p of op.pathParams) {
|
|
1705
|
+
placeholderParams[p.name] = "<ID>";
|
|
1706
|
+
}
|
|
1707
|
+
const path = resolvePath(op, placeholderParams);
|
|
1708
|
+
const queryParams = generateQueryParams(op, spec);
|
|
1709
|
+
const body = generatePayload(op, spec);
|
|
1710
|
+
const defaultSuccess = {
|
|
1711
|
+
post: 201,
|
|
1712
|
+
get: 200,
|
|
1713
|
+
put: 200,
|
|
1714
|
+
patch: 200,
|
|
1715
|
+
delete: 204
|
|
1716
|
+
};
|
|
1717
|
+
const status = defaultSuccess[op.httpMethod] ?? 200;
|
|
1718
|
+
exchanges.push({
|
|
1719
|
+
operationId,
|
|
1720
|
+
service: group.service,
|
|
1721
|
+
operationName: op.name,
|
|
1722
|
+
request: {
|
|
1723
|
+
method: op.httpMethod.toUpperCase(),
|
|
1724
|
+
path,
|
|
1725
|
+
queryParams,
|
|
1726
|
+
body
|
|
1727
|
+
},
|
|
1728
|
+
response: {
|
|
1729
|
+
status,
|
|
1730
|
+
body: null
|
|
1731
|
+
},
|
|
1732
|
+
outcome: "success",
|
|
1733
|
+
durationMs: 0
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const results = {
|
|
1738
|
+
source: "spec-baseline",
|
|
1739
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1740
|
+
specVersion: spec.version,
|
|
1741
|
+
exchanges
|
|
1742
|
+
};
|
|
1743
|
+
writeFileSync("smoke-results-spec-baseline.json", JSON.stringify(results, null, 2));
|
|
1744
|
+
console.log(`
|
|
1745
|
+
Generated ${exchanges.length} operations from spec`);
|
|
1746
|
+
console.log("Written to smoke-results-spec-baseline.json");
|
|
1747
|
+
}
|
|
1748
|
+
main().catch((err) => {
|
|
1749
|
+
console.error("Baseline generation failed:", err);
|
|
1750
|
+
process.exit(1);
|
|
1751
|
+
});
|