Skip to content

Commit ac7efa5

Browse files
authored
refactor: use @nullable String for tenant across all spec params records (#910)
The tenant field was semantically optional but typed as non-nullable String with "" as an absent-value sentinel in most params records, while GetExtendedAgentCardParams and TaskPushNotificationConfig correctly used @nullable String. This standardizes all params records to @nullable (null = no tenant), removes the Assert.checkNotNullParam assertions for tenant, and updates gRPC MapStruct mappers to skip setTenant when null (conditionExpression) and convert proto "" back to null on fromProto (emptyToNull). Fixes #844 Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
1 parent 5f20bf8 commit ac7efa5

18 files changed

Lines changed: 74 additions & 76 deletions

client/base/src/test/java/org/a2aproject/sdk/client/ClientBuilderTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,13 @@ public void shouldSelectCorrectInterfaceWithClientPreference() throws A2AClientE
163163
}
164164

165165
@Test
166-
public void shouldPreserveEmptyTenant() throws A2AClientException {
166+
public void shouldHaveNullTenantWhenNotSet() throws A2AClientException {
167167
ClientBuilder builder = Client
168168
.builder(card)
169169
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder());
170170

171171
AgentInterface selectedInterface = builder.findBestClientTransport();
172172

173-
Assertions.assertEquals("", selectedInterface.tenant());
173+
Assertions.assertNull(selectedInterface.tenant());
174174
}
175175
}

spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentInterfaceMapper.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
* Mapper between {@link org.a2aproject.sdk.spec.AgentInterface} and {@link org.a2aproject.sdk.grpc.AgentInterface}.
99
*/
1010
@Mapper(config = A2AProtoMapperConfig.class,
11-
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
11+
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED,
12+
uses = A2ACommonFieldMapper.class)
1213
public interface AgentInterfaceMapper {
1314

1415
AgentInterfaceMapper INSTANCE = A2AMappers.getMapper(AgentInterfaceMapper.class);
1516

17+
@Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)")
1618
org.a2aproject.sdk.grpc.AgentInterface toProto(org.a2aproject.sdk.spec.AgentInterface domain);
1719

20+
@Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull")
1821
org.a2aproject.sdk.spec.AgentInterface fromProto(org.a2aproject.sdk.grpc.AgentInterface proto);
1922
}

spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/DeleteTaskPushNotificationConfigParamsMapper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/**
1010
* Mapper between {@link org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest} and {@link org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams}.
1111
*/
12-
@Mapper(config = A2AProtoMapperConfig.class)
12+
@Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class)
1313
public interface DeleteTaskPushNotificationConfigParamsMapper {
1414

1515
DeleteTaskPushNotificationConfigParamsMapper INSTANCE = A2AMappers.getMapper(DeleteTaskPushNotificationConfigParamsMapper.class);
@@ -20,14 +20,14 @@ public interface DeleteTaskPushNotificationConfigParamsMapper {
2020
@BeanMapping(builder = @Builder(buildMethod = "build"))
2121
@Mapping(target = "taskId", source = "taskId")
2222
@Mapping(target = "id", source = "id")
23-
@Mapping(target = "tenant", source = "tenant")
23+
@Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull")
2424
DeleteTaskPushNotificationConfigParams fromProto(org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest proto);
2525

2626
/**
2727
* Converts domain DeleteTaskPushNotificationConfigParams to proto DeleteTaskPushNotificationConfigRequest.
2828
*/
2929
@Mapping(target = "taskId", source = "taskId")
3030
@Mapping(target = "id", source = "id")
31-
@Mapping(target = "tenant", source = "tenant")
31+
@Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)")
3232
org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest toProto(DeleteTaskPushNotificationConfigParams domain);
3333
}

spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/GetTaskPushNotificationConfigParamsMapper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/**
1010
* Mapper between {@link org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest} and {@link org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams}.
1111
*/
12-
@Mapper(config = A2AProtoMapperConfig.class)
12+
@Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class)
1313
public interface GetTaskPushNotificationConfigParamsMapper {
1414

1515
GetTaskPushNotificationConfigParamsMapper INSTANCE = A2AMappers.getMapper(GetTaskPushNotificationConfigParamsMapper.class);
@@ -20,14 +20,14 @@ public interface GetTaskPushNotificationConfigParamsMapper {
2020
@BeanMapping(builder = @Builder(buildMethod = "build"))
2121
@Mapping(target = "taskId", source = "taskId")
2222
@Mapping(target = "id", source = "id")
23-
@Mapping(target = "tenant", source = "tenant")
23+
@Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull")
2424
GetTaskPushNotificationConfigParams fromProto(org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest proto);
2525

2626
/**
2727
* Converts domain GetTaskPushNotificationConfigParams to proto GetTaskPushNotificationConfigRequest.
2828
*/
2929
@Mapping(target = "taskId", source = "taskId")
3030
@Mapping(target = "id", source = "id", conditionExpression = "java(domain.id() != null)")
31-
@Mapping(target = "tenant", source = "tenant")
31+
@Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)")
3232
org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest toProto(GetTaskPushNotificationConfigParams domain);
3333
}

spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTaskPushNotificationConfigsParamsMapper.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/**
1010
* Mapper between {@link org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest} and {@link org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams}.
1111
*/
12-
@Mapper(config = A2AProtoMapperConfig.class)
12+
@Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class)
1313
public interface ListTaskPushNotificationConfigsParamsMapper {
1414

1515
ListTaskPushNotificationConfigsParamsMapper INSTANCE = A2AMappers.getMapper(ListTaskPushNotificationConfigsParamsMapper.class);
@@ -19,13 +19,13 @@ public interface ListTaskPushNotificationConfigsParamsMapper {
1919
*/
2020
@BeanMapping(builder = @Builder(buildMethod = "build"))
2121
@Mapping(target = "id", source = "taskId")
22-
@Mapping(target = "tenant", source = "tenant")
22+
@Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull")
2323
ListTaskPushNotificationConfigsParams fromProto(org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest proto);
2424

2525
/**
2626
* Converts domain ListTaskPushNotificationConfigsParams to proto ListTaskPushNotificationConfigsRequest.
2727
*/
2828
@Mapping(target = "taskId", source = "id")
29-
@Mapping(target = "tenant", source = "tenant")
29+
@Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)")
3030
org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest toProto(ListTaskPushNotificationConfigsParams domain);
3131
}

spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendParamsMapper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public interface MessageSendParamsMapper {
2222
*/
2323
@Mapping(target = "configuration", source = "configuration", conditionExpression = "java(domain.configuration() != null)")
2424
@Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataToProto")
25+
@Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)")
2526
org.a2aproject.sdk.grpc.SendMessageRequest toProto(MessageSendParams domain);
2627

2728
/**
@@ -31,5 +32,6 @@ public interface MessageSendParamsMapper {
3132
*/
3233
@BeanMapping(builder = @Builder(buildMethod = "build"))
3334
@Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataFromProto")
35+
@Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull")
3436
MessageSendParams fromProto(org.a2aproject.sdk.grpc.SendMessageRequest proto);
3537
}

spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskIdParamsMapper.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public interface TaskIdParamsMapper {
2424
@BeanMapping(builder = @Builder(buildMethod = "build"))
2525
@Mapping(target = "id", source = "id")
2626
@Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataFromProto")
27+
@Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull")
2728
CancelTaskParams fromProtoCancelTaskRequest(org.a2aproject.sdk.grpc.CancelTaskRequest proto);
2829

2930
/**
@@ -33,6 +34,7 @@ public interface TaskIdParamsMapper {
3334
@BeanMapping(builder = @Builder(buildMethod = "build"))
3435
@Mapping(target = "id", source = "id")
3536
@Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataToProto")
37+
@Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)")
3638
org.a2aproject.sdk.grpc.CancelTaskRequest toProtoCancelTaskRequest(CancelTaskParams domain);
3739

3840

@@ -42,7 +44,7 @@ public interface TaskIdParamsMapper {
4244
*/
4345
@BeanMapping(builder = @Builder(buildMethod = "build"))
4446
@Mapping(target = "id", source = "id")
45-
@Mapping(target = "tenant", source = "tenant")
47+
@Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull")
4648
TaskIdParams fromProtoSubscribeToTaskRequest(org.a2aproject.sdk.grpc.SubscribeToTaskRequest proto);
4749

4850
/**
@@ -51,5 +53,6 @@ public interface TaskIdParamsMapper {
5153
*/
5254
@BeanMapping(builder = @Builder(buildMethod = "build"))
5355
@Mapping(target = "id", source = "id")
56+
@Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)")
5457
org.a2aproject.sdk.grpc.SubscribeToTaskRequest toProtoSubscribeToTaskRequest(TaskIdParams domain);
5558
}

spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskQueryParamsMapper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* <p>
1212
* Extracts task ID from resource name format "tasks/{id}" using {@link ResourceNameParser}.
1313
*/
14-
@Mapper(config = A2AProtoMapperConfig.class)
14+
@Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class)
1515
public interface TaskQueryParamsMapper {
1616

1717
TaskQueryParamsMapper INSTANCE = A2AMappers.getMapper(TaskQueryParamsMapper.class);
@@ -28,6 +28,6 @@ public interface TaskQueryParamsMapper {
2828
@BeanMapping(builder = @Builder(buildMethod = "build"))
2929
@Mapping(target = "id", source = "id")
3030
@Mapping(target = "historyLength", source = "historyLength")
31-
@Mapping(target = "tenant", source = "tenant")
31+
@Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)")
3232
org.a2aproject.sdk.grpc.GetTaskRequest toProto(TaskQueryParams domain);
3333
}

spec/src/main/java/org/a2aproject/sdk/spec/AgentInterface.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.a2aproject.sdk.spec;
22

33
import org.a2aproject.sdk.util.Assert;
4+
import org.jspecify.annotations.Nullable;
45

56
/**
67
* Declares a combination of a target URL and protocol binding for accessing an agent.
@@ -19,13 +20,13 @@
1920
* @param protocolBinding the protocol binding supported at this URL (e.g., "JSONRPC", "GRPC", "HTTP+JSON") (required)
2021
* @param url the endpoint URL where this interface is available; must be a valid absolute HTTPS URL in production
2122
* (required)
22-
* @param tenant the tenant to be set in the request when calling the agent.
23+
* @param tenant optional tenant to be set in the request when calling the agent.
2324
* @param protocolVersion the version of the A2A protocol this interface exposes (e.g., "1.0", "0.3") (required)
2425
* @see AgentCard
2526
* @see TransportProtocol
2627
* @see <a href="https://a2a-protocol.org/latest/">A2A Protocol Specification</a>
2728
*/
28-
public record AgentInterface(String protocolBinding, String url, String tenant, String protocolVersion) {
29+
public record AgentInterface(String protocolBinding, String url, @Nullable String tenant, String protocolVersion) {
2930

3031
/** The default A2A Protocol version used when not explicitly specified. */
3132
public static final String CURRENT_PROTOCOL_VERSION = "1.0";
@@ -42,7 +43,6 @@ public record AgentInterface(String protocolBinding, String url, String tenant,
4243
public AgentInterface {
4344
Assert.checkNotNullParam("protocolBinding", protocolBinding);
4445
Assert.checkNotNullParam("url", url);
45-
Assert.checkNotNullParam("tenant", tenant);
4646

4747
if (protocolVersion == null || protocolVersion.isEmpty()) {
4848
protocolVersion = CURRENT_PROTOCOL_VERSION;
@@ -67,6 +67,6 @@ public AgentInterface(String protocolBinding, String url, String tenant) {
6767
* @param url the endpoint URL (see class-level JavaDoc)
6868
*/
6969
public AgentInterface(String protocolBinding, String url) {
70-
this(protocolBinding, url, "", CURRENT_PROTOCOL_VERSION);
70+
this(protocolBinding, url, null, CURRENT_PROTOCOL_VERSION);
7171
}
7272
}

spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.a2aproject.sdk.spec;
22

33
import org.a2aproject.sdk.util.Assert;
4-
import org.a2aproject.sdk.util.Utils;
54
import java.util.Collections;
65
import java.util.Map;
76
import org.jspecify.annotations.Nullable;
@@ -17,7 +16,7 @@
1716
* @param metadata optional arbitrary key-value metadata (e.g. cancellation reason)
1817
* @see <a href="https://a2a-protocol.org/latest/">A2A Protocol Specification</a>
1918
*/
20-
public record CancelTaskParams(String id, String tenant, Map<String, Object> metadata) {
19+
public record CancelTaskParams(String id, @Nullable String tenant, Map<String, Object> metadata) {
2120

2221
/**
2322
* Compact constructor for validation.
@@ -28,7 +27,6 @@ public record CancelTaskParams(String id, String tenant, Map<String, Object> met
2827
*/
2928
public CancelTaskParams {
3029
Assert.checkNotNullParam("id", id);
31-
Assert.checkNotNullParam("tenant", tenant);
3230
}
3331

3432
/**
@@ -37,7 +35,7 @@ public record CancelTaskParams(String id, String tenant, Map<String, Object> met
3735
* @param id the task identifier (required)
3836
*/
3937
public CancelTaskParams(String id) {
40-
this(id, "", Collections.emptyMap());
38+
this(id, null, Collections.emptyMap());
4139
}
4240

4341
/**
@@ -80,7 +78,7 @@ public Builder id(String id) {
8078
* @param tenant the tenant identifier
8179
* @return this builder for method chaining
8280
*/
83-
public Builder tenant(String tenant) {
81+
public Builder tenant(@Nullable String tenant) {
8482
this.tenant = tenant;
8583
return this;
8684
}
@@ -105,7 +103,7 @@ public Builder metadata(Map<String, Object> metadata) {
105103
public CancelTaskParams build() {
106104
return new CancelTaskParams(
107105
Assert.checkNotNullParam("id", id),
108-
Utils.defaultIfNull(tenant,""),
106+
tenant,
109107
metadata
110108
);
111109
}

0 commit comments

Comments
 (0)