Skip to content

Commit 98477ff

Browse files
stephentoubCopilot
andcommitted
Fix Java account users RPC generation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3abf38a commit 98477ff

5 files changed

Lines changed: 166 additions & 23 deletions

File tree

java/scripts/codegen/java.ts

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,14 @@ async function generateRpcTypes(schemaPath: string): Promise<void> {
13931393
generatedClasses.set(resultRefName, true);
13941394
allFiles.push(await generateRpcDataClass(resultRefName, resultSchema, packageName, packageDir, method.rpcMethod, "result"));
13951395
}
1396+
} else if (resultRefName && resultSchema.type === "array") {
1397+
// Named array aliases (e.g. AccountGetAllUsersResult) are returned
1398+
// as List<T> by wrappers, but resolving them here discovers any
1399+
// referenced item records that need standalone generation.
1400+
schemaTypeToJava(resultSchema, false, resultRefName, "item", new Map());
1401+
} else if (resultSchema.type === "array") {
1402+
// Inline arrays also need their referenced item records generated.
1403+
schemaTypeToJava(resultSchema, false, `${className}Result`, "item", new Map());
13961404
}
13971405
}
13981406
}
@@ -1521,8 +1529,8 @@ function apiClassName(prefix: string, path: string[]): string {
15211529
}
15221530

15231531
/**
1524-
* Derive the result class name for an RPC method.
1525-
* Handles $ref to named definitions (enums, anyOf unions, objects with properties).
1532+
* Derive the Java result type for an RPC method.
1533+
* Handles $ref to named definitions (enums, anyOf unions, objects with properties, arrays).
15261534
* Falls back to Void for null results or schemas with no meaningful type.
15271535
*/
15281536
function wrapperResultClassName(method: RpcMethodNode): string {
@@ -1553,6 +1561,11 @@ function wrapperResultClassName(method: RpcMethodNode): string {
15531561
if (resolved.type === "object" && !resolved.properties) {
15541562
return refName;
15551563
}
1564+
// Named array aliases → use the underlying List<T> Java type.
1565+
if (resolved.type === "array") {
1566+
const result = schemaTypeToJava(resolved, false, refName, "item", new Map());
1567+
return result.javaType;
1568+
}
15561569
}
15571570
}
15581571

@@ -1568,6 +1581,11 @@ function wrapperResultClassName(method: RpcMethodNode): string {
15681581
return rpcMethodToClassName(method.rpcMethod) + "Result";
15691582
}
15701583

1584+
if (result && typeof result === "object" && result.type === "array") {
1585+
const javaResult = schemaTypeToJava(result, false, `${rpcMethodToClassName(method.rpcMethod)}Result`, "item", new Map());
1586+
return javaResult.javaType;
1587+
}
1588+
15711589
// Free-form object with additionalProperties (e.g., x-opaque-json) → JsonNode
15721590
if (
15731591
result &&
@@ -1582,6 +1600,41 @@ function wrapperResultClassName(method: RpcMethodNode): string {
15821600
return "Void";
15831601
}
15841602

1603+
function wrapperResultTypeExpression(resultType: string): string {
1604+
const listMatch = resultType.match(/^List<([^<>]+)>$/);
1605+
if (listMatch) {
1606+
return `RpcMapper.INSTANCE.getTypeFactory().constructCollectionType(List.class, ${javaClassLiteral(listMatch[1])})`;
1607+
}
1608+
1609+
return javaClassLiteral(resultType);
1610+
}
1611+
1612+
function javaClassLiteral(javaType: string): string {
1613+
return javaType === "Void" ? "Void.class" : `${javaType}.class`;
1614+
}
1615+
1616+
function addWrapperResultImports(resultType: string, allImports: Set<string>, packageName: string): void {
1617+
if (resultType === "Void") {
1618+
return;
1619+
}
1620+
1621+
if (resultType === "JsonNode") {
1622+
allImports.add("com.fasterxml.jackson.databind.JsonNode");
1623+
return;
1624+
}
1625+
1626+
if (resultType.startsWith("List<")) {
1627+
allImports.add("java.util.List");
1628+
}
1629+
1630+
const builtInTypes = new Set(["Boolean", "Double", "Long", "List", "Object", "String", "Void"]);
1631+
for (const typeName of resultType.match(/\b[A-Z][A-Za-z0-9_]*\b/g) ?? []) {
1632+
if (!builtInTypes.has(typeName)) {
1633+
allImports.add(`${packageName}.${typeName}`);
1634+
}
1635+
}
1636+
}
1637+
15851638
/**
15861639
* Return the params class name if the method has a params schema with properties
15871640
* other than sessionId (i.e. there are user-supplied parameters).
@@ -1659,18 +1712,18 @@ function generateApiMethod(
16591712
needsMapper = true;
16601713
lines.push(` com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params);`);
16611714
lines.push(` _p.put("sessionId", ${sessionIdExpr});`);
1662-
lines.push(` return caller.invoke("${method.rpcMethod}", _p, ${resultClass}.class);`);
1715+
lines.push(` return caller.invoke("${method.rpcMethod}", _p, ${wrapperResultTypeExpression(resultClass)});`);
16631716
} else if (hasSessionId) {
1664-
lines.push(` return caller.invoke("${method.rpcMethod}", java.util.Map.of("sessionId", ${sessionIdExpr}), ${resultClass}.class);`);
1717+
lines.push(` return caller.invoke("${method.rpcMethod}", java.util.Map.of("sessionId", ${sessionIdExpr}), ${wrapperResultTypeExpression(resultClass)});`);
16651718
} else {
1666-
lines.push(` return caller.invoke("${method.rpcMethod}", java.util.Map.of(), ${resultClass}.class);`);
1719+
lines.push(` return caller.invoke("${method.rpcMethod}", java.util.Map.of(), ${wrapperResultTypeExpression(resultClass)});`);
16671720
}
16681721
} else {
16691722
// Server-side: pass params directly (or empty map if no params)
16701723
if (hasExtraParams) {
1671-
lines.push(` return caller.invoke("${method.rpcMethod}", params, ${resultClass}.class);`);
1724+
lines.push(` return caller.invoke("${method.rpcMethod}", params, ${wrapperResultTypeExpression(resultClass)});`);
16721725
} else {
1673-
lines.push(` return caller.invoke("${method.rpcMethod}", java.util.Map.of(), ${resultClass}.class);`);
1726+
lines.push(` return caller.invoke("${method.rpcMethod}", java.util.Map.of(), ${wrapperResultTypeExpression(resultClass)});`);
16741727
}
16751728
}
16761729

@@ -1723,13 +1776,7 @@ async function generateNamespaceApiFile(
17231776
for (const [key, method] of tree.methods) {
17241777
const resultClass = wrapperResultClassName(method);
17251778
const paramsClass = wrapperParamsClassName(method);
1726-
if (resultClass !== "Void") {
1727-
if (resultClass === "JsonNode") {
1728-
allImports.add("com.fasterxml.jackson.databind.JsonNode");
1729-
} else {
1730-
allImports.add(`${packageName}.${resultClass}`);
1731-
}
1732-
}
1779+
addWrapperResultImports(resultClass, allImports, packageName);
17331780
if (paramsClass) allImports.add(`${packageName}.${paramsClass}`);
17341781

17351782
const { lines, needsMapper: nm, needsExperimentalImport } = generateApiMethod(key, method, isSession, sessionIdExpr);
@@ -1849,13 +1896,7 @@ async function generateRpcRootFile(
18491896
for (const [key, method] of tree.methods) {
18501897
const resultClass = wrapperResultClassName(method);
18511898
const paramsClass = wrapperParamsClassName(method);
1852-
if (resultClass !== "Void") {
1853-
if (resultClass === "JsonNode") {
1854-
allImports.add("com.fasterxml.jackson.databind.JsonNode");
1855-
} else {
1856-
allImports.add(`${packageName}.${resultClass}`);
1857-
}
1858-
}
1899+
addWrapperResultImports(resultClass, allImports, packageName);
18591900
if (paramsClass) allImports.add(`${packageName}.${paramsClass}`);
18601901

18611902
const { lines, needsMapper: nm, needsExperimentalImport } = generateApiMethod(key, method, isSession, sessionIdExpr);
@@ -1959,7 +2000,10 @@ async function generateRpcCallerInterface(packageName: string, packageDir: strin
19592000
lines.push(``);
19602001
lines.push(`package ${packageName};`);
19612002
lines.push(``);
2003+
lines.push(`import com.fasterxml.jackson.databind.JavaType;`);
2004+
lines.push(`import com.fasterxml.jackson.databind.JsonNode;`);
19622005
lines.push(`import java.util.concurrent.CompletableFuture;`);
2006+
lines.push(`import java.util.concurrent.CompletionException;`);
19632007
lines.push(`import javax.annotation.processing.Generated;`);
19642008
lines.push(``);
19652009
lines.push(`/**`);
@@ -1987,6 +2031,28 @@ async function generateRpcCallerInterface(packageName: string, packageDir: strin
19872031
lines.push(` * @return a {@link CompletableFuture} that completes with the deserialized result`);
19882032
lines.push(` */`);
19892033
lines.push(` <T> CompletableFuture<T> invoke(String method, Object params, Class<T> resultType);`);
2034+
lines.push(``);
2035+
lines.push(` /**`);
2036+
lines.push(` * Invokes a JSON-RPC method and returns a future for the typed response.`);
2037+
lines.push(` *`);
2038+
lines.push(` * @param <T> the expected response type`);
2039+
lines.push(` * @param method the JSON-RPC method name`);
2040+
lines.push(` * @param params the request parameters (may be a {@code Map}, DTO record, or {@code JsonNode})`);
2041+
lines.push(` * @param resultType the Jackson {@link JavaType} of the expected response type`);
2042+
lines.push(` * @return a {@link CompletableFuture} that completes with the deserialized result`);
2043+
lines.push(` */`);
2044+
lines.push(` default <T> CompletableFuture<T> invoke(String method, Object params, JavaType resultType) {`);
2045+
lines.push(` if (resultType.hasRawClass(Void.class) || resultType.hasRawClass(Void.TYPE)) {`);
2046+
lines.push(` return invoke(method, params, Void.class).thenApply(ignored -> null);`);
2047+
lines.push(` }`);
2048+
lines.push(` return invoke(method, params, JsonNode.class).thenApply(result -> {`);
2049+
lines.push(` try {`);
2050+
lines.push(` return RpcMapper.INSTANCE.readerFor(resultType).readValue(result);`);
2051+
lines.push(` } catch (java.io.IOException e) {`);
2052+
lines.push(` throw new CompletionException(e);`);
2053+
lines.push(` }`);
2054+
lines.push(` });`);
2055+
lines.push(` }`);
19902056
lines.push(`}`);
19912057
lines.push(``);
19922058

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
// AUTO-GENERATED FILE - DO NOT EDIT
6+
// Generated from: api.schema.json
7+
8+
package com.github.copilot.generated.rpc;
9+
10+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
11+
import com.fasterxml.jackson.annotation.JsonInclude;
12+
import com.fasterxml.jackson.annotation.JsonProperty;
13+
import javax.annotation.processing.Generated;
14+
15+
/**
16+
* Schema for the `AccountAllUsers` type.
17+
*
18+
* @since 1.0.0
19+
*/
20+
@javax.annotation.processing.Generated("copilot-sdk-codegen")
21+
@JsonInclude(JsonInclude.Include.NON_NULL)
22+
@JsonIgnoreProperties(ignoreUnknown = true)
23+
public record AccountAllUsers(
24+
/** Authentication information for this user */
25+
@JsonProperty("authInfo") Object authInfo,
26+
/** Associated token, if available */
27+
@JsonProperty("token") String token
28+
) {
29+
}

java/src/generated/java/com/github/copilot/generated/rpc/RpcCaller.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
package com.github.copilot.generated.rpc;
99

10+
import com.fasterxml.jackson.databind.JavaType;
11+
import com.fasterxml.jackson.databind.JsonNode;
1012
import java.util.concurrent.CompletableFuture;
13+
import java.util.concurrent.CompletionException;
1114
import javax.annotation.processing.Generated;
1215

1316
/**
@@ -35,4 +38,26 @@ public interface RpcCaller {
3538
* @return a {@link CompletableFuture} that completes with the deserialized result
3639
*/
3740
<T> CompletableFuture<T> invoke(String method, Object params, Class<T> resultType);
41+
42+
/**
43+
* Invokes a JSON-RPC method and returns a future for the typed response.
44+
*
45+
* @param <T> the expected response type
46+
* @param method the JSON-RPC method name
47+
* @param params the request parameters (may be a {@code Map}, DTO record, or {@code JsonNode})
48+
* @param resultType the Jackson {@link JavaType} of the expected response type
49+
* @return a {@link CompletableFuture} that completes with the deserialized result
50+
*/
51+
default <T> CompletableFuture<T> invoke(String method, Object params, JavaType resultType) {
52+
if (resultType.hasRawClass(Void.class) || resultType.hasRawClass(Void.TYPE)) {
53+
return invoke(method, params, Void.class).thenApply(ignored -> null);
54+
}
55+
return invoke(method, params, JsonNode.class).thenApply(result -> {
56+
try {
57+
return RpcMapper.INSTANCE.readerFor(resultType).readValue(result);
58+
} catch (java.io.IOException e) {
59+
throw new CompletionException(e);
60+
}
61+
});
62+
}
3863
}

java/src/generated/java/com/github/copilot/generated/rpc/ServerAccountApi.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
package com.github.copilot.generated.rpc;
99

10+
import java.util.List;
1011
import java.util.concurrent.CompletableFuture;
1112
import javax.annotation.processing.Generated;
1213

@@ -45,8 +46,8 @@ public CompletableFuture<AccountGetCurrentAuthResult> getCurrentAuth() {
4546
* List of all authenticated users
4647
* @since 1.0.0
4748
*/
48-
public CompletableFuture<Void> getAllUsers() {
49-
return caller.invoke("account.getAllUsers", java.util.Map.of(), Void.class);
49+
public CompletableFuture<List<AccountAllUsers>> getAllUsers() {
50+
return caller.invoke("account.getAllUsers", java.util.Map.of(), RpcMapper.INSTANCE.getTypeFactory().constructCollectionType(List.class, AccountAllUsers.class));
5051
}
5152

5253
/**

java/src/test/java/com/github/copilot/RpcWrappersTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,28 @@ void serverRpc_models_list_invokes_correct_rpc_method() {
8585
assertEquals("models.list", stub.calls.get(0).method());
8686
}
8787

88+
@Test
89+
void serverRpc_account_getAllUsers_returns_typed_list() throws Exception {
90+
var stub = new StubCaller();
91+
stub.nextResult = new ObjectMapper().readTree("""
92+
[
93+
{
94+
"authInfo": { "kind": "oauth" },
95+
"token": "token-1"
96+
}
97+
]
98+
""");
99+
100+
var server = new ServerRpc(stub);
101+
var users = server.account.getAllUsers().get();
102+
103+
assertEquals(1, stub.calls.size());
104+
assertEquals("account.getAllUsers", stub.calls.get(0).method());
105+
assertEquals(1, users.size());
106+
assertInstanceOf(Map.class, users.get(0).authInfo());
107+
assertEquals("token-1", users.get(0).token());
108+
}
109+
88110
@Test
89111
void serverRpc_ping_passes_params_directly() {
90112
var stub = new StubCaller();

0 commit comments

Comments
 (0)