Skip to content

Commit 1993292

Browse files
stephentoubCopilot
andauthored
Add enum value descriptions to generated docs (#1336)
Propagate x-enumDescriptions through the SDK code generators so enum values get language-appropriate documentation while retaining existing fallback comments. Add focused codegen coverage across C#, Go, Python, Rust, and TypeScript. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 310cc6c commit 1993292

9 files changed

Lines changed: 290 additions & 37 deletions

File tree

nodejs/test/python-codegen.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { JSONSchema7 } from "json-schema";
22
import { describe, expect, it } from "vitest";
33

4+
import { generateSessionEventsCode as generateCSharpSessionEventsCode } from "../../scripts/codegen/csharp.ts";
5+
import { generateGoSessionEventsCode } from "../../scripts/codegen/go.ts";
46
import { generatePythonSessionEventsCode } from "../../scripts/codegen/python.ts";
7+
import { generateSessionEventsCode as generateRustSessionEventsCode } from "../../scripts/codegen/rust.ts";
58

69
describe("python session event codegen", () => {
710
it("maps special schema formats to the expected Python types", () => {
@@ -374,3 +377,81 @@ describe("python session event codegen", () => {
374377
);
375378
});
376379
});
380+
381+
describe("enum value description codegen", () => {
382+
const schema: JSONSchema7 = {
383+
definitions: {
384+
SessionEvent: {
385+
anyOf: [
386+
{
387+
type: "object",
388+
required: ["type", "data"],
389+
properties: {
390+
type: { const: "session.synthetic" },
391+
data: {
392+
type: "object",
393+
required: ["mode", "fallback"],
394+
properties: {
395+
mode: {
396+
type: "string",
397+
enum: ["alpha", "beta"],
398+
title: "SyntheticMode",
399+
description: "Synthetic mode.",
400+
"x-enumDescriptions": {
401+
alpha: "Use alpha mode.",
402+
},
403+
},
404+
fallback: {
405+
type: "string",
406+
enum: ["plain"],
407+
title: "FallbackMode",
408+
},
409+
},
410+
},
411+
},
412+
},
413+
],
414+
},
415+
},
416+
};
417+
418+
it("emits Python comments for described enum values", () => {
419+
const code = generatePythonSessionEventsCode(schema);
420+
421+
expect(code).toContain("class SyntheticMode(Enum):");
422+
expect(code).toContain(' # Use alpha mode.\n ALPHA = "alpha"');
423+
expect(code).toContain(' BETA = "beta"');
424+
});
425+
426+
it("emits C# XML docs for described enum values and keeps fallback docs", () => {
427+
const code = generateCSharpSessionEventsCode(schema);
428+
429+
expect(code).toContain("public readonly struct SyntheticMode");
430+
expect(code).toContain(
431+
" /// <summary>Use alpha mode.</summary>\n public static SyntheticMode Alpha"
432+
);
433+
expect(code).toContain(
434+
" /// <summary>Gets the <c>plain</c> value.</summary>\n public static FallbackMode Plain"
435+
);
436+
});
437+
438+
it("emits Go comments for described enum values", () => {
439+
const code = generateGoSessionEventsCode(schema, "rpc").typeCode;
440+
441+
expect(code).toContain("type SyntheticMode string");
442+
expect(code).toContain(
443+
'\t// Use alpha mode.\n\tSyntheticModeAlpha SyntheticMode = "alpha"'
444+
);
445+
expect(code).toContain('\tSyntheticModeBeta SyntheticMode = "beta"');
446+
});
447+
448+
it("emits Rust docs for described enum values", () => {
449+
const code = generateRustSessionEventsCode(schema);
450+
451+
expect(code).toContain("pub enum SyntheticMode {");
452+
expect(code).toContain(
453+
' /// Use alpha mode.\n #[serde(rename = "alpha")]\n Alpha,'
454+
);
455+
expect(code).toContain(' #[serde(rename = "beta")]\n Beta,');
456+
});
457+
});

nodejs/test/shared-codegen.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
collectExperimentalOnlyRpcReferencedDefinitionNames,
77
collectReachableDefinitionNames,
88
findSharedSchemaDefinitions,
9+
getEnumValueDescriptions,
910
inlineExternalSchemaDefinitions,
1011
isIntegerSchemaBoundedToInt32,
1112
rewriteSharedDefinitionReferences,
@@ -59,6 +60,22 @@ describe("shared schema definition codegen utilities", () => {
5960
).toBe(false);
6061
});
6162

63+
it("extracts non-empty enum value descriptions from schema extensions", () => {
64+
expect(
65+
getEnumValueDescriptions({
66+
type: "string",
67+
enum: ["start", "stop"],
68+
"x-enumDescriptions": {
69+
start: " Start the operation. ",
70+
stop: "",
71+
ignored: 42,
72+
},
73+
} as JSONSchema7)
74+
).toEqual({ start: "Start the operation." });
75+
76+
expect(getEnumValueDescriptions({ type: "string", enum: ["start"] })).toBeUndefined();
77+
});
78+
6279
it("rewrites reachable identical shared definitions without enum-only assumptions", () => {
6380
const sessionSchema: JSONSchema7 = {
6481
definitions: {
@@ -87,6 +104,10 @@ describe("shared schema definition codegen utilities", () => {
87104
type: "string",
88105
enum: ["concise", "detailed"],
89106
description: "Reasoning summary mode used for model calls.",
107+
"x-enumDescriptions": {
108+
concise: "Use concise session reasoning summaries.",
109+
detailed: "Use detailed session reasoning summaries.",
110+
},
90111
},
91112
SharedPayload: {
92113
type: "object",
@@ -126,6 +147,10 @@ describe("shared schema definition codegen utilities", () => {
126147
type: "string",
127148
enum: ["concise", "detailed"],
128149
description: "Reasoning summary mode to request for supported model clients.",
150+
"x-enumDescriptions": {
151+
concise: "Request concise model reasoning summaries.",
152+
detailed: "Request detailed model reasoning summaries.",
153+
},
129154
},
130155
SharedPayload: {
131156
type: "object",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { JSONSchema7 } from "json-schema";
2+
import { compile } from "json-schema-to-typescript";
3+
import { describe, expect, it } from "vitest";
4+
5+
import { normalizeSchemaForTypeScript } from "../../scripts/codegen/typescript.ts";
6+
7+
describe("typescript schema codegen", () => {
8+
it("emits JSDoc comments for described enum values", async () => {
9+
const schema: JSONSchema7 = {
10+
title: "SyntheticOptions",
11+
type: "object",
12+
additionalProperties: false,
13+
properties: {
14+
namedMode: {
15+
title: "SyntheticMode",
16+
type: "string",
17+
enum: ["alpha", "beta"],
18+
description: "Synthetic mode.",
19+
"x-enumDescriptions": {
20+
alpha: "Use alpha mode.",
21+
},
22+
},
23+
inlineMode: {
24+
type: "string",
25+
enum: ["direct", "indirect"],
26+
description: "Inline mode.",
27+
"x-enumDescriptions": {
28+
direct: "Use a direct value.",
29+
},
30+
},
31+
},
32+
required: ["namedMode", "inlineMode"],
33+
};
34+
35+
const code = await compile(normalizeSchemaForTypeScript(schema), "SyntheticOptions", {
36+
bannerComment: "",
37+
style: { semi: true, singleQuote: false },
38+
additionalProperties: false,
39+
});
40+
41+
expect(code).toContain(
42+
'export type SyntheticMode = /** Use alpha mode. */ "alpha" | "beta";'
43+
);
44+
expect(code).toContain('inlineMode: /** Use a direct value. */ "direct" | "indirect";');
45+
});
46+
});

scripts/codegen/csharp.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { execFile } from "child_process";
1010
import fs from "fs/promises";
1111
import path from "path";
12+
import { fileURLToPath } from "url";
1213
import { promisify } from "util";
1314
import type { JSONSchema7 } from "json-schema";
1415
import {
@@ -38,12 +39,14 @@ import {
3839
isObjectSchema,
3940
isVoidSchema,
4041
getNullableInner,
42+
getEnumValueDescriptions,
4143
getSessionEventVariantSchemas,
4244
getSharedSessionEventEnvelopeProperties,
4345
rewriteSharedDefinitionReferences,
4446
REPO_ROOT,
4547
type ApiSchema,
4648
type DefinitionCollections,
49+
type EnumValueDescriptions,
4750
type RpcMethod,
4851
type SessionEventEnvelopeProperty,
4952
} from "./utils.js";
@@ -202,6 +205,12 @@ function xmlDocEnumComment(description: string | undefined, indent: string): str
202205
return rawXmlDocSummary(`Defines the allowed values.`, indent);
203206
}
204207

208+
function xmlDocEnumMemberComment(enumValueDescriptions: EnumValueDescriptions | undefined, value: string): string[] {
209+
const description = enumValueDescriptions?.[value];
210+
if (description) return xmlDocComment(description, " ");
211+
return rawXmlDocSummary(`Gets the <c>${escapeXml(value)}</c> value.`, " ");
212+
}
213+
205214
function toPascalCase(name: string): string {
206215
const parts = splitCSharpIdentifierParts(name);
207216
if (parts.length > 1) return parts.map(toPascalCasePart).join("");
@@ -500,6 +509,7 @@ function getOrCreateEnum(
500509
values: string[],
501510
enumOutput: string[],
502511
description?: string,
512+
enumValueDescriptions?: EnumValueDescriptions,
503513
explicitName?: string,
504514
deprecated?: boolean,
505515
experimental?: boolean
@@ -531,7 +541,7 @@ function getOrCreateEnum(
531541
const usedMemberNames = new Set(STRING_ENUM_RESERVED_MEMBER_NAMES);
532542
for (const value of values) {
533543
const memberName = uniqueCSharpIdentifier(value, usedMemberNames, "Value");
534-
lines.push(` /// <summary>Gets the <c>${escapeXml(value)}</c> value.</summary>`);
544+
lines.push(...xmlDocEnumMemberComment(enumValueDescriptions, value));
535545
lines.push(` public static ${enumName} ${memberName} { get; } = new("${escapeCSharpStringLiteral(value)}");`, "");
536546
}
537547
lines.push(` /// <summary>Returns a value indicating whether two <see cref="${enumName}"/> instances are equivalent.</summary>`);
@@ -1087,7 +1097,7 @@ function resolveSessionPropertyType(
10871097
}
10881098

10891099
if (refSchema.enum && Array.isArray(refSchema.enum)) {
1090-
const enumName = getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput, refSchema.description, undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema));
1100+
const enumName = getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput, refSchema.description, getEnumValueDescriptions(refSchema), undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema));
10911101
return isRequired ? enumName : `${enumName}?`;
10921102
}
10931103

@@ -1136,7 +1146,7 @@ function resolveSessionPropertyType(
11361146
return !isRequired ? "object?" : "object";
11371147
}
11381148
if (propSchema.enum && Array.isArray(propSchema.enum)) {
1139-
const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, propSchema.title as string | undefined, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema));
1149+
const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, getEnumValueDescriptions(propSchema), propSchema.title as string | undefined, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema));
11401150
return isRequired ? enumName : `${enumName}?`;
11411151
}
11421152
if (propSchema.type === "object" && propSchema.properties) {
@@ -1240,7 +1250,7 @@ function emitSessionEventEnvelopeProperty(
12401250
return lines;
12411251
}
12421252

1243-
function generateSessionEventsCode(schema: JSONSchema7): string {
1253+
export function generateSessionEventsCode(schema: JSONSchema7): string {
12441254
generatedEnums.clear();
12451255
sessionDefinitions = collectDefinitionCollections(schema as Record<string, unknown>);
12461256
const variants = extractEventVariants(schema);
@@ -1438,7 +1448,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam
14381448
}
14391449

14401450
if (refSchema.enum && Array.isArray(refSchema.enum)) {
1441-
const enumName = getOrCreateEnum(typeName, "", refSchema.enum as string[], rpcEnumOutput, refSchema.description, undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema) || experimentalRpcTypes.has(typeName));
1451+
const enumName = getOrCreateEnum(typeName, "", refSchema.enum as string[], rpcEnumOutput, refSchema.description, getEnumValueDescriptions(refSchema), undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema) || experimentalRpcTypes.has(typeName));
14421452
return isRequired ? enumName : `${enumName}?`;
14431453
}
14441454

@@ -1499,6 +1509,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam
14991509
schema.enum as string[],
15001510
rpcEnumOutput,
15011511
schema.description,
1512+
getEnumValueDescriptions(schema),
15021513
explicitName,
15031514
isSchemaDeprecated(schema),
15041515
isSchemaExperimental(schema) || experimentalRpcTypes.has(generatedEnumName),
@@ -2320,9 +2331,13 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro
23202331
}
23212332
}
23222333

2323-
const sessionArg = process.argv[2] || undefined;
2324-
const apiArg = process.argv[3] || undefined;
2325-
generate(sessionArg, apiArg).catch((err) => {
2326-
console.error("C# generation failed:", err);
2327-
process.exit(1);
2328-
});
2334+
const __filename = fileURLToPath(import.meta.url);
2335+
2336+
if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
2337+
const sessionArg = process.argv[2] || undefined;
2338+
const apiArg = process.argv[3] || undefined;
2339+
generate(sessionArg, apiArg).catch((err) => {
2340+
console.error("C# generation failed:", err);
2341+
process.exit(1);
2342+
});
2343+
}

0 commit comments

Comments
 (0)