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);