From 45aed42fdac60b0fe33f3ed4e50d41d8be526fb5 Mon Sep 17 00:00:00 2001
From: Copilot <223556219+Copilot@users.noreply.github.com>
Date: Tue, 19 May 2026 12:05:21 -0400
Subject: [PATCH] Add enum value descriptions to generated docs
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>
---
nodejs/test/python-codegen.test.ts | 81 ++++++++++++++++++++++++++
nodejs/test/shared-codegen.test.ts | 25 ++++++++
nodejs/test/typescript-codegen.test.ts | 46 +++++++++++++++
scripts/codegen/csharp.ts | 37 ++++++++----
scripts/codegen/go.ts | 38 ++++++++----
scripts/codegen/python.ts | 14 ++++-
scripts/codegen/rust.ts | 22 +++++--
scripts/codegen/typescript.ts | 41 ++++++++++---
scripts/codegen/utils.ts | 23 +++++++-
9 files changed, 290 insertions(+), 37 deletions(-)
create mode 100644 nodejs/test/typescript-codegen.test.ts
diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/python-codegen.test.ts
index dc404ea19..588a2ed7c 100644
--- a/nodejs/test/python-codegen.test.ts
+++ b/nodejs/test/python-codegen.test.ts
@@ -1,7 +1,10 @@
import type { JSONSchema7 } from "json-schema";
import { describe, expect, it } from "vitest";
+import { generateSessionEventsCode as generateCSharpSessionEventsCode } from "../../scripts/codegen/csharp.ts";
+import { generateGoSessionEventsCode } from "../../scripts/codegen/go.ts";
import { generatePythonSessionEventsCode } from "../../scripts/codegen/python.ts";
+import { generateSessionEventsCode as generateRustSessionEventsCode } from "../../scripts/codegen/rust.ts";
describe("python session event codegen", () => {
it("maps special schema formats to the expected Python types", () => {
@@ -374,3 +377,81 @@ describe("python session event codegen", () => {
);
});
});
+
+describe("enum value description codegen", () => {
+ const schema: JSONSchema7 = {
+ definitions: {
+ SessionEvent: {
+ anyOf: [
+ {
+ type: "object",
+ required: ["type", "data"],
+ properties: {
+ type: { const: "session.synthetic" },
+ data: {
+ type: "object",
+ required: ["mode", "fallback"],
+ properties: {
+ mode: {
+ type: "string",
+ enum: ["alpha", "beta"],
+ title: "SyntheticMode",
+ description: "Synthetic mode.",
+ "x-enumDescriptions": {
+ alpha: "Use alpha mode.",
+ },
+ },
+ fallback: {
+ type: "string",
+ enum: ["plain"],
+ title: "FallbackMode",
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ },
+ };
+
+ it("emits Python comments for described enum values", () => {
+ const code = generatePythonSessionEventsCode(schema);
+
+ expect(code).toContain("class SyntheticMode(Enum):");
+ expect(code).toContain(' # Use alpha mode.\n ALPHA = "alpha"');
+ expect(code).toContain(' BETA = "beta"');
+ });
+
+ it("emits C# XML docs for described enum values and keeps fallback docs", () => {
+ const code = generateCSharpSessionEventsCode(schema);
+
+ expect(code).toContain("public readonly struct SyntheticMode");
+ expect(code).toContain(
+ " /// Use alpha mode.\n public static SyntheticMode Alpha"
+ );
+ expect(code).toContain(
+ " /// Gets the plain value.\n public static FallbackMode Plain"
+ );
+ });
+
+ it("emits Go comments for described enum values", () => {
+ const code = generateGoSessionEventsCode(schema, "rpc").typeCode;
+
+ expect(code).toContain("type SyntheticMode string");
+ expect(code).toContain(
+ '\t// Use alpha mode.\n\tSyntheticModeAlpha SyntheticMode = "alpha"'
+ );
+ expect(code).toContain('\tSyntheticModeBeta SyntheticMode = "beta"');
+ });
+
+ it("emits Rust docs for described enum values", () => {
+ const code = generateRustSessionEventsCode(schema);
+
+ expect(code).toContain("pub enum SyntheticMode {");
+ expect(code).toContain(
+ ' /// Use alpha mode.\n #[serde(rename = "alpha")]\n Alpha,'
+ );
+ expect(code).toContain(' #[serde(rename = "beta")]\n Beta,');
+ });
+});
diff --git a/nodejs/test/shared-codegen.test.ts b/nodejs/test/shared-codegen.test.ts
index fddc57493..54f9d39e9 100644
--- a/nodejs/test/shared-codegen.test.ts
+++ b/nodejs/test/shared-codegen.test.ts
@@ -6,6 +6,7 @@ import {
collectExperimentalOnlyRpcReferencedDefinitionNames,
collectReachableDefinitionNames,
findSharedSchemaDefinitions,
+ getEnumValueDescriptions,
inlineExternalSchemaDefinitions,
isIntegerSchemaBoundedToInt32,
rewriteSharedDefinitionReferences,
@@ -59,6 +60,22 @@ describe("shared schema definition codegen utilities", () => {
).toBe(false);
});
+ it("extracts non-empty enum value descriptions from schema extensions", () => {
+ expect(
+ getEnumValueDescriptions({
+ type: "string",
+ enum: ["start", "stop"],
+ "x-enumDescriptions": {
+ start: " Start the operation. ",
+ stop: "",
+ ignored: 42,
+ },
+ } as JSONSchema7)
+ ).toEqual({ start: "Start the operation." });
+
+ expect(getEnumValueDescriptions({ type: "string", enum: ["start"] })).toBeUndefined();
+ });
+
it("rewrites reachable identical shared definitions without enum-only assumptions", () => {
const sessionSchema: JSONSchema7 = {
definitions: {
@@ -87,6 +104,10 @@ describe("shared schema definition codegen utilities", () => {
type: "string",
enum: ["concise", "detailed"],
description: "Reasoning summary mode used for model calls.",
+ "x-enumDescriptions": {
+ concise: "Use concise session reasoning summaries.",
+ detailed: "Use detailed session reasoning summaries.",
+ },
},
SharedPayload: {
type: "object",
@@ -126,6 +147,10 @@ describe("shared schema definition codegen utilities", () => {
type: "string",
enum: ["concise", "detailed"],
description: "Reasoning summary mode to request for supported model clients.",
+ "x-enumDescriptions": {
+ concise: "Request concise model reasoning summaries.",
+ detailed: "Request detailed model reasoning summaries.",
+ },
},
SharedPayload: {
type: "object",
diff --git a/nodejs/test/typescript-codegen.test.ts b/nodejs/test/typescript-codegen.test.ts
new file mode 100644
index 000000000..248b60968
--- /dev/null
+++ b/nodejs/test/typescript-codegen.test.ts
@@ -0,0 +1,46 @@
+import type { JSONSchema7 } from "json-schema";
+import { compile } from "json-schema-to-typescript";
+import { describe, expect, it } from "vitest";
+
+import { normalizeSchemaForTypeScript } from "../../scripts/codegen/typescript.ts";
+
+describe("typescript schema codegen", () => {
+ it("emits JSDoc comments for described enum values", async () => {
+ const schema: JSONSchema7 = {
+ title: "SyntheticOptions",
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ namedMode: {
+ title: "SyntheticMode",
+ type: "string",
+ enum: ["alpha", "beta"],
+ description: "Synthetic mode.",
+ "x-enumDescriptions": {
+ alpha: "Use alpha mode.",
+ },
+ },
+ inlineMode: {
+ type: "string",
+ enum: ["direct", "indirect"],
+ description: "Inline mode.",
+ "x-enumDescriptions": {
+ direct: "Use a direct value.",
+ },
+ },
+ },
+ required: ["namedMode", "inlineMode"],
+ };
+
+ const code = await compile(normalizeSchemaForTypeScript(schema), "SyntheticOptions", {
+ bannerComment: "",
+ style: { semi: true, singleQuote: false },
+ additionalProperties: false,
+ });
+
+ expect(code).toContain(
+ 'export type SyntheticMode = /** Use alpha mode. */ "alpha" | "beta";'
+ );
+ expect(code).toContain('inlineMode: /** Use a direct value. */ "direct" | "indirect";');
+ });
+});
diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts
index 9d5eb5e46..f867d71b2 100644
--- a/scripts/codegen/csharp.ts
+++ b/scripts/codegen/csharp.ts
@@ -9,6 +9,7 @@
import { execFile } from "child_process";
import fs from "fs/promises";
import path from "path";
+import { fileURLToPath } from "url";
import { promisify } from "util";
import type { JSONSchema7 } from "json-schema";
import {
@@ -38,12 +39,14 @@ import {
isObjectSchema,
isVoidSchema,
getNullableInner,
+ getEnumValueDescriptions,
getSessionEventVariantSchemas,
getSharedSessionEventEnvelopeProperties,
rewriteSharedDefinitionReferences,
REPO_ROOT,
type ApiSchema,
type DefinitionCollections,
+ type EnumValueDescriptions,
type RpcMethod,
type SessionEventEnvelopeProperty,
} from "./utils.js";
@@ -202,6 +205,12 @@ function xmlDocEnumComment(description: string | undefined, indent: string): str
return rawXmlDocSummary(`Defines the allowed values.`, indent);
}
+function xmlDocEnumMemberComment(enumValueDescriptions: EnumValueDescriptions | undefined, value: string): string[] {
+ const description = enumValueDescriptions?.[value];
+ if (description) return xmlDocComment(description, " ");
+ return rawXmlDocSummary(`Gets the ${escapeXml(value)} value.`, " ");
+}
+
function toPascalCase(name: string): string {
const parts = splitCSharpIdentifierParts(name);
if (parts.length > 1) return parts.map(toPascalCasePart).join("");
@@ -500,6 +509,7 @@ function getOrCreateEnum(
values: string[],
enumOutput: string[],
description?: string,
+ enumValueDescriptions?: EnumValueDescriptions,
explicitName?: string,
deprecated?: boolean,
experimental?: boolean
@@ -531,7 +541,7 @@ function getOrCreateEnum(
const usedMemberNames = new Set(STRING_ENUM_RESERVED_MEMBER_NAMES);
for (const value of values) {
const memberName = uniqueCSharpIdentifier(value, usedMemberNames, "Value");
- lines.push(` /// Gets the ${escapeXml(value)} value.`);
+ lines.push(...xmlDocEnumMemberComment(enumValueDescriptions, value));
lines.push(` public static ${enumName} ${memberName} { get; } = new("${escapeCSharpStringLiteral(value)}");`, "");
}
lines.push(` /// Returns a value indicating whether two instances are equivalent.`);
@@ -1087,7 +1097,7 @@ function resolveSessionPropertyType(
}
if (refSchema.enum && Array.isArray(refSchema.enum)) {
- const enumName = getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput, refSchema.description, undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema));
+ const enumName = getOrCreateEnum(className, "", refSchema.enum as string[], enumOutput, refSchema.description, getEnumValueDescriptions(refSchema), undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema));
return isRequired ? enumName : `${enumName}?`;
}
@@ -1136,7 +1146,7 @@ function resolveSessionPropertyType(
return !isRequired ? "object?" : "object";
}
if (propSchema.enum && Array.isArray(propSchema.enum)) {
- const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, propSchema.title as string | undefined, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema));
+ const enumName = getOrCreateEnum(parentClassName, propName, propSchema.enum as string[], enumOutput, propSchema.description, getEnumValueDescriptions(propSchema), propSchema.title as string | undefined, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema));
return isRequired ? enumName : `${enumName}?`;
}
if (propSchema.type === "object" && propSchema.properties) {
@@ -1240,7 +1250,7 @@ function emitSessionEventEnvelopeProperty(
return lines;
}
-function generateSessionEventsCode(schema: JSONSchema7): string {
+export function generateSessionEventsCode(schema: JSONSchema7): string {
generatedEnums.clear();
sessionDefinitions = collectDefinitionCollections(schema as Record);
const variants = extractEventVariants(schema);
@@ -1438,7 +1448,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam
}
if (refSchema.enum && Array.isArray(refSchema.enum)) {
- const enumName = getOrCreateEnum(typeName, "", refSchema.enum as string[], rpcEnumOutput, refSchema.description, undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema) || experimentalRpcTypes.has(typeName));
+ const enumName = getOrCreateEnum(typeName, "", refSchema.enum as string[], rpcEnumOutput, refSchema.description, getEnumValueDescriptions(refSchema), undefined, isSchemaDeprecated(refSchema), isSchemaExperimental(refSchema) || experimentalRpcTypes.has(typeName));
return isRequired ? enumName : `${enumName}?`;
}
@@ -1499,6 +1509,7 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam
schema.enum as string[],
rpcEnumOutput,
schema.description,
+ getEnumValueDescriptions(schema),
explicitName,
isSchemaDeprecated(schema),
isSchemaExperimental(schema) || experimentalRpcTypes.has(generatedEnumName),
@@ -2320,9 +2331,13 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro
}
}
-const sessionArg = process.argv[2] || undefined;
-const apiArg = process.argv[3] || undefined;
-generate(sessionArg, apiArg).catch((err) => {
- console.error("C# generation failed:", err);
- process.exit(1);
-});
+const __filename = fileURLToPath(import.meta.url);
+
+if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
+ const sessionArg = process.argv[2] || undefined;
+ const apiArg = process.argv[3] || undefined;
+ generate(sessionArg, apiArg).catch((err) => {
+ console.error("C# generation failed:", err);
+ process.exit(1);
+ });
+}
diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts
index 2e481bbba..217f0e168 100644
--- a/scripts/codegen/go.ts
+++ b/scripts/codegen/go.ts
@@ -9,6 +9,8 @@
import { execFile } from "child_process";
import fs from "fs/promises";
import type { JSONSchema7 } from "json-schema";
+import path from "path";
+import { fileURLToPath } from "url";
import { promisify } from "util";
import wordwrap from "wordwrap";
import {
@@ -23,6 +25,7 @@ import {
findSharedSchemaDefinitions,
getApiSchemaPath,
getNullableInner,
+ getEnumValueDescriptions,
getRpcSchemaTypeName,
getSessionEventsSchemaPath,
getSessionEventVariantSchemas,
@@ -45,6 +48,7 @@ import {
writeGeneratedFile,
type ApiSchema,
type DefinitionCollections,
+ type EnumValueDescriptions,
type RpcMethod,
type SessionEventEnvelopeProperty,
} from "./utils.js";
@@ -630,6 +634,7 @@ function getOrCreateGoEnum(
values: string[],
ctx: GoCodegenCtx,
description?: string,
+ enumValueDescriptions?: EnumValueDescriptions,
deprecated?: boolean,
experimental?: boolean
): string {
@@ -662,6 +667,10 @@ function getOrCreateGoEnum(
);
}
usedConstNames.set(constName, value);
+ const valueDescription = enumValueDescriptions?.[value];
+ if (valueDescription) {
+ pushGoCommentForContext(lines, valueDescription, ctx, "\t");
+ }
lines.push(`\t${constName} ${enumName} = "${value}"`);
}
lines.push(`)`);
@@ -754,7 +763,7 @@ function resolveGoPropertyType(
if (resolved) {
if (resolved.enum) {
if ((resolved.enum as unknown[]).every((value) => typeof value === "string")) {
- const enumType = getOrCreateGoEnum(typeName, resolved.enum as string[], ctx, resolved.description, isSchemaDeprecated(resolved), isSchemaExperimental(resolved));
+ const enumType = getOrCreateGoEnum(typeName, resolved.enum as string[], ctx, resolved.description, getEnumValueDescriptions(resolved), isSchemaDeprecated(resolved), isSchemaExperimental(resolved));
return isRequired ? enumType : `*${enumType}`;
}
if (resolved.enum.length === 1) {
@@ -814,7 +823,7 @@ function resolveGoPropertyType(
// Handle enum
if (propSchema.enum && Array.isArray(propSchema.enum)) {
if ((propSchema.enum as unknown[]).every((value) => typeof value === "string")) {
- const enumType = getOrCreateGoEnum((propSchema.title as string) || nestedName, propSchema.enum as string[], ctx, propSchema.description, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema));
+ const enumType = getOrCreateGoEnum((propSchema.title as string) || nestedName, propSchema.enum as string[], ctx, propSchema.description, getEnumValueDescriptions(propSchema), isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema));
return isRequired ? enumType : `*${enumType}`;
}
if (propSchema.enum.length === 1) {
@@ -829,7 +838,7 @@ function resolveGoPropertyType(
if (typeof propSchema.const !== "string") {
return resolveGoPropertyType(schemaForConstValue(propSchema.const), parentTypeName, jsonPropName, isRequired, ctx);
}
- const enumType = getOrCreateGoEnum((propSchema.title as string) || nestedName, [propSchema.const], ctx, propSchema.description, isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema));
+ const enumType = getOrCreateGoEnum((propSchema.title as string) || nestedName, [propSchema.const], ctx, propSchema.description, getEnumValueDescriptions(propSchema), isSchemaDeprecated(propSchema), isSchemaExperimental(propSchema));
return isRequired ? enumType : `*${enumType}`;
}
@@ -1612,6 +1621,7 @@ function emitGoFlatDiscriminatedUnion(
discValues,
ctx,
`${discGoName} discriminator for ${typeName}.`,
+ undefined,
false,
experimental
);
@@ -2479,7 +2489,7 @@ function goUntaggedUnionVariant(typeName: string, member: JSONSchema7, ctx: GoCo
}
if (resolved.enum && Array.isArray(resolved.enum)) {
- const enumType = getOrCreateGoEnum((resolved.title as string) || `${typeName}Enum`, resolved.enum as string[], ctx, resolved.description, isSchemaDeprecated(resolved));
+ const enumType = getOrCreateGoEnum((resolved.title as string) || `${typeName}Enum`, resolved.enum as string[], ctx, resolved.description, getEnumValueDescriptions(resolved), isSchemaDeprecated(resolved));
return { typeName: enumType, goType: enumType, jsonKind, returnExpr: "value" };
}
@@ -2785,7 +2795,7 @@ function emitGoRpcDefinition(definitionName: string, schema: JSONSchema7, ctx: G
const effectiveSchema = resolveObjectSchema(schema, ctx.definitions) ?? resolveSchema(schema, ctx.definitions) ?? schema;
if (isStringEnumDefinition(effectiveSchema)) {
- getOrCreateGoEnum(typeName, effectiveSchema.enum, ctx, effectiveSchema.description, isSchemaDeprecated(effectiveSchema), isSchemaExperimental(effectiveSchema));
+ getOrCreateGoEnum(typeName, effectiveSchema.enum, ctx, effectiveSchema.description, getEnumValueDescriptions(effectiveSchema), isSchemaDeprecated(effectiveSchema), isSchemaExperimental(effectiveSchema));
return typeName;
}
@@ -2918,7 +2928,7 @@ function goDeclaredTypeName(code: string): string {
/**
* Generate the complete Go session-events file content.
*/
-function generateGoSessionEventsCode(schema: JSONSchema7, packageName: string): GoGeneratedTypeCode {
+export function generateGoSessionEventsCode(schema: JSONSchema7, packageName: string): GoGeneratedTypeCode {
const variants = extractGoEventVariants(schema);
const ctx: GoCodegenCtx = {
structs: [],
@@ -4006,9 +4016,13 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro
}
}
-const sessionArg = process.argv[2] || undefined;
-const apiArg = process.argv[3] || undefined;
-generate(sessionArg, apiArg).catch((err) => {
- console.error("Go generation failed:", err);
- process.exit(1);
-});
+const __filename = fileURLToPath(import.meta.url);
+
+if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
+ const sessionArg = process.argv[2] || undefined;
+ const apiArg = process.argv[3] || undefined;
+ generate(sessionArg, apiArg).catch((err) => {
+ console.error("Go generation failed:", err);
+ process.exit(1);
+ });
+}
diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts
index dae1b875e..a219c9303 100644
--- a/scripts/codegen/python.ts
+++ b/scripts/codegen/python.ts
@@ -42,8 +42,10 @@ import {
withSharedDefinitions,
getSessionEventVariantSchemas,
getSharedSessionEventEnvelopeProperties,
+ getEnumValueDescriptions,
type ApiSchema,
type DefinitionCollections,
+ type EnumValueDescriptions,
type RpcMethod,
type SessionEventEnvelopeProperty,
} from "./utils.js";
@@ -1045,6 +1047,7 @@ function getPyNamedSchemaType(
resolved.enum as string[],
ctx,
resolved.description,
+ getEnumValueDescriptions(resolved),
isSchemaDeprecated(resolved),
isSchemaExperimental(resolved)
);
@@ -1126,6 +1129,7 @@ function getOrCreatePyEnum(
values: string[],
ctx: PyCodegenCtx,
description?: string,
+ enumValueDescriptions?: EnumValueDescriptions,
deprecated?: boolean,
experimental?: boolean
): string {
@@ -1148,6 +1152,12 @@ function getOrCreatePyEnum(
lines.push(`class ${enumName}(Enum):`);
}
for (const value of values) {
+ const valueDescription = enumValueDescriptions?.[value];
+ if (valueDescription) {
+ for (const line of valueDescription.split(/\r?\n/)) {
+ lines.push(` # ${line.trimEnd()}`);
+ }
+ }
lines.push(` ${toEnumMemberName(value)} = ${JSON.stringify(value)}`);
}
ctx.enumsByName.set(enumName, enumName);
@@ -1170,7 +1180,7 @@ function resolvePyPropertyType(
const resolved = resolveSchema(propSchema, ctx.definitions);
if (resolved && resolved !== propSchema) {
if (resolved.enum && Array.isArray(resolved.enum) && resolved.enum.every((value) => typeof value === "string")) {
- const enumType = getOrCreatePyEnum(typeName, resolved.enum as string[], ctx, resolved.description, isSchemaDeprecated(resolved), isSchemaExperimental(resolved));
+ const enumType = getOrCreatePyEnum(typeName, resolved.enum as string[], ctx, resolved.description, getEnumValueDescriptions(resolved), isSchemaDeprecated(resolved), isSchemaExperimental(resolved));
const enumResolved: PyResolvedType = {
annotation: enumType,
fromExpr: (expr) => `parse_enum(${enumType}, ${expr})`,
@@ -1262,6 +1272,7 @@ function resolvePyPropertyType(
propSchema.enum as string[],
ctx,
propSchema.description,
+ getEnumValueDescriptions(propSchema),
isSchemaDeprecated(propSchema),
isSchemaExperimental(propSchema)
);
@@ -1603,6 +1614,7 @@ function emitPyFlatDiscriminatedUnion(
[...mapping.keys()],
ctx,
description ? `${description} discriminator` : `${typeName} discriminator`,
+ undefined,
false,
experimental
);
diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts
index 3898ee7f2..253111b35 100644
--- a/scripts/codegen/rust.ts
+++ b/scripts/codegen/rust.ts
@@ -13,6 +13,7 @@
import { execFile } from "child_process";
import fs from "fs/promises";
import path from "path";
+import { fileURLToPath } from "url";
import { promisify } from "util";
import type { JSONSchema7, JSONSchema7Definition } from "json-schema";
import {
@@ -28,6 +29,7 @@ import {
collectRpcMethodReferencedDefinitionNames,
findSharedSchemaDefinitions,
getApiSchemaPath,
+ getEnumValueDescriptions,
getNullableInner,
getRpcSchemaTypeName,
getSessionEventsSchemaPath,
@@ -45,6 +47,7 @@ import {
resolveSchema,
rewriteSharedDefinitionReferences,
stripBooleanLiterals,
+ type EnumValueDescriptions,
} from "./utils.js";
const execFileAsync = promisify(execFile);
@@ -523,6 +526,7 @@ function resolveRustType(
resolved.enum as string[],
ctx,
resolved.description,
+ getEnumValueDescriptions(resolved),
isSchemaExperimental(resolved),
);
return wrapOption(typeName, isRequired);
@@ -625,6 +629,7 @@ function resolveRustType(
propSchema.enum as string[],
ctx,
propSchema.description,
+ getEnumValueDescriptions(propSchema),
isSchemaExperimental(propSchema),
);
return wrapOption(enumName, isRequired);
@@ -854,6 +859,7 @@ function emitRustStringEnum(
values: string[],
ctx: RustCodegenCtx,
description?: string,
+ enumValueDescriptions?: EnumValueDescriptions,
experimental = false,
): void {
if (ctx.generatedNames.has(enumName)) return;
@@ -880,6 +886,7 @@ function emitRustStringEnum(
"Value",
reservedVariantNames,
);
+ pushRustDoc(lines, enumValueDescriptions?.[value], " ");
if (variantName !== value) {
lines.push(` #[serde(rename = "${value}")]`);
}
@@ -995,7 +1002,7 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] {
.filter((v) => !EXCLUDED_EVENT_TYPES.has(v.typeName));
}
-function generateSessionEventsCode(schema: JSONSchema7): string {
+export function generateSessionEventsCode(schema: JSONSchema7): string {
const variants = extractEventVariants(schema);
const ctx = makeCtx(
collectDefinitionCollections(schema as Record),
@@ -1361,6 +1368,7 @@ function generateApiTypesCode(apiSchema: ApiSchema): string {
schema.enum as string[],
ctx,
schema.description,
+ getEnumValueDescriptions(schema),
isSchemaExperimental(schema),
);
} else if (asGeneratedObjectSchema(schema, defCollections)) {
@@ -2025,7 +2033,11 @@ async function generate(): Promise {
console.log(`Done! Generated files in ${GENERATED_DIR}`);
}
-generate().catch((err) => {
- console.error("Code generation failed:", err);
- process.exit(1);
-});
+const __filename = fileURLToPath(import.meta.url);
+
+if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
+ generate().catch((err) => {
+ console.error("Code generation failed:", err);
+ process.exit(1);
+ });
+}
diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts
index 78c232ed5..b3a929bf3 100644
--- a/scripts/codegen/typescript.ts
+++ b/scripts/codegen/typescript.ts
@@ -9,6 +9,8 @@
import fs from "fs/promises";
import type { JSONSchema7 } from "json-schema";
import { compile } from "json-schema-to-typescript";
+import path from "path";
+import { fileURLToPath } from "url";
import {
getApiSchemaPath,
fixNullableRequiredRefsInApiSchema,
@@ -34,6 +36,7 @@ import {
isNodeFullyDeprecated,
isVoidSchema,
isSchemaExperimental,
+ getEnumValueDescriptions,
type ApiSchema,
type DefinitionCollections,
type RpcMethod,
@@ -52,6 +55,12 @@ function sanitizeJsDocText(text: string): string {
return text.trim().replace(/\*\//g, "* /");
}
+function tsDocCommentText(text: string): string {
+ const lines = sanitizeJsDocText(text).split(/\r?\n/);
+ if (lines.length === 1) return `/** ${lines[0]} */`;
+ return ["/**", ...lines.map((line) => ` * ${line}`), " */"].join("\n");
+}
+
function pushTsJsDoc(lines: string[], indent: string, entries: string[]): void {
const cleaned = entries.map(sanitizeJsDocText).filter((entry) => entry.length > 0);
if (cleaned.length === 0) return;
@@ -237,7 +246,7 @@ function collectRpcMethods(node: Record): RpcMethod[] {
return results;
}
-function normalizeSchemaForTypeScript(schema: JSONSchema7): JSONSchema7 {
+export function normalizeSchemaForTypeScript(schema: JSONSchema7): JSONSchema7 {
const root = structuredClone(schema) as JSONSchema7 & {
definitions?: Record;
$defs?: Record;
@@ -271,6 +280,20 @@ function normalizeSchemaForTypeScript(schema: JSONSchema7): JSONSchema7 {
Object.entries(value as Record).map(([key, child]) => [key, rewrite(child)])
) as Record;
+ const enumValueDescriptions = getEnumValueDescriptions(rewritten as JSONSchema7);
+ if (enumValueDescriptions && Array.isArray(rewritten.enum) && rewritten.enum.every((entry) => typeof entry === "string")) {
+ rewritten.tsType = (rewritten.enum as string[])
+ .map((entry) => {
+ const comment = enumValueDescriptions[entry];
+ const literal = JSON.stringify(entry);
+ return comment ? `${tsDocCommentText(comment)}\n| ${literal}` : `| ${literal}`;
+ })
+ .join("\n");
+ delete rewritten.type;
+ delete rewritten.enum;
+ delete rewritten["x-enumDescriptions"];
+ }
+
if (typeof rewritten.$ref === "string") {
const externalRef = parseExternalSchemaRef(rewritten.$ref);
if (externalRef && EXTERNAL_SCHEMA_TS_IMPORT[externalRef.schemaFile]) {
@@ -881,9 +904,13 @@ async function generate(sessionSchemaPath?: string, apiSchemaPath?: string): Pro
}
}
-const sessionArg = process.argv[2] || undefined;
-const apiArg = process.argv[3] || undefined;
-generate(sessionArg, apiArg).catch((err) => {
- console.error("TypeScript generation failed:", err);
- process.exit(1);
-});
+const __filename = fileURLToPath(import.meta.url);
+
+if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
+ const sessionArg = process.argv[2] || undefined;
+ const apiArg = process.argv[3] || undefined;
+ generate(sessionArg, apiArg).catch((err) => {
+ console.error("TypeScript generation failed:", err);
+ process.exit(1);
+ });
+}
diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts
index ce79d6bb3..a06be7607 100644
--- a/scripts/codegen/utils.ts
+++ b/scripts/codegen/utils.ts
@@ -29,6 +29,8 @@ export interface DefinitionCollections {
$defs?: Record;
}
+export type EnumValueDescriptions = Record;
+
export interface SessionEventEnvelopeProperty {
name: string;
schema: JSONSchema7;
@@ -326,6 +328,25 @@ export function cloneSchemaForCodegen(value: T): T {
return value;
}
+export function getEnumValueDescriptions(schema: JSONSchema7 | null | undefined): EnumValueDescriptions | undefined {
+ if (!schema || typeof schema !== "object") return undefined;
+
+ const rawDescriptions = (schema as Record)["x-enumDescriptions"];
+ if (!rawDescriptions || typeof rawDescriptions !== "object" || Array.isArray(rawDescriptions)) return undefined;
+
+ const descriptions: EnumValueDescriptions = {};
+ for (const [value, description] of Object.entries(rawDescriptions)) {
+ if (typeof description !== "string") continue;
+
+ const trimmedDescription = description.trim();
+ if (trimmedDescription.length > 0) {
+ descriptions[value] = trimmedDescription;
+ }
+ }
+
+ return Object.keys(descriptions).length > 0 ? descriptions : undefined;
+}
+
const INT32_MIN = -(2 ** 31);
const INT32_MAX = 2 ** 31 - 1;
@@ -1091,7 +1112,7 @@ function normalizeDefinitionForComparison(definition: JSONSchema7Definition): un
const result: Record = {};
for (const [key, value] of Object.entries(definition as Record)) {
- if (key === "description" || key === "markdownDescription") {
+ if (key === "description" || key === "markdownDescription" || key === "x-enumDescriptions") {
continue;
} else if (key === "$ref" && typeof value === "string") {
const localRef = parseLocalDefinitionRef(value);