diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBindingIndex.java b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBindingIndex.java index baf6413d8e7..0a33dbea619 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBindingIndex.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/knowledge/HttpBindingIndex.java @@ -41,6 +41,7 @@ import software.amazon.smithy.model.traits.HttpQueryTrait; import software.amazon.smithy.model.traits.HttpTrait; import software.amazon.smithy.model.traits.MediaTypeTrait; +import software.amazon.smithy.model.traits.TimestampFormatTrait; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.utils.ListUtils; @@ -220,6 +221,39 @@ public List getResponseBindings(ToShapeId shapeOrId, HttpBinding.Lo .collect(Collectors.toList()); } + /** + * Determines the appropriate timestamp format for a member shape bound to + * a specific location. + * + * @param member Member to derive the timestamp format. + * @param location Location the member is bound to. + * @param defaultFormat The format to use for the body or a default. + * @return Returns the determined timestamp format. + */ + public TimestampFormatTrait.Format determineTimestampFormat( + ToShapeId member, + HttpBinding.Location location, + TimestampFormatTrait.Format defaultFormat + ) { + return index.getShape(member.toShapeId()) + .flatMap(Shape::asMemberShape) + // Use the timestampFormat trait on the member or target if present. + .flatMap(shape -> shape.getMemberTrait(index, TimestampFormatTrait.class)) + .map(TimestampFormatTrait::getFormat) + .orElseGet(() -> { + // Determine the format based on the location. + switch (location) { + case HEADER: + return TimestampFormatTrait.Format.HTTP_DATE; + case QUERY: + case LABEL: + return TimestampFormatTrait.Format.DATE_TIME; + default: + return defaultFormat; + } + }); + } + /** * Returns the expected request Content-Type of the given operation. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/TimestampFormatTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/TimestampFormatTrait.java index ee9247df7a1..03e03f2af94 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/TimestampFormatTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/TimestampFormatTrait.java @@ -35,9 +35,57 @@ public TimestampFormatTrait(String value) { this(value, SourceLocation.NONE); } + /** + * Gets the {@code timestampFormat} value as a {@code Format} enum. + * + * @return Returns the {@code Format} enum. + */ + public Format getFormat() { + return Format.fromString(getValue()); + } + public static final class Provider extends StringTrait.Provider { public Provider() { super(ID, TimestampFormatTrait::new); } } + + /** + * The known {@code timestampFormat} values. + */ + public enum Format { + EPOCH_SECONDS(TimestampFormatTrait.EPOCH_SECONDS), + DATE_TIME(TimestampFormatTrait.DATE_TIME), + HTTP_DATE(TimestampFormatTrait.HTTP_DATE), + UNKNOWN("unknown"); + + private String value; + + Format(String value) { + this.value = value; + } + + /** + * Create a {@code Format} from a string that would appear in a model. + * + *

Any unknown value is returned as {@code Unknown}. + * + * @param value Value from a trait or model. + * @return Returns the Format enum value. + */ + public static Format fromString(String value) { + for (Format format : values()) { + if (format.value.equals(value)) { + return format; + } + } + + return UNKNOWN; + } + + @Override + public String toString() { + return value; + } + } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/HttpBindingIndexTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/HttpBindingIndexTest.java index 52f3a22770e..8264cb561be 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/HttpBindingIndexTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/knowledge/HttpBindingIndexTest.java @@ -28,6 +28,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.SourceLocation; import software.amazon.smithy.model.pattern.UriPattern; +import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; @@ -42,6 +43,7 @@ import software.amazon.smithy.model.traits.HttpPayloadTrait; import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait; import software.amazon.smithy.model.traits.HttpTrait; +import software.amazon.smithy.model.traits.TimestampFormatTrait; public class HttpBindingIndexTest { @@ -209,7 +211,7 @@ public void findsUnboundMembers() { @Test public void checksForHttpRequestAndResponseBindings() { Shape shape = MemberShape.builder() - .target("smithy.api#String") + .target("smithy.api#Timestamp") .id("smithy.example#Baz$bar") .addTrait(new HttpLabelTrait(SourceLocation.NONE)) .build(); @@ -221,7 +223,7 @@ public void checksForHttpRequestAndResponseBindings() { @Test public void checksForHttpResponseBindings() { Shape shape = MemberShape.builder() - .target("smithy.api#String") + .target("smithy.api#Timestamp") .id("smithy.example#Baz$bar") .addTrait(new HttpHeaderTrait("hello", SourceLocation.NONE)) .build(); @@ -270,4 +272,80 @@ private static MemberShape expectMember(Model model, String id) { ShapeId shapeId = ShapeId.from(id); return model.getShapeIndex().getShape(shapeId).get().asMemberShape().get(); } + + @Test + public void usesTimestampFormatMemberTraitToDetermineFormat() { + MemberShape member = MemberShape.builder() + .id("foo.bar#Baz$member") + .target("smithy.api#Timestamp") + .addTrait(new TimestampFormatTrait(TimestampFormatTrait.EPOCH_SECONDS)) + .build(); + Model model = Model.assembler() + .addShape(member) + .addShape(ListShape.builder().member(member).id("foo.bar#Baz").build()) + .assemble() + .unwrap(); + HttpBindingIndex index = model.getKnowledge(HttpBindingIndex.class); + TimestampFormatTrait.Format format = index.determineTimestampFormat( + member, HttpBinding.Location.HEADER, TimestampFormatTrait.Format.DATE_TIME); + + assertThat(format, equalTo(TimestampFormatTrait.Format.EPOCH_SECONDS)); + } + + @Test + public void headerLocationUsesHttpDateTimestampFormat() { + MemberShape member = MemberShape.builder() + .id("foo.bar#Baz$member") + .target("smithy.api#Timestamp") + .build(); + Model model = Model.assembler() + .addShape(member) + .addShape(ListShape.builder().member(member).id("foo.bar#Baz").build()) + .assemble() + .unwrap(); + HttpBindingIndex index = model.getKnowledge(HttpBindingIndex.class); + + assertThat(index.determineTimestampFormat( + member, HttpBinding.Location.HEADER, TimestampFormatTrait.Format.EPOCH_SECONDS), + equalTo(TimestampFormatTrait.Format.HTTP_DATE)); + } + + @Test + public void queryAndLabelLocationUsesDateTimeTimestampFormat() { + MemberShape member = MemberShape.builder() + .id("foo.bar#Baz$member") + .target("smithy.api#Timestamp") + .build(); + Model model = Model.assembler() + .addShape(member) + .addShape(ListShape.builder().member(member).id("foo.bar#Baz").build()) + .assemble() + .unwrap(); + HttpBindingIndex index = model.getKnowledge(HttpBindingIndex.class); + + assertThat(index.determineTimestampFormat( + member, HttpBinding.Location.QUERY, TimestampFormatTrait.Format.EPOCH_SECONDS), + equalTo(TimestampFormatTrait.Format.DATE_TIME)); + assertThat(index.determineTimestampFormat( + member, HttpBinding.Location.LABEL, TimestampFormatTrait.Format.EPOCH_SECONDS), + equalTo(TimestampFormatTrait.Format.DATE_TIME)); + } + + @Test + public void otherLocationsUseDefaultTimestampFormat() { + MemberShape member = MemberShape.builder() + .id("foo.bar#Baz$member") + .target("smithy.api#Timestamp") + .build(); + Model model = Model.assembler() + .addShape(member) + .addShape(ListShape.builder().member(member).id("foo.bar#Baz").build()) + .assemble() + .unwrap(); + HttpBindingIndex index = model.getKnowledge(HttpBindingIndex.class); + + assertThat(index.determineTimestampFormat( + member, HttpBinding.Location.DOCUMENT, TimestampFormatTrait.Format.EPOCH_SECONDS), + equalTo(TimestampFormatTrait.Format.EPOCH_SECONDS)); + } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/traits/TimestampFormatTraitTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/traits/TimestampFormatTraitTest.java new file mode 100644 index 00000000000..0482aca76a9 --- /dev/null +++ b/smithy-model/src/test/java/software/amazon/smithy/model/traits/TimestampFormatTraitTest.java @@ -0,0 +1,33 @@ +package software.amazon.smithy.model.traits; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; + +public class TimestampFormatTraitTest { + @Test + public void createsFromString() { + assertThat(TimestampFormatTrait.Format.fromString("date-time"), + equalTo(TimestampFormatTrait.Format.DATE_TIME)); + assertThat(TimestampFormatTrait.Format.fromString("http-date"), + equalTo(TimestampFormatTrait.Format.HTTP_DATE)); + assertThat(TimestampFormatTrait.Format.fromString("epoch-seconds"), + equalTo(TimestampFormatTrait.Format.EPOCH_SECONDS)); + assertThat(TimestampFormatTrait.Format.fromString("foo-baz"), + equalTo(TimestampFormatTrait.Format.UNKNOWN)); + } + + @Test + public void convertsFormatToString() { + assertThat(TimestampFormatTrait.Format.fromString("date-time").toString(), + equalTo("date-time")); + } + + @Test + public void createsFormatFromTrait() { + TimestampFormatTrait trait = new TimestampFormatTrait("date-time"); + + assertThat(trait.getFormat(), equalTo(TimestampFormatTrait.Format.DATE_TIME)); + } +}