Skip to content

Commit

Permalink
Add protocol trait requirements
Browse files Browse the repository at this point in the history
Protocol traits now list the traits that protocol implementations must
understand in order to successfully implement the protocol. This
provides the ability to better ensure correctness of protocol
implementations as protocols evolve.
  • Loading branch information
mtdowling committed Feb 24, 2020
1 parent 63d72ed commit e235ad4
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 16 deletions.
16 changes: 15 additions & 1 deletion docs/source/spec/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4675,7 +4675,21 @@ Summary
Trait selector
``[trait|trait]``
Value type
Annotation trait.
An object with the following properties:

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

* - Property
- Type
- Description
* - traits
- [:ref:`shape-id`]
- List of shape IDs that protocol implementations MUST understand
in order to successfully use the protocol. Each shape MUST exist
and MUST be a trait. Code generators SHOULD ensure that they
support each listed trait.

Smithy is protocol agnostic, which means it focuses on the interfaces and
abstractions that are provided to end-users rather than how the data is sent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@
"smithy.api#trait": {
"selector": "service"
},
"smithy.api#protocolDefinition": true,
"smithy.api#protocolDefinition": {
"traits": [
"smithy.api#httpError",
"smithy.api#httpHeader",
"smithy.api#httpLabel",
"smithy.api#httpPayload",
"smithy.api#httpPrefixHeaders",
"smithy.api#httpQuery",
"smithy.api#jsonName"
]
},
"smithy.api#documentation": "A RESTful protocol that sends JSON in structured payloads."
}
},
Expand All @@ -33,7 +43,20 @@
"smithy.api#trait": {
"selector": "service"
},
"smithy.api#protocolDefinition": true,
"smithy.api#protocolDefinition": {
"traits": [
"smithy.api#httpError",
"smithy.api#httpHeader",
"smithy.api#httpLabel",
"smithy.api#httpPayload",
"smithy.api#httpPrefixHeaders",
"smithy.api#httpQuery",
"smithy.api#xmlAttribute",
"smithy.api#xmlFlattened",
"smithy.api#xmlName",
"smithy.api#xmlNamespace"
]
},
"smithy.api#documentation": "A RESTful protocol that sends XML in structured payloads.",
"smithy.api#deprecated": true
}
Expand All @@ -52,7 +75,11 @@
"smithy.api#trait": {
"selector": "service"
},
"smithy.api#protocolDefinition": true,
"smithy.api#protocolDefinition": {
"traits": [
"smithy.api#jsonName"
]
},
"smithy.api#documentation": "An RPC-based protocol that sends JSON payloads. This protocol does not use HTTP binding traits."
}
},
Expand All @@ -70,7 +97,11 @@
"smithy.api#trait": {
"selector": "service"
},
"smithy.api#protocolDefinition": true,
"smithy.api#protocolDefinition": {
"traits": [
"smithy.api#jsonName"
]
},
"smithy.api#documentation": "An RPC-based protocol that sends JSON payloads. This protocol does not use HTTP binding traits."
}
},
Expand All @@ -80,7 +111,14 @@
"smithy.api#trait": {
"selector": "service"
},
"smithy.api#protocolDefinition": true,
"smithy.api#protocolDefinition": {
"traits": [
"smithy.api#xmlAttribute",
"smithy.api#xmlFlattened",
"smithy.api#xmlName",
"smithy.api#xmlNamespace"
]
},
"smithy.api#documentation": "An RPC-based protocol that sends query string requests and XML responses. This protocol does not use HTTP binding traits.",
"smithy.api#deprecated": true
}
Expand All @@ -91,7 +129,15 @@
"smithy.api#trait": {
"selector": "service"
},
"smithy.api#protocolDefinition": true,
"smithy.api#protocolDefinition": {
"traits": [
"aws.api#ec2QueryName",
"smithy.api#xmlAttribute",
"smithy.api#xmlFlattened",
"smithy.api#xmlName",
"smithy.api#xmlNamespace"
]
},
"smithy.api#documentation": "An RPC-based protocol that sends Amazon EC2 formatted query string requests and XML responses. This protocol does not use HTTP binding traits.",
"smithy.api#deprecated": true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,96 @@

package software.amazon.smithy.model.traits;

import software.amazon.smithy.model.SourceLocation;
import java.util.ArrayList;
import java.util.List;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.ToSmithyBuilder;

/**
* A trait that is attached to other traits to define a Smithy protocol.
*/
public final class ProtocolDefinitionTrait extends BooleanTrait {
public final class ProtocolDefinitionTrait extends AbstractTrait implements ToSmithyBuilder<ProtocolDefinitionTrait> {

public static final ShapeId ID = ShapeId.from("smithy.api#protocolDefinition");
private final List<ShapeId> traits;

public ProtocolDefinitionTrait(SourceLocation sourceLocation) {
super(ID, sourceLocation);
public ProtocolDefinitionTrait(Builder builder) {
super(ID, builder.getSourceLocation());
traits = ListUtils.copyOf(builder.traits);
}

public ProtocolDefinitionTrait() {
this(SourceLocation.NONE);
/**
* Gets the list of shape IDs that protocol implementations must know about
* in order to successfully utilize the protocol.
*
* @return Returns the protocol traits.
*/
public List<ShapeId> getTraits() {
return traits;
}

public static final class Provider extends BooleanTrait.Provider<ProtocolDefinitionTrait> {
public static Builder builder() {
return new Builder();
}

@Override
protected Node createNode() {
if (traits.isEmpty()) {
return Node.objectNode();
}

ArrayNode ids = traits.stream()
.map(ShapeId::toString)
.map(Node::from)
.collect(ArrayNode.collect());

return Node.objectNode().withMember("traits", ids);
}

@Override
public Builder toBuilder() {
return builder().sourceLocation(getSourceLocation()).traits(traits);
}

public static final class Provider extends AbstractTrait.Provider {
public Provider() {
super(ID, ProtocolDefinitionTrait::new);
super(ID);
}

@Override
public ProtocolDefinitionTrait createTrait(ShapeId target, Node value) {
Builder builder = builder().sourceLocation(value);
ObjectNode objectNode = value.expectObjectNode();
objectNode.getArrayMember("traits").ifPresent(traits -> {
for (String string : Node.loadArrayOfString("traits", traits)) {
builder.addTrait(ShapeId.from(string));
}
});
return builder.build();
}
}

public static final class Builder extends AbstractTraitBuilder<ProtocolDefinitionTrait, Builder> {
private final List<ShapeId> traits = new ArrayList<>();

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

public Builder traits(List<ShapeId> traits) {
this.traits.clear();
this.traits.addAll(traits);
return this;
}

public Builder addTrait(ShapeId trait) {
traits.add(trait);
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,20 @@ string AuthTraitReference
/// structure, and must have the `trait` trait.
@trait(selector: "structure[trait|trait]")
@tags(["diff.error.add", "diff.error.remove"])
structure protocolDefinition {}
structure protocolDefinition {
/// Defines a list of traits that protocol implementations must
/// understand in order to successfully use the protocol.
traits: TraitShapeIdList,
}

@private
list TraitShapeIdList {
member: TraitShapeId,
}

@private
@idRef(failWhenMissing: true, selector: "[trait|trait]")
string TraitShapeId

/// Marks a trait as an auth scheme defining trait.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package software.amazon.smithy.model.traits;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Optional;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.ShapeId;

public class ProtocolDefinitionTraitTest {
@Test
public void loadsTrait() {
TraitFactory provider = TraitFactory.createServiceFactory();
ArrayNode values = Node.fromStrings(
JsonNameTrait.ID.toString(),
XmlNameTrait.ID.toString());
Node node = Node.objectNode().withMember("traits", values);
Optional<Trait> trait = provider.createTrait(
ShapeId.from("smithy.api#protocolDefinition"),
ShapeId.from("ns.qux#foo"),
node);

assertTrue(trait.isPresent());
assertThat(trait.get(), instanceOf(ProtocolDefinitionTrait.class));
ProtocolDefinitionTrait protocolDefinitionTrait = (ProtocolDefinitionTrait) trait.get();
assertThat(protocolDefinitionTrait.getTraits(), containsInAnyOrder(
JsonNameTrait.ID, XmlNameTrait.ID));
assertThat(protocolDefinitionTrait.toNode(), equalTo(node));
assertThat(protocolDefinitionTrait.toBuilder().build(), equalTo(protocolDefinitionTrait));
}
}

0 comments on commit e235ad4

Please sign in to comment.