Add new shapeExamples trait that communicates and enforces allowed and disallowed values#2851
Add new shapeExamples trait that communicates and enforces allowed and disallowed values#2851brandondahler wants to merge 3 commits intosmithy-lang:mainfrom
Conversation
…d disallowed values
.changes/next-release/feature-0dad6fe920f7dc053e14bed2d43b2e553a2f142a.json
Show resolved
Hide resolved
…3a2f142a.json Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
| } | ||
| } | ||
|
|
||
| private static final class ErrorsFileValidationEventFormatter implements ValidationEventFormatter { |
There was a problem hiding this comment.
Updated implementation here to make it easier to iterate on error files -- the previous implementation would output the extra file location information and hints if they exist, which are explicitly omitted from the errors file normally.
You can now copy+paste directly from the failure message in the test result into the errors file.
| if (value.isNullNode()) { | ||
|
|
||
| if (!value.isNullNode()) { | ||
| model.getShape(shape.getTarget()).ifPresent(target -> { |
There was a problem hiding this comment.
Originally traversing into the shape when the value was null pretty much guaranteed that an error would be reported -- generally of the form "<string, number, object> was expected but value was null" (not exact wording).
Since is the memberShape method, this only affects aggregate types which have an explicit null value as opposed to the member being omitted.
| allowed: ShapeExampleList | ||
| disallowed: ShapeExampleList |
There was a problem hiding this comment.
nit:
| allowed: ShapeExampleList | |
| disallowed: ShapeExampleList | |
| valid: ShapeExampleList | |
| invalid: ShapeExampleList |
AFAICT "valid" appears a whole lot more in the spec/documentation than "allowed"
JordonPhillips
left a comment
There was a problem hiding this comment.
I talked to the SDK teams and got some general consensus on bringing this in. There's a few changes I'd like to make, notably adding some optional documentation to each example to explain why something does or doesn't pass.
| @trait( | ||
| selector: ":test(number, string, blob, structure, list, map, member)" | ||
| ) |
There was a problem hiding this comment.
This should be able to target unions. Also, gotta make sure to restrict the target for member shapes.
| @trait( | |
| selector: ":test(number, string, blob, structure, list, map, member)" | |
| ) | |
| @trait( | |
| selector: """ | |
| :test( | |
| number, string, blob, structure, union, list, map, | |
| member > :test(number, string, blob, structure, union, list, map) | |
| )""" | |
| ) |
| @private | ||
| @length(min: 1) | ||
| @sparse | ||
| list ShapeExampleList { | ||
| member: Document | ||
| } |
There was a problem hiding this comment.
I'd like to open this up a bit by making the list value a structure. That'll let us add additional properties aside from the value itself. At the moment I've added a documentation property to describe why a given value does or does not validate. There was also some desire to add a specific error that invalid values trigger. I'm not so sure about that. But opening it up like this makes that possible too.
| @private | |
| @length(min: 1) | |
| @sparse | |
| list ShapeExampleList { | |
| member: Document | |
| } | |
| /// A list of values to be mapped to a shape. | |
| @private | |
| @length(min: 1) | |
| @sparse | |
| list ShapeExampleList { | |
| member: ShapeExample | |
| } | |
| /// A value that may be mapped to a shape. | |
| @private | |
| structure ShapeExample { | |
| /// The actual value that may be mapped to the shape. This must match the | |
| /// expected shape type, regardless of whether this example represents a | |
| /// valid or invalid value. | |
| @required | |
| value: Document | |
| /// Optional documentation in CommonMark format describing why the example | |
| /// does or does not match the shape's constraints. | |
| documentation: String | |
| } |
There was a problem hiding this comment.
+1. As the person with the desire mentioned :), I think it would be great to also have a validationEventId member stating precisely what event is expected, using the same matching rules as suppressions. That would enable the validation of this trait much more precise.
|
|
||
| events.addAll(nonErrorValidationEvents); | ||
|
|
||
| if (validationEvents.size() == nonErrorValidationEvents.size()) { |
There was a problem hiding this comment.
I don't think it's useful to allow examples that don't match the expected type, even for the invalid list. Perhaps we should forward that along.
There was a problem hiding this comment.
If you take my suggestion (https://github.com/smithy-lang/smithy/pull/2851/changes#r2834425764), this could be more precise and check that you only get events that match the expected id.
| private List<Node> allowed; | ||
| private List<Node> disallowed; | ||
|
|
||
| public ShapeExamplesTrait.Builder allowed(List<Node> allowed) { | ||
| this.allowed = allowed; | ||
| return this; | ||
| } | ||
|
|
||
| public ShapeExamplesTrait.Builder disallowed(List<Node> disallowed) { | ||
| this.disallowed = disallowed; |
There was a problem hiding this comment.
Using BuilderRef prevents leaking mutations and reduces copies.
| private List<Node> allowed; | |
| private List<Node> disallowed; | |
| public ShapeExamplesTrait.Builder allowed(List<Node> allowed) { | |
| this.allowed = allowed; | |
| return this; | |
| } | |
| public ShapeExamplesTrait.Builder disallowed(List<Node> disallowed) { | |
| this.disallowed = disallowed; | |
| private BuilderRef<List<Node>> allowed = BuilderRef.forList(); | |
| private BuilderRef<List<Node>> disallowed = BuilderRef.forList(); | |
| public ShapeExamplesTrait.Builder allowed(List<Node> allowed) { | |
| this.allowed.clear(); | |
| this.allowed.get().addAll(allowed); | |
| return this; | |
| } | |
| public ShapeExamplesTrait.Builder disallowed(List<Node> disallowed) { | |
| this.disallowed.clear(); | |
| this.disallowed.addAll(disallowed); |
| this.allowed = builder.allowed; | ||
| this.disallowed = builder.disallowed; | ||
| if (allowed == null && disallowed == null) { | ||
| throw new SourceException("One of 'allowed' or 'disallowed' must be provided.", getSourceLocation()); | ||
| } | ||
| if (allowed != null && allowed.isEmpty()) { | ||
| throw new SourceException("'allowed' must be non-empty when provided.", getSourceLocation()); | ||
| } | ||
| if (disallowed != null && disallowed.isEmpty()) { | ||
| throw new SourceException("'disallowed' must be non-empty when provided.", getSourceLocation()); | ||
| } |
There was a problem hiding this comment.
Empty lists are perfectly fine, so long as at least one is populated.
| this.allowed = builder.allowed; | |
| this.disallowed = builder.disallowed; | |
| if (allowed == null && disallowed == null) { | |
| throw new SourceException("One of 'allowed' or 'disallowed' must be provided.", getSourceLocation()); | |
| } | |
| if (allowed != null && allowed.isEmpty()) { | |
| throw new SourceException("'allowed' must be non-empty when provided.", getSourceLocation()); | |
| } | |
| if (disallowed != null && disallowed.isEmpty()) { | |
| throw new SourceException("'disallowed' must be non-empty when provided.", getSourceLocation()); | |
| } | |
| this.allowed = builder.allowed.copy(); | |
| this.disallowed = builder.disallowed.copy(); | |
| if (allowed.isEmpty() && disallowed.isEmpty()) { | |
| throw new SourceException("One of 'allowed' or 'disallowed' must be non-empty.", getSourceLocation()); | |
| } |
| public Optional<List<Node>> getAllowed() { | ||
| return Optional.ofNullable(allowed); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the disallowed values. | ||
| * | ||
| * @return returns the optional disallowed values. | ||
| */ | ||
| public Optional<List<Node>> getDisallowed() { | ||
| return Optional.ofNullable(disallowed); | ||
| } |
There was a problem hiding this comment.
| public Optional<List<Node>> getAllowed() { | |
| return Optional.ofNullable(allowed); | |
| } | |
| /** | |
| * Gets the disallowed values. | |
| * | |
| * @return returns the optional disallowed values. | |
| */ | |
| public Optional<List<Node>> getDisallowed() { | |
| return Optional.ofNullable(disallowed); | |
| } | |
| public List<Node> getAllowed() { | |
| return allowed; | |
| } | |
| /** | |
| * Gets the disallowed values. | |
| * | |
| * @return returns the optional disallowed values. | |
| */ | |
| public List<Node> getDisallowed() { | |
| return disallowed; | |
| } |
Background
@shapeExamplestrait which defines the set of allowed and disallowed values for a given shape.NodeValidationVisitorto be more accurate in some less-commonly used casesShapeExamplesTraitValidatorimplementation which verifies that:@pattern-based constrained valuesTesting
Links
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.