diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 5880ffc85..79649f1da 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -1320,7 +1320,7 @@ public partial class TaskInfoAgent : TaskInfo [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("activeTimeMs")] - public TimeSpan? ActiveTimeMs { get; set; } + public TimeSpan? ActiveTime { get; set; } /// Type of agent running this task. [JsonPropertyName("agentType")] @@ -2970,7 +2970,7 @@ public sealed class UsageGetMetricsResult [Range(0, double.MaxValue)] [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonPropertyName("totalApiDurationMs")] - public TimeSpan TotalApiDurationMs { get; set; } + public TimeSpan TotalApiDuration { get; set; } /// Session-wide accumulated nano-AI units cost. [Range((double)0, (double)long.MaxValue)] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index bd00fdc69..189c5104c 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -1395,7 +1395,7 @@ public sealed partial class SessionScheduleCreatedData /// Interval between ticks in milliseconds. [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonPropertyName("intervalMs")] - public required TimeSpan IntervalMs { get; set; } + public required TimeSpan Interval { get; set; } /// Prompt text that gets enqueued on every tick. [JsonPropertyName("prompt")] @@ -1672,7 +1672,7 @@ public sealed partial class SessionShutdownData /// Cumulative time spent in API calls during the session, in milliseconds. [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonPropertyName("totalApiDurationMs")] - public required TimeSpan TotalApiDurationMs { get; set; } + public required TimeSpan TotalApiDuration { get; set; } /// Session-wide accumulated nano-AI units cost. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2157,7 +2157,7 @@ public sealed partial class AssistantUsageData [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("interTokenLatencyMs")] - public TimeSpan? InterTokenLatencyMs { get; set; } + public TimeSpan? InterTokenLatency { get; set; } /// Model identifier used for this API call. [JsonPropertyName("model")] @@ -2199,7 +2199,7 @@ public sealed partial class AssistantUsageData [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("ttftMs")] - public TimeSpan? TtftMs { get; set; } + public TimeSpan? Ttft { get; set; } } /// Failed LLM API call metadata for telemetry. @@ -2214,7 +2214,7 @@ public sealed partial class ModelCallFailureData [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("durationMs")] - public TimeSpan? DurationMs { get; set; } + public TimeSpan? Duration { get; set; } /// Raw provider/runtime error message for restricted telemetry. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2464,7 +2464,7 @@ public sealed partial class SubagentCompletedData [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("durationMs")] - public TimeSpan? DurationMs { get; set; } + public TimeSpan? Duration { get; set; } /// Model used by the sub-agent. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -2501,7 +2501,7 @@ public sealed partial class SubagentFailedData [JsonConverter(typeof(MillisecondsTimeSpanConverter))] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("durationMs")] - public TimeSpan? DurationMs { get; set; } + public TimeSpan? Duration { get; set; } /// Error message describing why the sub-agent failed. [JsonPropertyName("error")] diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 9e2742deb..3622821dc 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -85,7 +85,7 @@ public class SessionEventSerializationTests { ShutdownType = ShutdownType.Routine, TotalPremiumRequests = 1, - TotalApiDurationMs = TimeSpan.FromMilliseconds(100), + TotalApiDuration = TimeSpan.FromMilliseconds(100), SessionStartTime = 1773609948932, CodeChanges = new ShutdownCodeChanges { diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/session-event-codegen.test.ts similarity index 85% rename from nodejs/test/python-codegen.test.ts rename to nodejs/test/session-event-codegen.test.ts index e70f65011..ffb8936e4 100644 --- a/nodejs/test/python-codegen.test.ts +++ b/nodejs/test/session-event-codegen.test.ts @@ -6,7 +6,7 @@ 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", () => { +describe("session event codegen", () => { it("maps special schema formats to the expected Python types", () => { const schema: JSONSchema7 = { definitions: { @@ -86,6 +86,91 @@ describe("python session event codegen", () => { expect(code).toContain("count: int"); }); + it("strips Ms suffixes from duration member names while preserving JSON names", () => { + const schema: JSONSchema7 = { + definitions: { + SessionEvent: { + anyOf: [ + { + type: "object", + required: ["type", "data"], + properties: { + type: { const: "session.synthetic" }, + data: { + type: "object", + required: ["durationMs", "integerDurationMs", "URLMs"], + properties: { + durationMs: { type: "number", format: "duration" }, + integerDurationMs: { type: "integer", format: "duration" }, + optionalDurationMs: { + type: ["number", "null"], + format: "duration", + }, + nullableDurationMs: { + anyOf: [ + { type: "number", format: "duration" }, + { type: "null" }, + ], + }, + URLMs: { type: "number", format: "duration" }, + }, + }, + }, + }, + ], + }, + }, + }; + + const pythonCode = generatePythonSessionEventsCode(schema); + + expect(pythonCode).toContain("duration: timedelta"); + expect(pythonCode).toContain("integer_duration: timedelta"); + expect(pythonCode).toContain("optional_duration: timedelta | None = None"); + expect(pythonCode).toContain("nullable_duration: timedelta | None = None"); + expect(pythonCode).toContain("urlms: timedelta"); + expect(pythonCode).toContain('duration = from_timedelta(obj.get("durationMs"))'); + expect(pythonCode).toContain('result["durationMs"] = to_timedelta(self.duration)'); + expect(pythonCode).toContain( + 'integer_duration = from_timedelta(obj.get("integerDurationMs"))' + ); + expect(pythonCode).toContain( + 'result["integerDurationMs"] = to_timedelta_int(self.integer_duration)' + ); + expect(pythonCode).toContain( + 'optional_duration = from_union([from_none, from_timedelta], obj.get("optionalDurationMs"))' + ); + expect(pythonCode).toContain( + 'result["optionalDurationMs"] = from_union([from_none, to_timedelta], self.optional_duration)' + ); + expect(pythonCode).toContain( + 'nullable_duration = from_union([from_none, from_timedelta], obj.get("nullableDurationMs"))' + ); + expect(pythonCode).toContain( + 'result["nullableDurationMs"] = from_union([from_none, to_timedelta], self.nullable_duration)' + ); + expect(pythonCode).toContain('urlms = from_timedelta(obj.get("URLMs"))'); + expect(pythonCode).toContain('result["URLMs"] = to_timedelta(self.urlms)'); + + const csharpCode = generateCSharpSessionEventsCode(schema); + + expect(csharpCode).toContain( + '[JsonPropertyName("durationMs")]\n public required TimeSpan Duration { get; set; }' + ); + expect(csharpCode).toContain( + '[JsonPropertyName("integerDurationMs")]\n public required TimeSpan IntegerDuration { get; set; }' + ); + expect(csharpCode).toContain( + '[JsonPropertyName("optionalDurationMs")]\n public TimeSpan? OptionalDuration { get; set; }' + ); + expect(csharpCode).toContain( + '[JsonPropertyName("nullableDurationMs")]\n public TimeSpan? NullableDuration { get; set; }' + ); + expect(csharpCode).toContain( + '[JsonPropertyName("URLMs")]\n public required TimeSpan URLMs { get; set; }' + ); + }); + it("collapses redundant callable wrapper lambdas", () => { const schema: JSONSchema7 = { definitions: { diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 056a9ca14..4f1b0bc03 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -685,7 +685,7 @@ class AssistantUsageData: duration: timedelta | None = None initiator: str | None = None input_tokens: float | None = None - inter_token_latency_ms: timedelta | None = None + inter_token_latency: timedelta | None = None output_tokens: float | None = None # Deprecated: this field is deprecated. parent_tool_call_id: str | None = None @@ -693,7 +693,7 @@ class AssistantUsageData: quota_snapshots: dict[str, AssistantUsageQuotaSnapshot] | None = None reasoning_effort: str | None = None reasoning_tokens: float | None = None - ttft_ms: timedelta | None = None + ttft: timedelta | None = None @staticmethod def from_dict(obj: Any) -> "AssistantUsageData": @@ -708,14 +708,14 @@ def from_dict(obj: Any) -> "AssistantUsageData": duration = from_union([from_none, from_timedelta], obj.get("duration")) initiator = from_union([from_none, from_str], obj.get("initiator")) input_tokens = from_union([from_none, from_float], obj.get("inputTokens")) - inter_token_latency_ms = from_union([from_none, from_timedelta], obj.get("interTokenLatencyMs")) + inter_token_latency = from_union([from_none, from_timedelta], obj.get("interTokenLatencyMs")) output_tokens = from_union([from_none, from_float], obj.get("outputTokens")) parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) provider_call_id = from_union([from_none, from_str], obj.get("providerCallId")) quota_snapshots = from_union([from_none, lambda x: from_dict(AssistantUsageQuotaSnapshot.from_dict, x)], obj.get("quotaSnapshots")) reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) reasoning_tokens = from_union([from_none, from_float], obj.get("reasoningTokens")) - ttft_ms = from_union([from_none, from_timedelta], obj.get("ttftMs")) + ttft = from_union([from_none, from_timedelta], obj.get("ttftMs")) return AssistantUsageData( model=model, api_call_id=api_call_id, @@ -727,14 +727,14 @@ def from_dict(obj: Any) -> "AssistantUsageData": duration=duration, initiator=initiator, input_tokens=input_tokens, - inter_token_latency_ms=inter_token_latency_ms, + inter_token_latency=inter_token_latency, output_tokens=output_tokens, parent_tool_call_id=parent_tool_call_id, provider_call_id=provider_call_id, quota_snapshots=quota_snapshots, reasoning_effort=reasoning_effort, reasoning_tokens=reasoning_tokens, - ttft_ms=ttft_ms, + ttft=ttft, ) def to_dict(self) -> dict: @@ -758,8 +758,8 @@ def to_dict(self) -> dict: result["initiator"] = from_union([from_none, from_str], self.initiator) if self.input_tokens is not None: result["inputTokens"] = from_union([from_none, to_float], self.input_tokens) - if self.inter_token_latency_ms is not None: - result["interTokenLatencyMs"] = from_union([from_none, to_timedelta], self.inter_token_latency_ms) + if self.inter_token_latency is not None: + result["interTokenLatencyMs"] = from_union([from_none, to_timedelta], self.inter_token_latency) if self.output_tokens is not None: result["outputTokens"] = from_union([from_none, to_float], self.output_tokens) if self.parent_tool_call_id is not None: @@ -772,8 +772,8 @@ def to_dict(self) -> dict: result["reasoningEffort"] = from_union([from_none, from_str], self.reasoning_effort) if self.reasoning_tokens is not None: result["reasoningTokens"] = from_union([from_none, to_float], self.reasoning_tokens) - if self.ttft_ms is not None: - result["ttftMs"] = from_union([from_none, to_timedelta], self.ttft_ms) + if self.ttft is not None: + result["ttftMs"] = from_union([from_none, to_timedelta], self.ttft) return result @@ -1751,7 +1751,7 @@ class ModelCallFailureData: "Failed LLM API call metadata for telemetry" source: ModelCallFailureSource api_call_id: str | None = None - duration_ms: timedelta | None = None + duration: timedelta | None = None error_message: str | None = None initiator: str | None = None model: str | None = None @@ -1763,7 +1763,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData": assert isinstance(obj, dict) source = parse_enum(ModelCallFailureSource, obj.get("source")) api_call_id = from_union([from_none, from_str], obj.get("apiCallId")) - duration_ms = from_union([from_none, from_timedelta], obj.get("durationMs")) + duration = from_union([from_none, from_timedelta], obj.get("durationMs")) error_message = from_union([from_none, from_str], obj.get("errorMessage")) initiator = from_union([from_none, from_str], obj.get("initiator")) model = from_union([from_none, from_str], obj.get("model")) @@ -1772,7 +1772,7 @@ def from_dict(obj: Any) -> "ModelCallFailureData": return ModelCallFailureData( source=source, api_call_id=api_call_id, - duration_ms=duration_ms, + duration=duration, error_message=error_message, initiator=initiator, model=model, @@ -1785,8 +1785,8 @@ def to_dict(self) -> dict: result["source"] = to_enum(ModelCallFailureSource, self.source) if self.api_call_id is not None: result["apiCallId"] = from_union([from_none, from_str], self.api_call_id) - if self.duration_ms is not None: - result["durationMs"] = from_union([from_none, to_timedelta], self.duration_ms) + if self.duration is not None: + result["durationMs"] = from_union([from_none, to_timedelta], self.duration) if self.error_message is not None: result["errorMessage"] = from_union([from_none, from_str], self.error_message) if self.initiator is not None: @@ -3046,7 +3046,7 @@ def to_dict(self) -> dict: class SessionScheduleCreatedData: "Scheduled prompt registered via /every or /after" id: int - interval_ms: timedelta + interval: timedelta prompt: str display_prompt: str | None = None recurring: bool | None = None @@ -3055,13 +3055,13 @@ class SessionScheduleCreatedData: def from_dict(obj: Any) -> "SessionScheduleCreatedData": assert isinstance(obj, dict) id = from_int(obj.get("id")) - interval_ms = from_timedelta(obj.get("intervalMs")) + interval = from_timedelta(obj.get("intervalMs")) prompt = from_str(obj.get("prompt")) display_prompt = from_union([from_none, from_str], obj.get("displayPrompt")) recurring = from_union([from_none, from_bool], obj.get("recurring")) return SessionScheduleCreatedData( id=id, - interval_ms=interval_ms, + interval=interval, prompt=prompt, display_prompt=display_prompt, recurring=recurring, @@ -3070,7 +3070,7 @@ def from_dict(obj: Any) -> "SessionScheduleCreatedData": def to_dict(self) -> dict: result: dict = {} result["id"] = to_int(self.id) - result["intervalMs"] = to_timedelta_int(self.interval_ms) + result["intervalMs"] = to_timedelta_int(self.interval) result["prompt"] = from_str(self.prompt) if self.display_prompt is not None: result["displayPrompt"] = from_union([from_none, from_str], self.display_prompt) @@ -3086,7 +3086,7 @@ class SessionShutdownData: model_metrics: dict[str, ShutdownModelMetric] session_start_time: float shutdown_type: ShutdownType - total_api_duration_ms: timedelta + total_api_duration: timedelta total_premium_requests: float conversation_tokens: float | None = None current_model: str | None = None @@ -3104,7 +3104,7 @@ def from_dict(obj: Any) -> "SessionShutdownData": model_metrics = from_dict(ShutdownModelMetric.from_dict, obj.get("modelMetrics")) session_start_time = from_float(obj.get("sessionStartTime")) shutdown_type = parse_enum(ShutdownType, obj.get("shutdownType")) - total_api_duration_ms = from_timedelta(obj.get("totalApiDurationMs")) + total_api_duration = from_timedelta(obj.get("totalApiDurationMs")) total_premium_requests = from_float(obj.get("totalPremiumRequests")) conversation_tokens = from_union([from_none, from_float], obj.get("conversationTokens")) current_model = from_union([from_none, from_str], obj.get("currentModel")) @@ -3119,7 +3119,7 @@ def from_dict(obj: Any) -> "SessionShutdownData": model_metrics=model_metrics, session_start_time=session_start_time, shutdown_type=shutdown_type, - total_api_duration_ms=total_api_duration_ms, + total_api_duration=total_api_duration, total_premium_requests=total_premium_requests, conversation_tokens=conversation_tokens, current_model=current_model, @@ -3137,7 +3137,7 @@ def to_dict(self) -> dict: result["modelMetrics"] = from_dict(lambda x: to_class(ShutdownModelMetric, x), self.model_metrics) result["sessionStartTime"] = to_float(self.session_start_time) result["shutdownType"] = to_enum(ShutdownType, self.shutdown_type) - result["totalApiDurationMs"] = to_timedelta(self.total_api_duration_ms) + result["totalApiDurationMs"] = to_timedelta(self.total_api_duration) result["totalPremiumRequests"] = to_float(self.total_premium_requests) if self.conversation_tokens is not None: result["conversationTokens"] = from_union([from_none, to_float], self.conversation_tokens) @@ -3728,7 +3728,7 @@ class SubagentCompletedData: agent_display_name: str agent_name: str tool_call_id: str - duration_ms: timedelta | None = None + duration: timedelta | None = None model: str | None = None total_tokens: float | None = None total_tool_calls: float | None = None @@ -3739,7 +3739,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData": agent_display_name = from_str(obj.get("agentDisplayName")) agent_name = from_str(obj.get("agentName")) tool_call_id = from_str(obj.get("toolCallId")) - duration_ms = from_union([from_none, from_timedelta], obj.get("durationMs")) + duration = from_union([from_none, from_timedelta], obj.get("durationMs")) model = from_union([from_none, from_str], obj.get("model")) total_tokens = from_union([from_none, from_float], obj.get("totalTokens")) total_tool_calls = from_union([from_none, from_float], obj.get("totalToolCalls")) @@ -3747,7 +3747,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData": agent_display_name=agent_display_name, agent_name=agent_name, tool_call_id=tool_call_id, - duration_ms=duration_ms, + duration=duration, model=model, total_tokens=total_tokens, total_tool_calls=total_tool_calls, @@ -3758,8 +3758,8 @@ def to_dict(self) -> dict: result["agentDisplayName"] = from_str(self.agent_display_name) result["agentName"] = from_str(self.agent_name) result["toolCallId"] = from_str(self.tool_call_id) - if self.duration_ms is not None: - result["durationMs"] = from_union([from_none, to_timedelta], self.duration_ms) + if self.duration is not None: + result["durationMs"] = from_union([from_none, to_timedelta], self.duration) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) if self.total_tokens is not None: @@ -3788,7 +3788,7 @@ class SubagentFailedData: agent_name: str error: str tool_call_id: str - duration_ms: timedelta | None = None + duration: timedelta | None = None model: str | None = None total_tokens: float | None = None total_tool_calls: float | None = None @@ -3800,7 +3800,7 @@ def from_dict(obj: Any) -> "SubagentFailedData": agent_name = from_str(obj.get("agentName")) error = from_str(obj.get("error")) tool_call_id = from_str(obj.get("toolCallId")) - duration_ms = from_union([from_none, from_timedelta], obj.get("durationMs")) + duration = from_union([from_none, from_timedelta], obj.get("durationMs")) model = from_union([from_none, from_str], obj.get("model")) total_tokens = from_union([from_none, from_float], obj.get("totalTokens")) total_tool_calls = from_union([from_none, from_float], obj.get("totalToolCalls")) @@ -3809,7 +3809,7 @@ def from_dict(obj: Any) -> "SubagentFailedData": agent_name=agent_name, error=error, tool_call_id=tool_call_id, - duration_ms=duration_ms, + duration=duration, model=model, total_tokens=total_tokens, total_tool_calls=total_tool_calls, @@ -3821,8 +3821,8 @@ def to_dict(self) -> dict: result["agentName"] = from_str(self.agent_name) result["error"] = from_str(self.error) result["toolCallId"] = from_str(self.tool_call_id) - if self.duration_ms is not None: - result["durationMs"] = from_union([from_none, to_timedelta], self.duration_ms) + if self.duration is not None: + result["durationMs"] = from_union([from_none, to_timedelta], self.duration) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) if self.total_tokens is not None: diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 354a5eda8..199a79913 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -217,6 +217,17 @@ function toPascalCase(name: string): string { return name.charAt(0).toUpperCase() + name.slice(1); } +function stripDurationMillisecondsSuffix(name: string): string { + if (name.length > 2 && name.endsWith("Ms") && /[a-z]/.test(name.charAt(name.length - 3))) { + return name.slice(0, -2); + } + return name; +} + +function toCSharpPropertyName(propName: string, schema: JSONSchema7): string { + return toPascalCase(isDurationProperty(schema) ? stripDurationMillisecondsSuffix(propName) : propName); +} + function typeToClassName(typeName: string): string { return splitCSharpIdentifierParts(typeName).map(toPascalCasePart).join(""); } @@ -441,6 +452,11 @@ function emitDataAnnotations(schema: JSONSchema7, indent: string): string[] { * milliseconds-based JSON converter rather than expecting ISO 8601 strings. */ function isDurationProperty(schema: JSONSchema7): boolean { + const nullableInner = getNullableInner(schema); + if (nullableInner) { + return isDurationProperty(nullableInner); + } + if (schema.format === "duration") { const t = schema.type; if (t === "number" || t === "integer") return true; @@ -736,7 +752,7 @@ function generateFlattenedBooleanDiscriminatedClass( const propertyEntries = Array.from(flattenedProperties.entries()).sort(([a], [b]) => a.localeCompare(b)); for (const [propName, info] of propertyEntries) { const isReq = info.variantCount === variants.length && info.requiredCount === variants.length; - const csharpName = toPascalCase(propName); + const csharpName = toCSharpPropertyName(propName, info.schema); const csharpType = resolver(info.schema, renamedBase, csharpName, isReq, knownTypes, nestedClasses, enumOutput); lines.push(""); @@ -839,13 +855,14 @@ function generateDerivedClass( if (propName === discriminatorProperty) continue; const isReq = required.has(propName); - const csharpName = toPascalCase(propName); - const csharpType = propertyResolver(propSchema as JSONSchema7, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput); - - lines.push(...xmlDocPropertyComment((propSchema as JSONSchema7).description, propName, " ")); - lines.push(...emitDataAnnotations(propSchema as JSONSchema7, " ")); - if (isSchemaDeprecated(propSchema as JSONSchema7)) pushObsoleteAttributes(lines, " "); - if (isDurationProperty(propSchema as JSONSchema7)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); + const prop = propSchema as JSONSchema7; + const csharpName = toCSharpPropertyName(propName, prop); + const csharpType = propertyResolver(prop, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput); + + lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); + lines.push(...emitDataAnnotations(prop, " ")); + if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); + if (isDurationProperty(prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; @@ -1064,7 +1081,7 @@ function generateNestedClass( if (typeof propSchema !== "object") continue; const prop = propSchema as JSONSchema7; const isReq = required.has(propName); - const csharpName = toPascalCase(propName); + const csharpName = toCSharpPropertyName(propName, prop); const csharpType = resolveSessionPropertyType(prop, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput); lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); @@ -1206,13 +1223,14 @@ function generateDataClass(variant: EventVariant, knownTypes: Map a.localeCompare(b))) { if (typeof propSchema !== "object") continue; const isReq = required.has(propName); - const csharpName = toPascalCase(propName); - const csharpType = resolveSessionPropertyType(propSchema as JSONSchema7, variant.dataClassName, csharpName, isReq, knownTypes, nestedClasses, enumOutput); + const prop = propSchema as JSONSchema7; + const csharpName = toCSharpPropertyName(propName, prop); + const csharpType = resolveSessionPropertyType(prop, variant.dataClassName, csharpName, isReq, knownTypes, nestedClasses, enumOutput); - lines.push(...xmlDocPropertyComment((propSchema as JSONSchema7).description, propName, " ")); - lines.push(...emitDataAnnotations(propSchema as JSONSchema7, " ")); - if (isSchemaDeprecated(propSchema as JSONSchema7)) pushObsoleteAttributes(lines, " "); - if (isDurationProperty(propSchema as JSONSchema7)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); + lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); + lines.push(...emitDataAnnotations(prop, " ")); + if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); + if (isDurationProperty(prop)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`); if (!isReq) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`); lines.push(` [JsonPropertyName("${propName}")]`); const reqMod = isReq && !csharpType.endsWith("?") ? "required " : ""; @@ -1229,7 +1247,7 @@ function emitSessionEventEnvelopeProperty( nestedClasses: Map, enumOutput: string[] ): string[] { - const csharpName = toPascalCase(property.name); + const csharpName = toCSharpPropertyName(property.name, property.schema); const csharpType = resolveSessionPropertyType( property.schema, "SessionEvent", @@ -1591,7 +1609,7 @@ function emitRpcClass( if (typeof propSchema !== "object") continue; const prop = propSchema as JSONSchema7; const isReq = requiredSet.has(propName); - const csharpName = toPascalCase(propName); + const csharpName = toCSharpPropertyName(propName, prop); const csharpType = resolveRpcType(prop, isReq, className, csharpName, extraClasses); lines.push(...xmlDocPropertyComment(prop.description, propName, " ")); @@ -1790,11 +1808,14 @@ function emitServerInstanceMethod( if (typeof pSchema !== "object") continue; const isReq = requiredSet.has(pName); const jsonSchema = pSchema as JSONSchema7; + const csharpName = requestClassName + ? toCSharpPropertyName(pName, jsonSchema) + : toPascalCase(pName); const csType = requestClassName - ? resolveRpcType(jsonSchema, isReq, requestClassName, toPascalCase(pName), classes) + ? resolveRpcType(jsonSchema, isReq, requestClassName, csharpName, classes) : schemaTypeToCSharp(jsonSchema, isReq, rpcKnownTypes); sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); - bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`); + bodyAssignments.push(`${csharpName} = ${pName}`); if (requiresArgumentNullCheck(csType, isReq)) { argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`); } @@ -1938,16 +1959,19 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas if (useRequestParameter) { sigParams.push(`${requestClassName}? request = null`); parameterDescriptions.push({ name: "request", description: rpcParamsDescription(method, effectiveParams) }); - for (const [pName] of paramEntries) { - bodyAssignments.push(`${toPascalCase(pName)} = request?.${toPascalCase(pName)}`); + for (const [pName, pSchema] of paramEntries) { + if (typeof pSchema !== "object") continue; + const csharpName = toCSharpPropertyName(pName, pSchema as JSONSchema7); + bodyAssignments.push(`${csharpName} = request?.${csharpName}`); } } else { for (const [pName, pSchema] of paramEntries) { if (typeof pSchema !== "object") continue; const isReq = requiredSet.has(pName); - const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, toPascalCase(pName), classes); + const csharpName = toCSharpPropertyName(pName, pSchema as JSONSchema7); + const csType = resolveRpcType(pSchema as JSONSchema7, isReq, requestClassName, csharpName, classes); sigParams.push(`${csType} ${pName}${isReq ? "" : " = null"}`); - bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`); + bodyAssignments.push(`${csharpName} = ${pName}`); if (requiresArgumentNullCheck(csType, isReq)) { argumentNullChecks.push(`${indent} ArgumentNullException.ThrowIfNull(${pName});`); } diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index d69e7fcc9..aaf151282 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -647,6 +647,57 @@ function toSnakeCase(s: string): string { .toLowerCase(); } +function stripDurationMillisecondsSuffix(name: string): string { + if (name.length > 2 && name.endsWith("Ms") && /[a-z]/.test(name.charAt(name.length - 3))) { + return name.slice(0, -2); + } + return name; +} + +function isPyDurationProperty(propSchema: JSONSchema7, ctx: PyCodegenCtx): boolean { + if (propSchema.$ref && typeof propSchema.$ref === "string") { + const resolved = resolveSchema(propSchema, ctx.definitions); + if (resolved && resolved !== propSchema) { + return isPyDurationProperty(resolved, ctx); + } + } + + if (propSchema.allOf && propSchema.allOf.length === 1 && typeof propSchema.allOf[0] === "object") { + return isPyDurationProperty(propSchema.allOf[0] as JSONSchema7, ctx); + } + + if (propSchema.anyOf) { + const variants = (propSchema.anyOf as JSONSchema7[]) + .filter((item) => typeof item === "object") + .map( + (item) => + resolveSchema(item as JSONSchema7, ctx.definitions) ?? + (item as JSONSchema7) + ); + const nonNull = variants.filter((item) => !isPyNullLikeSchema(item)); + return nonNull.length === 1 && isPyDurationProperty(nonNull[0], ctx); + } + + if (propSchema.format !== "duration") { + return false; + } + + const type = propSchema.type; + if (type === "number" || type === "integer") { + return true; + } + if (Array.isArray(type)) { + const nonNullTypes = type.filter((value) => value !== "null"); + return nonNullTypes.length === 1 && (nonNullTypes[0] === "number" || nonNullTypes[0] === "integer"); + } + + return false; +} + +function toPyFieldName(propName: string, propSchema: JSONSchema7, ctx: PyCodegenCtx): string { + return toSnakeCase(isPyDurationProperty(propSchema, ctx) ? stripDurationMillisecondsSuffix(propName) : propName); +} + function toPascalCase(s: string): string { return s .split(/[._]/) @@ -952,7 +1003,7 @@ function getPySharedEventEnvelopeProperties(schema: JSONSchema7, ctx: PyCodegenC return { ...property, jsonName: name, - fieldName: toSnakeCase(name), + fieldName: toPyFieldName(name, schema, ctx), required, hasDefault: !required || resolved.annotation.includes(" | None"), resolved, @@ -1473,7 +1524,7 @@ function emitPyClass( const resolved = resolvePyPropertyType(propSchema, typeName, propName, isRequired, ctx); return { jsonName: propName, - fieldName: toSnakeCase(propName), + fieldName: toPyFieldName(propName, propSchema, ctx), isRequired, resolved, }; @@ -1631,7 +1682,7 @@ function emitPyFlatDiscriminatedUnion( return { jsonName: propName, - fieldName: toSnakeCase(propName), + fieldName: toPyFieldName(propName, propSchema, ctx), isRequired: requiredInAll, resolved, };