Emit node validation events with locations

This commit rewrites a lot of NodeValidationVisitors to now emit events
from validation plugins and then allow the visitor to associate the
right context, shape, and severity. This allows the events emitted to
the visitor to contain a more specific source location than N/A.
This commit is contained in:
Michael Dowling 2020-05-04 11:38:44 -07:00
parent f045a42cee
commit 20d10458ac
14 changed files with 281 additions and 226 deletions

View File

@ -15,13 +15,13 @@
package software.amazon.smithy.model.validation;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.knowledge.BoxIndex;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.NodeType;
@ -52,19 +52,10 @@ 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.validation.node.BlobLengthPlugin;
import software.amazon.smithy.model.validation.node.CollectionLengthPlugin;
import software.amazon.smithy.model.validation.node.IdRefPlugin;
import software.amazon.smithy.model.validation.node.MapLengthPlugin;
import software.amazon.smithy.model.validation.node.NodeValidatorPlugin;
import software.amazon.smithy.model.validation.node.PatternTraitPlugin;
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.TimestampValidationStrategy;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.SmithyUnstableApi;
/**
* Validates {@link Node} values provided for {@link Shape} definitions.
@ -78,12 +69,13 @@ import software.amazon.smithy.utils.SmithyUnstableApi;
*/
public final class NodeValidationVisitor implements ShapeVisitor<List<ValidationEvent>> {
private static final List<NodeValidatorPlugin> BUILTIN = NodeValidatorPlugin.getBuiltins();
private final String eventId;
private final Node value;
private final Model model;
private final String context;
private final ShapeId eventShapeId;
private final List<NodeValidatorPlugin> plugins;
private final TimestampValidationStrategy timestampValidationStrategy;
private final boolean allowBoxedNull;
@ -95,18 +87,6 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
this.eventShapeId = builder.eventShapeId;
this.timestampValidationStrategy = builder.timestampValidationStrategy;
this.allowBoxedNull = builder.allowBoxedNull;
plugins = Arrays.asList(
new BlobLengthPlugin(),
new CollectionLengthPlugin(),
new IdRefPlugin(),
new MapLengthPlugin(),
new PatternTraitPlugin(),
new RangeTraitPlugin(),
new StringEnumPlugin(),
new StringLengthPlugin(),
timestampValidationStrategy
);
}
public static Builder builder() {
@ -168,9 +148,9 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
return value.asNumberNode()
.map(number -> {
if (!number.isNaturalNumber()) {
return ListUtils.of(event(
shape.getType() + " shapes must not have floating point values, but found `"
+ number.getValue() + "` provided for `" + shape.getId() + "`"));
return ListUtils.of(event(String.format(
"%s shapes must not have floating point values, but found `%s` provided for `%s`",
shape.getType(), number.getValue(), shape.getId())));
}
Long numberValue = number.getValue().longValue();
@ -273,19 +253,21 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
object.getMembers().forEach((keyNode, value) -> {
String key = keyNode.getValue();
if (!members.containsKey(key)) {
events.add(event("Invalid structure member `" + key + "` found for `"
+ shape.getId() + "`", Severity.WARNING));
String message = String.format(
"Invalid structure member `%s` found for `%s`", key, shape.getId());
events.add(event(message, Severity.WARNING));
} else {
events.addAll(withNode(key, value).memberShape(members.get(key)));
}
});
members.forEach((memberName, member) -> {
if (member.isRequired()
&& !object.getMember(memberName).isPresent()
// Ignore missing required primitive members because they have a default value.
&& !isMemberPrimitive(member)) {
events.add(event("Missing required structure member `" + memberName + "` for `"
+ shape.getId() + "`"));
events.add(event(String.format(
"Missing required structure member `%s` for `%s`", memberName, shape.getId())));
}
});
return events;
@ -309,8 +291,8 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
object.getMembers().forEach((keyNode, value) -> {
String key = keyNode.getValue();
if (!members.containsKey(key)) {
events.add(event(
"Invalid union member `" + key + "` found for `" + shape.getId() + "`"));
events.add(event(String.format(
"Invalid union member `%s` found for `%s`", key, shape.getId())));
} else {
events.addAll(withNode(key, value).memberShape(members.get(key)));
}
@ -373,20 +355,32 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
}
private ValidationEvent event(String message, Severity severity) {
return event(message, severity, value.getSourceLocation());
}
private ValidationEvent event(String message, Severity severity, SourceLocation sourceLocation) {
return ValidationEvent.builder()
.eventId(eventId)
.severity(severity)
.sourceLocation(value.getSourceLocation())
.sourceLocation(sourceLocation)
.shapeId(eventShapeId)
.message(context.isEmpty() ? message : context + ": " + message)
.build();
}
private List<ValidationEvent> applyPlugins(Shape shape) {
return plugins.stream()
.flatMap(plugin -> plugin.apply(shape, value, model).stream())
.map(this::event)
.collect(Collectors.toList());
List<ValidationEvent> events = new ArrayList<>();
timestampValidationStrategy.apply(shape, value, model, (location, message) -> {
events.add(event(message, Severity.ERROR, location.getSourceLocation()));
});
for (NodeValidatorPlugin plugin : BUILTIN) {
plugin.apply(shape, value, model, (location, message) -> {
events.add(event(message, Severity.ERROR, location.getSourceLocation()));
});
}
return events;
}
/**
@ -470,7 +464,6 @@ public final class NodeValidationVisitor implements ShapeVisitor<List<Validation
* @param timestampValidationStrategy Timestamp validation strategy.
* @return Returns the builder.
*/
@SmithyUnstableApi
public Builder timestampValidationStrategy(TimestampValidationStrategy timestampValidationStrategy) {
this.timestampValidationStrategy = timestampValidationStrategy;
return this;

View File

@ -16,8 +16,8 @@
package software.amazon.smithy.model.validation.node;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.BlobShape;
@ -29,28 +29,35 @@ import software.amazon.smithy.utils.SmithyInternalApi;
* Validates length trait on blob shapes and members that target blob shapes.
*/
@SmithyInternalApi
public final class BlobLengthPlugin extends MemberAndShapeTraitPlugin<BlobShape, StringNode, LengthTrait> {
public BlobLengthPlugin() {
final class BlobLengthPlugin extends MemberAndShapeTraitPlugin<BlobShape, StringNode, LengthTrait> {
BlobLengthPlugin() {
super(BlobShape.class, StringNode.class, LengthTrait.class);
}
@Override
protected List<String> check(Shape shape, LengthTrait trait, StringNode node, Model model) {
protected void check(
Shape shape,
LengthTrait trait,
StringNode node,
Model model,
BiConsumer<FromSourceLocation, String> emitter
) {
String value = node.getValue();
List<String> messages = new ArrayList<>();
int size = value.getBytes(Charset.forName("UTF-8")).length;
trait.getMin().ifPresent(min -> {
if (size < min) {
messages.add("Value provided for `" + shape.getId() + "` must have at least "
emitter.accept(node, "Value provided for `" + shape.getId() + "` must have at least "
+ min + " bytes, but the provided value only has " + size + " bytes");
}
});
trait.getMax().ifPresent(max -> {
if (value.getBytes(Charset.forName("UTF-8")).length > max) {
messages.add("Value provided for `" + shape.getId() + "` must have no more than "
emitter.accept(node, "Value provided for `" + shape.getId() + "` must have no more than "
+ max + " bytes, but the provided value has " + size + " bytes");
}
});
return messages;
}
}

View File

@ -15,8 +15,8 @@
package software.amazon.smithy.model.validation.node;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.shapes.CollectionShape;
@ -29,28 +29,34 @@ import software.amazon.smithy.utils.SmithyInternalApi;
* target them.
*/
@SmithyInternalApi
public final class CollectionLengthPlugin extends MemberAndShapeTraitPlugin<CollectionShape, ArrayNode, LengthTrait> {
public CollectionLengthPlugin() {
final class CollectionLengthPlugin extends MemberAndShapeTraitPlugin<CollectionShape, ArrayNode, LengthTrait> {
CollectionLengthPlugin() {
super(CollectionShape.class, ArrayNode.class, LengthTrait.class);
}
@Override
protected List<String> check(Shape shape, LengthTrait trait, ArrayNode node, Model model) {
List<String> messages = new ArrayList<>();
protected void check(
Shape shape,
LengthTrait trait,
ArrayNode node,
Model model,
BiConsumer<FromSourceLocation, String> emitter
) {
trait.getMin().ifPresent(min -> {
if (node.size() < min) {
messages.add(String.format(
emitter.accept(node, String.format(
"Value provided for `%s` must have at least %d elements, but the provided value only "
+ "has %d elements", shape.getId(), min, node.size()));
}
});
trait.getMax().ifPresent(max -> {
if (node.size() > max) {
messages.add(String.format(
emitter.accept(node, String.format(
"Value provided for `%s` must have no more than %d elements, but the provided value "
+ "has %d elements", shape.getId(), max, node.size()));
}
});
return messages;
}
}

View File

@ -15,11 +15,11 @@
package software.amazon.smithy.model.validation.node;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.utils.ListUtils;
abstract class FilteredPlugin<S extends Shape, N extends Node> implements NodeValidatorPlugin {
private final Class<S> shapeClass;
@ -30,14 +30,13 @@ abstract class FilteredPlugin<S extends Shape, N extends Node> implements NodeVa
this.nodeClass = nodeClass;
}
@Override
@SuppressWarnings("unchecked")
public final List<String> apply(Shape shape, Node node, Model model) {
if (shapeClass.isInstance(shape) && nodeClass.isInstance(node)) {
return check((S) shape, (N) node, model);
} else {
return ListUtils.of();
public final void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
if (shapeClass.isInstance(shape) && nodeClass.isInstance(value)) {
check((S) shape, (N) value, model, emitter);
}
}
abstract List<String> check(S shape, N node, Model model);
abstract void check(S shape, N node, Model model, BiConsumer<FromSourceLocation, String> emitter);
}

View File

@ -15,7 +15,8 @@
package software.amazon.smithy.model.validation.node;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.SourceException;
import software.amazon.smithy.model.node.StringNode;
@ -23,7 +24,6 @@ import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StringShape;
import software.amazon.smithy.model.traits.IdRefTrait;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SmithyInternalApi;
/**
@ -32,30 +32,36 @@ import software.amazon.smithy.utils.SmithyInternalApi;
* matching the selector.
*/
@SmithyInternalApi
public final class IdRefPlugin extends MemberAndShapeTraitPlugin<StringShape, StringNode, IdRefTrait> {
final class IdRefPlugin extends MemberAndShapeTraitPlugin<StringShape, StringNode, IdRefTrait> {
public IdRefPlugin() {
IdRefPlugin() {
super(StringShape.class, StringNode.class, IdRefTrait.class);
}
@Override
protected List<String> check(Shape shape, IdRefTrait trait, StringNode node, Model model) {
protected void check(
Shape shape,
IdRefTrait trait,
StringNode node,
Model model,
BiConsumer<FromSourceLocation, String> emitter
) {
try {
ShapeId target = node.expectShapeId();
Shape resolved = model.getShape(target).orElse(null);
if (resolved == null) {
return trait.failWhenMissing()
? failWhenNoMatch(trait, String.format("Shape ID `%s` was not found in the model", target))
: ListUtils.of();
} else if (matchesSelector(trait, resolved.getId(), model)) {
return ListUtils.of();
if (trait.failWhenMissing()) {
failWhenNoMatch(node, trait, emitter, String.format(
"Shape ID `%s` was not found in the model", target));
}
} else if (!matchesSelector(trait, resolved.getId(), model)) {
failWhenNoMatch(node, trait, emitter, String.format(
"Shape ID `%s` does not match selector `%s`",
resolved.getId(), trait.getSelector()));
}
return failWhenNoMatch(trait, String.format(
"Shape ID `%s` does not match selector `%s`", resolved.getId(), trait.getSelector()));
} catch (SourceException e) {
return ListUtils.of(e.getMessage());
emitter.accept(node, e.getMessageWithoutLocation());
}
}
@ -65,7 +71,12 @@ public final class IdRefPlugin extends MemberAndShapeTraitPlugin<StringShape, St
.anyMatch(shapeId -> shapeId.equals(needle));
}
private List<String> failWhenNoMatch(IdRefTrait trait, String message) {
return ListUtils.of(trait.getErrorMessage().orElse(message));
private void failWhenNoMatch(
FromSourceLocation location,
IdRefTrait trait,
BiConsumer<FromSourceLocation, String> emitter,
String message
) {
emitter.accept(location, trait.getErrorMessage().orElse(message));
}
}

View File

@ -15,8 +15,8 @@
package software.amazon.smithy.model.validation.node;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.MapShape;
@ -28,28 +28,34 @@ import software.amazon.smithy.utils.SmithyInternalApi;
* Validates the length trait on map shapes or members that target them.
*/
@SmithyInternalApi
public final class MapLengthPlugin extends MemberAndShapeTraitPlugin<MapShape, ObjectNode, LengthTrait> {
public MapLengthPlugin() {
final class MapLengthPlugin extends MemberAndShapeTraitPlugin<MapShape, ObjectNode, LengthTrait> {
MapLengthPlugin() {
super(MapShape.class, ObjectNode.class, LengthTrait.class);
}
@Override
protected List<String> check(Shape shape, LengthTrait trait, ObjectNode node, Model model) {
List<String> messages = new ArrayList<>();
protected void check(
Shape shape,
LengthTrait trait,
ObjectNode node,
Model model,
BiConsumer<FromSourceLocation, String> emitter
) {
trait.getMin().ifPresent(min -> {
if (node.size() < min) {
messages.add(String.format(
emitter.accept(node, String.format(
"Value provided for `%s` must have at least %d entries, but the provided value only "
+ "has %d entries", shape.getId(), min, node.size()));
}
});
trait.getMax().ifPresent(max -> {
if (node.size() > max) {
messages.add(String.format(
emitter.accept(node, String.format(
"Value provided for `%s` must have no more than %d entries, but the provided value "
+ "has %d entries", shape.getId(), max, node.size()));
}
});
return messages;
}
}

View File

@ -15,12 +15,12 @@
package software.amazon.smithy.model.validation.node;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.utils.ListUtils;
abstract class MemberAndShapeTraitPlugin<S extends Shape, N extends Node, T extends Trait>
implements NodeValidatorPlugin {
@ -35,15 +35,14 @@ abstract class MemberAndShapeTraitPlugin<S extends Shape, N extends Node, T exte
this.traitClass = traitClass;
}
@Override
@SuppressWarnings("unchecked")
public final List<String> apply(Shape shape, Node value, Model model) {
public final void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
if (nodeClass.isInstance(value)
&& shape.getTrait(traitClass).isPresent()
&& isMatchingShape(shape, model)) {
return check(shape, shape.getTrait(traitClass).get(), (N) value, model);
check(shape, shape.getTrait(traitClass).get(), (N) value, model, emitter);
}
return ListUtils.of();
}
private boolean isMatchingShape(Shape shape, Model model) {
@ -59,5 +58,10 @@ abstract class MemberAndShapeTraitPlugin<S extends Shape, N extends Node, T exte
.isPresent();
}
protected abstract List<String> check(Shape shape, T trait, N value, Model model);
protected abstract void check(
Shape shape,
T trait,
N value,
Model model,
BiConsumer<FromSourceLocation, String> emitter);
}

View File

@ -16,9 +16,12 @@
package software.amazon.smithy.model.validation.node;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SmithyInternalApi;
/**
@ -28,13 +31,29 @@ import software.amazon.smithy.utils.SmithyInternalApi;
*/
@SmithyInternalApi
public interface NodeValidatorPlugin {
/**
* Applies the plugin to the given shape, node value, and model.
*
* @param shape Shape being checked.
* @param value Value being evaluated.
* @param model Model to traverse.
* @return Returns any validation messages that were encountered.
* @param emitter Consumer to notify of validation event locations and messages.
*/
List<String> apply(Shape shape, Node value, Model model);
void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter);
/**
* @return Gets the built-in Node validation plugins.
*/
static List<NodeValidatorPlugin> getBuiltins() {
return ListUtils.of(
new BlobLengthPlugin(),
new CollectionLengthPlugin(),
new IdRefPlugin(),
new MapLengthPlugin(),
new PatternTraitPlugin(),
new RangeTraitPlugin(),
new StringEnumPlugin(),
new StringLengthPlugin());
}
}

View File

@ -15,32 +15,35 @@
package software.amazon.smithy.model.validation.node;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.StringShape;
import software.amazon.smithy.model.traits.PatternTrait;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SmithyInternalApi;
/**
* Validates the pattern trait on string shapes or members that target them.
*/
@SmithyInternalApi
public final class PatternTraitPlugin extends MemberAndShapeTraitPlugin<StringShape, StringNode, PatternTrait> {
public PatternTraitPlugin() {
final class PatternTraitPlugin extends MemberAndShapeTraitPlugin<StringShape, StringNode, PatternTrait> {
PatternTraitPlugin() {
super(StringShape.class, StringNode.class, PatternTrait.class);
}
@Override
protected List<String> check(Shape shape, PatternTrait trait, StringNode node, Model model) {
protected void check(
Shape shape,
PatternTrait trait,
StringNode node,
Model model,
BiConsumer<FromSourceLocation, String> emitter
) {
if (!trait.getPattern().matcher(node.getValue()).find()) {
return ListUtils.of(String.format(
emitter.accept(node, String.format(
"String value provided for `%s` must match regular expression: %s",
shape.getId(), trait.getPattern().pattern()));
}
return ListUtils.of();
}
}

View File

@ -16,43 +16,48 @@
package software.amazon.smithy.model.validation.node;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.NumberNode;
import software.amazon.smithy.model.shapes.NumberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.traits.RangeTrait;
import software.amazon.smithy.utils.SmithyInternalApi;
/**
* Validates the range trait on number shapes or members that target them.
*/
@SmithyInternalApi
public final class RangeTraitPlugin extends MemberAndShapeTraitPlugin<NumberShape, NumberNode, RangeTrait> {
public RangeTraitPlugin() {
final class RangeTraitPlugin extends MemberAndShapeTraitPlugin<NumberShape, NumberNode, RangeTrait> {
RangeTraitPlugin() {
super(NumberShape.class, NumberNode.class, RangeTrait.class);
}
@Override
protected List<String> check(Shape shape, RangeTrait trait, NumberNode node, Model model) {
List<String> messages = new ArrayList<>();
protected void check(
Shape shape,
RangeTrait trait,
NumberNode node,
Model model,
BiConsumer<FromSourceLocation, String> emitter
) {
Number number = node.getValue();
BigDecimal decimal = new BigDecimal(number.toString());
trait.getMin().ifPresent(min -> {
if (decimal.compareTo(new BigDecimal(min.toString())) < 0) {
messages.add(String.format(
emitter.accept(node, String.format(
"Value provided for `%s` must be greater than or equal to %s, but found %s",
shape.getId(), min.toString(), number));
}
});
trait.getMax().ifPresent(max -> {
if (decimal.compareTo(new BigDecimal(max.toString())) > 0) {
messages.add(String.format(
emitter.accept(node, String.format(
"Value provided for `%s` must be less than or equal to %s, but found %s",
shape.getId(), max.toString(), number));
}
});
return messages;
}
}

View File

@ -15,36 +15,38 @@
package software.amazon.smithy.model.validation.node;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.StringShape;
import software.amazon.smithy.model.traits.EnumTrait;
import software.amazon.smithy.model.validation.ValidationUtils;
import software.amazon.smithy.utils.SmithyInternalApi;
/**
* Validates the enum trait on string shapes.
*/
@SmithyInternalApi
public final class StringEnumPlugin extends FilteredPlugin<StringShape, StringNode> {
public StringEnumPlugin() {
final class StringEnumPlugin extends FilteredPlugin<StringShape, StringNode> {
StringEnumPlugin() {
super(StringShape.class, StringNode.class);
}
@Override
protected List<String> check(StringShape shape, StringNode node, Model model) {
List<String> messages = new ArrayList<>();
// Validate the enum trait.
protected void check(
StringShape shape,
StringNode node,
Model model,
BiConsumer<FromSourceLocation, String> emitter
) {
shape.getTrait(EnumTrait.class).ifPresent(trait -> {
List<String> values = trait.getEnumDefinitionValues();
if (!values.contains(node.getValue())) {
messages.add(String.format(
emitter.accept(node, String.format(
"String value provided for `%s` must be one of the following values: %s",
shape.getId(), ValidationUtils.tickedList(values)));
}
});
return messages;
}
}

View File

@ -15,41 +15,45 @@
package software.amazon.smithy.model.validation.node;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.StringShape;
import software.amazon.smithy.model.traits.LengthTrait;
import software.amazon.smithy.utils.SmithyInternalApi;
/**
* Validates the length trait on string shapes or members that target them.
*/
@SmithyInternalApi
public final class StringLengthPlugin extends MemberAndShapeTraitPlugin<StringShape, StringNode, LengthTrait> {
public StringLengthPlugin() {
final class StringLengthPlugin extends MemberAndShapeTraitPlugin<StringShape, StringNode, LengthTrait> {
StringLengthPlugin() {
super(StringShape.class, StringNode.class, LengthTrait.class);
}
@Override
protected List<String> check(Shape shape, LengthTrait trait, StringNode node, Model model) {
List<String> messages = new ArrayList<>();
protected void check(
Shape shape,
LengthTrait trait,
StringNode node,
Model model,
BiConsumer<FromSourceLocation, String> emitter
) {
trait.getMin().ifPresent(min -> {
if (node.getValue().length() < min) {
messages.add(String.format(
emitter.accept(node, String.format(
"String value provided for `%s` must be >= %d characters, but the provided value is "
+ "only %d characters.", shape.getId(), min, node.getValue().length()));
}
});
trait.getMax().ifPresent(max -> {
if (node.getValue().length() > max) {
messages.add(String.format(
emitter.accept(node, String.format(
"String value provided for `%s` must be <= %d characters, but the provided value is "
+ "%d characters.", shape.getId(), max, node.getValue().length()));
}
});
return messages;
}
}

View File

@ -17,122 +17,122 @@ package software.amazon.smithy.model.validation.node;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.logging.Logger;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
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.TimestampShape;
import software.amazon.smithy.model.traits.TimestampFormatTrait;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.SmithyInternalApi;
/**
* Validates that timestamp shapes contain values that are compatible with their
* timestampFormat traits or contain values that are numbers or an RFC 3339
* date-time production.
*/
@SmithyInternalApi
public final class TimestampFormatPlugin implements NodeValidatorPlugin {
final class TimestampFormatPlugin implements NodeValidatorPlugin {
private static final DateTimeFormatter HTTP_DATE = DateTimeFormatter.RFC_1123_DATE_TIME;
private static final DateTimeFormatter DATE_TIME_Z = DateTimeFormatter.ISO_INSTANT;
private static final Logger LOGGER = Logger.getLogger(TimestampFormatPlugin.class.getName());
@Override
public List<String> apply(Shape shape, Node value, Model model) {
public void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
if (shape instanceof TimestampShape) {
return validate(shape, shape.getTrait(TimestampFormatTrait.class).orElse(null), value);
validate(shape, shape.getTrait(TimestampFormatTrait.class).orElse(null), value, emitter);
} else if (shape instanceof MemberShape && shape.getTrait(TimestampFormatTrait.class).isPresent()) {
// Only perform timestamp format validation on a member when it references
// a timestamp shape and the member has an explicit timestampFormat trait.
return validate(shape, shape.getTrait(TimestampFormatTrait.class).get(), value);
} else {
// Ignore when not a timestamp or member that targets a timestamp.
return ListUtils.of();
validate(shape, shape.getTrait(TimestampFormatTrait.class).get(), value, emitter);
}
}
private List<String> validate(Shape shape, TimestampFormatTrait trait, Node value) {
private void validate(
Shape shape,
TimestampFormatTrait trait,
Node value,
BiConsumer<FromSourceLocation, String> emitter
) {
if (trait == null) {
return defaultValidation(shape, value);
}
defaultValidation(shape, value, emitter);
} else {
switch (trait.getValue()) {
case TimestampFormatTrait.DATE_TIME:
return validateDatetime(shape, value);
validateDatetime(shape, value, emitter);
break;
case TimestampFormatTrait.EPOCH_SECONDS:
// Accepts any number including floats.
if (!value.isNumberNode()) {
return ListUtils.of(String.format(
emitter.accept(value, String.format(
"Invalid %s value provided for a timestamp with a `%s` format.",
value.getType(), trait.getValue()));
}
return ListUtils.of();
break;
case TimestampFormatTrait.HTTP_DATE:
return validateHttpDate(value);
validateHttpDate(value, emitter);
break;
default:
// This validator plugin doesn't know this format, but other plugins might.
LOGGER.info(() -> "Unknown timestampFormat trait value: " + trait.getValue());
return ListUtils.of();
}
}
}
private List<String> defaultValidation(Shape shape, Node value) {
// If no timestampFormat trait is present, then the shape is
// validated by checking that the value is either a number or a
// string that matches the date-time format.
if (value.isNumberNode()) {
return ListUtils.of();
} else if (value.isStringNode()) {
return validateDatetime(shape, value);
private void defaultValidation(
Shape shape,
Node value,
BiConsumer<FromSourceLocation, String> emitter
) {
// If no timestampFormat trait is present, then the shape is validated by checking
// that the value is either a number or a string that matches the date-time format.
if (!value.isNumberNode()) {
if (value.isStringNode()) {
validateDatetime(shape, value, emitter);
} else {
return ListUtils.of(
"Invalid " + value.getType() + " value provided for timestamp, `"
+ shape.getId() + "`. Expected a number that contains epoch seconds with optional "
+ "millisecond precision, or a string that contains an RFC 3339 formatted timestamp "
+ "(e.g., \"1985-04-12T23:20:50.52Z\")");
emitter.accept(value, "Invalid " + value.getType() + " value provided for timestamp, `"
+ shape.getId() + "`. Expected a number that contains epoch seconds with "
+ "optional millisecond precision, or a string that contains an RFC 3339 "
+ "formatted timestamp (e.g., \"1985-04-12T23:20:50.52Z\")");
}
}
}
private List<String> validateDatetime(Shape shape, Node value) {
private void validateDatetime(Shape shape, Node value, BiConsumer<FromSourceLocation, String> emitter) {
if (!value.isStringNode()) {
return ListUtils.of(
"Expected a string value for a date-time timestamp (e.g., \"1985-04-12T23:20:50.52Z\")");
emitter.accept(value, "Expected a string value for a date-time timestamp "
+ "(e.g., \"1985-04-12T23:20:50.52Z\")");
return;
}
String timestamp = value.expectStringNode().getValue();
// Newer versions of Java support parsing instants that have an offset.
// See: https://bugs.openjdk.java.net/browse/JDK-8166138
// However, Smithy doesn't allow offsets for timestamp shapes.
if (timestamp.endsWith("Z") && isValidFormat(timestamp, DATE_TIME_Z)) {
return ListUtils.of();
} else {
return ListUtils.of(
"Invalid string value, `" + timestamp + "`, provided for timestamp, `"
+ shape.getId() + "`. Expected an RFC 3339 formatted timestamp "
+ "(e.g., \"1985-04-12T23:20:50.52Z\")");
if (!(timestamp.endsWith("Z") && isValidFormat(timestamp, DATE_TIME_Z))) {
emitter.accept(value, "Invalid string value, `" + timestamp + "`, provided for timestamp, `"
+ shape.getId() + "`. Expected an RFC 3339 formatted timestamp (e.g., "
+ "\"1985-04-12T23:20:50.52Z\")");
}
}
private List<String> validateHttpDate(Node value) {
private void validateHttpDate(Node value, BiConsumer<FromSourceLocation, String> emitter) {
if (!value.asStringNode().isPresent()) {
return createInvalidHttpDateMessage(value.getType().toString());
}
emitter.accept(value, createInvalidHttpDateMessage(value.getType().toString()));
} else {
String dateValue = value.asStringNode().get().getValue();
if (!isValidFormat(dateValue, HTTP_DATE) || !dateValue.endsWith("GMT")) {
return createInvalidHttpDateMessage(dateValue);
emitter.accept(value, createInvalidHttpDateMessage(dateValue));
}
}
}
return ListUtils.of();
}
private List<String> createInvalidHttpDateMessage(String dateValue) {
return ListUtils.of(String.format(
private String createInvalidHttpDateMessage(String dateValue) {
return String.format(
"Invalid value provided for %s formatted timestamp. Expected a string value that "
+ "matches the IMF-fixdate production of RFC 7231 section-7.1.1.1. Found: %s",
TimestampFormatTrait.HTTP_DATE, dateValue));
TimestampFormatTrait.HTTP_DATE, dateValue);
}
private boolean isValidFormat(String value, DateTimeFormatter format) {

View File

@ -15,20 +15,18 @@
package software.amazon.smithy.model.validation.node;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.Model;
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.utils.ListUtils;
import software.amazon.smithy.utils.SmithyInternalApi;
/**
* Defines how timestamps are validated.
*/
@SmithyInternalApi
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
@ -37,8 +35,8 @@ public enum TimestampValidationStrategy implements NodeValidatorPlugin {
*/
FORMAT {
@Override
public List<String> apply(Shape shape, Node value, Model model) {
return new TimestampFormatPlugin().apply(shape, value, model);
public void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
new TimestampFormatPlugin().apply(shape, value, model, emitter);
}
},
@ -48,13 +46,11 @@ public enum TimestampValidationStrategy implements NodeValidatorPlugin {
*/
EPOCH_SECONDS {
@Override
public List<String> apply(Shape shape, Node value, Model model) {
public void apply(Shape shape, Node value, Model model, BiConsumer<FromSourceLocation, String> emitter) {
if (isTimestampMember(model, 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();
emitter.accept(shape, "Invalid " + value.getType() + " value provided for timestamp, `"
+ shape.getId() + "`. Expected a number that contains epoch "
+ "seconds with optional millisecond precision");
}
}
};