Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helper to detect HTTP-based timestampFormats #193

Merged
merged 1 commit into from
Oct 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -220,6 +221,39 @@ public List<HttpBinding> 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TimestampFormatTrait> {
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.
*
* <p>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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}