Skip to content

Commit

Permalink
Add ability to configure timestamp validation
Browse files Browse the repository at this point in the history
This commit updates the Node validation visitor to allow for a
configurable strategy for validating timestamps. This is needed in order
to validate that the timestamps provided for protocol tests are always
in epoch seconds.
  • Loading branch information
mtdowling committed Dec 13, 2019
1 parent 9b67fe7 commit 16cb3e0
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.smithy.model.validation;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -61,7 +62,7 @@
import software.amazon.smithy.model.validation.node.RangeTraitPlugin;
import software.amazon.smithy.model.validation.node.StringEnumPlugin;
import software.amazon.smithy.model.validation.node.StringLengthPlugin;
import software.amazon.smithy.model.validation.node.TimestampFormatPlugin;
import software.amazon.smithy.model.validation.node.TimestampValidationStrategy;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SmithyBuilder;

Expand All @@ -76,29 +77,34 @@
* applied to the shape of the data.
*/
public final class NodeValidationVisitor implements ShapeVisitor<List<ValidationEvent>> {
private static final List<NodeValidatorPlugin> PLUGINS = ListUtils.of(
new BlobLengthPlugin(),
new CollectionLengthPlugin(),
new IdRefPlugin(),
new MapLengthPlugin(),
new PatternTraitPlugin(),
new RangeTraitPlugin(),
new StringEnumPlugin(),
new StringLengthPlugin(),
new TimestampFormatPlugin());

private final String eventId;
private final Node value;
private final ShapeIndex index;
private final String context;
private final ShapeId eventShapeId;
private final List<NodeValidatorPlugin> plugins;
private final TimestampValidationStrategy timestampValidationStrategy;

private NodeValidationVisitor(Builder builder) {
this.value = SmithyBuilder.requiredState("value", builder.value);
this.index = SmithyBuilder.requiredState("index", builder.index);
this.context = builder.context;
this.eventId = builder.eventId;
this.eventShapeId = builder.eventShapeId;
this.timestampValidationStrategy = builder.timestampValidationStrategy;

plugins = Arrays.asList(
new BlobLengthPlugin(),
new CollectionLengthPlugin(),
new IdRefPlugin(),
new MapLengthPlugin(),
new PatternTraitPlugin(),
new RangeTraitPlugin(),
new StringEnumPlugin(),
new StringLengthPlugin(),
timestampValidationStrategy
);
}

public static Builder builder() {
Expand All @@ -112,6 +118,7 @@ private NodeValidationVisitor withNode(String segment, Node node) {
builder.value(node);
builder.index(index);
builder.startingContext(context.isEmpty() ? segment : (context + "." + segment));
builder.timestampValidationStrategy(timestampValidationStrategy);
return new NodeValidationVisitor(builder);
}

Expand Down Expand Up @@ -377,7 +384,7 @@ private ValidationEvent event(String message) {
}

private List<ValidationEvent> applyPlugins(Shape shape) {
return PLUGINS.stream()
return plugins.stream()
.flatMap(plugin -> plugin.apply(shape, value, index).stream())
.map(this::event)
.collect(Collectors.toList());
Expand All @@ -387,11 +394,12 @@ private List<ValidationEvent> applyPlugins(Shape shape) {
* Builds a {@link NodeValidationVisitor}.
*/
public static final class Builder implements SmithyBuilder<NodeValidationVisitor> {
String eventId = Validator.MODEL_ERROR;
String context = "";
ShapeId eventShapeId;
Node value;
ShapeIndex index;
private String eventId = Validator.MODEL_ERROR;
private String context = "";
private ShapeId eventShapeId;
private Node value;
private ShapeIndex index;
private TimestampValidationStrategy timestampValidationStrategy = TimestampValidationStrategy.FORMAT;

Builder() {}

Expand Down Expand Up @@ -458,6 +466,20 @@ public Builder eventShapeId(ShapeId eventShapeId) {
return this;
}

/**
* Sets the strategy used to validate timestamps.
*
* <p>By default, timestamps are validated using
* {@link TimestampValidationStrategy#FORMAT}.
*
* @param timestampValidationStrategy Timestamp validation strategy.
* @return Returns the builder.
*/
public Builder timestampValidationStrategy(TimestampValidationStrategy timestampValidationStrategy) {
this.timestampValidationStrategy = timestampValidationStrategy;
return this;
}

@Override
public NodeValidationVisitor build() {
return new NodeValidationVisitor(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.smithy.model.validation.node;

import java.util.Collections;
import java.util.List;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeIndex;
import software.amazon.smithy.utils.ListUtils;

/**
* Defines how timestamps are validated.
*/
public enum TimestampValidationStrategy implements NodeValidatorPlugin {
/**
* Validates timestamps by requiring that the value uses matches the
* resolved timestamp format, or is a unix timestamp or integer in the
* case that a member or shape does not have a {@code timestampFormat}
* trait.
*/
FORMAT {
@Override
public List<String> apply(Shape shape, Node value, ShapeIndex index) {
return new TimestampFormatPlugin().apply(shape, value, index);
}
},

/**
* Requires that the value provided for all timestamp shapes is a
* unix timestamp.
*/
EPOCH_SECONDS {
@Override
public List<String> apply(Shape shape, Node value, ShapeIndex index) {
if (isTimestampMember(index, shape) && !value.isNumberNode()) {
return ListUtils.of("Invalid " + value.getType() + " value provided for timestamp, `"
+ shape.getId() + "`. Expected a number that contains epoch seconds "
+ "with optional millisecond precision");
} else {
return Collections.emptyList();
}
}
};

private static boolean isTimestampMember(ShapeIndex model, Shape shape) {
return shape.asMemberShape()
.map(MemberShape::getTarget)
.flatMap(model::getShape)
.filter(Shape::isTimestampShape)
.isPresent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.not;

import java.util.Arrays;
import java.util.Collection;
Expand All @@ -25,20 +27,19 @@
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.DefaultNodeFactory;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.validation.node.TimestampValidationStrategy;

public class NodeValidationVisitorTest {
private static Model MODEL;
private static DefaultNodeFactory FACTORY;

@BeforeAll
public static void onlyOnce() {
FACTORY = new DefaultNodeFactory();
MODEL = Model.assembler()
.addImport(NodeValidationVisitorTest.class.getResource("node-validator.json"))
.assemble()
Expand All @@ -47,15 +48,14 @@ public static void onlyOnce() {

@AfterAll
public static void after() {
FACTORY = null;
MODEL = null;
}

@ParameterizedTest
@MethodSource("data")
public void nodeValidationVisitorTest(String target, String value, String[] errors) {
ShapeId targetId = ShapeId.from(target);
Node nodeValue = FACTORY.createNode("N/A", value);
Node nodeValue = Node.parse(value);
NodeValidationVisitor cases = NodeValidationVisitor.builder()
.value(nodeValue)
.model(MODEL)
Expand Down Expand Up @@ -274,4 +274,32 @@ public static Collection<Object[]> data() {
{"ns.foo#Structure3", "{}", new String[] {"Missing required structure member `requiredInt2` for `ns.foo#Structure3`"}},
});
}

@Test
public void canSuccessfullyValidateTimestampsAsUnixTimestamps() {
NodeValidationVisitor cases = NodeValidationVisitor.builder()
.value(Node.from(1234))
.model(MODEL)
.timestampValidationStrategy(TimestampValidationStrategy.EPOCH_SECONDS)
.build();
List<ValidationEvent> events = MODEL
.expectShape(ShapeId.from("ns.foo#TimestampList$member"))
.accept(cases);

assertThat(events, empty());
}

@Test
public void canUnsuccessfullyValidateTimestampsAsUnixTimestamps() {
NodeValidationVisitor cases = NodeValidationVisitor.builder()
.value(Node.from("foo"))
.model(MODEL)
.timestampValidationStrategy(TimestampValidationStrategy.EPOCH_SECONDS)
.build();
List<ValidationEvent> events = MODEL
.expectShape(ShapeId.from("ns.foo#TimestampList$member"))
.accept(cases);

assertThat(events, not(empty()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.NodeValidationVisitor;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.model.validation.node.TimestampValidationStrategy;

abstract class ProtocolTestCaseValidator<T extends Trait> extends AbstractValidator {

Expand Down Expand Up @@ -94,6 +95,7 @@ private NodeValidationVisitor createVisitor(ObjectNode value, Model model, Shape
.value(value)
.startingContext(traitId + "." + position + ".params")
.eventId(getName())
.timestampValidationStrategy(TimestampValidationStrategy.EPOCH_SECONDS)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace smithy.example

use smithy.test#httpRequestTests

@http(method: "POST", uri: "/")
@httpRequestTests([
{
id: "foo3",
protocol: "example",
method: "POST",
uri: "/",
params: {
time: 946845296
}
}
])
operation HasTime(HasTimeInput)

structure HasTimeInput {
time: Timestamp,
}

0 comments on commit 16cb3e0

Please sign in to comment.