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 ability to configure timestamp validation #229

Merged
merged 1 commit into from
Dec 13, 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 @@ -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,
}