@workos/oagen 0.3.0 → 0.5.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 -1
- package/dist/cli/index.mjs +955 -90
- 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-CSy3g-Bq.mjs} +541 -43
- package/dist/diff-CSy3g-Bq.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 +111 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +340 -30
- package/dist/index.mjs.map +1 -1
- package/dist/scripts/smoke/baseline.mjs +317 -33
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +140 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +317 -33
- 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-D84LjrVG.d.mts} +167 -2
- package/dist/types-D84LjrVG.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/agents/architecture.md +1 -1
- package/docs/architecture/emitter-contract.md +1 -2
- package/docs/architecture/ir-types.md +73 -1
- package/docs/architecture/pipeline.md +0 -1
- package/docs/cli.md +29 -8
- package/docs/core/minimal-emitter.md +0 -4
- package/docs/core/quickstart.md +0 -1
- package/examples/reference-emitter/README.md +0 -1
- package/examples/reference-emitter/src/emitter.ts +0 -5
- 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
- package/examples/reference-emitter/src/generators/config.ts +0 -19
package/dist/cli/index.mjs
CHANGED
|
@@ -6,11 +6,125 @@ import { readFile } from "node:fs/promises";
|
|
|
6
6
|
import * as path$1 from "node:path";
|
|
7
7
|
import path, { relative, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
-
import { BaseResolver, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
|
|
9
|
+
import { BaseResolver, Oas3_1Types, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
|
|
10
10
|
import Parser from "tree-sitter";
|
|
11
11
|
import { execFile, execFileSync } from "node:child_process";
|
|
12
12
|
import { promisify } from "node:util";
|
|
13
13
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
14
|
+
//#region src/ir/sdk-behavior.ts
|
|
15
|
+
/**
|
|
16
|
+
* Canonical SDK behavior defaults. These match the PHP emitter's current
|
|
17
|
+
* implementation (the most complete). Per-language overrides can be applied
|
|
18
|
+
* via `mergeSdkBehavior()` in each SDK's `oagen.config.ts`.
|
|
19
|
+
*
|
|
20
|
+
* - Retry: exponential backoff with jitter, retries on 429/5xx
|
|
21
|
+
* - Errors: standard HTTP status → exception kind mapping
|
|
22
|
+
* - Telemetry: enabled by default, tracks request ID + latency
|
|
23
|
+
* - Pagination: no inter-page delay (Node overrides to 350ms)
|
|
24
|
+
* - Idempotency: auto-generate UUID v4 for retryable POSTs
|
|
25
|
+
* - Logging: all lifecycle events enabled
|
|
26
|
+
* - User-Agent: SDK identifier + runtime version + app info + AI agent detection
|
|
27
|
+
* - Request guards: detect misplaced RequestOptions keys in params
|
|
28
|
+
* - Timeout: 60s default (Python overrides to 30s)
|
|
29
|
+
*/
|
|
30
|
+
function defaultSdkBehavior() {
|
|
31
|
+
return {
|
|
32
|
+
retry: {
|
|
33
|
+
retryableStatusCodes: [
|
|
34
|
+
429,
|
|
35
|
+
500,
|
|
36
|
+
502,
|
|
37
|
+
503,
|
|
38
|
+
504
|
|
39
|
+
],
|
|
40
|
+
maxRetries: 3,
|
|
41
|
+
retryOnConnectionError: true,
|
|
42
|
+
retryOnTimeout: true,
|
|
43
|
+
backoff: {
|
|
44
|
+
initialDelay: 1,
|
|
45
|
+
multiplier: 2,
|
|
46
|
+
maxDelay: 30,
|
|
47
|
+
jitterFactor: .5
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
errors: {
|
|
51
|
+
statusCodeMap: {
|
|
52
|
+
400: "BadRequest",
|
|
53
|
+
401: "Authentication",
|
|
54
|
+
403: "Authorization",
|
|
55
|
+
404: "NotFound",
|
|
56
|
+
409: "Conflict",
|
|
57
|
+
422: "UnprocessableEntity",
|
|
58
|
+
429: "RateLimitExceeded"
|
|
59
|
+
},
|
|
60
|
+
serverErrorKind: "Server",
|
|
61
|
+
clientErrorKind: "Api",
|
|
62
|
+
errorDocUrlTemplate: "https://workos.com/docs/errors/{code}"
|
|
63
|
+
},
|
|
64
|
+
telemetry: {
|
|
65
|
+
enabledByDefault: true,
|
|
66
|
+
headerName: "X-WorkOS-Client-Telemetry",
|
|
67
|
+
requestIdHeader: "X-Request-ID"
|
|
68
|
+
},
|
|
69
|
+
pagination: { autoPageDelayMs: 0 },
|
|
70
|
+
idempotency: {
|
|
71
|
+
headerName: "Idempotency-Key",
|
|
72
|
+
autoGenerateForPost: true
|
|
73
|
+
},
|
|
74
|
+
logging: {
|
|
75
|
+
enabled: true,
|
|
76
|
+
events: [
|
|
77
|
+
"request.start",
|
|
78
|
+
"request.success",
|
|
79
|
+
"request.retry",
|
|
80
|
+
"request.rate_limited",
|
|
81
|
+
"request.error",
|
|
82
|
+
"request.connection_error"
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
userAgent: {
|
|
86
|
+
sdkIdentifierTemplate: "{name} {lang}/{version}",
|
|
87
|
+
includeRuntimeVersion: true,
|
|
88
|
+
allowAppInfo: true,
|
|
89
|
+
aiAgentEnvVars: [
|
|
90
|
+
{
|
|
91
|
+
envVar: "CLAUDE_CODE",
|
|
92
|
+
agentName: "ClaudeCode"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
envVar: "CURSOR_AGENT",
|
|
96
|
+
agentName: "Cursor"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
envVar: "CLINE_ACTIVE",
|
|
100
|
+
agentName: "Cline"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
envVar: "WINDSURF_ACTIVE",
|
|
104
|
+
agentName: "Windsurf"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
envVar: "COPILOT_AGENT",
|
|
108
|
+
agentName: "Copilot"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
requestGuard: { optionKeys: [
|
|
113
|
+
"api_key",
|
|
114
|
+
"apiKey",
|
|
115
|
+
"idempotency_key",
|
|
116
|
+
"idempotencyKey",
|
|
117
|
+
"extra_headers",
|
|
118
|
+
"extraHeaders",
|
|
119
|
+
"max_retries",
|
|
120
|
+
"maxRetries",
|
|
121
|
+
"base_url",
|
|
122
|
+
"baseUrl"
|
|
123
|
+
] },
|
|
124
|
+
timeout: { defaultTimeoutSeconds: 60 }
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
14
128
|
//#region src/errors.ts
|
|
15
129
|
/**
|
|
16
130
|
* Structured error hierarchy for oagen.
|
|
@@ -69,13 +183,11 @@ var RegistryError = class extends OagenError {
|
|
|
69
183
|
//#region src/parser/refs.ts
|
|
70
184
|
async function loadAndBundleSpec(specPath) {
|
|
71
185
|
const absolutePath = resolve(specPath);
|
|
72
|
-
const document = makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href);
|
|
73
|
-
const config = await createConfig({});
|
|
74
|
-
const resolver = new BaseResolver();
|
|
75
186
|
const result = await bundleDocument({
|
|
76
|
-
document,
|
|
77
|
-
config:
|
|
78
|
-
|
|
187
|
+
document: makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href),
|
|
188
|
+
config: await createConfig({}),
|
|
189
|
+
types: Oas3_1Types,
|
|
190
|
+
externalRefResolver: new BaseResolver(),
|
|
79
191
|
dereference: false
|
|
80
192
|
});
|
|
81
193
|
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.`);
|
|
@@ -224,11 +336,7 @@ function toSnakeCase(s) {
|
|
|
224
336
|
function toUpperSnakeCase(s) {
|
|
225
337
|
return splitWords(s).map((w) => w.toUpperCase()).join("_");
|
|
226
338
|
}
|
|
227
|
-
const BACKEND_SUFFIXES = [
|
|
228
|
-
"Dto",
|
|
229
|
-
"DTO",
|
|
230
|
-
"Controller"
|
|
231
|
-
];
|
|
339
|
+
const BACKEND_SUFFIXES = ["Controller"];
|
|
232
340
|
function stripBackendSuffixes(name) {
|
|
233
341
|
for (const suffix of BACKEND_SUFFIXES) if (name.endsWith(suffix) && name.length > suffix.length) return name.slice(0, -suffix.length);
|
|
234
342
|
return name;
|
|
@@ -255,7 +363,9 @@ const SINGULAR_SAFE_LIST = new Set([
|
|
|
255
363
|
"Canvas",
|
|
256
364
|
"Alias",
|
|
257
365
|
"Basis",
|
|
258
|
-
"Bus"
|
|
366
|
+
"Bus",
|
|
367
|
+
"Species",
|
|
368
|
+
"Series"
|
|
259
369
|
]);
|
|
260
370
|
/**
|
|
261
371
|
* Conservative singularize for the leading PascalCase word.
|
|
@@ -264,7 +374,7 @@ const SINGULAR_SAFE_LIST = new Set([
|
|
|
264
374
|
* - trailing `s` for words >4 chars (e.g. Organizations → Organization)
|
|
265
375
|
* Safe-listed words are never singularized.
|
|
266
376
|
*/
|
|
267
|
-
function singularize(word) {
|
|
377
|
+
function singularize$1(word) {
|
|
268
378
|
if (SINGULAR_SAFE_LIST.has(word)) return word;
|
|
269
379
|
if (word.endsWith("ies") && word.length > 4) return word.slice(0, -3) + "y";
|
|
270
380
|
if (word.endsWith("ses") && word.length > 4) return word.slice(0, -2);
|
|
@@ -290,7 +400,7 @@ function cleanSchemaName(name) {
|
|
|
290
400
|
const match = result.match(/^([A-Z][a-z]*)/);
|
|
291
401
|
if (match) {
|
|
292
402
|
const leadWord = match[1];
|
|
293
|
-
const singular = singularize(leadWord);
|
|
403
|
+
const singular = singularize$1(leadWord);
|
|
294
404
|
if (singular !== leadWord) result = singular + result.slice(leadWord.length);
|
|
295
405
|
}
|
|
296
406
|
return result;
|
|
@@ -310,7 +420,7 @@ function qualifyInlineModelName(baseName, parentName) {
|
|
|
310
420
|
const trailingMatch = baseName.match(/^(.*?)([A-Z][a-z]*)$/);
|
|
311
421
|
if (trailingMatch) {
|
|
312
422
|
const [, prefix, trailingWord] = trailingMatch;
|
|
313
|
-
return `${cleanParent}${prefix}${singularize(trailingWord)}`;
|
|
423
|
+
return `${cleanParent}${prefix}${singularize$1(trailingWord)}`;
|
|
314
424
|
}
|
|
315
425
|
return `${cleanParent}${baseName}`;
|
|
316
426
|
}
|
|
@@ -371,15 +481,39 @@ function buildInlineModel(name, schema) {
|
|
|
371
481
|
}
|
|
372
482
|
//#endregion
|
|
373
483
|
//#region src/parser/schemas.ts
|
|
374
|
-
|
|
484
|
+
/**
|
|
485
|
+
* Module-level transform set during extractSchemas(). Used by schemaToTypeRef()
|
|
486
|
+
* to apply the same name transform to $ref model/enum references.
|
|
487
|
+
*/
|
|
488
|
+
let activeSchemaNameTransform = null;
|
|
489
|
+
/** Apply cleanSchemaName + the active transform (if any) to a raw schema name. */
|
|
490
|
+
function resolveSchemaName(rawName) {
|
|
491
|
+
let name = cleanSchemaName(toPascalCase(rawName));
|
|
492
|
+
if (activeSchemaNameTransform) name = activeSchemaNameTransform(name);
|
|
493
|
+
return name;
|
|
494
|
+
}
|
|
495
|
+
function extractSchemas(schemas, options) {
|
|
375
496
|
const enums = [];
|
|
376
497
|
if (!schemas) return {
|
|
377
498
|
models: [],
|
|
378
499
|
enums
|
|
379
500
|
};
|
|
501
|
+
if (options?.schemaNameTransform) {
|
|
502
|
+
const transform = options.schemaNameTransform;
|
|
503
|
+
const cleanedNames = Object.keys(schemas).map((n) => cleanSchemaName(toPascalCase(n)));
|
|
504
|
+
const transformedToOriginals = /* @__PURE__ */ new Map();
|
|
505
|
+
for (const cleaned of cleanedNames) {
|
|
506
|
+
const transformed = transform(cleaned);
|
|
507
|
+
if (!transformedToOriginals.has(transformed)) transformedToOriginals.set(transformed, []);
|
|
508
|
+
transformedToOriginals.get(transformed).push(cleaned);
|
|
509
|
+
}
|
|
510
|
+
const unsafeToTransform = /* @__PURE__ */ new Set();
|
|
511
|
+
for (const [, originals] of transformedToOriginals) if (originals.length > 1) for (const n of originals) unsafeToTransform.add(n);
|
|
512
|
+
activeSchemaNameTransform = (name) => unsafeToTransform.has(name) ? name : transform(name);
|
|
513
|
+
} else activeSchemaNameTransform = null;
|
|
380
514
|
const modelsByCleanName = /* @__PURE__ */ new Map();
|
|
381
515
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
382
|
-
const pascalName =
|
|
516
|
+
const pascalName = resolveSchemaName(name);
|
|
383
517
|
if (schema.enum) enums.push(extractEnum(pascalName, schema));
|
|
384
518
|
else {
|
|
385
519
|
const model = extractModel(pascalName, schema, schemas);
|
|
@@ -387,6 +521,12 @@ function extractSchemas(schemas) {
|
|
|
387
521
|
if (existing) {
|
|
388
522
|
if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
|
|
389
523
|
} else modelsByCleanName.set(pascalName, model);
|
|
524
|
+
for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName)) {
|
|
525
|
+
const existingInline = modelsByCleanName.get(inlineModel.name);
|
|
526
|
+
if (existingInline) {
|
|
527
|
+
if (inlineModel.fields.length > existingInline.fields.length) modelsByCleanName.set(inlineModel.name, inlineModel);
|
|
528
|
+
} else modelsByCleanName.set(inlineModel.name, inlineModel);
|
|
529
|
+
}
|
|
390
530
|
}
|
|
391
531
|
}
|
|
392
532
|
const models = [...modelsByCleanName.values()];
|
|
@@ -402,6 +542,10 @@ function extractSchemas(schemas) {
|
|
|
402
542
|
enums
|
|
403
543
|
};
|
|
404
544
|
}
|
|
545
|
+
/** Clear the module-level schemaNameTransform. Called by parseSpec() after all extraction phases. */
|
|
546
|
+
function clearSchemaNameTransform() {
|
|
547
|
+
activeSchemaNameTransform = null;
|
|
548
|
+
}
|
|
405
549
|
/**
|
|
406
550
|
* Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
|
|
407
551
|
* Shared by both schemas.ts (field-level) and parse.ts (model-level) collection passes.
|
|
@@ -438,7 +582,7 @@ function collectInlineEnums(fields, enums) {
|
|
|
438
582
|
function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
|
|
439
583
|
walkTypeRef(ref, { model: (modelRef) => {
|
|
440
584
|
if (modelNames.has(modelRef.name)) return;
|
|
441
|
-
if (Object.keys(schemas).some((k) =>
|
|
585
|
+
if (Object.keys(schemas).some((k) => resolveSchemaName(k) === modelRef.name)) return;
|
|
442
586
|
const parentSchema = findSchemaByName(parentModelName, schemas);
|
|
443
587
|
if (!parentSchema) return;
|
|
444
588
|
const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
|
|
@@ -453,7 +597,7 @@ function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, mod
|
|
|
453
597
|
}
|
|
454
598
|
/** Find a component schema by PascalCase name. */
|
|
455
599
|
function findSchemaByName(pascalName, schemas) {
|
|
456
|
-
for (const [k, v] of Object.entries(schemas)) if (
|
|
600
|
+
for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
|
|
457
601
|
return null;
|
|
458
602
|
}
|
|
459
603
|
/** Walk into a schema to find a field's sub-schema, resolving allOf. */
|
|
@@ -611,13 +755,88 @@ function extractAllOfModel(name, schema, schemas) {
|
|
|
611
755
|
fields
|
|
612
756
|
};
|
|
613
757
|
}
|
|
758
|
+
function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
|
|
759
|
+
const models = [];
|
|
760
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
761
|
+
for (const subSchema of schema.allOf ?? []) for (const variant of subSchema.oneOf ?? []) {
|
|
762
|
+
if (!variant.properties || variant.type !== void 0 && variant.type !== "object") continue;
|
|
763
|
+
const variantName = deriveDiscriminatedVariantName(variant, fallbackName);
|
|
764
|
+
for (const model of extractInlineModelDeep(variantName, variant)) {
|
|
765
|
+
if (seenNames.has(model.name)) continue;
|
|
766
|
+
seenNames.add(model.name);
|
|
767
|
+
models.push(model);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return models;
|
|
771
|
+
}
|
|
772
|
+
function deriveDiscriminatedVariantName(schema, fallbackName) {
|
|
773
|
+
if (!schema.properties) return fallbackName;
|
|
774
|
+
for (const name of [
|
|
775
|
+
"event",
|
|
776
|
+
"type",
|
|
777
|
+
"object"
|
|
778
|
+
]) {
|
|
779
|
+
const field = schema.properties[name];
|
|
780
|
+
if (!field) continue;
|
|
781
|
+
if (typeof field.const === "string") return toPascalCase(field.const);
|
|
782
|
+
if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return toPascalCase(field.enum[0]);
|
|
783
|
+
}
|
|
784
|
+
for (const field of Object.values(schema.properties)) {
|
|
785
|
+
if (!field) continue;
|
|
786
|
+
if (typeof field.const === "string") return toPascalCase(field.const);
|
|
787
|
+
if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return toPascalCase(field.enum[0]);
|
|
788
|
+
}
|
|
789
|
+
return fallbackName;
|
|
790
|
+
}
|
|
791
|
+
function extractInlineModelDeep(name, schema) {
|
|
792
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
793
|
+
const fields = [];
|
|
794
|
+
const properties = schema.properties ?? {};
|
|
795
|
+
const nestedModels = [];
|
|
796
|
+
for (const [fieldName, fieldSchema] of Object.entries(properties)) {
|
|
797
|
+
if (!fieldSchema) continue;
|
|
798
|
+
fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
|
|
799
|
+
if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema));
|
|
800
|
+
if (fieldSchema.type === "array" && fieldSchema.items?.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema.items));
|
|
801
|
+
if (fieldSchema.allOf) {
|
|
802
|
+
const mergedProperties = {};
|
|
803
|
+
const mergedRequired = [];
|
|
804
|
+
for (const sub of fieldSchema.allOf) {
|
|
805
|
+
if (sub.properties) Object.assign(mergedProperties, sub.properties);
|
|
806
|
+
if (sub.required) mergedRequired.push(...sub.required);
|
|
807
|
+
}
|
|
808
|
+
if (Object.keys(mergedProperties).length > 0) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), {
|
|
809
|
+
type: "object",
|
|
810
|
+
properties: mergedProperties,
|
|
811
|
+
required: mergedRequired
|
|
812
|
+
}));
|
|
813
|
+
}
|
|
814
|
+
if (fieldSchema.oneOf) {
|
|
815
|
+
const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
|
|
816
|
+
if (objectVariant) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), objectVariant));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return [{
|
|
820
|
+
name,
|
|
821
|
+
description: schema.description,
|
|
822
|
+
fields
|
|
823
|
+
}, ...nestedModels];
|
|
824
|
+
}
|
|
825
|
+
function qualifyNestedInlineName(parentName, fieldName) {
|
|
826
|
+
const pascalField = toPascalCase(fieldName);
|
|
827
|
+
if (pascalField.startsWith(parentName)) return pascalField;
|
|
828
|
+
const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
|
|
829
|
+
const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
|
|
830
|
+
if (match && match[2]) return match[1] + singularize$1(match[2]);
|
|
831
|
+
return qualified;
|
|
832
|
+
}
|
|
614
833
|
function schemaToTypeRef(schema, contextName, parentModelName) {
|
|
615
834
|
if (schema.$ref) {
|
|
616
835
|
const segments = schema.$ref.split("/");
|
|
617
836
|
const rawName = segments[segments.length - 1];
|
|
618
837
|
return {
|
|
619
838
|
kind: "model",
|
|
620
|
-
name:
|
|
839
|
+
name: resolveSchemaName(rawName)
|
|
621
840
|
};
|
|
622
841
|
}
|
|
623
842
|
if (Array.isArray(schema.type)) {
|
|
@@ -881,6 +1100,7 @@ const KNOWN_DATA_PATHS = new Set([
|
|
|
881
1100
|
"edges"
|
|
882
1101
|
]);
|
|
883
1102
|
const PAGINATION_METADATA_PATTERNS = [
|
|
1103
|
+
"meta",
|
|
884
1104
|
"metadata",
|
|
885
1105
|
"pagination",
|
|
886
1106
|
"cursor",
|
|
@@ -912,11 +1132,12 @@ function detectListEnvelope(schema) {
|
|
|
912
1132
|
}
|
|
913
1133
|
if (arrayProps.length === 1 && nonArrayKeys.length >= 1) {
|
|
914
1134
|
const arrayPropName = arrayProps[0];
|
|
915
|
-
|
|
1135
|
+
const hasPaginationCompanion = nonArrayKeys.some((key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern)));
|
|
1136
|
+
if (KNOWN_DATA_PATHS.has(arrayPropName) && hasPaginationCompanion) return {
|
|
916
1137
|
isEnvelope: true,
|
|
917
1138
|
dataPath: arrayPropName
|
|
918
1139
|
};
|
|
919
|
-
if (
|
|
1140
|
+
if (hasPaginationCompanion) return {
|
|
920
1141
|
isEnvelope: true,
|
|
921
1142
|
dataPath: arrayPropName
|
|
922
1143
|
};
|
|
@@ -1140,7 +1361,7 @@ function qualifyNestedName(parentName, fieldName) {
|
|
|
1140
1361
|
const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
|
|
1141
1362
|
const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
|
|
1142
1363
|
if (match && match[2]) {
|
|
1143
|
-
const trailing = singularize(match[2]);
|
|
1364
|
+
const trailing = singularize$1(match[2]);
|
|
1144
1365
|
return match[1] + trailing;
|
|
1145
1366
|
}
|
|
1146
1367
|
return qualified;
|
|
@@ -1220,13 +1441,33 @@ function extractOperations(paths, operationIdTransform) {
|
|
|
1220
1441
|
const op = pathItem[method];
|
|
1221
1442
|
if (!op) continue;
|
|
1222
1443
|
const serviceName = inferServiceName(path, op.tags?.[0]);
|
|
1223
|
-
const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform);
|
|
1444
|
+
const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName);
|
|
1224
1445
|
inlineModels.push(...opModels);
|
|
1225
1446
|
const ops = serviceMap.get(serviceName) ?? [];
|
|
1226
1447
|
ops.push(operation);
|
|
1227
1448
|
serviceMap.set(serviceName, ops);
|
|
1228
1449
|
}
|
|
1229
1450
|
}
|
|
1451
|
+
for (const [serviceName, operations] of serviceMap) {
|
|
1452
|
+
const pathGroups = /* @__PURE__ */ new Map();
|
|
1453
|
+
for (const op of operations) {
|
|
1454
|
+
const firstSeg = op.path.split("/").filter(Boolean)[0] ?? "";
|
|
1455
|
+
const groupKey = firstSeg.startsWith("{") ? serviceName : toPascalCase(firstSeg);
|
|
1456
|
+
const group = pathGroups.get(groupKey) ?? [];
|
|
1457
|
+
group.push(op);
|
|
1458
|
+
pathGroups.set(groupKey, group);
|
|
1459
|
+
}
|
|
1460
|
+
if (pathGroups.size > 1) {
|
|
1461
|
+
if ([...pathGroups.keys()].some((k) => k !== serviceName)) {
|
|
1462
|
+
serviceMap.delete(serviceName);
|
|
1463
|
+
for (const [groupName, groupOps] of pathGroups) {
|
|
1464
|
+
const existing = serviceMap.get(groupName) ?? [];
|
|
1465
|
+
existing.push(...groupOps);
|
|
1466
|
+
serviceMap.set(groupName, existing);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1230
1471
|
for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
|
|
1231
1472
|
return {
|
|
1232
1473
|
services: Array.from(serviceMap.entries()).map(([name, operations]) => ({
|
|
@@ -1342,13 +1583,14 @@ function inferDeeperContext(path) {
|
|
|
1342
1583
|
if (nonParams.length >= 3) return toPascalCase(nonParams.slice(1, -1).join("_"));
|
|
1343
1584
|
return null;
|
|
1344
1585
|
}
|
|
1345
|
-
function buildOperation(method, path, op, pathLevelParams, operationIdTransform) {
|
|
1586
|
+
function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName) {
|
|
1346
1587
|
const allParams = [...pathLevelParams, ...op.parameters ?? []];
|
|
1347
1588
|
const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
|
|
1348
|
-
const
|
|
1349
|
-
const
|
|
1350
|
-
const
|
|
1351
|
-
const
|
|
1589
|
+
const opContext = serviceName;
|
|
1590
|
+
const pathParams = extractParams(allParams, "path", opContext);
|
|
1591
|
+
const queryParams = extractParams(allParams, "query", opContext);
|
|
1592
|
+
const headerParams = extractParams(allParams, "header", opContext).filter((p) => p.name.toLowerCase() !== "idempotency-key");
|
|
1593
|
+
const cookieParams = extractParams(allParams, "cookie", opContext);
|
|
1352
1594
|
const reqBodyModels = [];
|
|
1353
1595
|
const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
|
|
1354
1596
|
const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method);
|
|
@@ -1365,6 +1607,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform)
|
|
|
1365
1607
|
itemType
|
|
1366
1608
|
};
|
|
1367
1609
|
} else if (paginationFromParams) pagination = paginationFromParams;
|
|
1610
|
+
const security = extractOperationSecurity(op.security);
|
|
1368
1611
|
return {
|
|
1369
1612
|
operation: {
|
|
1370
1613
|
name: inferOperationName(method, path, op.operationId, operationIdTransform),
|
|
@@ -1383,19 +1626,35 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform)
|
|
|
1383
1626
|
pagination,
|
|
1384
1627
|
injectIdempotencyKey: hasIdempotencyHeader,
|
|
1385
1628
|
deprecated: op.deprecated || void 0,
|
|
1386
|
-
async: op["x-oagen-async"]
|
|
1629
|
+
async: op["x-oagen-async"],
|
|
1630
|
+
security
|
|
1387
1631
|
},
|
|
1388
1632
|
inlineModels
|
|
1389
1633
|
};
|
|
1390
1634
|
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Extract per-operation security requirements from an OpenAPI security directive.
|
|
1637
|
+
* Returns undefined when the operation uses the spec-level default security.
|
|
1638
|
+
*
|
|
1639
|
+
* OpenAPI security format: `security: [{ schemeName: [scope1, scope2] }]`
|
|
1640
|
+
*/
|
|
1641
|
+
function extractOperationSecurity(security) {
|
|
1642
|
+
if (!security || security.length === 0) return void 0;
|
|
1643
|
+
const requirements = [];
|
|
1644
|
+
for (const entry of security) for (const [schemeName, scopes] of Object.entries(entry)) requirements.push({
|
|
1645
|
+
schemeName,
|
|
1646
|
+
scopes: scopes ?? []
|
|
1647
|
+
});
|
|
1648
|
+
return requirements.length > 0 ? requirements : void 0;
|
|
1649
|
+
}
|
|
1391
1650
|
function buildDescription(summary, description) {
|
|
1392
1651
|
if (summary && description && summary !== description) return `${summary}\n\n${description}`;
|
|
1393
1652
|
return description ?? summary;
|
|
1394
1653
|
}
|
|
1395
|
-
function extractParams(params, location) {
|
|
1654
|
+
function extractParams(params, location, operationContext) {
|
|
1396
1655
|
return params.filter((p) => p.in === location).map((p) => ({
|
|
1397
1656
|
name: p.name,
|
|
1398
|
-
type: p.schema ? schemaToTypeRef(p.schema, p.name) : {
|
|
1657
|
+
type: p.schema ? schemaToTypeRef(p.schema, p.name, operationContext ? toPascalCase(operationContext) : void 0) : {
|
|
1399
1658
|
kind: "primitive",
|
|
1400
1659
|
type: "string"
|
|
1401
1660
|
},
|
|
@@ -1403,7 +1662,9 @@ function extractParams(params, location) {
|
|
|
1403
1662
|
description: p.description,
|
|
1404
1663
|
deprecated: p.deprecated || p.schema?.deprecated || void 0,
|
|
1405
1664
|
default: p.schema?.default,
|
|
1406
|
-
example: p.example ?? p.schema?.example
|
|
1665
|
+
example: p.example ?? p.schema?.example,
|
|
1666
|
+
style: p.style,
|
|
1667
|
+
explode: p.explode
|
|
1407
1668
|
}));
|
|
1408
1669
|
}
|
|
1409
1670
|
function extractRequestBody(body, op, inlineModels) {
|
|
@@ -1581,7 +1842,14 @@ function extractResponses(responses, op, path, method) {
|
|
|
1581
1842
|
statusCode: code,
|
|
1582
1843
|
type: extractedType
|
|
1583
1844
|
});
|
|
1584
|
-
} else if (code >= 400) {
|
|
1845
|
+
} else if (code >= 300 && code < 400) allSuccessResponses.push({
|
|
1846
|
+
statusCode: code,
|
|
1847
|
+
type: {
|
|
1848
|
+
kind: "primitive",
|
|
1849
|
+
type: "unknown"
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
else if (code >= 400) {
|
|
1585
1853
|
const jsonContent = resp.content?.["application/json"];
|
|
1586
1854
|
const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
|
|
1587
1855
|
errors.push({
|
|
@@ -1708,6 +1976,19 @@ function collectInlineEnumsFromModels(models, enums) {
|
|
|
1708
1976
|
const enumNames = new Set(enums.map((e) => e.name));
|
|
1709
1977
|
for (const model of models) for (const field of model.fields) collectInlineEnumFromRef(field.type, enums, enumNames);
|
|
1710
1978
|
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Collect inline enum definitions from operation parameters (query, path, header)
|
|
1981
|
+
* and promote them to top-level enums so emitters can generate typed enum files.
|
|
1982
|
+
*/
|
|
1983
|
+
function collectInlineEnumsFromOperations(services, enums) {
|
|
1984
|
+
const enumNames = new Set(enums.map((e) => e.name));
|
|
1985
|
+
for (const service of services) for (const op of service.operations) for (const param of [
|
|
1986
|
+
...op.pathParams,
|
|
1987
|
+
...op.queryParams,
|
|
1988
|
+
...op.headerParams,
|
|
1989
|
+
...op.cookieParams ?? []
|
|
1990
|
+
]) collectInlineEnumFromRef(param.type, enums, enumNames);
|
|
1991
|
+
}
|
|
1711
1992
|
//#endregion
|
|
1712
1993
|
//#region src/parser/normalize-model-refs.ts
|
|
1713
1994
|
function validateModelRefs(spec) {
|
|
@@ -1737,10 +2018,12 @@ async function parseSpec(specPath, options) {
|
|
|
1737
2018
|
const spec = parsed;
|
|
1738
2019
|
const version = spec.openapi ?? "";
|
|
1739
2020
|
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}\`.`);
|
|
1740
|
-
const { models, enums } = extractSchemas(spec.components?.schemas);
|
|
2021
|
+
const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
|
|
1741
2022
|
const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform);
|
|
2023
|
+
clearSchemaNameTransform();
|
|
1742
2024
|
const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(mergeInlineResponseModels(models, inlineModels), extractInlineModelsFromSchemas(spec.components?.schemas)), services);
|
|
1743
2025
|
collectInlineEnumsFromModels(finalModels, enums);
|
|
2026
|
+
collectInlineEnumsFromOperations(services, enums);
|
|
1744
2027
|
const auth = extractAuthSchemes(spec.components?.securitySchemes);
|
|
1745
2028
|
const serverEntries = (spec.servers ?? []).map((s) => ({
|
|
1746
2029
|
url: s.url ?? "",
|
|
@@ -1755,7 +2038,8 @@ async function parseSpec(specPath, options) {
|
|
|
1755
2038
|
services,
|
|
1756
2039
|
models: finalModels,
|
|
1757
2040
|
enums,
|
|
1758
|
-
auth
|
|
2041
|
+
auth,
|
|
2042
|
+
sdk: defaultSdkBehavior()
|
|
1759
2043
|
};
|
|
1760
2044
|
validateModelRefs(result);
|
|
1761
2045
|
return result;
|
|
@@ -2691,8 +2975,9 @@ const phpMergeAdapter = {
|
|
|
2691
2975
|
testFilePatterns: [/Test\.php$/],
|
|
2692
2976
|
urlFingerprintConfig: PHP_URL_FINGERPRINT_CONFIG,
|
|
2693
2977
|
resolveGrammar: (mod) => {
|
|
2694
|
-
|
|
2695
|
-
return
|
|
2978
|
+
const m = mod?.default ?? mod;
|
|
2979
|
+
if (typeof m === "object" && m !== null && "php" in m) return m.php;
|
|
2980
|
+
return m;
|
|
2696
2981
|
},
|
|
2697
2982
|
parseStatements(tree, source) {
|
|
2698
2983
|
const imports = [];
|
|
@@ -3143,6 +3428,30 @@ function safeParse(parser, source) {
|
|
|
3143
3428
|
* Supports any language with a tree-sitter grammar package.
|
|
3144
3429
|
*/
|
|
3145
3430
|
const parserCache = /* @__PURE__ */ new Map();
|
|
3431
|
+
/**
|
|
3432
|
+
* When replacing an existing docstring with a generated one, carry over any
|
|
3433
|
+
* `@deprecated` lines from the existing doc that aren't present in the
|
|
3434
|
+
* generated doc. This prevents hand-written deprecation notices from being
|
|
3435
|
+
* silently dropped during docstring refresh.
|
|
3436
|
+
*/
|
|
3437
|
+
function preserveDeprecatedTags(existingDoc, generatedDoc) {
|
|
3438
|
+
if (!existingDoc.includes("@deprecated")) return generatedDoc;
|
|
3439
|
+
if (generatedDoc.includes("@deprecated")) return generatedDoc;
|
|
3440
|
+
const deprecatedParts = [];
|
|
3441
|
+
for (const line of existingDoc.split("\n")) if (/@deprecated\b/.test(line)) {
|
|
3442
|
+
const match = line.match(/@deprecated\b.*/);
|
|
3443
|
+
if (match) {
|
|
3444
|
+
const cleaned = match[0].replace(/\s*\*\/\s*$/, "");
|
|
3445
|
+
deprecatedParts.push(` * ${cleaned}`);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
if (deprecatedParts.length === 0) return generatedDoc;
|
|
3449
|
+
const closingIdx = generatedDoc.lastIndexOf("*/");
|
|
3450
|
+
if (closingIdx === -1) return generatedDoc;
|
|
3451
|
+
const before = generatedDoc.slice(0, closingIdx);
|
|
3452
|
+
const after = generatedDoc.slice(closingIdx);
|
|
3453
|
+
return before + deprecatedParts.join("\n") + "\n " + after;
|
|
3454
|
+
}
|
|
3146
3455
|
function findIgnoredRegions(source) {
|
|
3147
3456
|
const regions = [];
|
|
3148
3457
|
const startTag = "@oagen-ignore-start";
|
|
@@ -3198,6 +3507,35 @@ async function extractStatements(source, language) {
|
|
|
3198
3507
|
return adapter.parseStatements(tree, source);
|
|
3199
3508
|
}
|
|
3200
3509
|
/**
|
|
3510
|
+
* Return a map of top-level class name → { bodyEndLine, bodyEndCol } using
|
|
3511
|
+
* tree-sitter. `bodyEndLine` is 0-based. For brace-delimited classes the
|
|
3512
|
+
* end points at the closing brace line; for indentation-delimited classes
|
|
3513
|
+
* it points one past the last line of the class body.
|
|
3514
|
+
*/
|
|
3515
|
+
async function extractClassEndLines(source, language) {
|
|
3516
|
+
const tree = safeParse(await getParser(language), source);
|
|
3517
|
+
const map = /* @__PURE__ */ new Map();
|
|
3518
|
+
function processClassNode(node) {
|
|
3519
|
+
const nameNode = node.childForFieldName("name");
|
|
3520
|
+
if (!nameNode) return;
|
|
3521
|
+
const name = nameNode.text;
|
|
3522
|
+
const body = node.childForFieldName("body");
|
|
3523
|
+
const endRow = body ? body.endPosition.row : node.endPosition.row;
|
|
3524
|
+
if (source[body ? body.endIndex - 1 : node.endIndex - 1] === "}") map.set(name, { bodyEndLine: endRow });
|
|
3525
|
+
else map.set(name, { bodyEndLine: endRow + 1 });
|
|
3526
|
+
}
|
|
3527
|
+
for (const child of tree.rootNode.children) {
|
|
3528
|
+
if (child.type === "class_definition" || child.type === "class_declaration") processClassNode(child);
|
|
3529
|
+
if (child.type === "decorated_definition") {
|
|
3530
|
+
for (const inner of child.namedChildren) if (inner.type === "class_definition") processClassNode(inner);
|
|
3531
|
+
}
|
|
3532
|
+
if (child.type === "export_statement") {
|
|
3533
|
+
for (const inner of child.namedChildren) if (inner.type === "class_declaration") processClassNode(inner);
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
return map;
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3201
3539
|
* Merge generated content into an existing file.
|
|
3202
3540
|
*
|
|
3203
3541
|
* - Parses both files with tree-sitter
|
|
@@ -3227,10 +3565,21 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3227
3565
|
}
|
|
3228
3566
|
for (const anchor of existingStatements.importAnchors) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(anchor)).split("\n").length - 1 + anchor.split("\n").length - 1;
|
|
3229
3567
|
if (lastImportEndIndex === -1 && existingStatements.importInsertionAnchor) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(existingStatements.importInsertionAnchor)).split("\n").length - 1 + existingStatements.importInsertionAnchor.split("\n").length - 1;
|
|
3568
|
+
const existingExportedNames = /* @__PURE__ */ new Set();
|
|
3230
3569
|
for (const stmt of existingStatements.statements) {
|
|
3231
|
-
if (stmt.kind === "reexport")
|
|
3232
|
-
|
|
3233
|
-
|
|
3570
|
+
if (stmt.kind === "reexport") {
|
|
3571
|
+
existingReexports.add(adapter.normalizeReexport ? adapter.normalizeReexport(stmt.text.trim()) : stmt.text.trim());
|
|
3572
|
+
const braceMatch = stmt.text.match(/\{([^}]+)\}/);
|
|
3573
|
+
if (braceMatch) for (const part of braceMatch[1].split(",")) {
|
|
3574
|
+
const segments = part.trim().split(/\s+as\s+/);
|
|
3575
|
+
const exportedName = (segments[1] ?? segments[0]).trim();
|
|
3576
|
+
if (exportedName) existingExportedNames.add(exportedName);
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
if (stmt.key) {
|
|
3580
|
+
existingKeys.add(stmt.key);
|
|
3581
|
+
existingExportedNames.add(stmt.key);
|
|
3582
|
+
} else existingUnnamedTexts.add(stmt.text.trim());
|
|
3234
3583
|
}
|
|
3235
3584
|
const headerLine = header.trim();
|
|
3236
3585
|
const newImports = [];
|
|
@@ -3290,6 +3639,17 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3290
3639
|
preserved++;
|
|
3291
3640
|
continue;
|
|
3292
3641
|
}
|
|
3642
|
+
const braceMatch = stmt.text.match(/\{([^}]+)\}/);
|
|
3643
|
+
if (braceMatch) {
|
|
3644
|
+
const exportedNames = braceMatch[1].split(",").map((n) => {
|
|
3645
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
3646
|
+
return (parts[1] ?? parts[0]).trim();
|
|
3647
|
+
}).filter(Boolean);
|
|
3648
|
+
if (exportedNames.length > 0 && exportedNames.every((n) => existingExportedNames.has(n))) {
|
|
3649
|
+
preserved++;
|
|
3650
|
+
continue;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3293
3653
|
}
|
|
3294
3654
|
if (stmt.key && (existingKeys.has(stmt.key) || existingImportedNames.has(stmt.key))) {
|
|
3295
3655
|
preserved++;
|
|
@@ -3391,10 +3751,11 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3391
3751
|
const genDoc = genInfo.docstring && genInfo.docstring.text.trim() !== headerLine ? genInfo.docstring : null;
|
|
3392
3752
|
if (genDoc) if (existInfo.docstring) {
|
|
3393
3753
|
if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
|
|
3754
|
+
const newText = preserveDeprecatedTags(existInfo.docstring.text, genDoc.text);
|
|
3394
3755
|
edits.push({
|
|
3395
3756
|
start: existInfo.docstring.startIndex,
|
|
3396
3757
|
end: existInfo.docstring.endIndex,
|
|
3397
|
-
newText
|
|
3758
|
+
newText
|
|
3398
3759
|
});
|
|
3399
3760
|
docstringUpdates++;
|
|
3400
3761
|
}
|
|
@@ -3411,14 +3772,16 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3411
3772
|
const matchedExistMembers = /* @__PURE__ */ new Set();
|
|
3412
3773
|
for (const [memberName, genMember] of genInfo.members) {
|
|
3413
3774
|
const existMember = existInfo.members.get(memberName);
|
|
3414
|
-
if (!existMember
|
|
3775
|
+
if (!existMember) continue;
|
|
3415
3776
|
matchedExistMembers.add(memberName);
|
|
3777
|
+
if (!genMember.docstring) continue;
|
|
3416
3778
|
if (existMember.docstring) {
|
|
3417
3779
|
if (!existMember.docstring.text.includes("@oagen-ignore") && existMember.docstring.text !== genMember.docstring.text) {
|
|
3780
|
+
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
3418
3781
|
edits.push({
|
|
3419
3782
|
start: existMember.docstring.startIndex,
|
|
3420
3783
|
end: existMember.docstring.endIndex,
|
|
3421
|
-
newText
|
|
3784
|
+
newText
|
|
3422
3785
|
});
|
|
3423
3786
|
docstringUpdates++;
|
|
3424
3787
|
}
|
|
@@ -3433,34 +3796,59 @@ async function mergeIntoExisting(existingContent, generatedContent, language, he
|
|
|
3433
3796
|
docstringUpdates++;
|
|
3434
3797
|
}
|
|
3435
3798
|
}
|
|
3436
|
-
|
|
3799
|
+
const unmatchedGenByFp = /* @__PURE__ */ new Map();
|
|
3800
|
+
for (const [genName, genMember] of genInfo.members) {
|
|
3801
|
+
if (matchedExistMembers.has(genName)) continue;
|
|
3437
3802
|
if (!genMember.docstring || !genMember.urlFingerprint) continue;
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3803
|
+
if (existInfo.members.has(genName)) continue;
|
|
3804
|
+
const bucket = unmatchedGenByFp.get(genMember.urlFingerprint) ?? [];
|
|
3805
|
+
bucket.push({
|
|
3806
|
+
name: genName,
|
|
3807
|
+
member: genMember
|
|
3808
|
+
});
|
|
3809
|
+
unmatchedGenByFp.set(genMember.urlFingerprint, bucket);
|
|
3810
|
+
}
|
|
3811
|
+
const unmatchedExistByFp = /* @__PURE__ */ new Map();
|
|
3812
|
+
for (const [existName, existMember] of existInfo.members) {
|
|
3813
|
+
if (matchedExistMembers.has(existName)) continue;
|
|
3814
|
+
if (!existMember.urlFingerprint) continue;
|
|
3815
|
+
const bucket = unmatchedExistByFp.get(existMember.urlFingerprint) ?? [];
|
|
3816
|
+
bucket.push({
|
|
3817
|
+
name: existName,
|
|
3818
|
+
member: existMember
|
|
3819
|
+
});
|
|
3820
|
+
unmatchedExistByFp.set(existMember.urlFingerprint, bucket);
|
|
3821
|
+
}
|
|
3822
|
+
for (const [fp, genBucket] of unmatchedGenByFp) {
|
|
3823
|
+
if (genBucket.length !== 1) continue;
|
|
3824
|
+
const existBucket = unmatchedExistByFp.get(fp);
|
|
3825
|
+
if (!existBucket || existBucket.length !== 1) continue;
|
|
3826
|
+
const genMember = genBucket[0].member;
|
|
3827
|
+
const existEntry = existBucket[0];
|
|
3828
|
+
const existMember = existEntry.member;
|
|
3829
|
+
if (!genMember.docstring) continue;
|
|
3830
|
+
if (existMember.docstring) {
|
|
3831
|
+
if (existMember.docstring.text.includes("@oagen-ignore")) continue;
|
|
3832
|
+
if (existMember.docstring.text !== genMember.docstring.text) {
|
|
3833
|
+
const newText = preserveDeprecatedTags(existMember.docstring.text, genMember.docstring.text);
|
|
3454
3834
|
edits.push({
|
|
3455
|
-
start:
|
|
3456
|
-
end:
|
|
3457
|
-
newText
|
|
3835
|
+
start: existMember.docstring.startIndex,
|
|
3836
|
+
end: existMember.docstring.endIndex,
|
|
3837
|
+
newText
|
|
3458
3838
|
});
|
|
3459
3839
|
docstringUpdates++;
|
|
3460
3840
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
3841
|
+
} else {
|
|
3842
|
+
const lineStart = existMember.declStartIndex - existMember.declColumn;
|
|
3843
|
+
const indent = " ".repeat(existMember.declColumn);
|
|
3844
|
+
edits.push({
|
|
3845
|
+
start: lineStart,
|
|
3846
|
+
end: lineStart,
|
|
3847
|
+
newText: indent + genMember.docstring.text + "\n"
|
|
3848
|
+
});
|
|
3849
|
+
docstringUpdates++;
|
|
3463
3850
|
}
|
|
3851
|
+
matchedExistMembers.add(existEntry.name);
|
|
3464
3852
|
}
|
|
3465
3853
|
}
|
|
3466
3854
|
if (edits.length > 0) {
|
|
@@ -3546,15 +3934,18 @@ async function writeFiles(files, outputDir, options) {
|
|
|
3546
3934
|
}
|
|
3547
3935
|
const adapter = language ? getMergeAdapter(language) : void 0;
|
|
3548
3936
|
if (adapter?.testFilePatterns ? adapter.testFilePatterns.some((p) => p.test(file.path)) : isDefaultTestFile(file.path)) {
|
|
3549
|
-
|
|
3550
|
-
|
|
3937
|
+
if (!(header && existingContent.startsWith(header))) {
|
|
3938
|
+
result.skipped.push(file.path);
|
|
3939
|
+
continue;
|
|
3940
|
+
}
|
|
3551
3941
|
}
|
|
3552
3942
|
if (existingContent === file.content) {
|
|
3553
3943
|
result.identical.push(file.path);
|
|
3554
3944
|
continue;
|
|
3555
3945
|
}
|
|
3556
3946
|
if (file.overwriteExisting) {
|
|
3557
|
-
|
|
3947
|
+
const content = existingContent.includes("@oagen-ignore-start") && language && hasGrammar(language) ? await overwriteWithPreservedRegions(existingContent, file.content, language) : file.content;
|
|
3948
|
+
await fs.writeFile(fullPath, content, "utf-8");
|
|
3558
3949
|
result.written.push(file.path);
|
|
3559
3950
|
continue;
|
|
3560
3951
|
}
|
|
@@ -3598,6 +3989,317 @@ async function writeFiles(files, outputDir, options) {
|
|
|
3598
3989
|
function isDefaultTestFile(filePath) {
|
|
3599
3990
|
return /\.(spec|test)\.[jt]sx?$/.test(filePath) || filePath.endsWith("_test.go") || filePath.endsWith("_test.rb") || filePath.endsWith("_spec.rb") || /(?:^|\/)test_.*\.py$/.test(filePath) || filePath.endsWith("_test.py") || filePath.endsWith("Test.php") || filePath.endsWith("_test.rs") || /(?:^|\/)tests\/.*\.rs$/.test(filePath);
|
|
3600
3991
|
}
|
|
3992
|
+
/**
|
|
3993
|
+
* Overwrite a file with generated content while preserving
|
|
3994
|
+
* `@oagen-ignore-start` / `@oagen-ignore-end` regions (and their
|
|
3995
|
+
* required imports) from the existing file.
|
|
3996
|
+
*
|
|
3997
|
+
* Uses tree-sitter via `extractClassEndLines` to find accurate class
|
|
3998
|
+
* boundaries so ignore blocks are inserted at the correct position even
|
|
3999
|
+
* when docstrings contain unindented text.
|
|
4000
|
+
*/
|
|
4001
|
+
async function overwriteWithPreservedRegions(existingContent, generatedContent, language) {
|
|
4002
|
+
const existingLines = existingContent.split("\n");
|
|
4003
|
+
const generatedLines = generatedContent.split("\n");
|
|
4004
|
+
const blocks = [];
|
|
4005
|
+
for (let i = 0; i < existingLines.length; i++) {
|
|
4006
|
+
if (!existingLines[i].includes("@oagen-ignore-start")) continue;
|
|
4007
|
+
const startIdx = i;
|
|
4008
|
+
while (i < existingLines.length && !existingLines[i].includes("@oagen-ignore-end")) i++;
|
|
4009
|
+
blocks.push({
|
|
4010
|
+
containingClass: findContainingClass(existingLines, startIdx),
|
|
4011
|
+
lines: existingLines.slice(startIdx, i + 1)
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
if (blocks.length === 0) return generatedContent;
|
|
4015
|
+
const result = [...generatedLines];
|
|
4016
|
+
spliceExtraImports(existingLines, result);
|
|
4017
|
+
const classEndLines = await extractClassEndLines(result.join("\n"), language);
|
|
4018
|
+
const blocksByClass = /* @__PURE__ */ new Map();
|
|
4019
|
+
for (const b of blocks) {
|
|
4020
|
+
if (!blocksByClass.has(b.containingClass)) blocksByClass.set(b.containingClass, []);
|
|
4021
|
+
blocksByClass.get(b.containingClass).push(b.lines);
|
|
4022
|
+
}
|
|
4023
|
+
const replacedClasses = /* @__PURE__ */ new Set();
|
|
4024
|
+
const handledBlockKeys = /* @__PURE__ */ new Set();
|
|
4025
|
+
const replacements = [];
|
|
4026
|
+
for (const [className, classBlocks] of blocksByClass) {
|
|
4027
|
+
let targetClass = className;
|
|
4028
|
+
if (!targetClass) for (const blockLines of classBlocks) {
|
|
4029
|
+
for (const l of blockLines) {
|
|
4030
|
+
const m = l.match(/^(?:export\s+)?class\s+(\w+)/);
|
|
4031
|
+
if (m && classEndLines.has(m[1])) {
|
|
4032
|
+
targetClass = m[1];
|
|
4033
|
+
break;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
if (targetClass) break;
|
|
4037
|
+
}
|
|
4038
|
+
if (!targetClass) continue;
|
|
4039
|
+
if (!classBlocks.some((blockLines) => blockLines.some((l) => {
|
|
4040
|
+
return l.match(/^(?:export\s+)?class\s+(\w+)/)?.[1] === targetClass;
|
|
4041
|
+
}))) continue;
|
|
4042
|
+
const info = classEndLines.get(targetClass);
|
|
4043
|
+
if (!info) continue;
|
|
4044
|
+
const startLine = findClassDeclarationStart(result, targetClass, info.bodyEndLine);
|
|
4045
|
+
if (startLine < 0) continue;
|
|
4046
|
+
const content = [];
|
|
4047
|
+
for (const block of classBlocks) {
|
|
4048
|
+
content.push("");
|
|
4049
|
+
content.push(...block);
|
|
4050
|
+
}
|
|
4051
|
+
replacements.push({
|
|
4052
|
+
startLine,
|
|
4053
|
+
endLine: info.bodyEndLine,
|
|
4054
|
+
content
|
|
4055
|
+
});
|
|
4056
|
+
replacedClasses.add(targetClass);
|
|
4057
|
+
handledBlockKeys.add(className);
|
|
4058
|
+
}
|
|
4059
|
+
replacements.sort((a, b) => b.startLine - a.startLine);
|
|
4060
|
+
for (const rep of replacements) result.splice(rep.startLine, rep.endLine - rep.startLine, ...rep.content);
|
|
4061
|
+
let effectiveClassEndLines = classEndLines;
|
|
4062
|
+
if (replacedClasses.size > 0) effectiveClassEndLines = await extractClassEndLines(result.join("\n"), language);
|
|
4063
|
+
const insertions = [];
|
|
4064
|
+
for (const [className, classBlocks] of blocksByClass) {
|
|
4065
|
+
if (handledBlockKeys.has(className) || replacedClasses.has(className)) continue;
|
|
4066
|
+
const info = className ? effectiveClassEndLines.get(className) : void 0;
|
|
4067
|
+
const insertLine = info ? info.bodyEndLine : result.length;
|
|
4068
|
+
const content = [];
|
|
4069
|
+
for (const block of classBlocks) {
|
|
4070
|
+
content.push("");
|
|
4071
|
+
content.push(...block);
|
|
4072
|
+
}
|
|
4073
|
+
insertions.push({
|
|
4074
|
+
line: insertLine,
|
|
4075
|
+
content
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
insertions.sort((a, b) => b.line - a.line);
|
|
4079
|
+
for (const ins of insertions) result.splice(ins.line, 0, ...ins.content);
|
|
4080
|
+
return result.join("\n");
|
|
4081
|
+
}
|
|
4082
|
+
/** Scan backwards from `lineIdx` to find the nearest class declaration name. */
|
|
4083
|
+
function findContainingClass(lines, lineIdx) {
|
|
4084
|
+
for (let j = lineIdx - 1; j >= 0; j--) {
|
|
4085
|
+
const m = lines[j].match(/^(?:export\s+)?class\s+(\w+)/);
|
|
4086
|
+
if (m) return m[1];
|
|
4087
|
+
}
|
|
4088
|
+
return "";
|
|
4089
|
+
}
|
|
4090
|
+
/**
|
|
4091
|
+
* Find the start line of a class declaration (including decorators above it)
|
|
4092
|
+
* by scanning backwards from `beforeLine`.
|
|
4093
|
+
*/
|
|
4094
|
+
function findClassDeclarationStart(lines, className, beforeLine) {
|
|
4095
|
+
const classPattern = new RegExp(`^(?:export\\s+)?class\\s+${className}\\b`);
|
|
4096
|
+
for (let i = beforeLine - 1; i >= 0; i--) {
|
|
4097
|
+
if (!classPattern.test(lines[i])) continue;
|
|
4098
|
+
let start = i;
|
|
4099
|
+
while (start > 0 && /^\s*@\w+/.test(lines[start - 1])) start--;
|
|
4100
|
+
return start;
|
|
4101
|
+
}
|
|
4102
|
+
return -1;
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Collect `from X import Y, Z` identifiers grouped by module, handling
|
|
4106
|
+
* both single-line and multiline (parenthesized) forms.
|
|
4107
|
+
*/
|
|
4108
|
+
function collectFromImports(lines) {
|
|
4109
|
+
const byModule = /* @__PURE__ */ new Map();
|
|
4110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4111
|
+
if (!/^from\s+/.test(lines[i])) continue;
|
|
4112
|
+
let fullText = lines[i];
|
|
4113
|
+
if (fullText.includes("(") && !fullText.includes(")")) while (i + 1 < lines.length) {
|
|
4114
|
+
i++;
|
|
4115
|
+
fullText += " " + lines[i].trim();
|
|
4116
|
+
if (lines[i].includes(")")) break;
|
|
4117
|
+
}
|
|
4118
|
+
const match = fullText.replace(/[()]/g, "").replace(/\s+/g, " ").trim().match(/^from\s+(\S+)\s+import\s+(.+)$/);
|
|
4119
|
+
if (!match) continue;
|
|
4120
|
+
const mod = match[1];
|
|
4121
|
+
if (!byModule.has(mod)) byModule.set(mod, /* @__PURE__ */ new Set());
|
|
4122
|
+
for (const id of match[2].split(",").map((s) => s.trim()).filter(Boolean)) byModule.get(mod).add(id);
|
|
4123
|
+
}
|
|
4124
|
+
return byModule;
|
|
4125
|
+
}
|
|
4126
|
+
/**
|
|
4127
|
+
* Add imports from the existing file that are absent from the generated
|
|
4128
|
+
* result. Handles both `import X` and `from X import Y` forms.
|
|
4129
|
+
* Mutates `resultLines` in place.
|
|
4130
|
+
*/
|
|
4131
|
+
function spliceExtraImports(existingLines, resultLines) {
|
|
4132
|
+
const isSimpleImport = (l) => /^import\s+\w/.test(l);
|
|
4133
|
+
const existingSimple = new Set(existingLines.filter(isSimpleImport).map((l) => l.trim()));
|
|
4134
|
+
const generatedSimple = new Set(resultLines.filter(isSimpleImport).map((l) => l.trim()));
|
|
4135
|
+
const extraSimple = [...existingSimple].filter((l) => !generatedSimple.has(l));
|
|
4136
|
+
const existingFrom = collectFromImports(existingLines);
|
|
4137
|
+
const generatedFrom = collectFromImports(resultLines);
|
|
4138
|
+
const allGeneratedIds = /* @__PURE__ */ new Set();
|
|
4139
|
+
for (const ids of generatedFrom.values()) for (const id of ids) allGeneratedIds.add(id);
|
|
4140
|
+
const extraFromLines = [];
|
|
4141
|
+
for (const [mod, existIds] of existingFrom) {
|
|
4142
|
+
const genIds = generatedFrom.get(mod);
|
|
4143
|
+
if (!genIds) {
|
|
4144
|
+
const extra = [...existIds].filter((id) => !allGeneratedIds.has(id));
|
|
4145
|
+
if (extra.length > 0) extraFromLines.push(`from ${mod} import ${extra.join(", ")}`);
|
|
4146
|
+
} else {
|
|
4147
|
+
const extra = [...existIds].filter((id) => !genIds.has(id) && !allGeneratedIds.has(id));
|
|
4148
|
+
if (extra.length > 0) extraFromLines.push(`from ${mod} import ${extra.join(", ")}`);
|
|
4149
|
+
}
|
|
4150
|
+
}
|
|
4151
|
+
const allExtra = [...extraSimple, ...extraFromLines];
|
|
4152
|
+
if (allExtra.length === 0) return;
|
|
4153
|
+
let lastImportIdx = -1;
|
|
4154
|
+
for (let i = 0; i < resultLines.length; i++) if (isSimpleImport(resultLines[i]) || /^from\s+/.test(resultLines[i])) {
|
|
4155
|
+
lastImportIdx = i;
|
|
4156
|
+
if (resultLines[i].includes("(") && !resultLines[i].includes(")")) while (i + 1 < resultLines.length) {
|
|
4157
|
+
i++;
|
|
4158
|
+
lastImportIdx = i;
|
|
4159
|
+
if (resultLines[i].includes(")")) break;
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
if (lastImportIdx >= 0) resultLines.splice(lastImportIdx + 1, 0, ...allExtra);
|
|
4163
|
+
}
|
|
4164
|
+
//#endregion
|
|
4165
|
+
//#region src/ir/operation-hints.ts
|
|
4166
|
+
/** Minimal snake_case converter for path segments (avoids cross-layer import). */
|
|
4167
|
+
function toSnake(s) {
|
|
4168
|
+
return s.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s.]+/g, "_").toLowerCase();
|
|
4169
|
+
}
|
|
4170
|
+
/**
|
|
4171
|
+
* Action verbs recognised at the terminal path segment.
|
|
4172
|
+
* When a path ends with one of these, we use it as the verb instead of
|
|
4173
|
+
* deriving a CRUD verb from the HTTP method.
|
|
4174
|
+
*/
|
|
4175
|
+
const ACTION_VERBS = new Set([
|
|
4176
|
+
"accept",
|
|
4177
|
+
"activate",
|
|
4178
|
+
"authorize",
|
|
4179
|
+
"challenge",
|
|
4180
|
+
"check",
|
|
4181
|
+
"complete",
|
|
4182
|
+
"confirm",
|
|
4183
|
+
"deactivate",
|
|
4184
|
+
"disable",
|
|
4185
|
+
"enable",
|
|
4186
|
+
"enroll",
|
|
4187
|
+
"generate",
|
|
4188
|
+
"reactivate",
|
|
4189
|
+
"resend",
|
|
4190
|
+
"revoke",
|
|
4191
|
+
"send",
|
|
4192
|
+
"verify"
|
|
4193
|
+
]);
|
|
4194
|
+
/** Map HTTP method → default CRUD verb. */
|
|
4195
|
+
const CRUD_VERB = {
|
|
4196
|
+
get: "list",
|
|
4197
|
+
post: "create",
|
|
4198
|
+
put: "update",
|
|
4199
|
+
patch: "update",
|
|
4200
|
+
delete: "delete",
|
|
4201
|
+
head: "get",
|
|
4202
|
+
options: "list",
|
|
4203
|
+
trace: "list"
|
|
4204
|
+
};
|
|
4205
|
+
/**
|
|
4206
|
+
* Naive singularization: strips trailing "s" with common-sense exceptions.
|
|
4207
|
+
* Good enough for API resource nouns; not a general-purpose English stemmer.
|
|
4208
|
+
*/
|
|
4209
|
+
function singularize(word) {
|
|
4210
|
+
if (word.length <= 2) return word;
|
|
4211
|
+
if (word.endsWith("ies")) return word.slice(0, -3) + "y";
|
|
4212
|
+
if (word.endsWith("ses") || word.endsWith("zes") || word.endsWith("xes") || word.endsWith("ches") || word.endsWith("shes")) return word.slice(0, -2);
|
|
4213
|
+
if (word.endsWith("ss") || word.endsWith("us") || word.endsWith("is")) return word;
|
|
4214
|
+
if (word.endsWith("s")) return word.slice(0, -1);
|
|
4215
|
+
return word;
|
|
4216
|
+
}
|
|
4217
|
+
/**
|
|
4218
|
+
* Derive a snake_case method name from an operation's HTTP method and path.
|
|
4219
|
+
*
|
|
4220
|
+
* Rules:
|
|
4221
|
+
* 1. Strip path-param segments (`{id}`, `{slug}`, etc.)
|
|
4222
|
+
* 2. If the terminal segment is an action verb, use it as the verb
|
|
4223
|
+
* 3. Otherwise, use the CRUD verb for the HTTP method
|
|
4224
|
+
* 4. For GET on a collection (no trailing {id}), use "list"; with {id}, use "get"
|
|
4225
|
+
* 5. For POST on a resource path, if only one path param, use "create" singular
|
|
4226
|
+
* 6. Singularize the resource noun for single-resource operations
|
|
4227
|
+
*/
|
|
4228
|
+
function deriveMethodName(op, _service) {
|
|
4229
|
+
const segments = op.path.split("/").filter(Boolean);
|
|
4230
|
+
const staticSegments = [];
|
|
4231
|
+
let trailingParam = false;
|
|
4232
|
+
for (const seg of segments) if (seg.startsWith("{") && seg.endsWith("}")) trailingParam = true;
|
|
4233
|
+
else {
|
|
4234
|
+
staticSegments.push(seg);
|
|
4235
|
+
trailingParam = false;
|
|
4236
|
+
}
|
|
4237
|
+
if (staticSegments.length === 0) return `${CRUD_VERB[op.httpMethod] ?? "get"}_root`;
|
|
4238
|
+
const terminal = toSnake(staticSegments[staticSegments.length - 1]);
|
|
4239
|
+
if (ACTION_VERBS.has(terminal)) {
|
|
4240
|
+
const context = staticSegments.slice(0, -1).filter((s) => !s.startsWith("{"));
|
|
4241
|
+
const resource = context.length > 0 ? toSnake(singularize(context[context.length - 1])) : "";
|
|
4242
|
+
if (resource) return `${terminal}_${resource}`;
|
|
4243
|
+
return terminal;
|
|
4244
|
+
}
|
|
4245
|
+
let verb = CRUD_VERB[op.httpMethod] ?? "get";
|
|
4246
|
+
if (op.httpMethod === "get" && trailingParam) verb = "get";
|
|
4247
|
+
let resource = toSnake(terminal);
|
|
4248
|
+
if (staticSegments.length >= 2) {
|
|
4249
|
+
const penultimate = staticSegments[staticSegments.length - 2];
|
|
4250
|
+
const penIdx = segments.lastIndexOf(penultimate);
|
|
4251
|
+
const termIdx = segments.lastIndexOf(staticSegments[staticSegments.length - 1]);
|
|
4252
|
+
if (segments.slice(penIdx + 1, termIdx).some((s) => s.startsWith("{"))) resource = `${toSnake(singularize(penultimate))}_${resource}`;
|
|
4253
|
+
}
|
|
4254
|
+
const noun = trailingParam ? singularize(resource) : resource;
|
|
4255
|
+
return `${verb}_${verb === "list" ? resource : noun}`;
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* Resolve all operations in the spec, applying hints and mount rules.
|
|
4259
|
+
*
|
|
4260
|
+
* @param spec - The parsed API spec
|
|
4261
|
+
* @param hints - Per-operation overrides keyed by "METHOD /path"
|
|
4262
|
+
* @param mountRules - Service name → target service mappings
|
|
4263
|
+
* @returns Resolved operations ready for emitter consumption
|
|
4264
|
+
*/
|
|
4265
|
+
function resolveOperations(spec, hints, mountRules) {
|
|
4266
|
+
const resolved = [];
|
|
4267
|
+
const hintMap = hints ?? {};
|
|
4268
|
+
const mounts = mountRules ?? {};
|
|
4269
|
+
for (const service of spec.services) for (const op of service.operations) {
|
|
4270
|
+
const hint = hintMap[`${op.httpMethod.toUpperCase()} ${op.path}`];
|
|
4271
|
+
const methodName = hint?.name ?? deriveMethodName(op, service);
|
|
4272
|
+
const mountOn = hint?.mountOn ?? mounts[service.name] ?? service.name;
|
|
4273
|
+
let wrappers;
|
|
4274
|
+
if (hint?.split) wrappers = hint.split.map((sh) => ({
|
|
4275
|
+
name: sh.name,
|
|
4276
|
+
targetVariant: sh.targetVariant,
|
|
4277
|
+
defaults: sh.defaults ?? {},
|
|
4278
|
+
inferFromClient: sh.inferFromClient ?? [],
|
|
4279
|
+
exposedParams: sh.exposedParams ?? [],
|
|
4280
|
+
optionalParams: [],
|
|
4281
|
+
responseModelName: resolveResponseModelName(op)
|
|
4282
|
+
}));
|
|
4283
|
+
resolved.push({
|
|
4284
|
+
operation: op,
|
|
4285
|
+
service,
|
|
4286
|
+
methodName,
|
|
4287
|
+
mountOn,
|
|
4288
|
+
wrappers,
|
|
4289
|
+
defaults: hint?.defaults ?? {},
|
|
4290
|
+
inferFromClient: hint?.inferFromClient ?? []
|
|
4291
|
+
});
|
|
4292
|
+
}
|
|
4293
|
+
return resolved;
|
|
4294
|
+
}
|
|
4295
|
+
/** Extract the response model name from an operation (for wrapper metadata). */
|
|
4296
|
+
function resolveResponseModelName(op) {
|
|
4297
|
+
if (op.httpMethod === "delete") return null;
|
|
4298
|
+
const ref = op.response;
|
|
4299
|
+
if (ref.kind === "model") return ref.name;
|
|
4300
|
+
if (ref.kind === "nullable" && ref.inner.kind === "model") return ref.inner.name;
|
|
4301
|
+
return null;
|
|
4302
|
+
}
|
|
3601
4303
|
//#endregion
|
|
3602
4304
|
//#region src/engine/generate-files.ts
|
|
3603
4305
|
function buildEmitterContext(spec, options) {
|
|
@@ -3607,7 +4309,8 @@ function buildEmitterContext(spec, options) {
|
|
|
3607
4309
|
spec,
|
|
3608
4310
|
outputDir: options.outputDir,
|
|
3609
4311
|
apiSurface: options.apiSurface,
|
|
3610
|
-
overlayLookup: options.overlayLookup
|
|
4312
|
+
overlayLookup: options.overlayLookup,
|
|
4313
|
+
resolvedOperations: resolveOperations(spec, options.operationHints, options.mountRules)
|
|
3611
4314
|
};
|
|
3612
4315
|
}
|
|
3613
4316
|
/**
|
|
@@ -3637,6 +4340,7 @@ function collectReferencedNames(services, models) {
|
|
|
3637
4340
|
for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
|
|
3638
4341
|
if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
|
|
3639
4342
|
}
|
|
4343
|
+
for (const model of models) if (isDiscriminatedModel(model)) referencedModels.add(model.name);
|
|
3640
4344
|
const modelsByName = new Map(models.map((m) => [m.name, m]));
|
|
3641
4345
|
const visited = /* @__PURE__ */ new Set();
|
|
3642
4346
|
const queue = [...referencedModels];
|
|
@@ -3656,6 +4360,9 @@ function collectReferencedNames(services, models) {
|
|
|
3656
4360
|
enums: referencedEnums
|
|
3657
4361
|
};
|
|
3658
4362
|
}
|
|
4363
|
+
function isDiscriminatedModel(model) {
|
|
4364
|
+
return model.fields.some((field) => (field.name === "event" || field.name === "type" || field.name === "object") && field.type.kind === "literal");
|
|
4365
|
+
}
|
|
3659
4366
|
/** Collect all generated files from an emitter (no headers, no path prefixes). */
|
|
3660
4367
|
function generateAllFiles(spec, emitter, ctx) {
|
|
3661
4368
|
const referenced = collectReferencedNames(spec.services, spec.models);
|
|
@@ -3672,7 +4379,6 @@ function generateAllFiles(spec, emitter, ctx) {
|
|
|
3672
4379
|
...emitter.generateResources(spec.services, ctx),
|
|
3673
4380
|
...emitter.generateClient(spec, ctx),
|
|
3674
4381
|
...emitter.generateErrors(ctx),
|
|
3675
|
-
...emitter.generateConfig(ctx),
|
|
3676
4382
|
...emitter.generateTypeSignatures?.(reachableSpec, ctx) ?? [],
|
|
3677
4383
|
...emitter.generateTests(reachableSpec, ctx),
|
|
3678
4384
|
...emitter.generateManifest?.(spec, ctx) ?? []
|
|
@@ -3756,11 +4462,33 @@ async function treeShakeFiles(files, language) {
|
|
|
3756
4462
|
}
|
|
3757
4463
|
deps.set(file.path, fileDeps);
|
|
3758
4464
|
}
|
|
4465
|
+
const codeExtensions = new Set([
|
|
4466
|
+
".ts",
|
|
4467
|
+
".js",
|
|
4468
|
+
".tsx",
|
|
4469
|
+
".jsx",
|
|
4470
|
+
".py",
|
|
4471
|
+
".rb",
|
|
4472
|
+
".go",
|
|
4473
|
+
".rs",
|
|
4474
|
+
".kt",
|
|
4475
|
+
".cs",
|
|
4476
|
+
".ex",
|
|
4477
|
+
".exs",
|
|
4478
|
+
".php"
|
|
4479
|
+
]);
|
|
3759
4480
|
const reachable = /* @__PURE__ */ new Set();
|
|
3760
4481
|
const queue = [];
|
|
3761
|
-
for (const file of files)
|
|
3762
|
-
|
|
3763
|
-
|
|
4482
|
+
for (const file of files) {
|
|
4483
|
+
const ext = file.path.slice(file.path.lastIndexOf("."));
|
|
4484
|
+
if (!codeExtensions.has(ext)) {
|
|
4485
|
+
reachable.add(file.path);
|
|
4486
|
+
continue;
|
|
4487
|
+
}
|
|
4488
|
+
if (isRootFile(file.path)) {
|
|
4489
|
+
reachable.add(file.path);
|
|
4490
|
+
queue.push(file.path);
|
|
4491
|
+
}
|
|
3764
4492
|
}
|
|
3765
4493
|
while (queue.length > 0) {
|
|
3766
4494
|
const current = queue.pop();
|
|
@@ -3784,11 +4512,61 @@ function mapFilesForTargetIntegration(files, language) {
|
|
|
3784
4512
|
};
|
|
3785
4513
|
});
|
|
3786
4514
|
}
|
|
4515
|
+
/**
|
|
4516
|
+
* Resolve file-to-directory conflicts in the target.
|
|
4517
|
+
*
|
|
4518
|
+
* When a generated file creates a package directory (e.g., `api_keys/__init__.py`)
|
|
4519
|
+
* that would shadow an existing module file (e.g., `api_keys.py`), move the
|
|
4520
|
+
* module file's content into `_compat.py` inside the package and add a
|
|
4521
|
+
* re-export from `__init__.py`. This preserves backward-compatible imports
|
|
4522
|
+
* (e.g., `from workos.api_keys import ApiKeysModule`) while the package directory
|
|
4523
|
+
* provides the generated models and resource classes.
|
|
4524
|
+
*/
|
|
4525
|
+
async function resolveFileToDirectoryConflicts(files, targetDir) {
|
|
4526
|
+
const result = [...files];
|
|
4527
|
+
const removals = [];
|
|
4528
|
+
for (let i = 0; i < result.length; i++) {
|
|
4529
|
+
const file = result[i];
|
|
4530
|
+
const match = file.path.match(/^(.+)\/__init__\.py$/);
|
|
4531
|
+
if (!match) continue;
|
|
4532
|
+
const dirPath = match[1];
|
|
4533
|
+
const moduleFilePath = `${dirPath}.py`;
|
|
4534
|
+
const fullModulePath = path.join(targetDir, moduleFilePath);
|
|
4535
|
+
let moduleContent;
|
|
4536
|
+
try {
|
|
4537
|
+
moduleContent = await fs.readFile(fullModulePath, "utf-8");
|
|
4538
|
+
} catch {
|
|
4539
|
+
continue;
|
|
4540
|
+
}
|
|
4541
|
+
const compatPath = `${dirPath}/_compat.py`;
|
|
4542
|
+
result.push({
|
|
4543
|
+
path: compatPath,
|
|
4544
|
+
content: moduleContent,
|
|
4545
|
+
overwriteExisting: true
|
|
4546
|
+
});
|
|
4547
|
+
const compatImport = "from ._compat import * # noqa: F401,F403 # pyright: ignore[reportUnusedImport]";
|
|
4548
|
+
const existingContent = file.content.endsWith("\n") ? file.content : file.content + "\n";
|
|
4549
|
+
result[i] = {
|
|
4550
|
+
...file,
|
|
4551
|
+
content: existingContent + compatImport + "\n"
|
|
4552
|
+
};
|
|
4553
|
+
removals.push(fullModulePath);
|
|
4554
|
+
}
|
|
4555
|
+
return {
|
|
4556
|
+
files: result,
|
|
4557
|
+
removals
|
|
4558
|
+
};
|
|
4559
|
+
}
|
|
3787
4560
|
async function integrateGeneratedFiles(opts) {
|
|
3788
|
-
|
|
4561
|
+
const { files: resolved, removals } = await resolveFileToDirectoryConflicts(await treeShakeFiles(mapFilesForTargetIntegration(opts.files, opts.language), opts.language), opts.targetDir);
|
|
4562
|
+
const result = await writeFiles(resolved, opts.targetDir, {
|
|
3789
4563
|
language: opts.language,
|
|
3790
4564
|
header: opts.header
|
|
3791
4565
|
});
|
|
4566
|
+
for (const filePath of removals) try {
|
|
4567
|
+
await fs.unlink(filePath);
|
|
4568
|
+
} catch {}
|
|
4569
|
+
return result;
|
|
3792
4570
|
}
|
|
3793
4571
|
//#endregion
|
|
3794
4572
|
//#region src/engine/formatter.ts
|
|
@@ -3849,7 +4627,11 @@ async function generate(spec, emitter, options) {
|
|
|
3849
4627
|
if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
|
|
3850
4628
|
if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
|
|
3851
4629
|
if (targetResult.skipped.length > 0) console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
|
|
3852
|
-
const allTargetFiles = [
|
|
4630
|
+
const allTargetFiles = [
|
|
4631
|
+
...targetResult.written,
|
|
4632
|
+
...targetResult.merged,
|
|
4633
|
+
...targetResult.identical
|
|
4634
|
+
];
|
|
3853
4635
|
if (allTargetFiles.length > 0) await formatTargetFiles(emitter, options.target, allTargetFiles);
|
|
3854
4636
|
}
|
|
3855
4637
|
return withHeaders;
|
|
@@ -4510,7 +5292,10 @@ function expandDocUrls(spec, docUrl) {
|
|
|
4510
5292
|
//#endregion
|
|
4511
5293
|
//#region src/cli/generate.ts
|
|
4512
5294
|
async function generateCommand(opts) {
|
|
4513
|
-
let ir = await parseSpec(opts.spec, {
|
|
5295
|
+
let ir = await parseSpec(opts.spec, {
|
|
5296
|
+
operationIdTransform: opts.operationIdTransform,
|
|
5297
|
+
schemaNameTransform: opts.schemaNameTransform
|
|
5298
|
+
});
|
|
4514
5299
|
if (opts.docUrl) ir = expandDocUrls(ir, opts.docUrl);
|
|
4515
5300
|
const emitter = getEmitter(opts.lang);
|
|
4516
5301
|
const namespace = opts.namespace ?? ir.name;
|
|
@@ -4533,7 +5318,9 @@ async function generateCommand(opts) {
|
|
|
4533
5318
|
outputDir: opts.output,
|
|
4534
5319
|
target: opts.target,
|
|
4535
5320
|
apiSurface,
|
|
4536
|
-
overlayLookup
|
|
5321
|
+
overlayLookup,
|
|
5322
|
+
operationHints: opts.operationHints,
|
|
5323
|
+
mountRules: opts.mountRules
|
|
4537
5324
|
});
|
|
4538
5325
|
if (opts.dryRun) for (const f of files) console.log(f.path);
|
|
4539
5326
|
else {
|
|
@@ -4890,7 +5677,10 @@ function summarize(changes) {
|
|
|
4890
5677
|
//#endregion
|
|
4891
5678
|
//#region src/cli/diff.ts
|
|
4892
5679
|
async function diffCommand(opts) {
|
|
4893
|
-
const parseOptions = {
|
|
5680
|
+
const parseOptions = {
|
|
5681
|
+
operationIdTransform: opts.operationIdTransform,
|
|
5682
|
+
schemaNameTransform: opts.schemaNameTransform
|
|
5683
|
+
};
|
|
4894
5684
|
let [oldSpec, newSpec] = await Promise.all([parseSpec(opts.old, parseOptions), parseSpec(opts.new, parseOptions)]);
|
|
4895
5685
|
if (opts.docUrl) {
|
|
4896
5686
|
oldSpec = expandDocUrls(oldSpec, opts.docUrl);
|
|
@@ -5685,7 +6475,7 @@ function runStalenessCheck(baseline, oldSpec, newSpec, lang) {
|
|
|
5685
6475
|
const __filename = fileURLToPath(import.meta.url);
|
|
5686
6476
|
const __dirname = path$1.dirname(__filename);
|
|
5687
6477
|
function resolveScript(scriptRelPath) {
|
|
5688
|
-
const distScript = path$1.resolve(__dirname, "..", "..", "dist", "scripts", scriptRelPath.replace(/\.ts$/, ".
|
|
6478
|
+
const distScript = path$1.resolve(__dirname, "..", "..", "dist", "scripts", scriptRelPath.replace(/\.ts$/, ".mjs"));
|
|
5689
6479
|
if (existsSync(distScript)) return {
|
|
5690
6480
|
bin: "node",
|
|
5691
6481
|
script: distScript
|
|
@@ -5804,7 +6594,7 @@ function printCompatResult(result, baseline) {
|
|
|
5804
6594
|
console.log(`compat: ${result.preservationScore}% (${result.preservedSymbols}/${result.totalBaselineSymbols} symbols preserved)`);
|
|
5805
6595
|
}
|
|
5806
6596
|
async function verifyCommand(opts) {
|
|
5807
|
-
const { spec, oldSpec, lang, output, apiSurface, rawResults, smokeConfig, smokeRunner, scope, diagnostics, operationIdTransform } = opts;
|
|
6597
|
+
const { spec, oldSpec, lang, output, apiSurface, rawResults, smokeConfig, smokeRunner, scope, diagnostics, operationIdTransform, schemaNameTransform } = opts;
|
|
5808
6598
|
const maxRetries = opts.maxRetries ?? 3;
|
|
5809
6599
|
const diagData = {};
|
|
5810
6600
|
let stepNum = 1;
|
|
@@ -5815,7 +6605,10 @@ async function verifyCommand(opts) {
|
|
|
5815
6605
|
console.log(separator);
|
|
5816
6606
|
const effectiveScope = scope ?? (spec ? "spec-only" : "full");
|
|
5817
6607
|
let parsedSpec;
|
|
5818
|
-
if (effectiveScope === "spec-only" && spec) parsedSpec = await parseSpec(spec, {
|
|
6608
|
+
if (effectiveScope === "spec-only" && spec) parsedSpec = await parseSpec(spec, {
|
|
6609
|
+
operationIdTransform,
|
|
6610
|
+
schemaNameTransform
|
|
6611
|
+
});
|
|
5819
6612
|
else if (effectiveScope === "spec-only") throw new CommandError("error: --scope spec-only requires --spec <path>", "", 1);
|
|
5820
6613
|
const compatFlow = parsedSpec && maxRetries > 0 ? await runOverlayRetryLoop({
|
|
5821
6614
|
baseline,
|
|
@@ -5858,7 +6651,13 @@ async function verifyCommand(opts) {
|
|
|
5858
6651
|
console.log(`\n${separator}`);
|
|
5859
6652
|
console.log(`Step ${stepNum}: Staleness detection`);
|
|
5860
6653
|
console.log(separator);
|
|
5861
|
-
const stalenessResult = runStalenessCheck(baseline, await parseSpec(oldSpec, {
|
|
6654
|
+
const stalenessResult = runStalenessCheck(baseline, await parseSpec(oldSpec, {
|
|
6655
|
+
operationIdTransform,
|
|
6656
|
+
schemaNameTransform
|
|
6657
|
+
}), await parseSpec(spec, {
|
|
6658
|
+
operationIdTransform,
|
|
6659
|
+
schemaNameTransform
|
|
6660
|
+
}), lang);
|
|
5862
6661
|
if (stalenessResult.violations.length > 0) {
|
|
5863
6662
|
console.log(`Found ${stalenessResult.violations.length} stale symbol(s):`);
|
|
5864
6663
|
for (const v of stalenessResult.violations) console.log(` [${v.category}] ${v.severity}: ${v.symbolPath} — ${v.message}`);
|
|
@@ -6037,10 +6836,6 @@ export const ${`${toCamelCase(lang)}Emitter`}: Emitter = {
|
|
|
6037
6836
|
return [];
|
|
6038
6837
|
},
|
|
6039
6838
|
|
|
6040
|
-
generateConfig(_ctx: EmitterContext): GeneratedFile[] {
|
|
6041
|
-
return [];
|
|
6042
|
-
},
|
|
6043
|
-
|
|
6044
6839
|
generateTypeSignatures(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile[] {
|
|
6045
6840
|
return [];
|
|
6046
6841
|
},
|
|
@@ -6135,6 +6930,50 @@ async function initCommand(opts) {
|
|
|
6135
6930
|
console.log(" - npm run sdk:verify -- --spec <path-to-spec>");
|
|
6136
6931
|
}
|
|
6137
6932
|
//#endregion
|
|
6933
|
+
//#region src/cli/resolve.ts
|
|
6934
|
+
async function resolveCommand(opts) {
|
|
6935
|
+
let ir = await parseSpec(opts.spec, {
|
|
6936
|
+
operationIdTransform: opts.operationIdTransform,
|
|
6937
|
+
schemaNameTransform: opts.schemaNameTransform
|
|
6938
|
+
});
|
|
6939
|
+
if (opts.docUrl) ir = expandDocUrls(ir, opts.docUrl);
|
|
6940
|
+
const resolved = resolveOperations(ir, opts.operationHints, opts.mountRules);
|
|
6941
|
+
if ((opts.format ?? "table") === "json") {
|
|
6942
|
+
const output = resolved.map(toJsonRow);
|
|
6943
|
+
console.log(JSON.stringify(output, null, 2));
|
|
6944
|
+
return;
|
|
6945
|
+
}
|
|
6946
|
+
printTable(resolved, opts.operationHints);
|
|
6947
|
+
}
|
|
6948
|
+
function toJsonRow(r) {
|
|
6949
|
+
return {
|
|
6950
|
+
service: r.service.name,
|
|
6951
|
+
method: r.operation.httpMethod.toUpperCase(),
|
|
6952
|
+
path: r.operation.path,
|
|
6953
|
+
derivedName: r.methodName,
|
|
6954
|
+
hintApplied: r.methodName !== r.operation.name,
|
|
6955
|
+
mountOn: r.mountOn,
|
|
6956
|
+
wrappers: r.wrappers?.map((w) => w.name)
|
|
6957
|
+
};
|
|
6958
|
+
}
|
|
6959
|
+
function printTable(resolved, hints) {
|
|
6960
|
+
const hintMap = hints ?? {};
|
|
6961
|
+
console.log("| Service | Method | Path | Resolved Name | Hint | Mount On |");
|
|
6962
|
+
console.log("|---|---|---|---|---|---|");
|
|
6963
|
+
const unhinted = [];
|
|
6964
|
+
for (const r of resolved) {
|
|
6965
|
+
const hasHint = `${r.operation.httpMethod.toUpperCase()} ${r.operation.path}` in hintMap;
|
|
6966
|
+
const hintMarker = hasHint ? "yes" : "";
|
|
6967
|
+
const mountChanged = r.mountOn !== r.service.name;
|
|
6968
|
+
const mountCol = mountChanged ? r.mountOn : "";
|
|
6969
|
+
console.log(`| ${r.service.name} | ${r.operation.httpMethod.toUpperCase()} | ${r.operation.path} | \`${r.methodName}\` | ${hintMarker} | ${mountCol} |`);
|
|
6970
|
+
if (r.wrappers) for (const w of r.wrappers) console.log(`| | | | \`${w.name}\` (wrapper) | yes | ${mountCol} |`);
|
|
6971
|
+
if (!hasHint && !mountChanged) unhinted.push(r);
|
|
6972
|
+
}
|
|
6973
|
+
console.log("");
|
|
6974
|
+
console.log(`Total: ${resolved.length} operations, ${resolved.length - unhinted.length} with hints/mounts, ${unhinted.length} algorithm-only`);
|
|
6975
|
+
}
|
|
6976
|
+
//#endregion
|
|
6138
6977
|
//#region src/cli/config-loader.ts
|
|
6139
6978
|
const CONFIG_NAMES = [
|
|
6140
6979
|
"oagen.config.ts",
|
|
@@ -6170,14 +7009,20 @@ function handleError(err) {
|
|
|
6170
7009
|
}
|
|
6171
7010
|
let configSmokeRunners;
|
|
6172
7011
|
let configOperationIdTransform;
|
|
7012
|
+
let configSchemaNameTransform;
|
|
6173
7013
|
let configDocUrl;
|
|
7014
|
+
let configOperationHints;
|
|
7015
|
+
let configMountRules;
|
|
6174
7016
|
try {
|
|
6175
7017
|
const config = await loadConfig();
|
|
6176
7018
|
if (config) {
|
|
6177
7019
|
applyConfig(config);
|
|
6178
7020
|
configSmokeRunners = config.smokeRunners;
|
|
6179
7021
|
configOperationIdTransform = config.operationIdTransform;
|
|
7022
|
+
configSchemaNameTransform = config.schemaNameTransform;
|
|
6180
7023
|
configDocUrl = config.docUrl;
|
|
7024
|
+
configOperationHints = config.operationHints;
|
|
7025
|
+
configMountRules = config.mountRules;
|
|
6181
7026
|
}
|
|
6182
7027
|
} catch (err) {
|
|
6183
7028
|
handleError(err);
|
|
@@ -6200,13 +7045,17 @@ program.command("generate").description("Run a registered emitter against an Ope
|
|
|
6200
7045
|
generateCommand({
|
|
6201
7046
|
...opts,
|
|
6202
7047
|
operationIdTransform: configOperationIdTransform,
|
|
6203
|
-
|
|
7048
|
+
schemaNameTransform: configSchemaNameTransform,
|
|
7049
|
+
docUrl: configDocUrl,
|
|
7050
|
+
operationHints: configOperationHints,
|
|
7051
|
+
mountRules: configMountRules
|
|
6204
7052
|
}).catch(handleError);
|
|
6205
7053
|
});
|
|
6206
7054
|
program.command("diff").description("Compare two OpenAPI specs and output a diff report").requiredOption("--old <path>", "Path to old spec").requiredOption("--new <path>", "Path to new spec").action((opts) => {
|
|
6207
7055
|
diffCommand({
|
|
6208
7056
|
...opts,
|
|
6209
7057
|
operationIdTransform: configOperationIdTransform,
|
|
7058
|
+
schemaNameTransform: configSchemaNameTransform,
|
|
6210
7059
|
docUrl: configDocUrl
|
|
6211
7060
|
}).catch(handleError);
|
|
6212
7061
|
});
|
|
@@ -6220,7 +7069,23 @@ program.command("verify").description("Run smoke tests (and optional compat chec
|
|
|
6220
7069
|
verifyCommand({
|
|
6221
7070
|
...opts,
|
|
6222
7071
|
maxRetries: parseInt(opts.maxRetries, 10),
|
|
6223
|
-
operationIdTransform: configOperationIdTransform
|
|
7072
|
+
operationIdTransform: configOperationIdTransform,
|
|
7073
|
+
schemaNameTransform: configSchemaNameTransform
|
|
7074
|
+
}).catch(handleError);
|
|
7075
|
+
});
|
|
7076
|
+
program.command("resolve").description("Resolve operation names from spec and output a review table").option("--spec <path>", "Path to OpenAPI spec file (or set OPENAPI_SPEC_PATH)").option("--format <format>", "Output format: table or json", "table").action((opts) => {
|
|
7077
|
+
opts.spec ??= process.env.OPENAPI_SPEC_PATH;
|
|
7078
|
+
if (!opts.spec) {
|
|
7079
|
+
console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");
|
|
7080
|
+
process.exit(1);
|
|
7081
|
+
}
|
|
7082
|
+
resolveCommand({
|
|
7083
|
+
...opts,
|
|
7084
|
+
operationIdTransform: configOperationIdTransform,
|
|
7085
|
+
schemaNameTransform: configSchemaNameTransform,
|
|
7086
|
+
docUrl: configDocUrl,
|
|
7087
|
+
operationHints: configOperationHints,
|
|
7088
|
+
mountRules: configMountRules
|
|
6224
7089
|
}).catch(handleError);
|
|
6225
7090
|
});
|
|
6226
7091
|
program.command("init").description("Scaffold a new emitter project").requiredOption("--lang <language>", "Target language").option("--project <dir>", "Project directory", ".").action((opts) => {
|