diff --git a/docs/source/spec/core.rst b/docs/source/spec/core.rst index 07dfd1e250f..e32eda2ab3e 100644 --- a/docs/source/spec/core.rst +++ b/docs/source/spec/core.rst @@ -3322,18 +3322,12 @@ Summary Trait selector ``string`` Value type - ``map`` of enum constant values to structures optionally containing a name, - documentation, tags, and/or a deprecation flag. + ``list`` of enum definition structures. Smithy models SHOULD apply the enum trait when string shapes have a fixed set of allowable values. -The enum trait is a map of allowed string values to enum constant definition -structures. Enum values do not allow aliasing; all enum constant values MUST be -unique across the entire set. - -An enum definition is a structure that supports the following optional -members: +An enum definition is a structure that supports the following members: .. list-table:: :header-rows: 1 @@ -3342,6 +3336,10 @@ members: * - Property - Type - Description + * - value + - string + - **Required**. Defines the enum value that is sent over the wire. + Values MUST be unique across all enum definitions in an ``enum`` trait. * - name - string - Defines a constant name to use when referencing an enum value. @@ -3358,6 +3356,8 @@ members: (``a-z``) and SHOULD NOT start with an ASCII underscore (``_``). That is, enum names SHOULD match the following regular expression: ``^[A-Z]+[A-Z_0-9]*$``. + + Names MUST be unique across all enum definitions in an ``enum`` trait. * - documentation - string - Defines documentation about the enum value in the CommonMark_ format. @@ -3384,8 +3384,9 @@ The following example defines an enum of valid string values for ``MyString``. .. code-tab:: smithy - @enum( - t2.nano: { + @enum([ + { + value: "t2.nano", name: "T2_NANO", documentation: """ T2 instances are Burstable Performance @@ -3394,7 +3395,8 @@ The following example defines an enum of valid string values for ``MyString``. baseline.""", tags: ["ebsOnly"] }, - t2.micro: { + { + value: "t2.micro", name: "T2_MICRO", documentation: """ T2 instances are Burstable Performance @@ -3403,11 +3405,12 @@ The following example defines an enum of valid string values for ``MyString``. baseline.""", tags: ["ebsOnly"] }, - m256.mega: { + { + value: "m256.mega", name: "M256_MEGA", deprecated: true } - ) + ]) string MyString .. code-tab:: json @@ -3418,26 +3421,29 @@ The following example defines an enum of valid string values for ``MyString``. "smithy.example#MyString": { "type": "string", "traits": { - "smithy.api#enum": { - "t2.nano": { + "smithy.api#enum": [ + { + "value": "t2.nano", "name": "T2_NANO", "documentation": "T2 instances are ...", "tags": [ "ebsOnly" ] }, - "t2.micro": { + { + "value": "t2.micro", "name": "T2_MICRO", "documentation": "T2 instances are ...", "tags": [ "ebsOnly" ] }, - "m256.mega": { + { + "value": "m256.mega", "name": "M256_MEGA", "deprecated": true } - } + ] } } } @@ -5022,7 +5028,7 @@ can also support configuration settings. } @private - @enum("SHA-2": {}) + @enum([{value": "SHA-2"}]) string AlgorithmAuthAlgorithm @algorithmAuth(algorithm: "SHA-2") diff --git a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-model.json b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-model.json index f02057090b6..efff8f51904 100644 --- a/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-model.json +++ b/smithy-aws-apigateway-openapi/src/test/resources/software/amazon/smithy/aws/apigateway/openapi/cors-model.json @@ -198,14 +198,16 @@ "example.smithy#EnumString": { "type": "string", "traits": { - "smithy.api#enum": { - "a": { + "smithy.api#enum": [ + { + "value": "a", "name": "A" }, - "c": { + { + "value": "c", "name": "C" } - } + ] } }, "example.smithy#PayloadDescriptions": { diff --git a/smithy-aws-apigateway-traits/src/main/resources/META-INF/smithy/aws.apigateway.json b/smithy-aws-apigateway-traits/src/main/resources/META-INF/smithy/aws.apigateway.json index 434857b4158..9a4a9bded3d 100644 --- a/smithy-aws-apigateway-traits/src/main/resources/META-INF/smithy/aws.apigateway.json +++ b/smithy-aws-apigateway-traits/src/main/resources/META-INF/smithy/aws.apigateway.json @@ -219,20 +219,24 @@ "aws.apigateway#IntegrationType": { "type": "string", "traits": { - "smithy.api#enum": { - "aws": { + "smithy.api#enum": [ + { + "value": "aws", "name": "AWS" }, - "aws_proxy": { + { + "value": "aws_proxy", "name": "AWS_PROXY" }, - "http": { + { + "value": "http", "name": "HTTP" }, - "http_proxy": { + { + "value": "http_proxy", "name": "HTTP_PROXY" } - } + ] } }, "aws.apigateway#IamRoleArn": { @@ -283,27 +287,30 @@ "type": "string", "traits": { "smithy.api#private": true, - "smithy.api#enum": { - "INTERNET": {}, - "VPC_LINK": {} - } + "smithy.api#enum": [ + {"value": "INTERNET"}, + {"value": "VPC_LINK"} + ] } }, "aws.apigateway#PassThroughBehavior": { "type": "string", "traits": { "smithy.api#documentation": "Defines the passThroughBehavior for the integration", - "smithy.api#enum": { - "when_no_templates": { + "smithy.api#enum": [ + { + "value": "when_no_templates", "name": "WHEN_NO_TEMPLATES" }, - "when_no_match": { + { + "value": "when_no_match", "name": "WHEN_NO_MATCH" }, - "never": { + { + "value": "never", "name": "NEVER" } - }, + ], "smithy.api#private": true } }, @@ -311,14 +318,10 @@ "type": "string", "traits": { "smithy.api#documentation": "Defines the contentHandling for the integration", - "smithy.api#enum": { - "CONVERT_TO_TEXT": { - "name": "CONVERT_TO_TEXT" - }, - "CONVERT_TO_BINARY": { - "name": "CONVERT_TO_BINARY" - } - }, + "smithy.api#enum": [ + {"value": "CONVERT_TO_TEXT", "name": "CONVERT_TO_TEXT"}, + {"value": "CONVERT_TO_BINARY", "name": "CONVERT_TO_BINARY"} + ], "smithy.api#private": true } }, diff --git a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json index a6b00367fe2..c0a1cc8e893 100644 --- a/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json +++ b/smithy-aws-iam-traits/src/main/resources/META-INF/smithy/aws.iam.json @@ -90,22 +90,22 @@ "traits": { "smithy.api#private": true, "smithy.api#documentation": "The IAM policy type of the value that will supplied for this context key", - "smithy.api#enum": { - "ARN": {}, - "ArrayOfARN": {}, - "Binary": {}, - "ArrayOfBinary": {}, - "String": {}, - "ArrayOfString": {}, - "Numeric": {}, - "ArrayOfNumeric": {}, - "Date": {}, - "ArrayOfDate": {}, - "Bool": {}, - "ArrayOfBool": {}, - "IPAddress": {}, - "ArrayOfIPAddress": {} - } + "smithy.api#enum": [ + {"value": "ARN"}, + {"value": "ArrayOfARN"}, + {"value": "Binary"}, + {"value": "ArrayOfBinary"}, + {"value": "String"}, + {"value": "ArrayOfString"}, + {"value": "Numeric"}, + {"value": "ArrayOfNumeric"}, + {"value": "Date"}, + {"value": "ArrayOfDate"}, + {"value": "Bool"}, + {"value": "ArrayOfBool"}, + {"value": "IPAddress"}, + {"value": "ArrayOfIPAddress"} + ] } } } diff --git a/smithy-aws-protocol-tests/model/shared-types.smithy b/smithy-aws-protocol-tests/model/shared-types.smithy index 5593ed15643..360f1b445ee 100644 --- a/smithy-aws-protocol-tests/model/shared-types.smithy +++ b/smithy-aws-protocol-tests/model/shared-types.smithy @@ -56,13 +56,13 @@ list TimestampList { member: Timestamp, } -@enum( - Foo: {}, - Baz: {}, - Bar: {}, - "1": {}, - "0": {}, -) +@enum([ + {value: "Foo"}, + {value: "Baz"}, + {value: "Bar"}, + {value: "1"}, + {value: "0"}, +]) string FooEnum list FooEnumList { diff --git a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.json b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.json index 65c29dc078b..8493f66b4a2 100644 --- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.json +++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.api.json @@ -79,28 +79,33 @@ "smithy.api#trait": { "selector": ":test(simpleType, collection, structure, union, member)" }, - "smithy.api#enum": { - "content": { + "smithy.api#enum": [ + { + "value": "content", "name": "CUSTOMER_CONTENT", "documentation": "Customer content means any software (including machine images), data, text, audio, video or images that customers or any customer end user transfers to AWS for processing, storage or hosting by AWS services in connection with the customer\u2019s accounts and any computational results that customers or any customer end user derive from the foregoing through their use of AWS services." }, - "account": { + { + "value": "account", "name": "CUSTOMER_ACCOUNT_INFORMATION", "documentation": "Account information means information about customers that customers provide to AWS in connection with the creation or administration of customers\u2019 accounts." }, - "usage": { + { + "value": "usage", "name": "SERVICE_ATTRIBUTES", "documentation": "Service Attributes means service usage data related to a customer\u2019s account, such as resource identifiers, metadata tags, security and access roles, rules, usage policies, permissions, usage statistics, logging data, and analytics." }, - "tagging": { + { + "value": "tagging", "name": "TAG_DATA", "documentation": "Designates metadata tags applied to AWS resources." }, - "permissions": { + { + "value": "permissions", "name": "PERMISSIONS_DATA", "documentation": "Designates security and access roles, rules, usage policies, and permissions." } - }, + ], "smithy.api#documentation": "Designates the target as containing data of a known classification level." } }, diff --git a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/ChangedEnumTrait.java b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/ChangedEnumTrait.java index 568a563ee76..f805db8a50e 100644 --- a/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/ChangedEnumTrait.java +++ b/smithy-diff/src/main/java/software/amazon/smithy/diff/evaluators/ChangedEnumTrait.java @@ -17,11 +17,12 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import software.amazon.smithy.diff.ChangedShape; import software.amazon.smithy.diff.Differences; import software.amazon.smithy.model.shapes.Shape; -import software.amazon.smithy.model.traits.EnumConstantBody; +import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.utils.OptionalUtils; @@ -46,26 +47,32 @@ private List validateEnum(ChangedShape change, Pair events = new ArrayList<>(); - oldTrait.getValues().forEach((key, value) -> { - if (!newTrait.getValues().containsKey(key)) { - events.add(error(change.getNewShape(), String.format("Enum value `%s` was removed", key))); + for (EnumDefinition definition : oldTrait.getValues()) { + Optional maybeNewValue = newTrait.getValues().stream() + .filter(d -> d.getValue().equals(definition.getValue())) + .findFirst(); + + if (!maybeNewValue.isPresent()) { + events.add(error(change.getNewShape(), String.format( + "Enum value `%s` was removed", definition.getValue()))); } else { - EnumConstantBody newValue = newTrait.getValues().get(key); - if (!newValue.getName().equals(value.getName())) { + EnumDefinition newValue = maybeNewValue.get(); + if (!newValue.getName().equals(definition.getName())) { events.add(error(change.getNewShape(), String.format( "Enum `name` changed from `%s` to `%s` for the `%s` value", - value.getName().orElse(null), + definition.getName().orElse(null), newValue.getName().orElse(null), - key))); + definition.getValue()))); } } - }); + } - newTrait.getValues().forEach((key, value) -> { - if (!oldTrait.getValues().containsKey(key)) { - events.add(note(change.getNewShape(), String.format("Enum value `%s` was added", key))); + for (EnumDefinition definition : newTrait.getValues()) { + if (!oldTrait.getEnumDefinitionValues().contains(definition.getValue())) { + events.add(note(change.getNewShape(), String.format( + "Enum value `%s` was added", definition.getValue()))); } - }); + } return events; } diff --git a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedEnumTraitTest.java b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedEnumTraitTest.java index c208b60d49f..e16397db8f7 100644 --- a/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedEnumTraitTest.java +++ b/smithy-diff/src/test/java/software/amazon/smithy/diff/evaluators/ChangedEnumTraitTest.java @@ -23,7 +23,7 @@ import software.amazon.smithy.diff.ModelDiff; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.StringShape; -import software.amazon.smithy.model.traits.EnumConstantBody; +import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidationEvent; @@ -34,14 +34,14 @@ public void detectsAddedEnums() { StringShape s1 = StringShape.builder() .id("foo.baz#Baz") .addTrait(EnumTrait.builder() - .addEnum("foo", EnumConstantBody.builder().build()) + .addEnum(EnumDefinition.builder().value("foo").build()) .build()) .build(); StringShape s2 = StringShape.builder() .id("foo.baz#Baz") .addTrait(EnumTrait.builder() - .addEnum("foo", EnumConstantBody.builder().build()) - .addEnum("baz", EnumConstantBody.builder().build()) + .addEnum(EnumDefinition.builder().value("foo").build()) + .addEnum(EnumDefinition.builder().value("baz").build()) .build()) .build(); Model modelA = Model.assembler().addShape(s1).assemble().unwrap(); @@ -57,14 +57,14 @@ public void detectsRemovedEnums() { StringShape s1 = StringShape.builder() .id("foo.baz#Baz") .addTrait(EnumTrait.builder() - .addEnum("foo", EnumConstantBody.builder().build()) - .addEnum("baz", EnumConstantBody.builder().build()) + .addEnum(EnumDefinition.builder().value("foo").build()) + .addEnum(EnumDefinition.builder().value("baz").build()) .build()) .build(); StringShape s2 = StringShape.builder() .id("foo.baz#Baz") .addTrait(EnumTrait.builder() - .addEnum("foo", EnumConstantBody.builder().build()) + .addEnum(EnumDefinition.builder().value("foo").build()) .build()) .build(); Model modelA = Model.assembler().addShape(s1).assemble().unwrap(); @@ -80,13 +80,13 @@ public void detectsRenamedEnums() { StringShape s1 = StringShape.builder() .id("foo.baz#Baz") .addTrait(EnumTrait.builder() - .addEnum("foo", EnumConstantBody.builder().name("OLD").build()) + .addEnum(EnumDefinition.builder().value("foo").name("OLD").build()) .build()) .build(); StringShape s2 = StringShape.builder() .id("foo.baz#Baz") .addTrait(EnumTrait.builder() - .addEnum("foo", EnumConstantBody.builder().name("NEW").build()) + .addEnum(EnumDefinition.builder().value("foo").name("NEW").build()) .build()) .build(); Model modelA = Model.assembler().addShape(s1).assemble().unwrap(); diff --git a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java index 8e526f3e47e..efbc39c5183 100644 --- a/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java +++ b/smithy-jsonschema/src/main/java/software/amazon/smithy/jsonschema/JsonSchemaShapeVisitor.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.regex.Pattern; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.BigDecimalShape; @@ -283,8 +282,7 @@ private Schema.Builder updateBuilder(Shape shape, Schema.Builder builder) { } shape.getTrait(EnumTrait.class) - .map(EnumTrait::getValues) - .map(Map::keySet) + .map(EnumTrait::getEnumDefinitionValues) .ifPresent(builder::enumValues); return builder; diff --git a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java index 604c7f6a05c..bfdb6c387c6 100644 --- a/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java +++ b/smithy-jsonschema/src/test/java/software/amazon/smithy/jsonschema/JsonSchemaConverterTest.java @@ -56,7 +56,7 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.DocumentationTrait; -import software.amazon.smithy.model.traits.EnumConstantBody; +import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.LengthTrait; import software.amazon.smithy.model.traits.MediaTypeTrait; @@ -400,7 +400,7 @@ public void supportsDocumentation() { public void supportsEnum() { StringShape string = StringShape.builder() .id("smithy.api#String") - .addTrait(EnumTrait.builder().addEnum("foo", EnumConstantBody.builder().build()).build()) + .addTrait(EnumTrait.builder().addEnum(EnumDefinition.builder().value("foo").build()).build()) .build(); Model model = Model.builder().addShapes(string).build(); SchemaDocument document = JsonSchemaConverter.builder().model(model).build().convertShape(string); diff --git a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/test-service.json b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/test-service.json index 89c8ddd6ffa..cd504b2d611 100644 --- a/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/test-service.json +++ b/smithy-jsonschema/src/test/resources/software/amazon/smithy/jsonschema/test-service.json @@ -217,14 +217,16 @@ "example.rest#EnumString": { "type": "string", "traits": { - "smithy.api#enum": { - "a": { + "smithy.api#enum": [ + { + "value": "a", "name": "A" }, - "c": { + { + "value": "c", "name": "C" } - } + ] } }, "example.rest#TaggedUnion": { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumConstantBody.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java similarity index 74% rename from smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumConstantBody.java rename to smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java index a58b35399a0..ecaab9f53e1 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumConstantBody.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java @@ -27,25 +27,32 @@ /** * An enum definition for the enum trait. */ -public final class EnumConstantBody implements ToSmithyBuilder, Tagged { +public final class EnumDefinition implements ToSmithyBuilder, Tagged { + public static final String VALUE = "value"; public static final String NAME = "name"; public static final String DOCUMENTATION = "documentation"; public static final String TAGS = "tags"; + private final String value; private final String documentation; private final List tags; private final String name; - private EnumConstantBody(Builder builder) { + private EnumDefinition(Builder builder) { + value = SmithyBuilder.requiredState("value", builder.value); + name = builder.name; documentation = builder.documentation; tags = new ArrayList<>(builder.tags); - name = builder.name; } public static Builder builder() { return new Builder(); } + public String getValue() { + return value; + } + public Optional getName() { return Optional.ofNullable(name); } @@ -61,41 +68,43 @@ public List getTags() { @Override public Builder toBuilder() { - return builder().tags(tags).documentation(documentation).name(name); + return builder().value(value).tags(tags).documentation(documentation).name(name); } @Override public boolean equals(Object other) { - if (!(other instanceof EnumConstantBody)) { + if (!(other instanceof EnumDefinition)) { return false; } - EnumConstantBody otherEnum = (EnumConstantBody) other; - return Objects.equals(name, otherEnum.name) + EnumDefinition otherEnum = (EnumDefinition) other; + return value.equals(otherEnum.value) + && Objects.equals(name, otherEnum.name) && Objects.equals(documentation, otherEnum.documentation) && tags.equals(otherEnum.tags); } @Override public int hashCode() { - return Objects.hash(name, tags, documentation); + return Objects.hash(value, name, tags, documentation); } /** - * Builds a {@link EnumConstantBody}. + * Builds a {@link EnumDefinition}. */ - public static final class Builder implements SmithyBuilder { + public static final class Builder implements SmithyBuilder { + private String value; private String documentation; private String name; private final List tags = new ArrayList<>(); @Override - public EnumConstantBody build() { - return new EnumConstantBody(this); + public EnumDefinition build() { + return new EnumDefinition(this); } - public Builder documentation(String documentation) { - this.documentation = documentation; + public Builder value(String value) { + this.value = Objects.requireNonNull(value); return this; } @@ -104,6 +113,11 @@ public Builder name(String name) { return this; } + public Builder documentation(String documentation) { + this.documentation = documentation; + return this; + } + public Builder tags(Collection tags) { this.tags.clear(); this.tags.addAll(tags); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumTrait.java index 3e1137aadaa..d16ba5888a7 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumTrait.java @@ -15,18 +15,17 @@ package software.amazon.smithy.model.traits; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; +import java.util.List; +import java.util.stream.Collectors; import software.amazon.smithy.model.SourceException; 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.node.StringNode; import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.ToSmithyBuilder; /** @@ -35,12 +34,12 @@ public final class EnumTrait extends AbstractTrait implements ToSmithyBuilder { public static final ShapeId ID = ShapeId.from("smithy.api#enum"); - private final Map constants; + private final List definitions; private EnumTrait(Builder builder) { super(ID, builder.sourceLocation); - this.constants = Collections.unmodifiableMap(new LinkedHashMap<>(builder.constants)); - if (constants.isEmpty()) { + this.definitions = ListUtils.copyOf(builder.definitions); + if (definitions.isEmpty()) { throw new SourceException("enum must have at least one entry", getSourceLocation()); } } @@ -48,10 +47,19 @@ private EnumTrait(Builder builder) { /** * Gets the enum value to body. * - * @return returns the enum constant mapping. + * @return returns the enum constant definitions. */ - public Map getValues() { - return constants; + public List getValues() { + return definitions; + } + + /** + * Gets the acceptable enum literal values. + * + * @return returns the enum constant definitions. + */ + public List getEnumDefinitionValues() { + return definitions.stream().map(EnumDefinition::getValue).collect(Collectors.toList()); } /** @@ -63,31 +71,25 @@ public Map getValues() { * @return Returns true if all constants define a name. */ public boolean hasNames() { - return constants.values().stream().allMatch(body -> body.getName().isPresent()); + return definitions.stream().allMatch(body -> body.getName().isPresent()); } @Override protected Node createNode() { - return constants.entrySet().stream() - .map(entry -> { - ObjectNode value = Node.objectNode() - .withOptionalMember(EnumConstantBody.NAME, entry.getValue().getName().map(Node::from)) - .withOptionalMember(EnumConstantBody.DOCUMENTATION, - entry.getValue().getDocumentation().map(Node::from)); - if (!entry.getValue().getTags().isEmpty()) { - value = value.withMember(EnumConstantBody.TAGS, entry.getValue().getTags().stream() - .map(Node::from) - .collect(ArrayNode.collect())); - } - return Pair.of(entry.getKey(), value); - }) - .collect(ObjectNode.collectStringKeys(Pair::getLeft, Pair::getRight)); + return definitions.stream() + .map(definition -> Node.objectNodeBuilder() + .withMember(EnumDefinition.VALUE, definition.getValue()) + .withOptionalMember(EnumDefinition.NAME, definition.getName().map(Node::from)) + .withOptionalMember(EnumDefinition.DOCUMENTATION, + definition.getDocumentation().map(Node::from)) + .build()) + .collect(ArrayNode.collect()); } @Override public Builder toBuilder() { Builder builder = builder().sourceLocation(getSourceLocation()); - constants.forEach(builder::addEnum); + definitions.forEach(builder::addEnum); return builder; } @@ -102,20 +104,25 @@ public static Builder builder() { * Builder used to create the enum trait. */ public static final class Builder extends AbstractTraitBuilder { - private final Map constants = new LinkedHashMap<>(); + private final List definitions = new ArrayList<>(); - public Builder addEnum(String name, EnumConstantBody value) { - constants.put(Objects.requireNonNull(name), Objects.requireNonNull(value)); + public Builder addEnum(EnumDefinition value) { + definitions.add(value); return this; } public Builder removeEnum(String value) { - constants.remove(value); + definitions.removeIf(def -> def.getValue().equals(value)); + return this; + } + + public Builder removeEnumByName(String name) { + definitions.removeIf(def -> def.getName().filter(n -> n.equals(name)).isPresent()); return this; } public Builder clearEnums() { - constants.clear(); + definitions.clear(); return this; } @@ -134,23 +141,24 @@ public ShapeId getShapeId() { @Override public EnumTrait createTrait(ShapeId target, Node value) { Builder builder = builder().sourceLocation(value); - value.expectObjectNode().getMembers().forEach((k, v) -> { - builder.addEnum(k.expectStringNode().getValue(), parseBody(v.expectObjectNode())); - }); + for (ObjectNode definition : value.expectArrayNode().getElementsAs(ObjectNode.class)) { + builder.addEnum(parseEnum(definition)); + } return builder.build(); } - private EnumConstantBody parseBody(ObjectNode value) { + private EnumDefinition parseEnum(ObjectNode value) { value.warnIfAdditionalProperties(Arrays.asList( - EnumConstantBody.NAME, EnumConstantBody.DOCUMENTATION, EnumConstantBody.TAGS)); - EnumConstantBody.Builder builder = EnumConstantBody.builder() - .name(value.getStringMember(EnumConstantBody.NAME).map(StringNode::getValue).orElse(null)) - .documentation(value.getStringMember(EnumConstantBody.DOCUMENTATION) + EnumDefinition.VALUE, EnumDefinition.NAME, EnumDefinition.DOCUMENTATION, EnumDefinition.TAGS)); + EnumDefinition.Builder builder = EnumDefinition.builder() + .value(value.expectStringMember(EnumDefinition.VALUE).getValue()) + .name(value.getStringMember(EnumDefinition.NAME).map(StringNode::getValue).orElse(null)) + .documentation(value.getStringMember(EnumDefinition.DOCUMENTATION) .map(StringNode::getValue) .orElse(null)); - value.getMember(EnumConstantBody.TAGS).ifPresent(node -> { - builder.tags(Node.loadArrayOfString(EnumConstantBody.TAGS, node)); + value.getMember(EnumDefinition.TAGS).ifPresent(node -> { + builder.tags(Node.loadArrayOfString(EnumDefinition.TAGS, node)); }); return builder.build(); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/StringEnumPlugin.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/StringEnumPlugin.java index 8a288723033..41b20f14b2b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/StringEnumPlugin.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/StringEnumPlugin.java @@ -36,10 +36,11 @@ protected List check(StringShape shape, StringNode node, Model model) { List messages = new ArrayList<>(); // Validate the enum trait. shape.getTrait(EnumTrait.class).ifPresent(trait -> { - if (!trait.getValues().containsKey(node.getValue())) { + List values = trait.getEnumDefinitionValues(); + if (!values.contains(node.getValue())) { messages.add(String.format( "String value provided for `%s` must be one of the following values: %s", - shape.getId(), ValidationUtils.tickedList(trait.getValues().keySet()))); + shape.getId(), ValidationUtils.tickedList(values))); } }); return messages; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumTraitValidator.java index 9de041e00b2..7079f447d24 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumTraitValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumTraitValidator.java @@ -18,14 +18,13 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.StringShape; -import software.amazon.smithy.model.traits.EnumConstantBody; +import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.validation.AbstractValidator; @@ -33,6 +32,10 @@ /** * Ensures that enum traits are valid. + * + *

If one enum definition contains a name, then all definitions must contain + * a name. All enum values and names must be unique across the list of + * definitions. */ public class EnumTraitValidator extends AbstractValidator { private static final Pattern RECOMMENDED_NAME_PATTERN = Pattern.compile("^[A-Z]+[A-Z_0-9]*$"); @@ -48,29 +51,42 @@ public List validate(Model model) { private List validateEnumTrait(Shape shape, EnumTrait trait) { List events = new ArrayList<>(); Set names = new HashSet<>(); - for (Map.Entry entry : trait.getValues().entrySet()) { - if (entry.getValue().getName().isPresent()) { - String name = entry.getValue().getName().get(); - if (names.contains(name)) { + Set values = new HashSet<>(); + + // Ensure that names are unique. + for (EnumDefinition definition : trait.getValues()) { + if (!values.add(definition.getValue())) { + events.add(error(shape, trait, String.format( + "Duplicate enum trait values found with the same `value` property of '%s'", + definition.getValue()))); + } + } + + // Ensure that names are unique. + for (EnumDefinition definition : trait.getValues()) { + if (definition.getName().isPresent()) { + String name = definition.getName().get(); + if (!names.add(name)) { events.add(error(shape, trait, String.format( "Duplicate enum trait values found with the same `name` property of '%s'", name))); } if (!RECOMMENDED_NAME_PATTERN.matcher(name).find()) { events.add(warning(shape, trait, String.format( - "The name `%s` does not match our recommended enum name format of beginning with an " + "The name `%s` does not match the recommended enum name format of beginning with an " + "uppercase letter, followed by any number of uppercase letters, numbers, or underscores.", name))); } - names.add(name); } } + // If one enum definition has a name, then they all must have names. if (!names.isEmpty()) { - for (Map.Entry entry : trait.getValues().entrySet()) { - if (!entry.getValue().getName().isPresent()) { + for (EnumDefinition definition : trait.getValues()) { + if (!definition.getName().isPresent()) { events.add(error(shape, trait, String.format( "`%s` enum value body is missing the `name` property; if any enum trait value contains a " - + "`name` property, then all values must contain the `name` property.", entry.getKey()))); + + "`name` property, then all values must contain the `name` property.", + definition.getValue()))); } } } diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-traits.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-traits.smithy index 59bef9952e9..95b02a19b65 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-traits.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude-traits.smithy @@ -127,7 +127,9 @@ structure httpApiKeyAuth { } @private -@enum(header: {}, query: {}) +@enum([ + {value: "header"}, + {value: "query"}]) string HttpApiKeyLocations /// Indicates that an operation can be called without authentication. @@ -158,8 +160,9 @@ structure Example { /// targeted with this trait. @trait(selector: "structure", conflicts: [trait]) @tags(["diff.error.const"]) -@enum(client: {name: "CLIENT"}, - server: {name: "SERVER"}) +@enum([ + {value: "client", name: "CLIENT"}, + {value: "server", name: "SERVER"}]) string error /// Indicates that an error MAY be retried by the client. @@ -317,21 +320,26 @@ string title /// of constant values. @trait(selector: "string") @tags(["diff.error.add", "diff.error.remove"]) -map enum { - key: String, - value: EnumConstantBody +@length(min: 1) +list enum { + member: EnumDefinition } /// An enum definition for the enum trait. @private -structure EnumConstantBody { +structure EnumDefinition { + /// Defines the enum value that is sent over the wire. + @required + value: NonEmptyString, + + /// Defines the name, or label, that is used in code to represent this variant. + name: EnumConstantBodyName, + /// Provides optional documentation about the enum constant value. documentation: String, /// Applies a list of tags to the enum constant. tags: NonEmptyStringList, - - name: EnumConstantBodyName, } /// The optional name or label of the enum constant value. @@ -559,23 +567,30 @@ structure idRef { @trait(selector: ":test(timestamp, member > timestamp)") @tags(["diff.error.const"]) -@enum( - "date-time": { +@enum([ + { + value: "date-time", + name: "DATE_TIME", documentation: """ Date time as defined by the date-time production in RFC3339 section 5.6 with no UTC offset (for example, 1985-04-12T23:20:50.52Z).""" }, - "epoch-seconds": { + { + value: "epoch-seconds", + name: "EPOCH_SECONDS", documentation: """ Also known as Unix time, the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970, with decimal precision (for example, 1515531081.1234).""" }, - "http-date": { + { + value: "http-date", + name: "HTTP_DATE", documentation: """ An HTTP date as defined by the IMF-fixdate production in RFC 7231#section-7.1.1.1 (for example, Tue, 29 Apr 2014 18:30:38 GMT).""" - }) + } +]) string timestampFormat /// Configures a custom operation endpoint. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/traits/EnumTraitTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/traits/EnumTraitTest.java index 52532227882..d81fd12ead9 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/traits/EnumTraitTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/traits/EnumTraitTest.java @@ -16,8 +16,8 @@ package software.amazon.smithy.model.traits; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.is; import org.junit.jupiter.api.Assertions; @@ -29,14 +29,14 @@ public class EnumTraitTest { @Test public void loadsTrait() { - Node node = Node.parse("{\"foo\": {}, \"bam\": {}, \"boozled\": {}}"); + Node node = Node.parse("[{\"value\": \"foo\"}, " + + "{\"value\": \"bam\"}, " + + "{\"value\": \"boozled\"}]"); EnumTrait trait = new EnumTrait.Provider().createTrait(ShapeId.from("ns.foo#baz"), node); assertThat(trait.toNode(), equalTo(node)); assertThat(trait.toBuilder().build(), equalTo(trait)); - assertThat(trait.getValues(), hasKey("foo")); - assertThat(trait.getValues(), hasKey("bam")); - assertThat(trait.getValues(), hasKey("boozled")); + assertThat(trait.getEnumDefinitionValues(), contains("foo", "bam", "boozled")); } @Test @@ -49,7 +49,8 @@ public void expectsAtLeastOneConstant() { @Test public void checksIfAllDefineNames() { - Node node = Node.parse("{\"foo\": {\"name\": \"FOO\"}, \"bam\": {\"name\": \"BAM\"}}"); + Node node = Node.parse("[{\"value\": \"foo\", \"name\": \"FOO\"}, " + + "{\"value\": \"bam\", \"name\": \"BAM\"}]"); EnumTrait trait = new EnumTrait.Provider().createTrait(ShapeId.from("ns.foo#baz"), node); assertThat(trait.hasNames(), is(true)); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.errors index a9b91701841..a577619c96c 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.errors @@ -1,9 +1,10 @@ +[WARNING] ns.foo#Warn1: The name `_bar` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait +[WARNING] ns.foo#Warn1: The name `baz` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait +[WARNING] ns.foo#Invalid2: The name `invalid!` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait +[WARNING] ns.foo#Invalid2: The name `invalid2!` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait +[WARNING] ns.foo#Invalid3: The name `a` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait [ERROR] ns.foo#Invalid1: `foo` enum value body is missing the `name` property; if any enum trait value contains a `name` property, then all values must contain the `name` property. | EnumTrait -[ERROR] ns.foo#Invalid2: Error validating trait `enum`.bar.name: String value provided for `smithy.api#EnumConstantBodyName` must match regular expression: ^[a-zA-Z_]+[a-zA-Z_0-9]*$ | TraitValue -[ERROR] ns.foo#Invalid2: Error validating trait `enum`.foo.name: String value provided for `smithy.api#EnumConstantBodyName` must match regular expression: ^[a-zA-Z_]+[a-zA-Z_0-9]*$ | TraitValue +[ERROR] ns.foo#Invalid2: Error validating trait `enum`.0.name: String value provided for `smithy.api#EnumConstantBodyName` must match regular expression: ^[a-zA-Z_]+[a-zA-Z_0-9]*$ | TraitValue +[ERROR] ns.foo#Invalid2: Error validating trait `enum`.1.name: String value provided for `smithy.api#EnumConstantBodyName` must match regular expression: ^[a-zA-Z_]+[a-zA-Z_0-9]*$ | TraitValue [ERROR] ns.foo#Invalid3: Duplicate enum trait values found with the same `name` property of 'a' | EnumTrait -[WARNING] ns.foo#Invalid2: The name `invalid!` does not match our recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait -[WARNING] ns.foo#Invalid2: The name `invalid2!` does not match our recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait -[WARNING] ns.foo#Invalid3: The name `a` does not match our recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait -[WARNING] ns.foo#Warn1: The name `_bar` does not match our recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait -[WARNING] ns.foo#Warn1: The name `baz` does not match our recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumTrait +[ERROR] ns.foo#Invalid4: Duplicate enum trait values found with the same `value` property of 'a' | EnumTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.json b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.json index 459102c5ba9..d62b3d35419 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-trait-validation.json @@ -4,17 +4,18 @@ "ns.foo#Valid1": { "type": "string", "traits": { - "smithy.api#enum": { - "foo": {}, - "bar": {} - } + "smithy.api#enum": [ + {"value": "foo"}, + {"value": "bar"} + ] } }, "ns.foo#Valid2": { "type": "string", "traits": { - "smithy.api#enum": { - "foo": { + "smithy.api#enum": [ + { + "value": "foo", "name": "FOO", "documentation": "foo", "tags": [ @@ -22,7 +23,8 @@ "b" ] }, - "bar": { + { + "value": "bar", "name": "BAR", "documentation": "bar", "tags": [ @@ -30,28 +32,33 @@ "b" ] } - } + ] } }, "ns.foo#Warn1": { "type": "string", "traits": { - "smithy.api#enum": { - "bar": { + "smithy.api#enum": [ + { + "value": "bar", "name": "_bar" }, - "baz": { + { + "value": "baz", "name": "baz" } - } + ] } }, "ns.foo#Invalid1": { "type": "string", "traits": { - "smithy.api#enum": { - "foo": {}, - "bar": { + "smithy.api#enum": [ + { + "value": "foo" + }, + { + "value": "bar", "name": "BAR", "documentation": "bar", "tags": [ @@ -59,33 +66,34 @@ "b" ] } - } + ] } }, "ns.foo#Invalid2": { "type": "string", "traits": { - "smithy.api#enum": { - "foo": { - "name": "invalid!" - }, - "bar": { - "name": "invalid2!" - } - } + "smithy.api#enum": [ + {"value": "foo", "name": "invalid!"}, + {"value": "bar", "name": "invalid2!"} + ] } }, "ns.foo#Invalid3": { "type": "string", "traits": { - "smithy.api#enum": { - "foo": { - "name": "a" - }, - "bar": { - "name": "a" - } - } + "smithy.api#enum": [ + {"value": "a", "name": "a"}, + {"value": "b", "name": "a"} + ] + } + }, + "ns.foo#Invalid4": { + "type": "string", + "traits": { + "smithy.api#enum": [ + {"value": "a"}, + {"value": "a"} + ] } } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json index 3067945ecde..9c24c35a7a2 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/validation/node-validator.json @@ -91,14 +91,10 @@ "ns.foo#String3": { "type": "string", "traits": { - "smithy.api#enum": { - "foo": { - "name": "FOO" - }, - "bar": { - "name": "BAR" - } - } + "smithy.api#enum": [ + {"value": "foo", "name": "FOO"}, + {"value": "bar", "name": "BAR"} + ] } }, "ns.foo#String4": { diff --git a/smithy-mqtt-traits/src/test/resources/software/amazon/smithy/mqtt/traits/errorfiles/job-service.smithy b/smithy-mqtt-traits/src/test/resources/software/amazon/smithy/mqtt/traits/errorfiles/job-service.smithy index 9e34af1090b..fdf6f3ed72d 100644 --- a/smithy-mqtt-traits/src/test/resources/software/amazon/smithy/mqtt/traits/errorfiles/job-service.smithy +++ b/smithy-mqtt-traits/src/test/resources/software/amazon/smithy/mqtt/traits/errorfiles/job-service.smithy @@ -47,17 +47,17 @@ structure RejectedError { executionState: JobExecutionState, } -@enum( - InvalidTopic: {}, - InvalidJson: {}, - InvalidRequest: {}, - InvalidStateTransition: {}, - ResourceNotFound: {}, - VersionMismatch: {}, - InternalError: {}, - RequestThrottled: {}, - TerminalStateReached: {}, -) +@enum([ + {value: "InvalidTopic"}, + {value: "InvalidJson"}, + {value: "InvalidRequest"}, + {value: "InvalidStateTransition"}, + {value: "ResourceNotFound"}, + {value: "VersionMismatch"}, + {value: "InternalError"}, + {value: "RequestThrottled"}, + {value: "TerminalStateReached"}, +]) string RejectedErrorCode // ------ GetPendingJobExecutions ------- @@ -192,16 +192,16 @@ structure JobExecutionData { executionNumber: smithy.api#Long, } -@enum( - QUEUED: {}, - IN_PROGRESS: {}, - TIMED_OUT: {}, - FAILED: {}, - SUCCEEDED: {}, - CANCELED: {}, - REJECTED: {}, - REMOVED: {}, -) +@enum([ + {value: "QUEUED"}, + {value: "IN_PROGRESS"}, + {value: "TIMED_OUT"}, + {value: "FAILED"}, + {value: "SUCCEEDED"}, + {value: "CANCELED"}, + {value: "REJECTED"}, + {value: "REMOVED"}, +]) string JobStatus diff --git a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.json b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.json index 5257cc8cb35..61ec75c4a96 100644 --- a/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.json +++ b/smithy-openapi/src/test/resources/software/amazon/smithy/openapi/fromsmithy/test-service.json @@ -235,14 +235,10 @@ "example.rest#EnumString": { "type": "string", "traits": { - "smithy.api#enum": { - "a": { - "name": "A" - }, - "c": { - "name": "C" - } - } + "smithy.api#enum": [ + {"value": "a", "name": "A"}, + {"value": "c", "name": "C"} + ] } }, "example.rest#TaggedUnion": {