@workos/oagen 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/cli/index.mjs +844 -56
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +1 -1
- package/dist/compat/index.mjs +9 -4
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-Cn6S74vV.mjs → diff-DcY93sii.mjs} +433 -14
- package/dist/diff-DcY93sii.mjs.map +1 -0
- package/dist/{differ-B6472Yef.mjs → differ-CG5ZErKl.mjs} +2 -2
- package/dist/{differ-B6472Yef.mjs.map → differ-CG5ZErKl.mjs.map} +1 -1
- package/dist/index.d.mts +107 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +337 -29
- package/dist/index.mjs.map +1 -1
- package/dist/scripts/smoke/baseline.mjs +314 -32
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +138 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +314 -32
- package/dist/smoke.mjs.map +1 -1
- package/dist/{tree-sitter-DFRXfWg9.mjs → tree-sitter-C53w_0WE.mjs} +5 -7
- package/dist/tree-sitter-C53w_0WE.mjs.map +1 -0
- package/dist/{types-O25qZxMl.d.mts → types-D1tMqLVB.d.mts} +165 -2
- package/dist/types-D1tMqLVB.d.mts.map +1 -0
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.mjs +4 -4
- package/dist/verify/index.mjs.map +1 -1
- package/docs/architecture/ir-types.md +73 -1
- package/docs/cli.md +29 -8
- package/package.json +4 -4
- package/skills/review-operations/SKILL.md +83 -0
- package/dist/diff-Cn6S74vV.mjs.map +0 -1
- package/dist/tree-sitter-DFRXfWg9.mjs.map +0 -1
- package/dist/types-O25qZxMl.d.mts.map +0 -1
|
@@ -2,8 +2,122 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { BaseResolver, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
|
|
5
|
+
import { BaseResolver, Oas3_1Types, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
|
|
6
6
|
import "dotenv/config";
|
|
7
|
+
//#region src/ir/sdk-behavior.ts
|
|
8
|
+
/**
|
|
9
|
+
* Canonical SDK behavior defaults. These match the PHP emitter's current
|
|
10
|
+
* implementation (the most complete). Per-language overrides can be applied
|
|
11
|
+
* via `mergeSdkBehavior()` in each SDK's `oagen.config.ts`.
|
|
12
|
+
*
|
|
13
|
+
* - Retry: exponential backoff with jitter, retries on 429/5xx
|
|
14
|
+
* - Errors: standard HTTP status → exception kind mapping
|
|
15
|
+
* - Telemetry: enabled by default, tracks request ID + latency
|
|
16
|
+
* - Pagination: no inter-page delay (Node overrides to 350ms)
|
|
17
|
+
* - Idempotency: auto-generate UUID v4 for retryable POSTs
|
|
18
|
+
* - Logging: all lifecycle events enabled
|
|
19
|
+
* - User-Agent: SDK identifier + runtime version + app info + AI agent detection
|
|
20
|
+
* - Request guards: detect misplaced RequestOptions keys in params
|
|
21
|
+
* - Timeout: 60s default (Python overrides to 30s)
|
|
22
|
+
*/
|
|
23
|
+
function defaultSdkBehavior() {
|
|
24
|
+
return {
|
|
25
|
+
retry: {
|
|
26
|
+
retryableStatusCodes: [
|
|
27
|
+
429,
|
|
28
|
+
500,
|
|
29
|
+
502,
|
|
30
|
+
503,
|
|
31
|
+
504
|
|
32
|
+
],
|
|
33
|
+
maxRetries: 3,
|
|
34
|
+
retryOnConnectionError: true,
|
|
35
|
+
retryOnTimeout: true,
|
|
36
|
+
backoff: {
|
|
37
|
+
initialDelay: 1,
|
|
38
|
+
multiplier: 2,
|
|
39
|
+
maxDelay: 30,
|
|
40
|
+
jitterFactor: .5
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
errors: {
|
|
44
|
+
statusCodeMap: {
|
|
45
|
+
400: "BadRequest",
|
|
46
|
+
401: "Authentication",
|
|
47
|
+
403: "Authorization",
|
|
48
|
+
404: "NotFound",
|
|
49
|
+
409: "Conflict",
|
|
50
|
+
422: "UnprocessableEntity",
|
|
51
|
+
429: "RateLimitExceeded"
|
|
52
|
+
},
|
|
53
|
+
serverErrorKind: "Server",
|
|
54
|
+
clientErrorKind: "Api",
|
|
55
|
+
errorDocUrlTemplate: "https://workos.com/docs/errors/{code}"
|
|
56
|
+
},
|
|
57
|
+
telemetry: {
|
|
58
|
+
enabledByDefault: true,
|
|
59
|
+
headerName: "X-WorkOS-Client-Telemetry",
|
|
60
|
+
requestIdHeader: "X-Request-ID"
|
|
61
|
+
},
|
|
62
|
+
pagination: { autoPageDelayMs: 0 },
|
|
63
|
+
idempotency: {
|
|
64
|
+
headerName: "Idempotency-Key",
|
|
65
|
+
autoGenerateForPost: true
|
|
66
|
+
},
|
|
67
|
+
logging: {
|
|
68
|
+
enabled: true,
|
|
69
|
+
events: [
|
|
70
|
+
"request.start",
|
|
71
|
+
"request.success",
|
|
72
|
+
"request.retry",
|
|
73
|
+
"request.rate_limited",
|
|
74
|
+
"request.error",
|
|
75
|
+
"request.connection_error"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
userAgent: {
|
|
79
|
+
sdkIdentifierTemplate: "{name} {lang}/{version}",
|
|
80
|
+
includeRuntimeVersion: true,
|
|
81
|
+
allowAppInfo: true,
|
|
82
|
+
aiAgentEnvVars: [
|
|
83
|
+
{
|
|
84
|
+
envVar: "CLAUDE_CODE",
|
|
85
|
+
agentName: "ClaudeCode"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
envVar: "CURSOR_AGENT",
|
|
89
|
+
agentName: "Cursor"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
envVar: "CLINE_ACTIVE",
|
|
93
|
+
agentName: "Cline"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
envVar: "WINDSURF_ACTIVE",
|
|
97
|
+
agentName: "Windsurf"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
envVar: "COPILOT_AGENT",
|
|
101
|
+
agentName: "Copilot"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
requestGuard: { optionKeys: [
|
|
106
|
+
"api_key",
|
|
107
|
+
"apiKey",
|
|
108
|
+
"idempotency_key",
|
|
109
|
+
"idempotencyKey",
|
|
110
|
+
"extra_headers",
|
|
111
|
+
"extraHeaders",
|
|
112
|
+
"max_retries",
|
|
113
|
+
"maxRetries",
|
|
114
|
+
"base_url",
|
|
115
|
+
"baseUrl"
|
|
116
|
+
] },
|
|
117
|
+
timeout: { defaultTimeoutSeconds: 60 }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
7
121
|
//#region src/errors.ts
|
|
8
122
|
/**
|
|
9
123
|
* Structured error hierarchy for oagen.
|
|
@@ -32,13 +146,11 @@ var SpecParseError = class extends OagenError {
|
|
|
32
146
|
//#region src/parser/refs.ts
|
|
33
147
|
async function loadAndBundleSpec(specPath) {
|
|
34
148
|
const absolutePath = resolve(specPath);
|
|
35
|
-
const document = makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href);
|
|
36
|
-
const config = await createConfig({});
|
|
37
|
-
const resolver = new BaseResolver();
|
|
38
149
|
const result = await bundleDocument({
|
|
39
|
-
document,
|
|
40
|
-
config:
|
|
41
|
-
|
|
150
|
+
document: makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href),
|
|
151
|
+
config: await createConfig({}),
|
|
152
|
+
types: Oas3_1Types,
|
|
153
|
+
externalRefResolver: new BaseResolver(),
|
|
42
154
|
dereference: false
|
|
43
155
|
});
|
|
44
156
|
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.`);
|
|
@@ -187,11 +299,7 @@ function toSnakeCase(s) {
|
|
|
187
299
|
function toUpperSnakeCase(s) {
|
|
188
300
|
return splitWords(s).map((w) => w.toUpperCase()).join("_");
|
|
189
301
|
}
|
|
190
|
-
const BACKEND_SUFFIXES = [
|
|
191
|
-
"Dto",
|
|
192
|
-
"DTO",
|
|
193
|
-
"Controller"
|
|
194
|
-
];
|
|
302
|
+
const BACKEND_SUFFIXES = ["Controller"];
|
|
195
303
|
function stripBackendSuffixes(name) {
|
|
196
304
|
for (const suffix of BACKEND_SUFFIXES) if (name.endsWith(suffix) && name.length > suffix.length) return name.slice(0, -suffix.length);
|
|
197
305
|
return name;
|
|
@@ -218,7 +326,9 @@ const SINGULAR_SAFE_LIST = new Set([
|
|
|
218
326
|
"Canvas",
|
|
219
327
|
"Alias",
|
|
220
328
|
"Basis",
|
|
221
|
-
"Bus"
|
|
329
|
+
"Bus",
|
|
330
|
+
"Species",
|
|
331
|
+
"Series"
|
|
222
332
|
]);
|
|
223
333
|
/**
|
|
224
334
|
* Conservative singularize for the leading PascalCase word.
|
|
@@ -321,15 +431,39 @@ function buildInlineModel(name, schema) {
|
|
|
321
431
|
}
|
|
322
432
|
//#endregion
|
|
323
433
|
//#region src/parser/schemas.ts
|
|
324
|
-
|
|
434
|
+
/**
|
|
435
|
+
* Module-level transform set during extractSchemas(). Used by schemaToTypeRef()
|
|
436
|
+
* to apply the same name transform to $ref model/enum references.
|
|
437
|
+
*/
|
|
438
|
+
let activeSchemaNameTransform = null;
|
|
439
|
+
/** Apply cleanSchemaName + the active transform (if any) to a raw schema name. */
|
|
440
|
+
function resolveSchemaName(rawName) {
|
|
441
|
+
let name = cleanSchemaName(toPascalCase(rawName));
|
|
442
|
+
if (activeSchemaNameTransform) name = activeSchemaNameTransform(name);
|
|
443
|
+
return name;
|
|
444
|
+
}
|
|
445
|
+
function extractSchemas(schemas, options) {
|
|
325
446
|
const enums = [];
|
|
326
447
|
if (!schemas) return {
|
|
327
448
|
models: [],
|
|
328
449
|
enums
|
|
329
450
|
};
|
|
451
|
+
if (options?.schemaNameTransform) {
|
|
452
|
+
const transform = options.schemaNameTransform;
|
|
453
|
+
const cleanedNames = Object.keys(schemas).map((n) => cleanSchemaName(toPascalCase(n)));
|
|
454
|
+
const transformedToOriginals = /* @__PURE__ */ new Map();
|
|
455
|
+
for (const cleaned of cleanedNames) {
|
|
456
|
+
const transformed = transform(cleaned);
|
|
457
|
+
if (!transformedToOriginals.has(transformed)) transformedToOriginals.set(transformed, []);
|
|
458
|
+
transformedToOriginals.get(transformed).push(cleaned);
|
|
459
|
+
}
|
|
460
|
+
const unsafeToTransform = /* @__PURE__ */ new Set();
|
|
461
|
+
for (const [, originals] of transformedToOriginals) if (originals.length > 1) for (const n of originals) unsafeToTransform.add(n);
|
|
462
|
+
activeSchemaNameTransform = (name) => unsafeToTransform.has(name) ? name : transform(name);
|
|
463
|
+
} else activeSchemaNameTransform = null;
|
|
330
464
|
const modelsByCleanName = /* @__PURE__ */ new Map();
|
|
331
465
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
332
|
-
const pascalName =
|
|
466
|
+
const pascalName = resolveSchemaName(name);
|
|
333
467
|
if (schema.enum) enums.push(extractEnum(pascalName, schema));
|
|
334
468
|
else {
|
|
335
469
|
const model = extractModel(pascalName, schema, schemas);
|
|
@@ -337,6 +471,12 @@ function extractSchemas(schemas) {
|
|
|
337
471
|
if (existing) {
|
|
338
472
|
if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
|
|
339
473
|
} else modelsByCleanName.set(pascalName, model);
|
|
474
|
+
for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName)) {
|
|
475
|
+
const existingInline = modelsByCleanName.get(inlineModel.name);
|
|
476
|
+
if (existingInline) {
|
|
477
|
+
if (inlineModel.fields.length > existingInline.fields.length) modelsByCleanName.set(inlineModel.name, inlineModel);
|
|
478
|
+
} else modelsByCleanName.set(inlineModel.name, inlineModel);
|
|
479
|
+
}
|
|
340
480
|
}
|
|
341
481
|
}
|
|
342
482
|
const models = [...modelsByCleanName.values()];
|
|
@@ -352,6 +492,10 @@ function extractSchemas(schemas) {
|
|
|
352
492
|
enums
|
|
353
493
|
};
|
|
354
494
|
}
|
|
495
|
+
/** Clear the module-level schemaNameTransform. Called by parseSpec() after all extraction phases. */
|
|
496
|
+
function clearSchemaNameTransform() {
|
|
497
|
+
activeSchemaNameTransform = null;
|
|
498
|
+
}
|
|
355
499
|
/**
|
|
356
500
|
* Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
|
|
357
501
|
* Shared by both schemas.ts (field-level) and parse.ts (model-level) collection passes.
|
|
@@ -388,7 +532,7 @@ function collectInlineEnums(fields, enums) {
|
|
|
388
532
|
function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
|
|
389
533
|
walkTypeRef(ref, { model: (modelRef) => {
|
|
390
534
|
if (modelNames.has(modelRef.name)) return;
|
|
391
|
-
if (Object.keys(schemas).some((k) =>
|
|
535
|
+
if (Object.keys(schemas).some((k) => resolveSchemaName(k) === modelRef.name)) return;
|
|
392
536
|
const parentSchema = findSchemaByName(parentModelName, schemas);
|
|
393
537
|
if (!parentSchema) return;
|
|
394
538
|
const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
|
|
@@ -403,7 +547,7 @@ function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, mod
|
|
|
403
547
|
}
|
|
404
548
|
/** Find a component schema by PascalCase name. */
|
|
405
549
|
function findSchemaByName(pascalName, schemas) {
|
|
406
|
-
for (const [k, v] of Object.entries(schemas)) if (
|
|
550
|
+
for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
|
|
407
551
|
return null;
|
|
408
552
|
}
|
|
409
553
|
/** Walk into a schema to find a field's sub-schema, resolving allOf. */
|
|
@@ -561,13 +705,88 @@ function extractAllOfModel(name, schema, schemas) {
|
|
|
561
705
|
fields
|
|
562
706
|
};
|
|
563
707
|
}
|
|
708
|
+
function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
|
|
709
|
+
const models = [];
|
|
710
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
711
|
+
for (const subSchema of schema.allOf ?? []) for (const variant of subSchema.oneOf ?? []) {
|
|
712
|
+
if (!variant.properties || variant.type !== void 0 && variant.type !== "object") continue;
|
|
713
|
+
const variantName = deriveDiscriminatedVariantName(variant, fallbackName);
|
|
714
|
+
for (const model of extractInlineModelDeep(variantName, variant)) {
|
|
715
|
+
if (seenNames.has(model.name)) continue;
|
|
716
|
+
seenNames.add(model.name);
|
|
717
|
+
models.push(model);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return models;
|
|
721
|
+
}
|
|
722
|
+
function deriveDiscriminatedVariantName(schema, fallbackName) {
|
|
723
|
+
if (!schema.properties) return fallbackName;
|
|
724
|
+
for (const name of [
|
|
725
|
+
"event",
|
|
726
|
+
"type",
|
|
727
|
+
"object"
|
|
728
|
+
]) {
|
|
729
|
+
const field = schema.properties[name];
|
|
730
|
+
if (!field) continue;
|
|
731
|
+
if (typeof field.const === "string") return toPascalCase(field.const);
|
|
732
|
+
if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return toPascalCase(field.enum[0]);
|
|
733
|
+
}
|
|
734
|
+
for (const field of Object.values(schema.properties)) {
|
|
735
|
+
if (!field) continue;
|
|
736
|
+
if (typeof field.const === "string") return toPascalCase(field.const);
|
|
737
|
+
if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return toPascalCase(field.enum[0]);
|
|
738
|
+
}
|
|
739
|
+
return fallbackName;
|
|
740
|
+
}
|
|
741
|
+
function extractInlineModelDeep(name, schema) {
|
|
742
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
743
|
+
const fields = [];
|
|
744
|
+
const properties = schema.properties ?? {};
|
|
745
|
+
const nestedModels = [];
|
|
746
|
+
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
|
747
|
+
if (!fieldSchema) continue;
|
|
748
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
749
|
+
if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema));
|
|
750
|
+
if (fieldSchema.type === "array" && fieldSchema.items?.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema.items));
|
|
751
|
+
if (fieldSchema.allOf) {
|
|
752
|
+
const mergedProperties = {};
|
|
753
|
+
const mergedRequired = [];
|
|
754
|
+
for (const sub of fieldSchema.allOf) {
|
|
755
|
+
if (sub.properties) Object.assign(mergedProperties, sub.properties);
|
|
756
|
+
if (sub.required) mergedRequired.push(...sub.required);
|
|
757
|
+
}
|
|
758
|
+
if (Object.keys(mergedProperties).length > 0) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), {
|
|
759
|
+
type: "object",
|
|
760
|
+
properties: mergedProperties,
|
|
761
|
+
required: mergedRequired
|
|
762
|
+
}));
|
|
763
|
+
}
|
|
764
|
+
if (fieldSchema.oneOf) {
|
|
765
|
+
const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
|
|
766
|
+
if (objectVariant) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), objectVariant));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return [{
|
|
770
|
+
name,
|
|
771
|
+
description: schema.description,
|
|
772
|
+
fields
|
|
773
|
+
}, ...nestedModels];
|
|
774
|
+
}
|
|
775
|
+
function qualifyNestedInlineName(parentName, fieldName) {
|
|
776
|
+
const pascalField = toPascalCase(fieldName);
|
|
777
|
+
if (pascalField.startsWith(parentName)) return pascalField;
|
|
778
|
+
const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
|
|
779
|
+
const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
|
|
780
|
+
if (match && match[2]) return match[1] + singularize(match[2]);
|
|
781
|
+
return qualified;
|
|
782
|
+
}
|
|
564
783
|
function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
565
784
|
if (schema.$ref) {
|
|
566
785
|
const segments = schema.$ref.split("/");
|
|
567
786
|
const rawName = segments[segments.length - 1];
|
|
568
787
|
return {
|
|
569
788
|
kind: "model",
|
|
570
|
-
name:
|
|
789
|
+
name: resolveSchemaName(rawName)
|
|
571
790
|
};
|
|
572
791
|
}
|
|
573
792
|
if (Array.isArray(schema.type)) {
|
|
@@ -831,6 +1050,7 @@ const KNOWN_DATA_PATHS = new Set([
|
|
|
831
1050
|
"edges"
|
|
832
1051
|
]);
|
|
833
1052
|
const PAGINATION_METADATA_PATTERNS = [
|
|
1053
|
+
"meta",
|
|
834
1054
|
"metadata",
|
|
835
1055
|
"pagination",
|
|
836
1056
|
"cursor",
|
|
@@ -862,11 +1082,12 @@ function detectListEnvelope(schema) {
|
|
|
862
1082
|
}
|
|
863
1083
|
if (arrayProps.length === 1 && nonArrayKeys.length >= 1) {
|
|
864
1084
|
const arrayPropName = arrayProps[0];
|
|
865
|
-
|
|
1085
|
+
const hasPaginationCompanion = nonArrayKeys.some((key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern)));
|
|
1086
|
+
if (KNOWN_DATA_PATHS.has(arrayPropName) && hasPaginationCompanion) return {
|
|
866
1087
|
isEnvelope: true,
|
|
867
1088
|
dataPath: arrayPropName
|
|
868
1089
|
};
|
|
869
|
-
if (
|
|
1090
|
+
if (hasPaginationCompanion) return {
|
|
870
1091
|
isEnvelope: true,
|
|
871
1092
|
dataPath: arrayPropName
|
|
872
1093
|
};
|
|
@@ -1170,13 +1391,33 @@ function extractOperations(paths, operationIdTransform) {
|
|
|
1170
1391
|
const op = pathItem[method];
|
|
1171
1392
|
if (!op) continue;
|
|
1172
1393
|
const serviceName = inferServiceName(path, op.tags?.[0]);
|
|
1173
|
-
const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform);
|
|
1394
|
+
const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName);
|
|
1174
1395
|
inlineModels.push(...opModels);
|
|
1175
1396
|
const ops = serviceMap.get(serviceName) ?? [];
|
|
1176
1397
|
ops.push(operation);
|
|
1177
1398
|
serviceMap.set(serviceName, ops);
|
|
1178
1399
|
}
|
|
1179
1400
|
}
|
|
1401
|
+
for (const [serviceName, operations] of serviceMap) {
|
|
1402
|
+
const pathGroups = /* @__PURE__ */ new Map();
|
|
1403
|
+
for (const op of operations) {
|
|
1404
|
+
const firstSeg = op.path.split("/").filter(Boolean)[0] ?? "";
|
|
1405
|
+
const groupKey = firstSeg.startsWith("{") ? serviceName : toPascalCase(firstSeg);
|
|
1406
|
+
const group = pathGroups.get(groupKey) ?? [];
|
|
1407
|
+
group.push(op);
|
|
1408
|
+
pathGroups.set(groupKey, group);
|
|
1409
|
+
}
|
|
1410
|
+
if (pathGroups.size > 1) {
|
|
1411
|
+
if ([...pathGroups.keys()].some((k) => k !== serviceName)) {
|
|
1412
|
+
serviceMap.delete(serviceName);
|
|
1413
|
+
for (const [groupName, groupOps] of pathGroups) {
|
|
1414
|
+
const existing = serviceMap.get(groupName) ?? [];
|
|
1415
|
+
existing.push(...groupOps);
|
|
1416
|
+
serviceMap.set(groupName, existing);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1180
1421
|
for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
|
|
1181
1422
|
return {
|
|
1182
1423
|
services: Array.from(serviceMap.entries()).map(([name, operations]) => ({
|
|
@@ -1292,13 +1533,14 @@ function inferDeeperContext(path) {
|
|
|
1292
1533
|
if (nonParams.length >= 3) return toPascalCase(nonParams.slice(1, -1).join("_"));
|
|
1293
1534
|
return null;
|
|
1294
1535
|
}
|
|
1295
|
-
function buildOperation(method, path, op, pathLevelParams, operationIdTransform) {
|
|
1536
|
+
function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName) {
|
|
1296
1537
|
const allParams = [...pathLevelParams, ...op.parameters ?? []];
|
|
1297
1538
|
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
1298
|
-
const
|
|
1299
|
-
const
|
|
1300
|
-
const
|
|
1301
|
-
const
|
|
1539
|
+
const opContext = serviceName;
|
|
1540
|
+
const pathParams = extractParams(allParams, "path", opContext);
|
|
1541
|
+
const queryParams = extractParams(allParams, "query", opContext);
|
|
1542
|
+
const headerParams = extractParams(allParams, "header", opContext).filter((p) => p.name.toLowerCase() !== "idempotency-key");
|
|
1543
|
+
const cookieParams = extractParams(allParams, "cookie", opContext);
|
|
1302
1544
|
const reqBodyModels = [];
|
|
1303
1545
|
const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
|
|
1304
1546
|
const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method);
|
|
@@ -1315,6 +1557,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform)
|
|
|
1315
1557
|
itemType
|
|
1316
1558
|
};
|
|
1317
1559
|
} else if (paginationFromParams) pagination = paginationFromParams;
|
|
1560
|
+
const security = extractOperationSecurity(op.security);
|
|
1318
1561
|
return {
|
|
1319
1562
|
operation: {
|
|
1320
1563
|
name: inferOperationName(method, path, op.operationId, operationIdTransform),
|
|
@@ -1333,19 +1576,35 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform)
|
|
|
1333
1576
|
pagination,
|
|
1334
1577
|
injectIdempotencyKey: hasIdempotencyHeader,
|
|
1335
1578
|
deprecated: op.deprecated || void 0,
|
|
1336
|
-
async: op["x-oagen-async"]
|
|
1579
|
+
async: op["x-oagen-async"],
|
|
1580
|
+
security
|
|
1337
1581
|
},
|
|
1338
1582
|
inlineModels
|
|
1339
1583
|
};
|
|
1340
1584
|
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Extract per-operation security requirements from an OpenAPI security directive.
|
|
1587
|
+
* Returns undefined when the operation uses the spec-level default security.
|
|
1588
|
+
*
|
|
1589
|
+
* OpenAPI security format: `security: [{ schemeName: [scope1, scope2] }]`
|
|
1590
|
+
*/
|
|
1591
|
+
function extractOperationSecurity(security) {
|
|
1592
|
+
if (!security || security.length === 0) return void 0;
|
|
1593
|
+
const requirements = [];
|
|
1594
|
+
for (const entry of security) for (const [schemeName, scopes] of Object.entries(entry)) requirements.push({
|
|
1595
|
+
schemeName,
|
|
1596
|
+
scopes: scopes ?? []
|
|
1597
|
+
});
|
|
1598
|
+
return requirements.length > 0 ? requirements : void 0;
|
|
1599
|
+
}
|
|
1341
1600
|
function buildDescription(summary, description) {
|
|
1342
1601
|
if (summary && description && summary !== description) return `${summary}\n\n${description}`;
|
|
1343
1602
|
return description ?? summary;
|
|
1344
1603
|
}
|
|
1345
|
-
function extractParams(params, location) {
|
|
1604
|
+
function extractParams(params, location, operationContext) {
|
|
1346
1605
|
return params.filter((p) => p.in === location).map((p) => ({
|
|
1347
1606
|
name: p.name,
|
|
1348
|
-
type: p.schema ? schemaToTypeRef(p.schema, p.name) : {
|
|
1607
|
+
type: p.schema ? schemaToTypeRef(p.schema, p.name, operationContext ? toPascalCase(operationContext) : void 0) : {
|
|
1349
1608
|
kind: "primitive",
|
|
1350
1609
|
type: "string"
|
|
1351
1610
|
},
|
|
@@ -1531,7 +1790,14 @@ function extractResponses(responses, op, path, method) {
|
|
|
1531
1790
|
statusCode: code,
|
|
1532
1791
|
type: extractedType
|
|
1533
1792
|
});
|
|
1534
|
-
} else if (code >= 400) {
|
|
1793
|
+
} else if (code >= 300 && code < 400) allSuccessResponses.push({
|
|
1794
|
+
statusCode: code,
|
|
1795
|
+
type: {
|
|
1796
|
+
kind: "primitive",
|
|
1797
|
+
type: "unknown"
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
else if (code >= 400) {
|
|
1535
1801
|
const jsonContent = resp.content?.["application/json"];
|
|
1536
1802
|
const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
|
|
1537
1803
|
errors.push({
|
|
@@ -1658,6 +1924,19 @@ function collectInlineEnumsFromModels(models, enums) {
|
|
|
1658
1924
|
const enumNames = new Set(enums.map((e) => e.name));
|
|
1659
1925
|
for (const model of models) for (const field of model.fields) collectInlineEnumFromRef(field.type, enums, enumNames);
|
|
1660
1926
|
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Collect inline enum definitions from operation parameters (query, path, header)
|
|
1929
|
+
* and promote them to top-level enums so emitters can generate typed enum files.
|
|
1930
|
+
*/
|
|
1931
|
+
function collectInlineEnumsFromOperations(services, enums) {
|
|
1932
|
+
const enumNames = new Set(enums.map((e) => e.name));
|
|
1933
|
+
for (const service of services) for (const op of service.operations) for (const param of [
|
|
1934
|
+
...op.pathParams,
|
|
1935
|
+
...op.queryParams,
|
|
1936
|
+
...op.headerParams,
|
|
1937
|
+
...op.cookieParams ?? []
|
|
1938
|
+
]) collectInlineEnumFromRef(param.type, enums, enumNames);
|
|
1939
|
+
}
|
|
1661
1940
|
//#endregion
|
|
1662
1941
|
//#region src/parser/normalize-model-refs.ts
|
|
1663
1942
|
function validateModelRefs(spec) {
|
|
@@ -1687,10 +1966,12 @@ async function parseSpec(specPath, options) {
|
|
|
1687
1966
|
const spec = parsed;
|
|
1688
1967
|
const version = spec.openapi ?? "";
|
|
1689
1968
|
if (!version.startsWith("3.")) throw new SpecParseError(`Unsupported OpenAPI version: ${version}. oagen requires OpenAPI 3.x`, `Update the spec to OpenAPI 3.0 or 3.1. If you are using Swagger 2.x, convert it first with \`npx swagger2openapi ${specPath}\`.`);
|
|
1690
|
-
const { models, enums } = extractSchemas(spec.components?.schemas);
|
|
1969
|
+
const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
|
|
1691
1970
|
const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform);
|
|
1971
|
+
clearSchemaNameTransform();
|
|
1692
1972
|
const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(mergeInlineResponseModels(models, inlineModels), extractInlineModelsFromSchemas(spec.components?.schemas)), services);
|
|
1693
1973
|
collectInlineEnumsFromModels(finalModels, enums);
|
|
1974
|
+
collectInlineEnumsFromOperations(services, enums);
|
|
1694
1975
|
const auth = extractAuthSchemes(spec.components?.securitySchemes);
|
|
1695
1976
|
const serverEntries = (spec.servers ?? []).map((s) => ({
|
|
1696
1977
|
url: s.url ?? "",
|
|
@@ -1705,7 +1986,8 @@ async function parseSpec(specPath, options) {
|
|
|
1705
1986
|
services,
|
|
1706
1987
|
models: finalModels,
|
|
1707
1988
|
enums,
|
|
1708
|
-
auth
|
|
1989
|
+
auth,
|
|
1990
|
+
sdk: defaultSdkBehavior()
|
|
1709
1991
|
};
|
|
1710
1992
|
validateModelRefs(result);
|
|
1711
1993
|
return result;
|