Skip to content

Commit 7c50785

Browse files
committed
Making progress on the client side
Signed-off-by: Emmanuel Hugonnet <[email protected]>
1 parent fb6af2a commit 7c50785

File tree

11 files changed

+284
-154
lines changed

11 files changed

+284
-154
lines changed

client/transport/jsonrpc/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
<groupId>${project.groupId}</groupId>
3434
<artifactId>a2a-java-sdk-spec</artifactId>
3535
</dependency>
36+
<dependency>
37+
<groupId>${project.groupId}</groupId>
38+
<artifactId>a2a-java-sdk-spec-grpc</artifactId>
39+
</dependency>
3640
<dependency>
3741
<groupId>org.junit.jupiter</groupId>
3842
<artifactId>junit-jupiter-api</artifactId>

client/transport/jsonrpc/src/main/java/io/a2a/client/transport/jsonrpc/JSONRPCTransport.java

Lines changed: 39 additions & 88 deletions
Large diffs are not rendered by default.

reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) {
9696
Multi<? extends JSONRPCResponse<?>> streamingResponse = null;
9797
JSONRPCErrorResponse error = null;
9898
try {
99-
JSONRPCRequest<?> request = JSONRPCUtils.parseBody(body);
99+
JSONRPCRequest<?> request = JSONRPCUtils.parseRequestBody(body);
100100
context.getState().put(METHOD_NAME_KEY, request.getMethod());
101101
if (request instanceof NonStreamingJSONRPCRequest nonStreamingRequest) {
102102
nonStreamingResponse = processNonStreamingRequest(nonStreamingRequest, context);

spec-grpc/src/main/java/io/a2a/grpc/mapper/GetTaskPushNotificationConfigParamsMapper.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,12 @@ public interface GetTaskPushNotificationConfigParamsMapper {
2828
@Mapping(target = "pushNotificationConfigId", expression = "java(ResourceNameParser.parseGetTaskPushNotificationConfigName(proto.getName())[1])")
2929
@Mapping(target = "metadata", ignore = true)
3030
GetTaskPushNotificationConfigParams fromProto(io.a2a.grpc.GetTaskPushNotificationConfigRequest proto);
31+
32+
/**
33+
* Converts domain Message to proto Message.Uses CommonFieldMapper for metadata conversion and ADDER_PREFERRED for lists.
34+
* @param domain
35+
* @return
36+
*/
37+
@Mapping(target = "name", expression = "java(ResourceNameParser.defineGetTaskPushNotificationConfigName(domain.id(), domain.pushNotificationConfigId()))")
38+
io.a2a.grpc.GetTaskPushNotificationConfigRequest toProto(GetTaskPushNotificationConfigParams domain);
3139
}

spec-grpc/src/main/java/io/a2a/grpc/mapper/ListTasksResultMapper.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@ public interface ListTasksResultMapper {
2323
*/
2424
@Mapping(target = "nextPageToken", source = "nextPageToken", conditionExpression = "java(domain.nextPageToken() != null)")
2525
io.a2a.grpc.ListTasksResponse toProto(ListTasksResult domain);
26+
27+
@Mapping(source = "nextPageToken", target = "nextPageToken", conditionExpression = "java(proto.getNextPageToken() != null)")
28+
ListTasksResult fromProto(io.a2a.grpc.ListTasksResponse proto);
2629
}

spec-grpc/src/main/java/io/a2a/grpc/mapper/ResourceNameParser.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ public static String extractTaskId(String resourceName) {
1818
return resourceName.substring(resourceName.lastIndexOf('/') + 1);
1919
}
2020

21+
/**
22+
* Define the task name form its taskId as in "tasks/{taskId}".
23+
*
24+
* @param taskId the taskId
25+
* @return the task name
26+
*/
27+
public static String defineTaskName(String taskId) {
28+
return "tasks/" + taskId;
29+
}
30+
2131
/**
2232
* Parses a task push notification config resource name and extracts taskId and configId.
2333
* <p>
@@ -63,6 +73,13 @@ public static String[] parseGetTaskPushNotificationConfigName(String resourceNam
6373

6474
return new String[]{taskId, configId};
6575
}
76+
public static String defineGetTaskPushNotificationConfigName(String taskId, String configId) {
77+
String name= "tasks/" + taskId + "/pushNotificationConfigs";
78+
if(configId != null && !configId.isBlank()) {
79+
name = name + '/' + configId;
80+
}
81+
return name;
82+
}
6683

6784
/**
6885
* Extracts the parent ID (task ID) from a parent resource name like "tasks/{taskId}".

spec-grpc/src/main/java/io/a2a/grpc/mapper/SetTaskPushNotificationConfigMapper.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ public interface SetTaskPushNotificationConfigMapper {
3838
@Mapping(target = "pushNotificationConfig", expression = "java(mapPushNotificationConfigWithId(request))")
3939
TaskPushNotificationConfig fromProto(SetTaskPushNotificationConfigRequest request);
4040

41+
/**
42+
* Converts SetTaskPushNotificationConfigRequest to domain TaskPushNotificationConfig.
43+
* <p>
44+
* Extracts taskId from parent resource name and maps PushNotificationConfig with
45+
* ID override from config_id field.
46+
*
47+
* @param config the domainTaskPushNotificationConfig
48+
* @return proto SetTaskPushNotificationConfigRequest
49+
*/
50+
@Mapping(target = "parent", expression = "java(ResourceNameParser.defineTaskName(config.taskId()))")
51+
@Mapping(target = "configId", expression = "java(config.pushNotificationConfig().id())")
52+
@Mapping(target = "config", expression = "java(mapPushNotificationConfig(config))")
53+
SetTaskPushNotificationConfigRequest toProto(TaskPushNotificationConfig config);
4154
/**
4255
* Extracts the task ID from the parent resource name.
4356
* <p>
@@ -94,4 +107,16 @@ default PushNotificationConfig mapPushNotificationConfigWithId(SetTaskPushNotifi
94107

95108
return result;
96109
}
110+
111+
/**
112+
* Maps the protobuf PushNotificationConfig to domain, injecting config_id from request.
113+
* <p>
114+
* The config_id from the request overrides the ID in the proto's PushNotificationConfig.
115+
*
116+
* @param request the protobuf SetTaskPushNotificationConfigRequest
117+
* @return domain PushNotificationConfig with config_id injected
118+
*/
119+
default io.a2a.grpc.TaskPushNotificationConfig mapPushNotificationConfig(TaskPushNotificationConfig domain) {
120+
return TaskPushNotificationConfigMapper.INSTANCE.toProto(domain);
121+
}
97122
}

spec-grpc/src/main/java/io/a2a/grpc/mapper/TaskIdParamsMapper.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ public interface TaskIdParamsMapper {
2424
@Mapping(target = "id", expression = "java(ResourceNameParser.extractTaskId(proto.getName()))")
2525
@Mapping(target = "metadata", ignore = true)
2626
TaskIdParams fromProtoCancelTaskRequest(io.a2a.grpc.CancelTaskRequest proto);
27+
28+
/**
29+
* Converts proto CancelTaskRequest to domain TaskIdParams.
30+
* Extracts task ID from the resource name.
31+
*/
32+
@BeanMapping(builder = @Builder(buildMethod = "build"))
33+
@Mapping(target = "name", expression = "java(ResourceNameParser.defineTaskName(domain.id()))")
34+
io.a2a.grpc.CancelTaskRequest toProtoCancelTaskRequest(TaskIdParams domain);
35+
2736

2837
/**
2938
* Converts proto SubscribeToTaskRequest to domain TaskIdParams.

spec-grpc/src/main/java/io/a2a/grpc/mapper/TaskQueryParamsMapper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ public interface TaskQueryParamsMapper {
2525
@Mapping(target = "historyLength", source = "historyLength")
2626
@Mapping(target = "metadata", ignore = true)
2727
TaskQueryParams fromProto(io.a2a.grpc.GetTaskRequest proto);
28+
29+
@BeanMapping(builder = @Builder(buildMethod = "build"))
30+
@Mapping(target = "name", expression = "java(ResourceNameParser.defineTaskName(domain.id()))")
31+
@Mapping(target = "historyLength", source = "historyLength")
32+
io.a2a.grpc.GetTaskRequest toProto(TaskQueryParams domain);
2833
}

spec-grpc/src/main/java/io/a2a/grpc/utils/JSONRPCUtils.java

Lines changed: 120 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.a2a.grpc.utils;
22

3+
34
import com.fasterxml.jackson.core.JsonProcessingException;
45
import com.fasterxml.jackson.databind.JsonMappingException;
56
import com.google.gson.Gson;
@@ -8,28 +9,39 @@
89
import com.google.gson.JsonObject;
910
import com.google.gson.JsonParser;
1011
import com.google.gson.Strictness;
12+
import com.google.gson.stream.JsonWriter;
1113
import com.google.protobuf.InvalidProtocolBufferException;
1214
import com.google.protobuf.util.JsonFormat;
1315
import io.a2a.spec.CancelTaskRequest;
16+
import io.a2a.spec.CancelTaskResponse;
1417
import io.a2a.spec.DeleteTaskPushNotificationConfigRequest;
18+
import io.a2a.spec.DeleteTaskPushNotificationConfigResponse;
1519
import io.a2a.spec.GetAuthenticatedExtendedCardRequest;
1620
import io.a2a.spec.GetTaskPushNotificationConfigRequest;
21+
import io.a2a.spec.GetTaskPushNotificationConfigResponse;
1722
import io.a2a.spec.GetTaskRequest;
23+
import io.a2a.spec.GetTaskResponse;
1824
import io.a2a.spec.IdJsonMappingException;
1925
import io.a2a.spec.InvalidParamsError;
2026
import io.a2a.spec.JSONRPCError;
2127
import io.a2a.spec.JSONRPCMessage;
2228
import io.a2a.spec.JSONRPCRequest;
29+
import io.a2a.spec.JSONRPCResponse;
2330
import io.a2a.spec.ListTaskPushNotificationConfigRequest;
31+
import io.a2a.spec.ListTaskPushNotificationConfigResponse;
2432
import io.a2a.spec.ListTasksRequest;
33+
import io.a2a.spec.ListTasksResponse;
2534
import io.a2a.spec.MethodNotFoundJsonMappingException;
2635
import io.a2a.spec.SendMessageRequest;
36+
import io.a2a.spec.SendMessageResponse;
2737
import io.a2a.spec.SendStreamingMessageRequest;
2838
import io.a2a.spec.SetTaskPushNotificationConfigRequest;
39+
import io.a2a.spec.SetTaskPushNotificationConfigResponse;
2940
import io.a2a.spec.SubscribeToTaskRequest;
3041
import java.io.IOException;
3142
import java.io.StringWriter;
3243
import java.io.Writer;
44+
import java.util.UUID;
3345
import java.util.logging.Level;
3446
import java.util.logging.Logger;
3547

@@ -38,15 +50,15 @@ public class JSONRPCUtils {
3850
private static final Logger log = Logger.getLogger(JSONRPCUtils.class.getName());
3951
private static final Gson GSON = new GsonBuilder().setStrictness(Strictness.STRICT).create();
4052

41-
public static JSONRPCRequest<?> parseBody(String body) throws JsonProcessingException {
53+
public static JSONRPCRequest<?> parseRequestBody(String body) throws JsonProcessingException {
4254
JsonElement jelement = JsonParser.parseString(body);
4355
JsonObject jsonRpc = jelement.getAsJsonObject();
4456
if (!jsonRpc.has("method")) {
4557
throw new IdJsonMappingException("Missing method", getIdIfPossible(jsonRpc));
4658
}
4759
String version = getAndValidateJsonrpc(jsonRpc);
48-
String method = jsonRpc.get("method").getAsString();
4960
Object id = getAndValidateId(jsonRpc);
61+
String method = jsonRpc.get("method").getAsString();
5062
JsonElement paramsNode = jsonRpc.get("params");
5163

5264
switch (method) {
@@ -93,24 +105,78 @@ public static JSONRPCRequest<?> parseBody(String body) throws JsonProcessingExce
93105
case GetAuthenticatedExtendedCardRequest.METHOD -> {
94106
return new GetAuthenticatedExtendedCardRequest(version, id, method, null);
95107
}
96-
case SendStreamingMessageRequest.METHOD-> {
108+
case SendStreamingMessageRequest.METHOD -> {
97109
io.a2a.grpc.SendMessageRequest.Builder builder = io.a2a.grpc.SendMessageRequest.newBuilder();
98110
parseRequestBody(paramsNode, builder);
99111
return new SendStreamingMessageRequest(version, id, method, ProtoUtils.FromProto.messageSendParams(builder));
100112
}
101-
case SubscribeToTaskRequest.METHOD-> {
113+
case SubscribeToTaskRequest.METHOD -> {
102114
io.a2a.grpc.SubscribeToTaskRequest.Builder builder = io.a2a.grpc.SubscribeToTaskRequest.newBuilder();
103115
parseRequestBody(paramsNode, builder);
104116
return new SubscribeToTaskRequest(version, id, method, ProtoUtils.FromProto.taskIdParams(builder));
105117
}
106-
default -> throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(jsonRpc));
118+
default ->
119+
throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(jsonRpc));
120+
}
121+
}
122+
123+
public static JSONRPCResponse<?> parseResponseBody(String body, String method) throws JsonProcessingException {
124+
JsonElement jelement = JsonParser.parseString(body);
125+
JsonObject jsonRpc = jelement.getAsJsonObject();
126+
String version = getAndValidateJsonrpc(jsonRpc);
127+
Object id = getAndValidateId(jsonRpc);
128+
JsonElement paramsNode = jsonRpc.get("result");
129+
switch (method) {
130+
case GetTaskRequest.METHOD -> {
131+
io.a2a.grpc.Task.Builder builder = io.a2a.grpc.Task.newBuilder();
132+
parseRequestBody(paramsNode, builder);
133+
return new GetTaskResponse(id, ProtoUtils.FromProto.task(builder));
134+
}
135+
case CancelTaskRequest.METHOD -> {
136+
io.a2a.grpc.Task.Builder builder = io.a2a.grpc.Task.newBuilder();
137+
parseRequestBody(paramsNode, builder);
138+
return new CancelTaskResponse(id, ProtoUtils.FromProto.task(builder));
139+
}
140+
case ListTasksRequest.METHOD -> {
141+
io.a2a.grpc.ListTasksResponse.Builder builder = io.a2a.grpc.ListTasksResponse.newBuilder();
142+
parseRequestBody(paramsNode, builder);
143+
return new ListTasksResponse(id, ProtoUtils.FromProto.listTasksResult(builder));
144+
}
145+
case SetTaskPushNotificationConfigRequest.METHOD -> {
146+
io.a2a.grpc.TaskPushNotificationConfig.Builder builder = io.a2a.grpc.TaskPushNotificationConfig.newBuilder();
147+
parseRequestBody(paramsNode, builder);
148+
return new SetTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.taskPushNotificationConfig(builder));
149+
}
150+
case GetTaskPushNotificationConfigRequest.METHOD -> {
151+
io.a2a.grpc.TaskPushNotificationConfig.Builder builder = io.a2a.grpc.TaskPushNotificationConfig.newBuilder();
152+
parseRequestBody(paramsNode, builder);
153+
return new GetTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.taskPushNotificationConfig(builder));
154+
}
155+
case SendMessageRequest.METHOD -> {
156+
io.a2a.grpc.SendMessageResponse.Builder builder = io.a2a.grpc.SendMessageResponse.newBuilder();
157+
parseRequestBody(paramsNode, builder);
158+
if(builder.hasMsg()) {
159+
return new SendMessageResponse(id, ProtoUtils.FromProto.message(builder.getMsg()));
160+
}
161+
return new SendMessageResponse(id, ProtoUtils.FromProto.task(builder.getTask()));
162+
}
163+
case ListTaskPushNotificationConfigRequest.METHOD -> {
164+
io.a2a.grpc.ListTaskPushNotificationConfigResponse.Builder builder = io.a2a.grpc.ListTaskPushNotificationConfigResponse.newBuilder();
165+
parseRequestBody(paramsNode, builder);
166+
return new ListTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.listTaskPushNotificationConfigParams(builder));
167+
}
168+
case DeleteTaskPushNotificationConfigRequest.METHOD -> {
169+
return new DeleteTaskPushNotificationConfigResponse(id);
170+
}
171+
default ->
172+
throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(jsonRpc));
107173
}
108174
}
109175

110176
protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf.Message.Builder builder) throws JSONRPCError {
111177
try (Writer writer = new StringWriter()) {
112178
GSON.toJson(jsonRpc, writer);
113-
parseRequestBody(writer.toString(), builder);
179+
JSONRPCUtils.parseRequestBody(writer.toString(), builder);
114180
} catch (IOException e) {
115181
log.log(Level.SEVERE, "Error parsing JSON request body: {0}", jsonRpc);
116182
log.log(Level.SEVERE, "Parse error details", e);
@@ -160,38 +226,58 @@ protected static Object getAndValidateId(JsonObject jsonRpc) throws JsonProcessi
160226
return id;
161227
}
162228

163-
164-
/**
165-
166-
public static String toJsonRPCString(String id, com.google.protobuf.Message.Builder builder) {
167-
try (StringWriter result = new StringWriter()) {
168-
JsonObjectBuilder json = Json.createObjectBuilder();
169-
json.add("jsonrpc", "2.0");
170-
json.add("id", id);
171-
json.add("result", Json.createObjectBuilder(Json.createParser(new StringReader(JsonFormat.printer().print(builder))).getObject()));
172-
Json.createWriter(result).writeObject(json.build());
229+
public static String toJsonRPCString(String requestId, String method, com.google.protobuf.MessageOrBuilder builder) {
230+
try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result).beginObject()) {
231+
output.name("jsonrpc").value("2.0");
232+
String id = requestId;
233+
if (requestId == null) {
234+
id = UUID.randomUUID().toString();
235+
}
236+
output.name("id").value(id);
237+
if (method != null) {
238+
output.name("method").value(method);
239+
}
240+
output.name("result").beginObject().jsonValue(JsonFormat.printer().print(builder));
241+
output.endObject().endObject();
173242
return result.toString();
174243
} catch (IOException ex) {
175244
throw new RuntimeException(ex);
176245
}
177246
}
178247

179-
public static String toJsonRPCString(String id, JSONRPCError error) {
180-
try (StringWriter result = new StringWriter()) {
181-
JsonObjectBuilder errorBuilder = Json.createObjectBuilder();
182-
errorBuilder.add("code", error.getCode());
183-
errorBuilder.add("message", error.getMessage());
184-
if (error.getData() != null) {
185-
errorBuilder.add("data", error.getData().toString());
186-
}
187-
JsonObjectBuilder json = Json.createObjectBuilder();
188-
json.add("jsonrpc", "2.0");
189-
json.add("id", id);
190-
json.add("error", errorBuilder);
191-
Json.createWriter(result).writeObject(json.build());
192-
return result.toString();
193-
} catch (IOException ex) {
194-
throw new RuntimeException(ex);
195-
}
196-
}*/
248+
/**
249+
*
250+
* public static String toJsonRPCString(String id, com.google.protobuf.Message.Builder builder) {
251+
* try (StringWriter result = new StringWriter()) {
252+
* JsonObjectBuilder json = Json.createObjectBuilder();
253+
* json.add("jsonrpc", "2.0");
254+
* json.add("id", id);
255+
* json.add("result", Json.createObjectBuilder(Json.createParser(new
256+
* StringReader(JsonFormat.printer().print(builder))).getObject()));
257+
* Json.createWriter(result).writeObject(json.build());
258+
* return result.toString();
259+
* } catch (IOException ex) {
260+
* throw new RuntimeException(ex);
261+
* }
262+
* }
263+
*
264+
* public static String toJsonRPCString(String id, JSONRPCError error) {
265+
* try (StringWriter result = new StringWriter()) {
266+
* JsonObjectBuilder errorBuilder = Json.createObjectBuilder();
267+
* errorBuilder.add("code", error.getCode());
268+
* errorBuilder.add("message", error.getMessage());
269+
* if (error.getData() != null) {
270+
* errorBuilder.add("data", error.getData().toString());
271+
* }
272+
* JsonObjectBuilder json = Json.createObjectBuilder();
273+
* json.add("jsonrpc", "2.0");
274+
* json.add("id", id);
275+
* json.add("error", errorBuilder);
276+
* Json.createWriter(result).writeObject(json.build());
277+
* return result.toString();
278+
* } catch (IOException ex) {
279+
* throw new RuntimeException(ex);
280+
* }
281+
* }
282+
*/
197283
}

0 commit comments

Comments
 (0)