Skip to content

Revert "Revert "Parse timestamps strictly"" #416

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

Merged
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 @@ -89,6 +89,21 @@ public DocumentMemberDeserVisitor(
this.defaultTimestampFormat = defaultTimestampFormat;
}

/**
* @return the member this visitor is being run against. Used to discover member-applied
* traits, such as @timestampFormat. Can be, and defaults, to, null.
*/
protected MemberShape getMemberShape() {
return null;
}

/**
* @return true if string-formatted epoch seconds in payloads are disallowed. Defaults to false.
*/
protected boolean requiresNumericEpochSecondsInPayload() {
return false;
}

/**
* Gets the generation context.
*
Expand Down Expand Up @@ -208,8 +223,25 @@ public final String memberShape(MemberShape shape) {
@Override
public String timestampShape(TimestampShape shape) {
HttpBindingIndex httpIndex = HttpBindingIndex.of(context.getModel());
Format format = httpIndex.determineTimestampFormat(shape, Location.DOCUMENT, defaultTimestampFormat);
return HttpProtocolGeneratorUtils.getTimestampOutputParam(dataSource, Location.DOCUMENT, shape, format);
Format format;
if (getMemberShape() == null) {
format = httpIndex.determineTimestampFormat(shape, Location.DOCUMENT, defaultTimestampFormat);
} else {
if (!shape.getId().equals(getMemberShape().getTarget())) {
throw new IllegalArgumentException(
String.format("Encountered timestamp shape %s that was not the target of member shape %s",
shape.getId(), getMemberShape().getId()));
}
format = httpIndex.determineTimestampFormat(getMemberShape(), Location.DOCUMENT, defaultTimestampFormat);
}

return HttpProtocolGeneratorUtils.getTimestampOutputParam(
context.getWriter(),
dataSource,
Location.DOCUMENT,
shape,
format,
requiresNumericEpochSecondsInPayload());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2579,7 +2579,9 @@ private String getOutputValue(
} else if (target instanceof TimestampShape) {
HttpBindingIndex httpIndex = HttpBindingIndex.of(context.getModel());
Format format = httpIndex.determineTimestampFormat(member, bindingType, getDocumentTimestampFormat());
return HttpProtocolGeneratorUtils.getTimestampOutputParam(dataSource, bindingType, member, format);
return HttpProtocolGeneratorUtils.getTimestampOutputParam(
context.getWriter(), dataSource, bindingType, member, format,
requiresNumericEpochSecondsInPayload());
} else if (target instanceof BlobShape) {
return getBlobOutputParam(bindingType, dataSource);
} else if (target instanceof CollectionShape) {
Expand Down Expand Up @@ -2933,4 +2935,9 @@ protected abstract void deserializeErrorDocumentBody(
StructureShape error,
List<HttpBinding> documentBindings
);

/**
* @return true if this protocol disallows string epoch timestamps in payloads.
*/
protected abstract boolean requiresNumericEpochSecondsInPayload();
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,35 +88,44 @@ public static String getTimestampInputParam(
* Given a format and a source of data, generate an output value provider for the
* timestamp.
*
* @param writer The current writer (so that imports may be added)
* @param dataSource The in-code location of the data to provide an output of
* ({@code output.foo}, {@code entry}, etc.)
* @param bindingType How this value is bound to the operation output.
* @param shape The shape that represents the value being received.
* @param format The timestamp format to provide.
* @param requireNumericEpochSecondsInPayload if true, paylaod epoch seconds are not allowed to be coerced
* from strings.
* @return Returns a value or expression of the output timestamp.
*/
public static String getTimestampOutputParam(String dataSource, Location bindingType, Shape shape, Format format) {
String modifiedSource;
public static String getTimestampOutputParam(TypeScriptWriter writer,
String dataSource,
Location bindingType,
Shape shape,
Format format,
boolean requireNumericEpochSecondsInPayload) {
// This has always explicitly wrapped the dataSource in "new Date(..)", so it could never generate
// an expression that evaluates to null. Codegen relies on this.
writer.addImport("expectNonNull", "__expectNonNull", "@aws-sdk/smithy-client");
switch (format) {
case DATE_TIME:
writer.addImport("parseRfc3339DateTime", "__parseRfc3339DateTime", "@aws-sdk/smithy-client");
return String.format("__expectNonNull(__parseRfc3339DateTime(%s))", dataSource);
case HTTP_DATE:
modifiedSource = dataSource;
break;
writer.addImport("parseRfc7231DateTime", "__parseRfc7231DateTime", "@aws-sdk/smithy-client");
return String.format("__expectNonNull(__parseRfc7231DateTime(%s))", dataSource);
case EPOCH_SECONDS:
modifiedSource = dataSource;
// Make sure we turn a header, label, or query's forced string into a number.
if (bindingType.equals(Location.HEADER) || bindingType.equals(Location.QUERY)
|| bindingType.equals(Location.LABEL)) {
modifiedSource = "parseInt(" + modifiedSource + ", 10)";
writer.addImport("parseEpochTimestamp", "__parseEpochTimestamp", "@aws-sdk/smithy-client");
String modifiedDataSource = dataSource;
if (requireNumericEpochSecondsInPayload
&& (bindingType == Location.DOCUMENT || bindingType == Location.PAYLOAD)) {
writer.addImport("expectNumber", "__expectNumber", "@aws-sdk/smithy-client");
modifiedDataSource = String.format("__expectNumber(%s)", dataSource);
}
// Convert whole and decimal numbers to milliseconds.
modifiedSource = "Math.round(" + modifiedSource + " * 1000)";
break;
return String.format("__expectNonNull(__parseEpochTimestamp(%s))", modifiedDataSource);
default:
throw new CodegenException("Unexpected timestamp format `" + format.toString() + "` on " + shape);
}

return "new Date(" + modifiedSource + ")";
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.BlobShape;
import software.amazon.smithy.model.shapes.BooleanShape;
import software.amazon.smithy.model.shapes.ByteShape;
Expand All @@ -31,8 +32,10 @@
import software.amazon.smithy.model.shapes.ShortShape;
import software.amazon.smithy.model.shapes.StringShape;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.shapes.TimestampShape;
import software.amazon.smithy.model.shapes.UnionShape;
import software.amazon.smithy.model.traits.MediaTypeTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext;
Expand All @@ -53,40 +56,65 @@ public class DocumentMemberDeserVisitorTest {

@ParameterizedTest
@MethodSource("validMemberTargetTypes")
public void providesExpectedDefaults(Shape shape, String expected) {
DocumentMemberDeserVisitor visitor = new DocumentMemberDeserVisitor(mockContext, DATA_SOURCE, FORMAT);
public void providesExpectedDefaults(Shape shape, String expected, MemberShape memberShape) {
Shape fakeStruct = StructureShape.builder().id("com.smithy.example#Enclosing").addMember(memberShape).build();
mockContext.setModel(Model.builder().addShapes(shape, fakeStruct).build());
DocumentMemberDeserVisitor visitor =
new DocumentMemberDeserVisitor(mockContext, DATA_SOURCE, FORMAT) {
@Override
protected MemberShape getMemberShape() {
return memberShape;
}
};
assertThat(expected, equalTo(shape.accept(visitor)));
}

public static Collection<Object[]> validMemberTargetTypes() {
String id = "com.smithy.example#Foo";
String targetId = id + "Target";
MemberShape source = MemberShape.builder().id("com.smithy.example#Enclosing$sourceMember").target(id).build();
MemberShape member = MemberShape.builder().id(id + "$member").target(targetId).build();
MemberShape key = MemberShape.builder().id(id + "$key").target(targetId).build();
MemberShape value = MemberShape.builder().id(id + "$value").target(targetId).build();
String delegate = "deserialize" + ProtocolGenerator.getSanitizedName(PROTOCOL) + "Foo"
+ "(" + DATA_SOURCE + ", context)";

return ListUtils.of(new Object[][]{
{BooleanShape.builder().id(id).build(), "__expectBoolean(" + DATA_SOURCE + ")"},
{ByteShape.builder().id(id).build(), "__expectByte(" + DATA_SOURCE + ")"},
{DoubleShape.builder().id(id).build(), "__limitedParseDouble(" + DATA_SOURCE + ")"},
{FloatShape.builder().id(id).build(), "__limitedParseFloat32(" + DATA_SOURCE + ")"},
{IntegerShape.builder().id(id).build(), "__expectInt32(" + DATA_SOURCE + ")"},
{LongShape.builder().id(id).build(), "__expectLong(" + DATA_SOURCE + ")"},
{ShortShape.builder().id(id).build(), "__expectShort(" + DATA_SOURCE + ")"},
{StringShape.builder().id(id).build(), "__expectString(" + DATA_SOURCE + ")"},
{BooleanShape.builder().id(id).build(), "__expectBoolean(" + DATA_SOURCE + ")", source},
{ByteShape.builder().id(id).build(), "__expectByte(" + DATA_SOURCE + ")", source},
{DoubleShape.builder().id(id).build(), "__limitedParseDouble(" + DATA_SOURCE + ")", source},
{FloatShape.builder().id(id).build(), "__limitedParseFloat32(" + DATA_SOURCE + ")", source},
{IntegerShape.builder().id(id).build(), "__expectInt32(" + DATA_SOURCE + ")", source},
{LongShape.builder().id(id).build(), "__expectLong(" + DATA_SOURCE + ")", source},
{ShortShape.builder().id(id).build(), "__expectShort(" + DATA_SOURCE + ")", source},
{StringShape.builder().id(id).build(), "__expectString(" + DATA_SOURCE + ")", source},
{
StringShape.builder().id(id).addTrait(new MediaTypeTrait("foo+json")).build(),
"new __LazyJsonString(" + DATA_SOURCE + ")"
"new __LazyJsonString(" + DATA_SOURCE + ")",
source
},
{BlobShape.builder().id(id).build(), "context.base64Decoder(" + DATA_SOURCE + ")", source},
{DocumentShape.builder().id(id).build(), delegate, source},
{ListShape.builder().id(id).member(member).build(), delegate, source},
{SetShape.builder().id(id).member(member).build(), delegate, source},
{MapShape.builder().id(id).key(key).value(value).build(), delegate, source},
{StructureShape.builder().id(id).build(), delegate, source},
{UnionShape.builder().id(id).addMember(member).build(), delegate, source},
{
TimestampShape.builder().id(id).build(),
"__expectNonNull(__parseEpochTimestamp(" + DATA_SOURCE + "))",
source
},
{
TimestampShape.builder().id(id).build(),
"__expectNonNull(__parseRfc3339DateTime(" + DATA_SOURCE + "))",
source.toBuilder().addTrait(new TimestampFormatTrait(TimestampFormatTrait.DATE_TIME)).build()
},
{
TimestampShape.builder().id(id).build(),
"__expectNonNull(__parseRfc7231DateTime(" + DATA_SOURCE + "))",
source.toBuilder().addTrait(new TimestampFormatTrait(TimestampFormatTrait.HTTP_DATE)).build()
},
{BlobShape.builder().id(id).build(), "context.base64Decoder(" + DATA_SOURCE + ")"},
{DocumentShape.builder().id(id).build(), delegate},
{ListShape.builder().id(id).member(member).build(), delegate},
{SetShape.builder().id(id).member(member).build(), delegate},
{MapShape.builder().id(id).key(key).value(value).build(), delegate},
{StructureShape.builder().id(id).build(), delegate},
{UnionShape.builder().id(id).addMember(member).build(), delegate},
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@ public void givesCorrectTimestampSerialization() {
@Test
public void givesCorrectTimestampDeserialization() {
TimestampShape shape = TimestampShape.builder().id("com.smithy.example#Foo").build();
TypeScriptWriter writer = new TypeScriptWriter("foo");

assertThat("new Date(" + DATA_SOURCE + ")",
equalTo(HttpProtocolGeneratorUtils.getTimestampOutputParam(DATA_SOURCE, Location.DOCUMENT, shape, Format.DATE_TIME)));
assertThat("new Date(Math.round(" + DATA_SOURCE + " * 1000))",
equalTo(HttpProtocolGeneratorUtils.getTimestampOutputParam(DATA_SOURCE, Location.DOCUMENT, shape, Format.EPOCH_SECONDS)));
assertThat("new Date(" + DATA_SOURCE + ")",
equalTo(HttpProtocolGeneratorUtils.getTimestampOutputParam(DATA_SOURCE, Location.DOCUMENT, shape, Format.HTTP_DATE)));
assertThat("__expectNonNull(__parseRfc3339DateTime(" + DATA_SOURCE + "))",
equalTo(HttpProtocolGeneratorUtils.getTimestampOutputParam(writer, DATA_SOURCE, Location.DOCUMENT, shape, Format.DATE_TIME, false)));
assertThat("__expectNonNull(__parseEpochTimestamp(__expectNumber(" + DATA_SOURCE + ")))",
equalTo(HttpProtocolGeneratorUtils.getTimestampOutputParam(writer, DATA_SOURCE, Location.DOCUMENT, shape, Format.EPOCH_SECONDS, true)));
assertThat("__expectNonNull(__parseEpochTimestamp(" + DATA_SOURCE + "))",
equalTo(HttpProtocolGeneratorUtils.getTimestampOutputParam(writer, DATA_SOURCE, Location.DOCUMENT, shape, Format.EPOCH_SECONDS, false)));
assertThat("__expectNonNull(__parseRfc7231DateTime(" + DATA_SOURCE + "))",
equalTo(HttpProtocolGeneratorUtils.getTimestampOutputParam(writer, DATA_SOURCE, Location.DOCUMENT, shape, Format.HTTP_DATE, false)));
}

@Test
Expand Down