Skip to content

Commit 9fa0f35

Browse files
authored
Add @conditions trait
This commit adds the `@conditions` trait in the `smithy-contracts` package. The trait restricts shape values to those which satisfy the given JMESPath expressions. To enable this, the `smithy-model-jmespath` package was created to share utilities related to JMESPath atop `smithy-model` across implementations. Minor updates to the `smithy-waiters` package have been made to use this package. Additionally, this commit brings an SPI to supply `NodeValidatorPlugin`s to the `NodeValidationVisitor`. Plugins are now cached by the `ShapeType`s they apply to.
1 parent f4e4a29 commit 9fa0f35

58 files changed

Lines changed: 1527 additions & 333 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "feature",
3+
"description": "Added the `@smithy.contracts#conditions` trait, available in the new `smithy-contract-traits` package. This trait defines restrictions on shape values using JMESPath expressions." ,
4+
"pull_requests": [
5+
"[#2935](https://github.com/smithy-lang/smithy/pull/2935)"
6+
]
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "feature",
3+
"description": "Added a service provider interface for `NodeValidationVisitor` plugins, and optimized to index plugins by the `ShapeType` they apply to.",
4+
"pull_requests": [
5+
"[#2935](https://github.com/smithy-lang/smithy/pull/2935)"
6+
]
7+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
.. _contract-traits:
2+
3+
===============
4+
Contract traits
5+
===============
6+
7+
Contract traits are used to further constrain the valid values and behaviors of a model.
8+
Like constraint traits, contract traits are for validation only and SHOULD NOT
9+
impact the types signatures of generated code.
10+
11+
--------------------------
12+
Contract trait enforcement
13+
--------------------------
14+
15+
Contract traits provide structured documentation of implicit API constraints,
16+
and are useful for generating tests or applying static analysis to client or service code.
17+
18+
Contract traits SHOULD NOT be directly enforced by default when serializing or deserializing.
19+
These traits often express contracts using higher-level constructs and simpler but less efficient expressions.
20+
Services will usually check these contracts outside of service frameworks in more efficient ways.
21+
22+
.. smithy-trait:: smithy.contracts#conditions
23+
.. _conditions-trait:
24+
25+
--------------------
26+
``conditions`` trait
27+
--------------------
28+
29+
Summary
30+
Restricts shape values to those which satisfy the given JMESPath expressions.
31+
Trait selector
32+
``:not(:test(service, operation, resource))``
33+
34+
*Any shape other than services, operations, and resources*
35+
Value type
36+
``map``
37+
38+
The ``conditions`` trait is a map from condition names to ``Condition`` structures that contain
39+
the following members:
40+
41+
.. list-table::
42+
:header-rows: 1
43+
:widths: 10 23 67
44+
45+
* - Property
46+
- Type
47+
- Description
48+
* - expression
49+
- ``string``
50+
- **Required**. JMESPath_ expression that must evaluate to true.
51+
* - documentation
52+
- ``string``
53+
- **Required**. Documentation about the condition defined using CommonMark_.
54+
55+
See the :ref:`JMESPath data model <waiter-jmespath-data-model>` for details on how Smithy types are mapped to JMESPath types.
56+
57+
.. code-block:: smithy
58+
59+
@conditions({
60+
StartBeforeEnd: {
61+
description: "The start time must be strictly less than the end time",
62+
expression: "start < end"
63+
}
64+
})
65+
structure FetchLogsInput {
66+
@required
67+
start: Timestamp
68+
69+
@required
70+
end: Timestamp
71+
}
72+
73+
@conditions({
74+
NoKeywords: {
75+
description: "The name cannot contain either 'id' or 'name', as these are reserved keywords"
76+
expression: "!contains(@, 'id') && !contains(@, 'name')"
77+
}
78+
})
79+
string Name
80+
81+
82+
.. _CommonMark: https://spec.commonmark.org/
83+
.. _JMESPath: https://jmespath.org/

docs/source-2.0/additional-specs/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ start with ``smithy.*`` where "*" is anything other than ``api``.
1111
:caption: smithy.* specifications
1212

1313
ai-traits
14+
contract-traits
1415
http-protocol-compliance-tests
1516
smoke-tests
1617
waiters
1718
mqtt
1819
rules-engine/index
1920
protocols/index
2021

22+
2123
.. seealso::
2224

2325
* :doc:`../spec/index`

docs/source-2.0/additional-specs/waiters.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,7 @@ support the following members:
465465
``expression`` with the ``expected`` value. The string value MUST
466466
be a valid :ref:`PathComparator-enum`.
467467

468+
.. _waiter-JMESPath-data-model:
468469

469470
JMESPath data model
470471
-------------------

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ include(":smithy-utils")
2727
include(":smithy-protocol-test-traits")
2828
include(":smithy-jmespath")
2929
include(":smithy-jmespath-tests")
30+
include(":smithy-model-jmespath")
3031
include(":smithy-waiters")
3132
include(":smithy-aws-cloudformation-traits")
3233
include(":smithy-aws-cloudformation")
@@ -41,3 +42,4 @@ include(":smithy-protocol-traits")
4142
include(":smithy-protocol-tests")
4243
include(":smithy-trait-codegen")
4344
include(":smithy-docgen")
45+
include(":smithy-contract-traits")
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
plugins {
6+
id("smithy.module-conventions")
7+
}
8+
9+
description = "This module provides Smithy traits for declaring contracts on models."
10+
11+
extra["displayName"] = "Smithy :: Contract Traits"
12+
extra["moduleName"] = "software.amazon.smithy.contract.traits"
13+
14+
dependencies {
15+
api(project(":smithy-model-jmespath"))
16+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.contracts;
6+
7+
import software.amazon.smithy.jmespath.JmespathException;
8+
import software.amazon.smithy.jmespath.JmespathExpression;
9+
import software.amazon.smithy.model.FromSourceLocation;
10+
import software.amazon.smithy.model.SourceException;
11+
import software.amazon.smithy.model.SourceLocation;
12+
import software.amazon.smithy.model.node.ExpectationNotMetException;
13+
import software.amazon.smithy.model.node.Node;
14+
import software.amazon.smithy.model.node.ToNode;
15+
import software.amazon.smithy.utils.SmithyBuilder;
16+
import software.amazon.smithy.utils.ToSmithyBuilder;
17+
18+
/**
19+
* Defines an individual condition.
20+
*/
21+
public final class Condition implements ToNode, ToSmithyBuilder<Condition>, FromSourceLocation {
22+
private final SourceLocation sourceLocation;
23+
private final String expressionText;
24+
private final JmespathExpression expression;
25+
private final String documentation;
26+
27+
private Condition(Builder builder) {
28+
this.sourceLocation = SmithyBuilder.requiredState("sourceLocation", builder.sourceLocation);
29+
this.expressionText = SmithyBuilder.requiredState("expression", builder.expression);
30+
try {
31+
this.expression = JmespathExpression.parse(expressionText);
32+
} catch (JmespathException e) {
33+
throw new SourceException(
34+
"Invalid condition JMESPath expression: `" + expressionText + "`. " + e.getMessage(),
35+
builder.sourceLocation);
36+
}
37+
this.documentation = SmithyBuilder.requiredState("documentation", builder.documentation);
38+
}
39+
40+
@Override
41+
public Node toNode() {
42+
return Node.objectNodeBuilder()
43+
.withMember("expression", Node.from(expressionText))
44+
.withMember("documentation", Node.from(documentation))
45+
.build();
46+
}
47+
48+
/**
49+
* Creates a {@link Condition} from a {@link Node}.
50+
*
51+
* @param node Node to create the Condition from.
52+
* @return Returns the created Condition.
53+
* @throws ExpectationNotMetException if the given Node is invalid.
54+
*/
55+
public static Condition fromNode(Node node) {
56+
Builder builder = builder().sourceLocation(node.getSourceLocation());
57+
node.expectObjectNode()
58+
.expectStringMember("expression", builder::expression)
59+
.expectStringMember("documentation", builder::documentation);
60+
return builder.build();
61+
}
62+
63+
@Override
64+
public SourceLocation getSourceLocation() {
65+
return sourceLocation;
66+
}
67+
68+
/**
69+
* JMESPath expression that must evaluate to true.
70+
*
71+
* @return Return the JMESPath expression.
72+
*/
73+
public JmespathExpression getExpression() {
74+
return expression;
75+
}
76+
77+
/**
78+
* Documentation about the condition.
79+
*
80+
* @return Return the documentation.
81+
*/
82+
public String getDocumentation() {
83+
return documentation;
84+
}
85+
86+
@Override
87+
public Builder toBuilder() {
88+
return builder()
89+
.expression(expressionText)
90+
.documentation(documentation);
91+
}
92+
93+
/**
94+
* Creates a builder used to build an equivalent {@link Condition}.
95+
* @return the builder.
96+
*/
97+
public static Builder builder() {
98+
return new Builder();
99+
}
100+
101+
/**
102+
* Builder for {@link Condition}.
103+
*/
104+
public static final class Builder implements SmithyBuilder<Condition> {
105+
private SourceLocation sourceLocation;
106+
private String expression;
107+
private String documentation;
108+
109+
private Builder() {}
110+
111+
public Builder sourceLocation(SourceLocation sourceLocation) {
112+
this.sourceLocation = sourceLocation;
113+
return this;
114+
}
115+
116+
public Builder expression(String expression) {
117+
this.expression = expression;
118+
return this;
119+
}
120+
121+
public Builder documentation(String documentation) {
122+
this.documentation = documentation;
123+
return this;
124+
}
125+
126+
@Override
127+
public Condition build() {
128+
return new Condition(this);
129+
}
130+
}
131+
132+
@Override
133+
public boolean equals(Object other) {
134+
if (other == this) {
135+
return true;
136+
} else if (!(other instanceof Condition)) {
137+
return false;
138+
} else {
139+
Condition b = (Condition) other;
140+
return toNode().equals(b.toNode());
141+
}
142+
}
143+
144+
@Override
145+
public int hashCode() {
146+
return toNode().hashCode();
147+
}
148+
}

0 commit comments

Comments
 (0)