Skip to content

Commit 29df505

Browse files
committed
first simple batch of Mappers
1 parent e879e8c commit 29df505

File tree

12 files changed

+307
-31
lines changed

12 files changed

+307
-31
lines changed

client/base/src/main/java/io/a2a/client/ClientBuilder.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,17 @@ private ClientTransport buildClientTransport() throws A2AClientException {
9292
AgentInterface agentInterface = findBestClientTransport();
9393

9494
// Get the transport provider associated with the protocol
95-
ClientTransportProvider clientTransportProvider = transportProviderRegistry.get(agentInterface.transport());
95+
ClientTransportProvider clientTransportProvider = transportProviderRegistry.get(agentInterface.protocolBinding());
9696
if (clientTransportProvider == null) {
97-
throw new A2AClientException("No client available for " + agentInterface.transport());
97+
throw new A2AClientException("No client available for " + agentInterface.protocolBinding());
9898
}
9999
Class<? extends ClientTransport> transportProtocolClass = clientTransportProvider.getTransportProtocolClass();
100100

101101
// Retrieve the configuration associated with the preferred transport
102102
ClientTransportConfig<? extends ClientTransport> clientTransportConfig = clientTransports.get(transportProtocolClass);
103103

104104
if (clientTransportConfig == null) {
105-
throw new A2AClientException("Missing required TransportConfig for " + agentInterface.transport());
105+
throw new A2AClientException("Missing required TransportConfig for " + agentInterface.protocolBinding());
106106
}
107107

108108
return clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url());
@@ -113,7 +113,7 @@ private Map<String, String> getServerPreferredTransports() {
113113
serverPreferredTransports.put(agentCard.preferredTransport(), agentCard.url());
114114
if (agentCard.additionalInterfaces() != null) {
115115
for (AgentInterface agentInterface : agentCard.additionalInterfaces()) {
116-
serverPreferredTransports.putIfAbsent(agentInterface.transport(), agentInterface.url());
116+
serverPreferredTransports.putIfAbsent(agentInterface.protocolBinding(), agentInterface.url());
117117
}
118118
}
119119
return serverPreferredTransports;

client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JsonMessages.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ public class JsonMessages {
1414
"url": "https://georoute-agent.example.com/a2a/v1",
1515
"preferredTransport": "JSONRPC",
1616
"additionalInterfaces" : [
17-
{"url": "https://georoute-agent.example.com/a2a/v1", "transport": "JSONRPC"},
18-
{"url": "https://georoute-agent.example.com/a2a/grpc", "transport": "GRPC"},
19-
{"url": "https://georoute-agent.example.com/a2a/json", "transport": "HTTP+JSON"}
17+
{"url": "https://georoute-agent.example.com/a2a/v1", "protocolBinding": "JSONRPC"},
18+
{"url": "https://georoute-agent.example.com/a2a/grpc", "protocolBinding": "GRPC"},
19+
{"url": "https://georoute-agent.example.com/a2a/json", "protocolBinding": "HTTP+JSON"}
2020
],
2121
"provider": {
2222
"organization": "Example Geo Services Inc.",

http-client/src/test/java/io/a2a/client/http/JsonMessages.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ public class JsonMessages {
1414
"url": "https://georoute-agent.example.com/a2a/v1",
1515
"preferredTransport": "JSONRPC",
1616
"additionalInterfaces" : [
17-
{"url": "https://georoute-agent.example.com/a2a/v1", "transport": "JSONRPC"},
18-
{"url": "https://georoute-agent.example.com/a2a/grpc", "transport": "GRPC"},
19-
{"url": "https://georoute-agent.example.com/a2a/json", "transport": "HTTP+JSON"}
17+
{"url": "https://georoute-agent.example.com/a2a/v1", "protocolBinding": "JSONRPC"},
18+
{"url": "https://georoute-agent.example.com/a2a/grpc", "protocolBinding": "GRPC"},
19+
{"url": "https://georoute-agent.example.com/a2a/json", "protocolBinding": "HTTP+JSON"}
2020
],
2121
"provider": {
2222
"organization": "Example Geo Services Inc.",

pom.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<jakarta.json-api.version>2.1.3</jakarta.json-api.version>
5555
<jakarta.ws.rs-api.version>3.1.0</jakarta.ws.rs-api.version>
5656
<junit.version>5.13.4</junit.version>
57+
<mapstruct.version>1.6.3</mapstruct.version>
5758
<mockito-core.version>5.17.0</mockito-core.version>
5859
<mockserver.version>5.15.0</mockserver.version>
5960
<mutiny-zero.version>1.1.1</mutiny-zero.version>
@@ -223,6 +224,16 @@
223224
<artifactId>jakarta.inject-api</artifactId>
224225
<version>${jakarta.inject.jakarta.inject-api.version}</version>
225226
</dependency>
227+
<dependency>
228+
<groupId>org.mapstruct</groupId>
229+
<artifactId>mapstruct</artifactId>
230+
<version>${mapstruct.version}</version>
231+
</dependency>
232+
<dependency>
233+
<groupId>org.mapstruct</groupId>
234+
<artifactId>mapstruct-processor</artifactId>
235+
<version>${mapstruct.version}</version>
236+
</dependency>
226237
<dependency>
227238
<groupId>jakarta.json</groupId>
228239
<artifactId>jakarta.json-api</artifactId>
@@ -341,6 +352,11 @@
341352
<artifactId>nullaway</artifactId>
342353
<version>${nullaway.version}</version>
343354
</path>
355+
<path>
356+
<groupId>org.mapstruct</groupId>
357+
<artifactId>mapstruct-processor</artifactId>
358+
<version>${mapstruct.version}</version>
359+
</path>
344360
<!-- Other annotation processors go here.
345361
346362
If 'annotationProcessorPaths' is set, processors will no longer be

server-common/src/main/java/io/a2a/server/AgentCardValidator.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ static void validateTransportConfiguration(AgentCard agentCard, Set<String> avai
8888
// check that the primary URL matches the URL for the preferred transport
8989
if (agentCard.additionalInterfaces() != null) {
9090
agentCard.additionalInterfaces().stream()
91-
.filter(agentInterface -> agentInterface.transport().equals(agentCard.preferredTransport()))
91+
.filter(agentInterface -> agentInterface.protocolBinding().equals(agentCard.preferredTransport()))
9292
.findFirst()
9393
.ifPresent(preferredTransportAgentInterface -> {
9494
if (!preferredTransportAgentInterface.url().equals(agentCard.url())) {
@@ -120,8 +120,8 @@ private static Set<String> getAgentCardTransports(AgentCard agentCard) {
120120
// Add additional interface transports
121121
if (agentCard.additionalInterfaces() != null) {
122122
for (AgentInterface agentInterface : agentCard.additionalInterfaces()) {
123-
if (agentInterface.transport() != null) {
124-
transportStrings.add(agentInterface.transport());
123+
if (agentInterface.protocolBinding() != null) {
124+
transportStrings.add(agentInterface.protocolBinding());
125125
}
126126
}
127127
}

spec-grpc/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
<groupId>com.google.api.grpc</groupId>
5151
<artifactId>proto-google-common-protos</artifactId>
5252
</dependency>
53+
<dependency>
54+
<groupId>org.mapstruct</groupId>
55+
<artifactId>mapstruct</artifactId>
56+
</dependency>
57+
5358

5459
<!-- Annotation dependency for generated code -->
5560
<dependency>
@@ -72,6 +77,11 @@
7277
<artifactId>junit-jupiter-engine</artifactId>
7378
<scope>test</scope>
7479
</dependency>
80+
<dependency>
81+
<groupId>com.google.protobuf</groupId>
82+
<artifactId>protobuf-java-util</artifactId>
83+
<scope>provided</scope>
84+
</dependency>
7585
</dependencies>
7686

7787
<build>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.a2a.grpc.mapper;
2+
3+
import org.mapstruct.CollectionMappingStrategy;
4+
import org.mapstruct.Mapper;
5+
import org.mapstruct.Mapping;
6+
import org.mapstruct.factory.Mappers;
7+
8+
/**
9+
* Mapper between {@link io.a2a.spec.AgentInterface} and {@link io.a2a.grpc.AgentInterface}.
10+
*/
11+
@Mapper(config = ProtoMapperConfig.class,
12+
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
13+
public interface AgentInterfaceMapper {
14+
15+
AgentInterfaceMapper INSTANCE = Mappers.getMapper(AgentInterfaceMapper.class);
16+
17+
@IgnoreProtobufInternals
18+
@Mapping(target = "urlBytes", ignore = true)
19+
@Mapping(target = "protocolBindingBytes", ignore = true)
20+
io.a2a.grpc.AgentInterface toProto(io.a2a.spec.AgentInterface domain);
21+
22+
io.a2a.spec.AgentInterface fromProto(io.a2a.grpc.AgentInterface proto);
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.a2a.grpc.mapper;
2+
3+
import org.mapstruct.CollectionMappingStrategy;
4+
import org.mapstruct.Mapper;
5+
import org.mapstruct.Mapping;
6+
import org.mapstruct.factory.Mappers;
7+
8+
/**
9+
* Mapper between {@link io.a2a.spec.AgentProvider} and {@link io.a2a.grpc.AgentProvider}.
10+
*/
11+
@Mapper(config = ProtoMapperConfig.class,
12+
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
13+
public interface AgentProviderMapper {
14+
15+
AgentProviderMapper INSTANCE = Mappers.getMapper(AgentProviderMapper.class);
16+
17+
@IgnoreProtobufInternals
18+
@Mapping(target = "urlBytes", ignore = true)
19+
@Mapping(target = "organizationBytes", ignore = true)
20+
io.a2a.grpc.AgentProvider toProto(io.a2a.spec.AgentProvider domain);
21+
22+
io.a2a.spec.AgentProvider fromProto(io.a2a.grpc.AgentProvider proto);
23+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.a2a.grpc.mapper;
2+
3+
import org.mapstruct.Mapping;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
/**
11+
* Reusable annotation to ignore common protobuf-generated builder methods.
12+
* <p>
13+
* Protobuf generates many internal methods that are not data fields:
14+
* <ul>
15+
* <li>mergeFrom, clearField, clearOneof - Builder manipulation</li>
16+
* <li>unknownFields, mergeUnknownFields - Unknown field handling</li>
17+
* <li>allFields - Field descriptor access</li>
18+
* </ul>
19+
* <p>
20+
* This annotation ignores these methods while keeping {@code unmappedTargetPolicy = ERROR}
21+
* active for actual data fields, ensuring compile-time validation when proto definitions change.
22+
*/
23+
@Retention(RetentionPolicy.CLASS)
24+
@Target(ElementType.METHOD)
25+
@Mapping(target = "mergeFrom", ignore = true)
26+
@Mapping(target = "clearField", ignore = true)
27+
@Mapping(target = "clearOneof", ignore = true)
28+
@Mapping(target = "unknownFields", ignore = true)
29+
@Mapping(target = "mergeUnknownFields", ignore = true)
30+
@Mapping(target = "allFields", ignore = true)
31+
public @interface IgnoreProtobufInternals {
32+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package io.a2a.grpc.mapper;
2+
3+
import com.google.protobuf.*;
4+
import com.google.protobuf.util.Timestamps;
5+
import org.mapstruct.MapperConfig;
6+
import org.mapstruct.ReportingPolicy;
7+
8+
import java.nio.ByteBuffer;
9+
import java.time.Instant;
10+
import java.util.ArrayList;
11+
import java.util.HashMap;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.stream.Collectors;
15+
16+
@MapperConfig(
17+
// 1. FAIL THE BUILD if fields are missing in either Spec or Proto
18+
unmappedTargetPolicy = ReportingPolicy.ERROR,
19+
20+
// 2. Use the default component model (Singleton instance pattern)
21+
componentModel = "default"
22+
)
23+
public interface ProtoMapperConfig {
24+
25+
// ========================================================================
26+
// 1. Time & Duration
27+
// ========================================================================
28+
29+
default Instant map(Timestamp value) {
30+
if (value == null) return null;
31+
return Instant.ofEpochSecond(value.getSeconds(), value.getNanos());
32+
}
33+
34+
default Timestamp map(Instant value) {
35+
if (value == null) return null;
36+
return Timestamp.newBuilder()
37+
.setSeconds(value.getEpochSecond())
38+
.setNanos(value.getNano())
39+
.build();
40+
}
41+
42+
default java.time.Duration map(com.google.protobuf.Duration value) {
43+
if (value == null) return null;
44+
return java.time.Duration.ofSeconds(value.getSeconds(), value.getNanos());
45+
}
46+
47+
default com.google.protobuf.Duration map(java.time.Duration value) {
48+
if (value == null) return null;
49+
return com.google.protobuf.Duration.newBuilder()
50+
.setSeconds(value.getSeconds())
51+
.setNanos(value.getNano())
52+
.build();
53+
}
54+
55+
// ========================================================================
56+
// 2. Binary Data (ByteString)
57+
// ========================================================================
58+
59+
default byte[] map(ByteString value) {
60+
return value == null ? null : value.toByteArray();
61+
}
62+
63+
default ByteString map(byte[] value) {
64+
return value == null ? null : ByteString.copyFrom(value);
65+
}
66+
67+
default ByteBuffer mapToBuffer(ByteString value) {
68+
return value == null ? null : value.asReadOnlyByteBuffer();
69+
}
70+
71+
default ByteString mapFromBuffer(ByteBuffer value) {
72+
return value == null ? null : ByteString.copyFrom(value);
73+
}
74+
75+
// ========================================================================
76+
// 3. Nullable Wrappers (Google Wrappers)
77+
// ========================================================================
78+
79+
// String
80+
default String map(StringValue value) {
81+
return value == null ? null : value.getValue();
82+
}
83+
default StringValue mapString(String value) {
84+
return value == null ? null : StringValue.of(value);
85+
}
86+
87+
// Integer
88+
default Integer map(Int32Value value) {
89+
return value == null ? null : value.getValue();
90+
}
91+
default Int32Value mapInt(Integer value) {
92+
return value == null ? null : Int32Value.of(value);
93+
}
94+
95+
// Long
96+
default Long map(Int64Value value) {
97+
return value == null ? null : value.getValue();
98+
}
99+
default Int64Value mapLong(Long value) {
100+
return value == null ? null : Int64Value.of(value);
101+
}
102+
103+
// Boolean
104+
default Boolean map(BoolValue value) {
105+
return value == null ? null : value.getValue();
106+
}
107+
default BoolValue mapBool(Boolean value) {
108+
return value == null ? null : BoolValue.of(value);
109+
}
110+
111+
// ========================================================================
112+
// 4. JSON-RPC Support (Struct & Value)
113+
// Maps "Struct" -> Map<String, Object>
114+
// Maps "Value" -> Object
115+
// ========================================================================
116+
117+
default Map<String, Object> map(Struct struct) {
118+
if (struct == null) return null;
119+
Map<String, Object> map = new HashMap<>();
120+
for (Map.Entry<String, Value> entry : struct.getFieldsMap().entrySet()) {
121+
map.put(entry.getKey(), map(entry.getValue()));
122+
}
123+
return map;
124+
}
125+
126+
default Struct mapStruct(Map<String, Object> map) {
127+
if (map == null) return null;
128+
Struct.Builder builder = Struct.newBuilder();
129+
for (Map.Entry<String, Object> entry : map.entrySet()) {
130+
builder.putFields(entry.getKey(), mapValue(entry.getValue()));
131+
}
132+
return builder.build();
133+
}
134+
135+
default Object map(Value value) {
136+
if (value == null) return null;
137+
switch (value.getKindCase()) {
138+
case NULL_VALUE: return null;
139+
case NUMBER_VALUE: return value.getNumberValue(); // Returns Double
140+
case STRING_VALUE: return value.getStringValue();
141+
case BOOL_VALUE: return value.getBoolValue();
142+
case STRUCT_VALUE: return map(value.getStructValue());
143+
case LIST_VALUE:
144+
return value.getListValue().getValuesList().stream()
145+
.map(this::map)
146+
.collect(Collectors.toList());
147+
default: return null;
148+
}
149+
}
150+
151+
default Value mapValue(Object object) {
152+
if (object == null) {
153+
return Value.newBuilder().setNullValue(com.google.protobuf.NullValue.NULL_VALUE).build();
154+
}
155+
156+
if (object instanceof String) return Value.newBuilder().setStringValue((String) object).build();
157+
if (object instanceof Boolean) return Value.newBuilder().setBoolValue((Boolean) object).build();
158+
if (object instanceof Number) return Value.newBuilder().setNumberValue(((Number) object).doubleValue()).build();
159+
if (object instanceof Map) {
160+
// Unchecked cast is unavoidable here unless we force Map<String, Object>
161+
@SuppressWarnings("unchecked")
162+
Map<String, Object> map = (Map<String, Object>) object;
163+
return Value.newBuilder().setStructValue(mapStruct(map)).build();
164+
}
165+
if (object instanceof List) {
166+
ListValue.Builder listBuilder = ListValue.newBuilder();
167+
for (Object item : (List<?>) object) {
168+
listBuilder.addValues(mapValue(item));
169+
}
170+
return Value.newBuilder().setListValue(listBuilder).build();
171+
}
172+
173+
// Fallback for unknown types (e.g. custom objects inside Map) -> convert to String?
174+
// For now, throw to catch unexpected types early
175+
throw new IllegalArgumentException("Unsupported type for Proto Value conversion: " + object.getClass().getName());
176+
}
177+
}

0 commit comments

Comments
 (0)