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
4 changes: 2 additions & 2 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ let forked = client
.rpc()
.sessions()
.fork(github_copilot_sdk::generated::api_types::SessionsForkRequest {
session_id: "session-id".to_string(),
from_message_id: None,
session_id: "session-id".into(),
to_event_id: None,
})
.await?;
```
Expand Down
232 changes: 116 additions & 116 deletions rust/src/generated/api_types.rs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions rust/tests/e2e/multi_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ async fn both_clients_see_tool_request_and_completion_events() {
#[tokio::test]
async fn one_client_approves_permission_and_both_see_the_result() {
with_e2e_context(
"rust_multi_client",
"multi_client",
"one_client_approves_permission_and_both_see_the_result",
|ctx| {
Box::pin(async move {
Expand Down Expand Up @@ -193,7 +193,7 @@ async fn one_client_approves_permission_and_both_see_the_result() {
#[tokio::test]
async fn one_client_rejects_permission_and_both_see_the_result() {
with_e2e_context(
"rust_multi_client",
"multi_client",
"one_client_rejects_permission_and_both_see_the_result",
|ctx| {
Box::pin(async move {
Expand Down
41 changes: 34 additions & 7 deletions scripts/codegen/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,42 @@ function xmlDocEnumComment(description: string | undefined, indent: string): str
}

function toPascalCase(name: string): string {
if (name.includes("_") || name.includes("-")) {
return name.split(/[-_]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
}
const parts = splitCSharpIdentifierParts(name);
if (parts.length > 1) return parts.map(toPascalCasePart).join("");
return name.charAt(0).toUpperCase() + name.slice(1);
}

function typeToClassName(typeName: string): string {
return typeName.split(/[._]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
return splitCSharpIdentifierParts(typeName).map(toPascalCasePart).join("");
}

function splitCSharpIdentifierParts(value: string): string[] {
return value.split(/[^A-Za-z0-9]+/).filter(Boolean);
}

function toPascalCasePart(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1);
}

function toPascalCaseEnumMember(value: string): string {
return value.split(/[-_.]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
function toCSharpIdentifier(value: string, fallback: string): string {
let identifier = splitCSharpIdentifierParts(value).map(toPascalCasePart).join("");
if (!identifier) {
identifier = fallback;
} else if (!/^[A-Za-z_]/.test(identifier)) {
identifier = `${fallback}${identifier}`;
}
return identifier;
}

function uniqueCSharpIdentifier(value: string, used: Set<string>, fallback: string): string {
const identifier = toCSharpIdentifier(value, fallback);
if (used.has(identifier)) {
throw new Error(
`Generated C# string enum member identifier "${identifier}" is not unique for value "${value}". Add an explicit naming rule instead of stabilizing an arbitrary public member name.`
);
}
used.add(identifier);
return identifier;
}

async function formatCSharpFile(filePath: string): Promise<void> {
Expand Down Expand Up @@ -311,6 +335,7 @@ const COPYRIGHT = `/*-----------------------------------------------------------

const EXPERIMENTAL_ATTRIBUTE = "[Experimental(Diagnostics.Experimental)]";
const OBSOLETE_ATTRIBUTE = `[Obsolete("This member is deprecated and will be removed in a future version.")]`;
const STRING_ENUM_RESERVED_MEMBER_NAMES = new Set(["Value", "Equals", "GetHashCode", "ToString", "Converter"]);

function experimentalAttribute(indent = ""): string {
return `${indent}${EXPERIMENTAL_ATTRIBUTE}`;
Expand Down Expand Up @@ -374,9 +399,11 @@ function getOrCreateEnum(
lines.push(` }`, "");
lines.push(` /// <summary>Gets the value associated with this <see cref="${enumName}"/>.</summary>`);
lines.push(` public string Value => _value ?? string.Empty;`, "");
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(` public static ${enumName} ${toPascalCaseEnumMember(value)} { get; } = new("${value}");`, "");
lines.push(` public static ${enumName} ${memberName} { get; } = new("${escapeCSharpStringLiteral(value)}");`, "");
}
Comment thread
stephentoub marked this conversation as resolved.
lines.push(` /// <summary>Returns a value indicating whether two <see cref="${enumName}"/> instances are equivalent.</summary>`);
lines.push(` public static bool operator ==(${enumName} left, ${enumName} right) => left.Equals(right);`, "");
Expand Down
20 changes: 15 additions & 5 deletions scripts/codegen/go.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ const wrapGoCommentText = wordwrap(goCommentTextWrapLength);

function toPascalCase(s: string): string {
return s
.split(/[._]/)
.split(/[^A-Za-z0-9]+/)
.filter((word) => word.length > 0)
.map((w) => goInitialisms.has(w.toLowerCase()) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1))
.join("");
}
Expand Down Expand Up @@ -92,7 +93,7 @@ function splitGoIdentifierWords(name: string): string[] {
return name
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.split(/[._-]/)
.split(/[^A-Za-z0-9]+/)
.filter((word) => word.length > 0);
}

Expand Down Expand Up @@ -547,8 +548,17 @@ function getOrCreateGoEnum(
const consts = values
.map((value) => ({ value, constSuffix: goEnumConstSuffix(value) }))
.sort((left, right) => `${enumName}${left.constSuffix}`.localeCompare(`${enumName}${right.constSuffix}`));
const usedConstNames = new Map<string, string>();
for (const { value, constSuffix } of consts) {
lines.push(`\t${enumName}${constSuffix} ${enumName} = "${value}"`);
const constName = `${enumName}${constSuffix}`;
const existingValue = usedConstNames.get(constName);
if (existingValue !== undefined) {
throw new Error(
`Generated Go enum const identifier "${constName}" is not unique for values "${existingValue}" and "${value}". Add an explicit naming rule instead of stabilizing an arbitrary public const name.`
);
}
usedConstNames.set(constName, value);
lines.push(`\t${constName} ${enumName} = "${value}"`);
}
lines.push(`)`);

Expand All @@ -558,14 +568,14 @@ function getOrCreateGoEnum(
}

function goEnumConstSuffix(value: string): string {
return value
.split(/[-_.]/)
const suffix = splitGoIdentifierWords(value)
.map((word) =>
goInitialisms.has(word.toLowerCase())
? word.toUpperCase()
: word.charAt(0).toUpperCase() + word.slice(1)
)
.join("");
return suffix || "Value";
}
Comment thread
stephentoub marked this conversation as resolved.

function goDiscriminatedUnionVariantTypeName(
Expand Down
51 changes: 47 additions & 4 deletions scripts/codegen/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,39 @@ const STRING_NEWTYPE_OVERRIDES: Record<string, string> = {

function toPascalCase(s: string): string {
return s
.split(/[._\-\s]+/)
.split(/[^A-Za-z0-9]+/)
.filter(Boolean)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join("");
}

function toRustPascalIdentifier(value: string, fallback: string): string {
let identifier = toPascalCase(value);
if (!identifier) {
identifier = fallback;
} else if (!/^[A-Za-z_]/.test(identifier)) {
identifier = `${fallback}${identifier}`;
}

return RUST_KEYWORDS.has(identifier) ? `${identifier}Value` : identifier;
}

function uniqueRustPascalIdentifier(
value: string,
used: Set<string>,
fallback: string,
reserved: Set<string> = new Set(),
): string {
const identifier = toRustPascalIdentifier(value, fallback);
if (used.has(identifier) || reserved.has(identifier)) {
throw new Error(
`Generated Rust enum variant identifier "${identifier}" is not unique for value "${value}". Add an explicit naming rule instead of stabilizing an arbitrary public variant name.`,
);
}
used.add(identifier);
return identifier;
}

function toSnakeCase(s: string): string {
return s
.replace(/([A-Z])/g, "_$1")
Expand Down Expand Up @@ -233,10 +261,16 @@ function tryEmitRustDiscriminatedUnion(
lines.push("#[serde(untagged)]");
lines.push(`pub enum ${enumName} {`);

const usedVariantNames = new Set<string>();
for (const { schema: variantSchema, typeName } of resolvedVariants) {
const kind = ((variantSchema.properties?.kind as JSONSchema7 | undefined)
?.const ?? typeName) as string;
lines.push(` ${toPascalCase(kind)}(${stripOption(typeName)}),`);
const variantName = uniqueRustPascalIdentifier(
kind,
usedVariantNames,
"Variant",
);
lines.push(` ${variantName}(${stripOption(typeName)}),`);
}

lines.push("}");
Expand Down Expand Up @@ -617,8 +651,15 @@ function emitRustStringEnum(
lines.push("#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]");
lines.push(`pub enum ${enumName} {`);

const usedVariantNames = new Set<string>();
const reservedVariantNames = new Set(["Unknown"]);
for (const value of values) {
const variantName = toPascalCase(value);
const variantName = uniqueRustPascalIdentifier(
value,
usedVariantNames,
"Value",
reservedVariantNames,
);
if (variantName !== value) {
lines.push(` #[serde(rename = "${value}")]`);
}
Expand Down Expand Up @@ -651,7 +692,7 @@ function emitRustConstStringEnum(
}
lines.push("#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]");
lines.push(`pub enum ${enumName} {`);
const variantName = toPascalCase(value);
const variantName = toRustPascalIdentifier(value, "Value");
if (variantName !== value) {
lines.push(` #[serde(rename = "${value}")]`);
}
Expand Down Expand Up @@ -927,6 +968,8 @@ function generateApiTypesCode(apiSchema: ApiSchema): string {
schema.description,
isSchemaExperimental(schema),
);
} else if (getUnionVariants(schema)) {
tryEmitRustDiscriminatedUnion(schema, name, "", ctx);
} else if (isObjectSchema(schema)) {
emitRustStruct(name, schema, ctx, schema.description);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,30 @@ conversations:
function:
name: view
arguments: '{"path":"${workdir}/protected.txt"}'
- messages:
- role: system
content: ${system}
- role: user
content: Edit protected.txt and replace 'protected' with 'hacked'.
- role: assistant
content: I'll help you edit protected.txt to replace 'protected' with 'hacked'. Let me first view the file and then make
the change.
tool_calls:
- id: toolcall_0
type: function
function:
name: report_intent
arguments: '{"intent":"Editing protected.txt file"}'
- id: toolcall_1
type: function
function:
name: view
arguments: '{"path":"${workdir}/protected.txt"}'
- role: tool
tool_call_id: toolcall_0
content: Intent logged
- role: tool
tool_call_id: toolcall_1
content: Permission denied and could not request permission from user
- role: assistant
content: I don't have permission to view or edit protected.txt, so I can't make that change.
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,29 @@ conversations:
function:
name: view
arguments: '{"path":"${workdir}/protected.txt"}'
- messages:
- role: system
content: ${system}
- role: user
content: Edit protected.txt and replace 'protected' with 'hacked'.
- role: assistant
content: I'll view the file first, then make the edit.
tool_calls:
- id: toolcall_0
type: function
function:
name: report_intent
arguments: '{"intent":"Editing protected.txt file"}'
- id: toolcall_1
type: function
function:
name: view
arguments: '{"path":"${workdir}/protected.txt"}'
- role: tool
tool_call_id: toolcall_0
content: Intent logged
- role: tool
tool_call_id: toolcall_1
content: Permission denied and could not request permission from user
- role: assistant
content: I don't have permission to view or edit protected.txt, so I can't make that change.

This file was deleted.

This file was deleted.

Loading