Skip to content

Commit 0238c29

Browse files
authored
fix: Change to use OffsetDateTime for compatibility with other languages (#242)
fixed #232 replace LocalDateTime to OffsetDateTime and deleted pattern because JavaTimeModule correctly serializes/deserializes
1 parent ef190ab commit 0238c29

File tree

4 files changed

+130
-6
lines changed

4 files changed

+130
-6
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
import java.nio.charset.StandardCharsets;
55
import java.time.Instant;
6-
import java.time.LocalDateTime;
76
import java.time.ZoneOffset;
7+
import java.time.OffsetDateTime;
88
import java.util.ArrayList;
99
import java.util.List;
1010
import java.util.Map;
@@ -292,7 +292,7 @@ private static io.a2a.grpc.TaskStatus taskStatus(TaskStatus taskStatus) {
292292
builder.setUpdate(message(taskStatus.message()));
293293
}
294294
if (taskStatus.timestamp() != null) {
295-
Instant instant = taskStatus.timestamp().toInstant(ZoneOffset.UTC);
295+
Instant instant = taskStatus.timestamp().toInstant();
296296
builder.setTimestamp(com.google.protobuf.Timestamp.newBuilder().setSeconds(instant.getEpochSecond()).setNanos(instant.getNano()).build());
297297
}
298298
return builder.build();
@@ -892,7 +892,7 @@ private static TaskStatus taskStatus(io.a2a.grpc.TaskStatus taskStatus) {
892892
return new TaskStatus(
893893
taskState(taskStatus.getState()),
894894
taskStatus.hasUpdate() ? message(taskStatus.getUpdate()) : null,
895-
LocalDateTime.ofInstant(Instant.ofEpochSecond(taskStatus.getTimestamp().getSeconds(), taskStatus.getTimestamp().getNanos()), ZoneOffset.UTC)
895+
OffsetDateTime.ofInstant(Instant.ofEpochSecond(taskStatus.getTimestamp().getSeconds(), taskStatus.getTimestamp().getNanos()), ZoneOffset.UTC)
896896
);
897897
}
898898

spec-grpc/src/test/java/io/a2a/grpc/utils/ToProtoTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import io.a2a.spec.TaskStatus;
2323
import io.a2a.spec.TaskStatusUpdateEvent;
2424
import io.a2a.spec.TextPart;
25+
26+
import java.time.OffsetDateTime;
2527
import java.util.Collections;
2628
import java.util.List;
2729
import java.util.Map;
@@ -269,4 +271,22 @@ public void convertSendMessageConfiguration() {
269271
assertEquals(1, result.getAcceptedOutputModesCount());
270272
assertEquals("text", result.getAcceptedOutputModesBytes(0).toStringUtf8());
271273
}
274+
275+
@Test
276+
public void convertTaskTimestampStatus() {
277+
OffsetDateTime expectedTimestamp = OffsetDateTime.parse("2024-10-05T12:34:56Z");
278+
TaskStatus testStatus = new TaskStatus(TaskState.COMPLETED, null, expectedTimestamp);
279+
Task task = new Task.Builder()
280+
.id("task-123")
281+
.contextId("context-456")
282+
.status(testStatus)
283+
.build();
284+
285+
io.a2a.grpc.Task grpcTask = ProtoUtils.ToProto.task(task);
286+
task = ProtoUtils.FromProto.task(grpcTask);
287+
TaskStatus status = task.getStatus();
288+
assertEquals(TaskState.COMPLETED, status.state());
289+
assertNotNull(status.timestamp());
290+
assertEquals(expectedTimestamp, status.timestamp());
291+
}
272292
}

spec/src/main/java/io/a2a/spec/TaskStatus.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.a2a.spec;
22

3-
import java.time.LocalDateTime;
3+
import java.time.OffsetDateTime;
4+
import java.time.ZoneOffset;
45

56
import com.fasterxml.jackson.annotation.JsonFormat;
67
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@@ -14,14 +15,23 @@
1415
@JsonInclude(JsonInclude.Include.NON_ABSENT)
1516
@JsonIgnoreProperties(ignoreUnknown = true)
1617
public record TaskStatus(TaskState state, Message message,
17-
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS") LocalDateTime timestamp) {
18+
@JsonFormat(shape = JsonFormat.Shape.STRING) OffsetDateTime timestamp) {
1819

1920
public TaskStatus {
2021
Assert.checkNotNullParam("state", state);
21-
timestamp = timestamp == null ? LocalDateTime.now() : timestamp;
22+
timestamp = timestamp == null ? OffsetDateTime.now(ZoneOffset.UTC) : timestamp;
2223
}
2324

2425
public TaskStatus(TaskState state) {
2526
this(state, null, null);
2627
}
28+
29+
/**
30+
* Constructor for testing purposes.
31+
* @param state the task state
32+
* @param timestamp timestamp generation
33+
*/
34+
TaskStatus(TaskState state, OffsetDateTime timestamp) {
35+
this(state, null, timestamp);
36+
}
2737
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package io.a2a.spec;
2+
3+
import com.fasterxml.jackson.databind.DeserializationFeature;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.time.OffsetDateTime;
9+
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
import static org.junit.jupiter.api.Assertions.assertNotNull;
12+
import static org.junit.jupiter.api.Assertions.assertThrows;
13+
14+
public class TaskStatusTest {
15+
16+
private static final ObjectMapper OBJECT_MAPPER;
17+
18+
private static final String REPLACE_TIMESTAMP_PATTERN = ".*\"timestamp\":\"([^\"]+)\",?.*";
19+
20+
static {
21+
OBJECT_MAPPER = new ObjectMapper();
22+
OBJECT_MAPPER.registerModule(new JavaTimeModule());
23+
OBJECT_MAPPER.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false);
24+
}
25+
26+
@Test
27+
public void testTaskStatusWithSetTimestamp() {
28+
TaskState state = TaskState.WORKING;
29+
OffsetDateTime offsetDateTime = OffsetDateTime.parse("2023-10-01T12:00:00Z");
30+
TaskStatus status = new TaskStatus(state, offsetDateTime);
31+
32+
assertNotNull(status.timestamp());
33+
assertEquals(offsetDateTime, status.timestamp());
34+
}
35+
36+
@Test
37+
public void testTaskStatusWithProvidedTimestamp() {
38+
OffsetDateTime providedTimestamp = OffsetDateTime.parse("2024-01-01T00:00:00Z");
39+
TaskState state = TaskState.COMPLETED;
40+
TaskStatus status = new TaskStatus(state, providedTimestamp);
41+
42+
assertEquals(providedTimestamp, status.timestamp());
43+
}
44+
45+
@Test
46+
public void testTaskStatusSerializationUsesISO8601Format() throws Exception {
47+
OffsetDateTime expectedTimestamp = OffsetDateTime.parse("2023-10-01T12:00:00.234-05:00");
48+
TaskState state = TaskState.WORKING;
49+
TaskStatus status = new TaskStatus(state, expectedTimestamp);
50+
51+
String json = OBJECT_MAPPER.writeValueAsString(status);
52+
53+
String expectedJson = "{\"state\":\"working\",\"timestamp\":\"2023-10-01T12:00:00.234-05:00\"}";
54+
assertEquals(expectedJson, json);
55+
}
56+
57+
@Test
58+
public void testTaskStatusDeserializationWithValidISO8601Format() throws Exception {
59+
String validJson = "{"
60+
+ "\"state\": \"auth-required\","
61+
+ "\"timestamp\": \"2023-10-01T12:00:00.10+03:00\""
62+
+ "}";
63+
64+
TaskStatus result = OBJECT_MAPPER.readValue(validJson, TaskStatus.class);
65+
assertEquals(TaskState.AUTH_REQUIRED, result.state());
66+
assertNotNull(result.timestamp());
67+
assertEquals(OffsetDateTime.parse("2023-10-01T12:00:00.100+03:00"), result.timestamp());
68+
}
69+
70+
@Test
71+
public void testTaskStatusDeserializationWithInvalidISO8601FormatFails() {
72+
String invalidJson = "{"
73+
+ "\"state\": \"completed\","
74+
+ "\"timestamp\": \"2023/10/01 12:00:00\""
75+
+ "}";
76+
77+
assertThrows(
78+
com.fasterxml.jackson.databind.exc.InvalidFormatException.class,
79+
() -> OBJECT_MAPPER.readValue(invalidJson, TaskStatus.class)
80+
);
81+
}
82+
83+
@Test
84+
public void testTaskStatusJsonTimestampMatchesISO8601Regex() throws Exception {
85+
TaskState state = TaskState.WORKING;
86+
OffsetDateTime expectedTimestamp = OffsetDateTime.parse("2023-10-01T12:00:00.234Z");
87+
TaskStatus status = new TaskStatus(state, expectedTimestamp);
88+
89+
String json = OBJECT_MAPPER.writeValueAsString(status);
90+
91+
String timestampValue = json.replaceAll(REPLACE_TIMESTAMP_PATTERN, "$1");
92+
assertEquals(expectedTimestamp, OffsetDateTime.parse(timestampValue));
93+
}
94+
}

0 commit comments

Comments
 (0)