Skip to content

Commit 49a5d0e

Browse files
authored
Merge branch 'main' into dynamic-client-s3-and-bdd
2 parents a92f26b + 9408c36 commit 49a5d0e

2 files changed

Lines changed: 113 additions & 1 deletion

File tree

dynamic-schemas/src/main/java/software/amazon/smithy/java/dynamicschemas/StructDocument.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package software.amazon.smithy.java.dynamicschemas;
77

8+
import java.time.Instant;
89
import java.util.ArrayList;
910
import java.util.Collections;
1011
import java.util.LinkedHashMap;
@@ -16,6 +17,7 @@
1617
import software.amazon.smithy.java.core.schema.SerializableStruct;
1718
import software.amazon.smithy.java.core.schema.TraitKey;
1819
import software.amazon.smithy.java.core.serde.ShapeSerializer;
20+
import software.amazon.smithy.java.core.serde.TimestampFormatter;
1921
import software.amazon.smithy.java.core.serde.document.Document;
2022
import software.amazon.smithy.model.shapes.ShapeId;
2123
import software.amazon.smithy.model.shapes.ShapeType;
@@ -186,7 +188,7 @@ static Document convertDocument(Schema schema, Document delegate, ShapeId servic
186188
}
187189
case BOOLEAN -> new ContentDocument(schema, delegate.asBoolean());
188190
case STRING, ENUM -> new ContentDocument(schema, delegate.asString());
189-
case TIMESTAMP -> new ContentDocument(schema, delegate.asTimestamp());
191+
case TIMESTAMP -> new ContentDocument(schema, convertTimestamp(delegate));
190192
case BYTE, SHORT, INTEGER, INT_ENUM,
191193
LONG, FLOAT, DOUBLE, BIG_INTEGER, BIG_DECIMAL ->
192194
new ContentDocument(schema, delegate.asNumber());
@@ -201,6 +203,28 @@ static Document convertDocument(Schema schema, Document delegate, ShapeId servic
201203
};
202204
}
203205

206+
/**
207+
* Coerces an untyped document into an {@link Instant} for a modeled timestamp member.
208+
*
209+
* <p>The DynamicClient input model is the protocol-agnostic, in-memory Smithy data model, so a timestamp value
210+
* is interpreted by its modeled representation rather than any wire format: a number is epoch-seconds and a string
211+
* is {@code date-time} (ISO-8601). This mirrors how Smithy interprets timestamp values in model nodes (which also
212+
* ignore {@code @timestampFormat}). The protocol's {@code @timestampFormat} is applied later, when the resulting
213+
* Instant is serialized onto the wire.
214+
*/
215+
private static Instant convertTimestamp(Document delegate) {
216+
var type = delegate.type();
217+
if (type == ShapeType.TIMESTAMP) {
218+
// Already a typed timestamp document (e.g. an Instant passed via Document.ofObject).
219+
return delegate.asTimestamp();
220+
} else if (type != ShapeType.STRING) {
221+
// Not-string means number, and numbers are epoch-seconds.
222+
return TimestampFormatter.Prelude.EPOCH_SECONDS.readFromNumber(delegate.asNumber());
223+
} else {
224+
return TimestampFormatter.Prelude.DATE_TIME.readFromString(delegate.asString(), false);
225+
}
226+
}
227+
204228
@Override
205229
public Schema schema() {
206230
return schema;

dynamic-schemas/src/test/java/software/amazon/smithy/java/dynamicschemas/StructDocumentTest.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,94 @@ public void writeString(Schema schema, String value) {
373373
assertThat(set[1], equalTo(schema.member("a")));
374374
}
375375

376+
@Test
377+
public void parsesIsoStringForTimestampMember() {
378+
var schema = Schema.structureBuilder(ShapeId.from("foo#Bar"))
379+
.putMember("when", PreludeSchemas.TIMESTAMP)
380+
.build();
381+
var document = Document.ofObject(Map.of("when", "2024-01-01T00:00:00Z"));
382+
383+
var sd = StructDocument.of(schema, document, ShapeId.from("smithy.example#S"));
384+
385+
var when = sd.getMember("when");
386+
assertThat(when.type(), is(ShapeType.TIMESTAMP));
387+
assertThat(when.asTimestamp(), equalTo(Instant.parse("2024-01-01T00:00:00Z")));
388+
}
389+
390+
@Test
391+
public void readsEpochNumberForTimestampMember() {
392+
var schema = Schema.structureBuilder(ShapeId.from("foo#Bar"))
393+
.putMember("when", PreludeSchemas.TIMESTAMP)
394+
.build();
395+
var document = Document.ofObject(Map.of("when", 1704067200));
396+
397+
var sd = StructDocument.of(schema, document, ShapeId.from("smithy.example#S"));
398+
399+
assertThat(sd.getMember("when").asTimestamp(), equalTo(Instant.parse("2024-01-01T00:00:00Z")));
400+
}
401+
402+
@Test
403+
public void parsesIsoStringsForNestedTimestamps() {
404+
var model = Model.assembler()
405+
.addUnparsedModel("test.smithy", """
406+
$version: "2"
407+
namespace smithy.example
408+
409+
structure Foo {
410+
items: ItemList
411+
}
412+
413+
list ItemList {
414+
member: Item
415+
}
416+
417+
structure Item {
418+
when: Timestamp
419+
}
420+
""")
421+
.assemble()
422+
.unwrap();
423+
var converter = new SchemaConverter(model);
424+
var schema = converter.getSchema(model.expectShape(ShapeId.from("smithy.example#Foo")));
425+
426+
var input = Document.ofObject(Map.of(
427+
"items",
428+
List.of(
429+
Map.of("when", "2024-01-01T00:00:00Z"),
430+
Map.of("when", "2024-06-22T12:00:00Z"))));
431+
var sd = StructDocument.of(schema, input);
432+
433+
var items = sd.getMember("items").asList();
434+
assertThat(items, hasSize(2));
435+
assertThat(items.get(0).getMember("when").asTimestamp(), equalTo(Instant.parse("2024-01-01T00:00:00Z")));
436+
assertThat(items.get(1).getMember("when").asTimestamp(), equalTo(Instant.parse("2024-06-22T12:00:00Z")));
437+
}
438+
439+
@Test
440+
public void ignoresTimestampFormatTraitWhenCoercingInput() {
441+
// @timestampFormat is a wire-serialization concern; the protocol-agnostic input model always treats
442+
// a string as date-time (ISO-8601), regardless of any trait on the member.
443+
var model = Model.assembler()
444+
.addUnparsedModel("test.smithy", """
445+
$version: "2"
446+
namespace smithy.example
447+
448+
structure Foo {
449+
@timestampFormat("epoch-seconds")
450+
when: Timestamp
451+
}
452+
""")
453+
.assemble()
454+
.unwrap();
455+
var converter = new SchemaConverter(model);
456+
var schema = converter.getSchema(model.expectShape(ShapeId.from("smithy.example#Foo")));
457+
458+
var input = Document.ofObject(Map.of("when", "2024-01-01T00:00:00Z"));
459+
var sd = StructDocument.of(schema, input);
460+
461+
assertThat(sd.getMember("when").asTimestamp(), equalTo(Instant.parse("2024-01-01T00:00:00Z")));
462+
}
463+
376464
@Test
377465
public void convertDocumentPassesThroughDataStream() {
378466
var model = Model.assembler()

0 commit comments

Comments
 (0)