Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
@@ -0,0 +1,7 @@
{
"type": "feature",
"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." ,
"pull_requests": [
"[#2935](https://github.com/smithy-lang/smithy/pull/2935)"
]
}
77 changes: 77 additions & 0 deletions docs/source-2.0/additional-specs/contract-traits.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.. _contract-traits:

===============
Contract traits
===============

Contract traits are used to further constrain the valid values and behaviors of a model.
Like constraint traits, contract traits are for validation only and SHOULD NOT
impact the types signatures of generated code.

--------------------------
Contract trait enforcement
--------------------------

Contract traits SHOULD NOT be directly enforced by default when serializing or deserializing.
These traits often express contracts using higher-level constructs and simpler but less efficient expressions.
Services will usually check these contracts outside of service frameworks in more efficient ways.

Contract traits are instead intended to be useful for generating tests
or applying static analysis to client or service code.

.. smithy-trait:: smithy.contracts#conditions
.. _conditions-trait:

--------------------
``conditions`` trait
--------------------

Summary
Restricts shape values to those which satisfy the given JMESPath expressions.
Trait selector
``:not(:test(service, operation, resource))``

*Any shape other than services, operations, and resources*
Value type
``map``

The ``conditions`` trait is a map from condition names to ``Condition`` structures that contain
the following members:

.. list-table::
:header-rows: 1
:widths: 10 23 67

* - Property
- Type
- Description
* - expression
- ``string``
- **Required**. JMESPath expression that must evaluate to true.
* - description
- ``string``
- **Required**. Description of the condition. Used in error messages when violated.

.. code-block:: smithy

@conditions({
StartBeforeEnd: {
description: "The start time must be strictly less than the end time",
expression: "start < end"
}
})
structure FetchLogsInput {
@required
start: Timestamp

@required
end: Timestamp
}

@conditions({
NoKeywords: {
description: "The name cannot contain either 'id' or 'name', as these are reserved keywords"
expression: "!contains(@, 'id') && !contains(@, 'name')"
}
})
string Name
2 changes: 2 additions & 0 deletions docs/source-2.0/additional-specs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ start with ``smithy.*`` where "*" is anything other than ``api``.
:caption: smithy.* specifications

ai-traits
contract-traits
http-protocol-compliance-tests
smoke-tests
waiters
mqtt
rules-engine/index
protocols/index


.. seealso::

* :doc:`../spec/index`
Expand Down
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ include(":smithy-utils")
include(":smithy-protocol-test-traits")
include(":smithy-jmespath")
include(":smithy-jmespath-tests")
include(":smithy-model-jmespath")
include(":smithy-waiters")
include(":smithy-aws-cloudformation-traits")
include(":smithy-aws-cloudformation")
Expand All @@ -41,3 +42,4 @@ include(":smithy-protocol-traits")
include(":smithy-protocol-tests")
include(":smithy-trait-codegen")
include(":smithy-docgen")
include(":smithy-contract-traits")
16 changes: 16 additions & 0 deletions smithy-contract-traits/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
plugins {
id("smithy.module-conventions")
}

description = "This module provides Smithy traits for declaring contracts on models."

extra["displayName"] = "Smithy :: Contract Traits"
extra["moduleName"] = "software.amazon.smithy.contract.traits"

dependencies {
api(project(":smithy-model-jmespath"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.contracts;

import software.amazon.smithy.jmespath.JmespathException;
import software.amazon.smithy.jmespath.JmespathExpression;
import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.SourceException;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.node.ExpectationNotMetException;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ToNode;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.ToSmithyBuilder;

public final class Condition implements ToNode, ToSmithyBuilder<Condition>, FromSourceLocation {
private final SourceLocation sourceLocation;
private final String expressionText;
private final JmespathExpression expression;
private final String description;

private Condition(Builder builder) {
this.sourceLocation = SmithyBuilder.requiredState("sourceLocation", builder.sourceLocation);
this.expressionText = SmithyBuilder.requiredState("expression", builder.expression);
try {
this.expression = JmespathExpression.parse(expressionText);
} catch (JmespathException e) {
throw new SourceException(
"Invalid condition JMESPath expression: `" + expressionText + "`. " + e.getMessage(),
builder.sourceLocation);
}
this.description = SmithyBuilder.requiredState("description", builder.description);
}

@Override
public Node toNode() {
return Node.objectNodeBuilder()
.withMember("expression", Node.from(expressionText))
.withMember("description", Node.from(description))
.build();
}

/**
* Creates a {@link Condition} from a {@link Node}.
*
* @param node Node to create the Condition from.
* @return Returns the created Condition.
* @throws ExpectationNotMetException if the given Node is invalid.
*/
public static Condition fromNode(Node node) {
Builder builder = builder().sourceLocation(node.getSourceLocation());
node.expectObjectNode()
.expectStringMember("expression", builder::expression)
.expectStringMember("description", builder::description);
return builder.build();
}

@Override
public SourceLocation getSourceLocation() {
return sourceLocation;
}

/**
* JMESPath expression that must evaluate to true.
*/
public JmespathExpression getExpression() {
return expression;
}

/**
* Description of the condition. Used in error messages when violated.
*/
public String getDescription() {
return description;
}

/**
* Creates a builder used to build a {@link Condition}.
*/
public SmithyBuilder<Condition> toBuilder() {
return builder()
.expression(expressionText)
.description(description);
}

/**
* Creates a builder used to build an equivalent {@link Condition}.
*/
public static Builder builder() {
return new Builder();
}

/**
* Builder for {@link Condition}.
*/
public static final class Builder implements SmithyBuilder<Condition> {
private SourceLocation sourceLocation;
private String expression;
private String description;

private Builder() {}

public Builder sourceLocation(SourceLocation sourceLocation) {
this.sourceLocation = sourceLocation;
return this;
}

public Builder expression(String expression) {
this.expression = expression;
return this;
}

public Builder description(String description) {
this.description = description;
return this;
}

@Override
public Condition build() {
return new Condition(this);
}
}

@Override
public boolean equals(Object other) {
if (other == this) {
return true;
} else if (!(other instanceof Condition)) {
return false;
} else {
Condition b = (Condition) other;
return toNode().equals(b.toNode());
}
}

@Override
public int hashCode() {
return toNode().hashCode();
}
}
Loading
Loading