Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions nodejs/test/python-codegen.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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(
" /// <summary>Use alpha mode.</summary>\n public static SyntheticMode Alpha"
);
expect(code).toContain(
" /// <summary>Gets the <c>plain</c> value.</summary>\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,');
});
});
25 changes: 25 additions & 0 deletions nodejs/test/shared-codegen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
collectExperimentalOnlyRpcReferencedDefinitionNames,
collectReachableDefinitionNames,
findSharedSchemaDefinitions,
getEnumValueDescriptions,
inlineExternalSchemaDefinitions,
isIntegerSchemaBoundedToInt32,
rewriteSharedDefinitionReferences,
Expand Down Expand Up @@ -59,6 +60,22 @@
).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: {
Expand Down Expand Up @@ -87,6 +104,10 @@
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",
Expand Down Expand Up @@ -126,6 +147,10 @@
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",
Expand Down Expand Up @@ -294,7 +319,7 @@
"SessionStartData",
"SessionStartEvent",
]);
const inlinedDefinitions = inlined.definitions as Record<string, any>;

Check warning on line 322 in nodejs/test/shared-codegen.test.ts

View workflow job for this annotation

GitHub Actions / Node.js SDK Tests (windows-latest)

Unexpected any. Specify a different type

Check warning on line 322 in nodejs/test/shared-codegen.test.ts

View workflow job for this annotation

GitHub Actions / Node.js SDK Tests (ubuntu-latest)

Unexpected any. Specify a different type

Check warning on line 322 in nodejs/test/shared-codegen.test.ts

View workflow job for this annotation

GitHub Actions / Node.js SDK Tests (macos-latest)

Unexpected any. Specify a different type
expect(inlinedDefinitions.EventsReadResult.properties.events.items.$ref).toBe(
"#/definitions/SessionEvent"
);
Expand Down
46 changes: 46 additions & 0 deletions nodejs/test/typescript-codegen.test.ts
Original file line number Diff line number Diff line change
@@ -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";');
});
});
37 changes: 26 additions & 11 deletions scripts/codegen/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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 <c>${escapeXml(value)}</c> value.`, " ");
}

function toPascalCase(name: string): string {
const parts = splitCSharpIdentifierParts(name);
if (parts.length > 1) return parts.map(toPascalCasePart).join("");
Expand Down Expand Up @@ -500,6 +509,7 @@ function getOrCreateEnum(
values: string[],
enumOutput: string[],
description?: string,
enumValueDescriptions?: EnumValueDescriptions,
explicitName?: string,
deprecated?: boolean,
experimental?: boolean
Expand Down Expand Up @@ -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(` /// <summary>Gets the <c>${escapeXml(value)}</c> value.</summary>`);
lines.push(...xmlDocEnumMemberComment(enumValueDescriptions, value));
lines.push(` public static ${enumName} ${memberName} { get; } = new("${escapeCSharpStringLiteral(value)}");`, "");
}
lines.push(` /// <summary>Returns a value indicating whether two <see cref="${enumName}"/> instances are equivalent.</summary>`);
Expand Down Expand Up @@ -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}?`;
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string, unknown>);
const variants = extractEventVariants(schema);
Expand Down Expand Up @@ -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}?`;
}

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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);
});
}
Loading
Loading