From 5ef1d93ea1cee58e77e24c5d61d9d0c0ecb837f6 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Wed, 9 Feb 2022 17:52:48 +0100 Subject: [PATCH 01/45] Add enum shape class and necessary supports This adds the enum shape java class and all the necessary code and traits to make it feasible. It doesn't do things like add parsing support or extensive updates to neighbor code. --- .../model/shapes/AbstractShapeBuilder.java | 4 +- .../amazon/smithy/model/shapes/EnumShape.java | 315 ++++++++++++++++ .../amazon/smithy/model/shapes/Shape.java | 16 +- .../smithy/model/shapes/ShapeToBuilder.java | 5 + .../amazon/smithy/model/shapes/ShapeType.java | 3 + .../smithy/model/shapes/ShapeVisitor.java | 4 + .../smithy/model/shapes/StringShape.java | 6 +- .../smithy/model/traits/EnumDefinition.java | 64 +++- .../smithy/model/traits/EnumValueTrait.java | 118 ++++++ .../validators/UnitTypeValidator.java | 2 +- ...re.amazon.smithy.model.traits.TraitService | 1 + .../amazon/smithy/model/loader/prelude.smithy | 10 + .../smithy/model/shapes/EnumShapeTest.java | 338 ++++++++++++++++++ 13 files changed, 877 insertions(+), 9 deletions(-) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java create mode 100644 smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/AbstractShapeBuilder.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/AbstractShapeBuilder.java index f3104bd432e..9822b48ae5f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/AbstractShapeBuilder.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/AbstractShapeBuilder.java @@ -153,7 +153,7 @@ public final B addTraits(Collection traitsToAdd) { * @return Returns the builder. */ @SuppressWarnings("unchecked") - public final B addTrait(Trait trait) { + public B addTrait(Trait trait) { Objects.requireNonNull(trait, "trait must not be null"); traits.get().put(trait.toShapeId(), trait); @@ -180,7 +180,7 @@ public final B removeTrait(String traitId) { * @return Returns the builder. */ @SuppressWarnings("unchecked") - public final B removeTrait(ShapeId traitId) { + public B removeTrait(ShapeId traitId) { if (traits.hasValue()) { traits.get().remove(traitId); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java new file mode 100644 index 00000000000..95987bc6f73 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -0,0 +1,315 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.shapes; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.traits.UnitTypeTrait; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.ListUtils; + +public final class EnumShape extends StringShape { + + private final Map members; + private volatile List memberNames; + + private EnumShape(Builder builder) { + super(builder); + members = builder.members.get(); + } + + /** + * Gets the members of the shape, including mixin members. + * + * @return Returns the immutable member map. + */ + public Map getAllMembers() { + return members; + } + + /** + * Returns an ordered list of member names based on the order they are + * defined in the model, including mixin members. + * + * @return Returns an immutable list of member names. + */ + public List getMemberNames() { + List names = memberNames; + if (names == null) { + names = ListUtils.copyOf(members.keySet()); + memberNames = names; + } + + return names; + } + + @Override + public boolean equals(Object other) { + if (!super.equals(other)) { + return false; + } + + // Members are ordered, so do a test on the ordering and their values. + EnumShape b = (EnumShape) other; + return getMemberNames().equals(b.getMemberNames()) && members.equals(b.members); + } + + /** + * Get a specific member by name. + * + * @param name Name of the member to retrieve. + * @return Returns the optional member. + */ + public Optional getMember(String name) { + return Optional.ofNullable(members.get(name)); + } + + @Override + public Collection members() { + return members.values(); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public Builder toBuilder() { + return (Builder) updateBuilder(builder()); + } + + @Override + public R accept(ShapeVisitor cases) { + return cases.enumShape(this); + } + + @Override + public Optional asEnumShape() { + return Optional.of(this); + } + + /** + * Converts a base {@link StringShape} to an {@link EnumShape} if possible. + * + * The result will be empty if the given shape doesn't have the {@link EnumTrait} + * or if the enum definitions don't have names. + * + * @param shape A base {@link StringShape} to convert. + * @return Optionally returns an {@link EnumShape} equivalent of the given shape. + */ + public static Optional fromStringShape(StringShape shape) { + if (!shape.hasTrait(EnumTrait.ID)) { + return Optional.empty(); + } + StringShape stringWithoutEnumTrait = shape.toBuilder().removeTrait(EnumTrait.ID).build(); + Builder enumBuilder = EnumShape.builder(); + stringWithoutEnumTrait.updateBuilder(enumBuilder); + try { + return Optional.of(enumBuilder.members(shape.expectTrait(EnumTrait.class)).build()); + } catch (IllegalStateException e) { + return Optional.empty(); + } + } + + @Override + public ShapeType getType() { + return ShapeType.ENUM; + } + + public static final class Builder extends StringShape.Builder { + private final BuilderRef> members = BuilderRef.forOrderedMap(); + + @Override + public EnumShape build() { + return new EnumShape(this); + } + + @Override + public ShapeType getShapeType() { + return ShapeType.ENUM; + } + + @Override + public Builder id(ShapeId shapeId) { + super.id(shapeId); + for (MemberShape member : members.peek().values()) { + addMember(member.toBuilder().id(shapeId.withMember(member.getMemberName())).build()); + } + return this; + } + + public Builder members(EnumTrait trait) { + if (getId() == null) { + throw new IllegalStateException("An id must be set before adding a named enum trait to a string."); + } + clearMembers(); + super.addTrait(trait); + + for (EnumDefinition definition : trait.getValues()) { + Optional member = definition.asMember(getId()); + if (member.isPresent()) { + addMember(member.get(), false); + } else { + throw new IllegalStateException(String.format( + "Unable to convert enum trait entry with name: `%s` and value `%s` to an enum member.", + definition.getName().orElse(""), definition.getValue() + )); + } + } + + return this; + } + + /** + * Replaces the members of the builder. + * + * @param members Members to add to the builder. + * @return Returns the builder. + */ + public Builder members(Collection members) { + clearMembers(); + for (MemberShape member : members) { + addMember(member); + } + return this; + } + + /** + * Removes all members from the shape. + * + * @return Returns the builder. + */ + public Builder clearMembers() { + members.clear(); + super.removeTrait(EnumTrait.ID); + return this; + } + + @Override + public Builder addMember(MemberShape member) { + return addMember(member, true); + } + + private Builder addMember(MemberShape member, boolean updateEnumTrait) { + if (!member.getTarget().equals(UnitTypeTrait.UNIT)) { + throw new SourceException(String.format( + "Enum members may only target `smithy.api#Unit`, but found `%s`", member.getTarget() + ), getSourceLocation()); + } + if (!member.hasTrait(EnumValueTrait.ID) + || !member.expectTrait(EnumValueTrait.class).getStringValue().isPresent()) { + throw new SourceException( + "Enum members MUST have the enumValue trait with the `string` member set", + getSourceLocation()); + } + members.get().put(member.getMemberName(), member); + + if (updateEnumTrait) { + EnumTrait.Builder builder; + if (getTraits().containsKey(EnumTrait.ID)) { + builder = ((EnumTrait) getTraits().get(EnumTrait.ID)).toBuilder(); + } else { + builder = EnumTrait.builder(); + } + builder.addEnum(EnumDefinition.fromMember(member)); + super.addTrait(builder.build()); + } + + return this; + } + + /** + * Adds a member to the builder. + * + * @param memberName Member name to add. + * @param enumValue The value of the enum. + * @return Returns the builder. + */ + public Builder addMember(String memberName, String enumValue) { + return addMember(memberName, enumValue, null); + } + + /** + * Adds a member to the builder. + * + * @param memberName Member name to add. + * @param enumValue The value of the enum. + * @param memberUpdater Consumer that can update the created member shape. + * @return Returns the builder. + */ + public Builder addMember(String memberName, String enumValue, Consumer memberUpdater) { + if (getId() == null) { + throw new IllegalStateException("An id must be set before setting a member with a target"); + } + + MemberShape.Builder builder = MemberShape.builder() + .target(UnitTypeTrait.UNIT) + .id(getId().withMember(memberName)) + .addTrait(EnumValueTrait.builder().stringValue(enumValue).build()); + + if (memberUpdater != null) { + memberUpdater.accept(builder); + } + + return addMember(builder.build()); + } + + /** + * Removes a member by name. + * + *

Note that removing a member that was added by a mixin results in + * an inconsistent model. It's best to use ModelTransform to ensure + * that the model remains consistent when removing members. + * + * @param member Member name to remove. + * @return Returns the builder. + */ + public Builder removeMember(String member) { + if (members.hasValue()) { + members.get().remove(member); + EnumTrait trait = (EnumTrait) getTraits().get(EnumTrait.ID); + super.addTrait(trait.toBuilder().removeEnumByName(member).build()); + } + return this; + } + + @Override + public Builder addTrait(Trait trait) { + if (trait instanceof EnumTrait) { + throw new SourceException( + "The enum trait cannot be added directly to an enum shape.", getSourceLocation()); + } + return (Builder) super.addTrait(trait); + } + + @Override + public Builder removeTrait(ShapeId traitId) { + if (traitId.equals(EnumTrait.ID)) { + throw new SourceException( + "The enum trait cannot be removed directly from an enum shape.", getSourceLocation()); + } + return (Builder) super.removeTrait(traitId); + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java index e361d9b6769..645af62aebf 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java @@ -465,6 +465,13 @@ public Optional asStringShape() { return Optional.empty(); } + /** + * @return Optionally returns the shape as a {@link EnumShape}. + */ + public Optional asEnumShape() { + return Optional.empty(); + } + /** * @return Optionally returns the shape as a {@link StructureShape}. */ @@ -616,7 +623,14 @@ public final boolean isServiceShape() { * @return Returns true if the shape is a {@link StringShape} shape. */ public final boolean isStringShape() { - return getType() == ShapeType.STRING; + return getType() == ShapeType.STRING || getType() == ShapeType.ENUM; + } + + /** + * @return Returns true if the shape is an {@link EnumShape} shape. + */ + public final boolean isEnumShape() { + return getType() == ShapeType.ENUM; } /** diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeToBuilder.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeToBuilder.java index 845f38228e7..8194a1500ed 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeToBuilder.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeToBuilder.java @@ -115,6 +115,11 @@ public AbstractShapeBuilder stringShape(StringShape shape) { return shape.toBuilder(); } + @Override + public AbstractShapeBuilder enumShape(EnumShape shape) { + return shape.toBuilder(); + } + @Override public AbstractShapeBuilder structureShape(StructureShape shape) { return shape.toBuilder(); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java index 3d9363b3640..f962e5290ba 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java @@ -33,6 +33,7 @@ public enum ShapeType { DOUBLE("double", DoubleShape.class, Category.SIMPLE), BIG_DECIMAL("bigDecimal", BigDecimalShape.class, Category.SIMPLE), BIG_INTEGER("bigInteger", BigIntegerShape.class, Category.SIMPLE), + ENUM("enum", EnumShape.class, Category.SIMPLE), LIST("list", ListShape.class, Category.AGGREGATE), SET("set", SetShape.class, Category.AGGREGATE), @@ -110,6 +111,8 @@ public static Optional fromString(String text) { return BooleanShape.builder(); case STRING: return StringShape.builder(); + case ENUM: + return EnumShape.builder(); case TIMESTAMP: return TimestampShape.builder(); case BYTE: diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeVisitor.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeVisitor.java index de78e0b1020..8f6b46d4d15 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeVisitor.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeVisitor.java @@ -59,6 +59,10 @@ public interface ShapeVisitor { R stringShape(StringShape shape); + default R enumShape(EnumShape shape) { + return stringShape(shape); + } + R structureShape(StructureShape shape); R unionShape(UnionShape shape); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/StringShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/StringShape.java index a0915c5cc92..e06c76b0a35 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/StringShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/StringShape.java @@ -21,9 +21,9 @@ /** * Represents a {@code string} shape. */ -public final class StringShape extends SimpleShape implements ToSmithyBuilder { +public class StringShape extends SimpleShape implements ToSmithyBuilder { - private StringShape(Builder builder) { + StringShape(Builder builder) { super(builder); } @@ -54,7 +54,7 @@ public ShapeType getType() { /** * Builder used to create a {@link StringShape}. */ - public static final class Builder extends AbstractShapeBuilder { + public static class Builder extends AbstractShapeBuilder { @Override public StringShape build() { return new StringShape(this); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java index 30ddc6cfea5..974463d7fe6 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java @@ -24,6 +24,9 @@ import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeIdSyntaxException; import software.amazon.smithy.utils.SmithyBuilder; import software.amazon.smithy.utils.Tagged; import software.amazon.smithy.utils.ToSmithyBuilder; @@ -97,8 +100,8 @@ public static EnumDefinition fromNode(Node node) { .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)) + .map(StringNode::getValue) + .orElse(null)) .deprecated(value.getBooleanMemberOrDefault(EnumDefinition.DEPRECATED)); value.getMember(EnumDefinition.TAGS).ifPresent(tags -> { @@ -108,6 +111,62 @@ public static EnumDefinition fromNode(Node node) { return builder.build(); } + /** + * Converts an enum definition to the equivalent enum member shape. + * + * This is only possible if the enum definition has a name. + * + * @param parentId The {@link ShapeId} of the enum shape. + * @return An optional member shape representing the enum definition, + * or empty if conversion is impossible. + */ + public Optional asMember(ShapeId parentId) { + if (!getName().isPresent()) { + return Optional.empty(); + } + + try { + MemberShape.Builder builder = MemberShape.builder() + .id(parentId.withMember(name)) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue(value).build()); + + getDocumentation().ifPresent(docs -> builder.addTrait(new DocumentationTrait(docs))); + if (!tags.isEmpty()) { + builder.addTrait(TagsTrait.builder().values(tags).build()); + } + if (deprecated) { + builder.addTrait(DeprecatedTrait.builder().build()); + } + + return Optional.of(builder.build()); + } catch (ShapeIdSyntaxException e) { + return Optional.empty(); + } + } + + /** + * Converts an enum member into an equivalent enum definition object. + * + * @param member The enum member to convert. + * @return An {@link EnumDefinition} representing the given member. + */ + public static EnumDefinition fromMember(MemberShape member) { + EnumDefinition.Builder builder = EnumDefinition.builder().name(member.getMemberName()); + + EnumValueTrait valueTrait = member.expectTrait(EnumValueTrait.class); + if (valueTrait.getStringValue().isPresent()) { + builder.value(valueTrait.getStringValue().get()); + } else { + throw new IllegalStateException("Enum definitions can only be made for string enums."); + } + + member.getTrait(DocumentationTrait.class).ifPresent(docTrait -> builder.documentation(docTrait.getValue())); + member.getTrait(TagsTrait.class).ifPresent(tagsTrait -> builder.tags(tagsTrait.getValues())); + member.getTrait(DeprecatedTrait.class).ifPresent(deprecatedTrait -> builder.deprecated(true)); + return builder.build(); + } + @Override public List getTags() { return tags; @@ -194,3 +253,4 @@ public Builder deprecated(boolean deprecated) { } } } + diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java new file mode 100644 index 00000000000..d2489d4069b --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java @@ -0,0 +1,118 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.traits; + +import java.util.Optional; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NumberNode; +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.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuilder { + public static final ShapeId ID = ShapeId.from("smithy.api#enumValue"); + + private final String string; + private final Integer integer; + + private EnumValueTrait(Builder builder) { + super(ID, builder.sourceLocation); + string = builder.string; + integer = builder.integer; + if (string == null && integer == null) { + throw new SourceException( + "Either a string value or an integer value must be set for the enumValue trait.", + getSourceLocation() + ); + } + } + + public Optional getStringValue() { + return Optional.ofNullable(string); + } + + public Optional getIntValue() { + return Optional.ofNullable(integer); + } + + public static final class Provider extends AbstractTrait.Provider { + public Provider() { + super(ID); + } + + @Override + public Trait createTrait(ShapeId target, Node value) { + Builder builder = builder().sourceLocation(value); + ObjectNode objectNode = value.expectObjectNode(); + objectNode.getMember("string") + .map(v -> v.expectStringNode().getValue()) + .ifPresent(builder::stringValue); + objectNode.getMember("int") + .map(v -> v.expectNumberNode().getValue().intValue()) + .ifPresent(builder::intValue); + return builder.build(); + } + } + + @Override + protected Node createNode() { + return ObjectNode.builder() + .sourceLocation(getSourceLocation()) + .withOptionalMember("string", getStringValue().map(StringNode::from)) + .withOptionalMember("int", getIntValue().map(NumberNode::from)) + .build(); + } + + @Override + public SmithyBuilder toBuilder() { + Builder builder = builder().sourceLocation(getSourceLocation()); + if (getIntValue().isPresent()) { + builder.intValue(getIntValue().get()); + } else if (getStringValue().isPresent()) { + builder.stringValue(getStringValue().get()); + } + return builder; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder extends AbstractTraitBuilder { + private String string; + private Integer integer; + + @Override + public EnumValueTrait build() { + return new EnumValueTrait(this); + } + + public Builder stringValue(String string) { + this.string = string; + this.integer = null; + return this; + } + + public Builder intValue(int integer) { + this.integer = integer; + this.string = null; + return this; + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnitTypeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnitTypeValidator.java index 806320a82b1..8878ab3be47 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnitTypeValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnitTypeValidator.java @@ -54,7 +54,7 @@ public List validate(Model model) { .asMemberShape() .map(MemberShape::getContainer) .flatMap(model::getShape) - .filter(shape -> shape.getType() != ShapeType.UNION) + .filter(shape -> !(shape.getType() == ShapeType.UNION || shape.getType() == ShapeType.ENUM)) .ifPresent(container -> { events.add(error(relationship.getShape(), "Only members of a union can reference smithy.api#Unit")); diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService index 5044083ee81..9be2fc12f34 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -7,6 +7,7 @@ software.amazon.smithy.model.traits.DeprecatedTrait$Provider software.amazon.smithy.model.traits.DocumentationTrait$Provider software.amazon.smithy.model.traits.EndpointTrait$Provider software.amazon.smithy.model.traits.EnumTrait$Provider +software.amazon.smithy.model.traits.EnumValueTrait$Provider software.amazon.smithy.model.traits.ErrorTrait$Provider software.amazon.smithy.model.traits.EventHeaderTrait$Provider software.amazon.smithy.model.traits.EventPayloadTrait$Provider diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index c35183ebb44..a13d0c4a9eb 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -434,6 +434,16 @@ structure EnumDefinition { @pattern("^[a-zA-Z_]+[a-zA-Z_0-9]*$") string EnumConstantBodyName +/// Defines the value of an enum entry. +@trait(selector: "enum > member") +union enumValue { + /// The value for the enum entry if it is a string. + string: String + + /// The value for the enum entry if it is an integer. + int: Integer +} + /// Constrains a shape to minimum and maximum number of elements or size. @trait(selector: ":test(collection, map, string, blob, member > :is(collection, map, string, blob))") structure length { diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java new file mode 100644 index 00000000000..d17d385bad2 --- /dev/null +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -0,0 +1,338 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.shapes; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.traits.UnitTypeTrait; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SetUtils; + +public class EnumShapeTest { + @Test + public void returnsAppropriateType() { + EnumShape shape = (EnumShape) EnumShape.builder().id("ns.foo#bar").build(); + + assertEquals(shape.getType(), ShapeType.ENUM); + assertTrue(shape.isEnumShape()); + assertTrue(shape.isStringShape()); + assertTrue(shape.asEnumShape().isPresent()); + assertTrue(shape.asStringShape().isPresent()); + } + + @Test + public void mustNotContainMembersInShapeId() { + Assertions.assertThrows(SourceException.class, () -> { + EnumShape.builder().id("ns.foo#bar$baz").build(); + }); + } + + @Test + public void addMember() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumShape shape = builder.addMember("foo", "bar").build(); + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of( + EnumDefinition.builder() + .name("foo") + .value("bar") + .build() + )); + } + + @Test + public void addMemberShape() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + MemberShape member = MemberShape.builder() + .id("ns.foo#bar$foo") + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build(); + EnumShape shape = builder.addMember(member).build(); + assertEquals(shape.getMember("foo").get(), member); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of( + EnumDefinition.builder() + .name("foo") + .value("bar") + .build() + )); + } + + @Test + public void addMemberFromEnumTrait() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumDefinition enumDefinition = EnumDefinition.builder() + .name("foo") + .value("bar") + .build(); + EnumShape shape = builder.members(EnumTrait.builder().addEnum(enumDefinition).build()).build(); + + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of(enumDefinition)); + } + + @Test + public void givenEnumTraitMustUseNames() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("bar") + .build()) + .build(); + + Assertions.assertThrows(IllegalStateException.class, () -> { + builder.members(trait); + }); + } + + @Test + public void addMultipleMembers() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + + EnumShape shape = builder.members(ListUtils.of( + MemberShape.builder() + .id("ns.foo#bar$foo") + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build(), + MemberShape.builder() + .id("ns.foo#bar$baz") + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bam").build()) + .build() + )).build(); + + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build()); + + assertEquals(shape.getMember("baz").get(), + MemberShape.builder() + .id(shape.getId().withMember("baz")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bam").build()) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of( + EnumDefinition.builder() + .name("foo") + .value("bar") + .build(), + EnumDefinition.builder() + .name("baz") + .value("bam") + .build() + )); + } + + @Test + public void removeMember() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + + builder.members(ListUtils.of( + MemberShape.builder() + .id("ns.foo#bar$foo") + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build(), + MemberShape.builder() + .id("ns.foo#bar$baz") + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bam").build()) + .build() + )); + + EnumShape shape = builder.removeMember("foo").build(); + + assertFalse(shape.getMember("foo").isPresent()); + + assertEquals(shape.getMember("baz").get(), + MemberShape.builder() + .id(shape.getId().withMember("baz")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bam").build()) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of( + EnumDefinition.builder() + .name("baz") + .value("bam") + .build() + )); + } + + @Test + public void clearMembers() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + + EnumShape shape = builder.addMember("foo", "bar") + .clearMembers() + .addMember("baz", "bam") + .build(); + + assertEquals(1, shape.members().size()); + + assertEquals(shape.getMember("baz").get(), + MemberShape.builder() + .id(shape.getId().withMember("baz")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bam").build()) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of( + EnumDefinition.builder() + .name("baz") + .value("bam") + .build() + )); + } + + @Test + public void cannotDirectlyAddEnumTrait() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .name("foo") + .value("bar") + .build()) + .build(); + Assertions.assertThrows(SourceException.class, () -> { + EnumShape.builder().addTrait(trait).id("ns.foo#bar").build(); + }); + } + + @Test + public void cannotDirectlyRemoveEnumTrait() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + Assertions.assertThrows(SourceException.class, () -> { + builder.removeTrait(EnumTrait.ID).build(); + }); + } + + @Test + public void membersMustHaveEnumValue() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + MemberShape member = MemberShape.builder() + .id("ns.foo#bar$foo") + .target(UnitTypeTrait.UNIT) + .build(); + Assertions.assertThrows(SourceException.class, () -> { + builder.addMember(member); + }); + } + + @Test + public void membersMustHaveEnumValueWithStringSet() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + MemberShape member = MemberShape.builder() + .id("ns.foo#bar$foo") + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().intValue(1).build()) + .build(); + Assertions.assertThrows(SourceException.class, () -> { + builder.addMember(member); + }); + } + + @Test + public void membersMustTargetUnit() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + MemberShape member = MemberShape.builder() + .id("ns.foo#bar$foo") + .target("smithy.api#String") + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build(); + Assertions.assertThrows(SourceException.class, () -> { + builder.addMember(member); + }); + } + + @Test + public void canConvertBaseString() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .name("foo") + .value("bar") + .build()) + .build(); + StringShape string = StringShape.builder() + .id("ns.foo#bar") + .addTrait(trait) + .build(); + Optional optionalEnum = EnumShape.fromStringShape(string); + assertTrue(optionalEnum.isPresent()); + assertEquals(trait, optionalEnum.get().expectTrait(EnumTrait.class)); + + assertEquals(optionalEnum.get().getMember("foo").get(), + MemberShape.builder() + .id(optionalEnum.get().getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build()); + } + + @Test + public void cantConvertBaseStringWithoutEnumTrait() { + StringShape string = StringShape.builder() + .id("ns.foo#bar") + .build(); + Optional optionalEnum = EnumShape.fromStringShape(string); + assertFalse(optionalEnum.isPresent()); + } + + @Test + public void cantConvertBaseStringWithNamelessEnumTrait() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("bar") + .build()) + .build(); + StringShape string = StringShape.builder() + .id("ns.foo#bar") + .addTrait(trait) + .build(); + Optional optionalEnum = EnumShape.fromStringShape(string); + assertFalse(optionalEnum.isPresent()); + } +} From 52bc9294ae75f8b4177aa905d56ad7081212cf8c Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 10 Feb 2022 12:08:13 +0100 Subject: [PATCH 02/45] Add intEnum shape class --- .../smithy/model/shapes/IntEnumShape.java | 234 ++++++++++++++++++ .../smithy/model/shapes/IntegerShape.java | 6 +- .../amazon/smithy/model/shapes/Shape.java | 16 +- .../smithy/model/shapes/ShapeToBuilder.java | 5 + .../amazon/smithy/model/shapes/ShapeType.java | 3 + .../smithy/model/shapes/ShapeVisitor.java | 4 + .../validators/UnitTypeValidator.java | 9 +- .../amazon/smithy/model/loader/prelude.smithy | 2 +- .../smithy/model/shapes/IntEnumShapeTest.java | 168 +++++++++++++ .../validators/unit/invalid-members.errors | 8 +- 10 files changed, 443 insertions(+), 12 deletions(-) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java create mode 100644 smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java new file mode 100644 index 00000000000..1daa7b70567 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -0,0 +1,234 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.shapes; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.traits.UnitTypeTrait; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.ListUtils; + +public final class IntEnumShape extends IntegerShape { + + private final Map members; + private volatile List memberNames; + + private IntEnumShape(Builder builder) { + super(builder); + members = builder.members.get(); + } + + /** + * Gets the members of the shape, including mixin members. + * + * @return Returns the immutable member map. + */ + public Map getAllMembers() { + return members; + } + + /** + * Returns an ordered list of member names based on the order they are + * defined in the model, including mixin members. + * + * @return Returns an immutable list of member names. + */ + public List getMemberNames() { + List names = memberNames; + if (names == null) { + names = ListUtils.copyOf(members.keySet()); + memberNames = names; + } + + return names; + } + + @Override + public boolean equals(Object other) { + if (!super.equals(other)) { + return false; + } + + // Members are ordered, so do a test on the ordering and their values. + IntEnumShape b = (IntEnumShape) other; + return getMemberNames().equals(b.getMemberNames()) && members.equals(b.members); + } + + /** + * Get a specific member by name. + * + * @param name Name of the member to retrieve. + * @return Returns the optional member. + */ + public Optional getMember(String name) { + return Optional.ofNullable(members.get(name)); + } + + @Override + public Collection members() { + return members.values(); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public Builder toBuilder() { + return (Builder) updateBuilder(builder()); + } + + @Override + public R accept(ShapeVisitor cases) { + return cases.intEnumShape(this); + } + + @Override + public Optional asIntEnumShape() { + return Optional.of(this); + } + + @Override + public ShapeType getType() { + return ShapeType.INT_ENUM; + } + + /** + * Builder used to create a {@link IntegerShape}. + */ + public static class Builder extends IntegerShape.Builder { + private final BuilderRef> members = BuilderRef.forOrderedMap(); + + @Override + public IntEnumShape build() { + return new IntEnumShape(this); + } + + @Override + public ShapeType getShapeType() { + return ShapeType.INT_ENUM; + } + + @Override + public Builder id(ShapeId shapeId) { + super.id(shapeId); + for (MemberShape member : members.peek().values()) { + addMember(member.toBuilder().id(shapeId.withMember(member.getMemberName())).build()); + } + return this; + } + + /** + * Replaces the members of the builder. + * + * @param members Members to add to the builder. + * @return Returns the builder. + */ + public Builder members(Collection members) { + clearMembers(); + for (MemberShape member : members) { + addMember(member); + } + return this; + } + + /** + * Removes all members from the shape. + * + * @return Returns the builder. + */ + public Builder clearMembers() { + members.clear(); + return this; + } + + @Override + public Builder addMember(MemberShape member) { + if (!member.getTarget().equals(UnitTypeTrait.UNIT)) { + throw new SourceException(String.format( + "intEnum members may only target `smithy.api#Unit`, but found `%s`", member.getTarget() + ), getSourceLocation()); + } + if (!member.hasTrait(EnumValueTrait.ID) + || !member.expectTrait(EnumValueTrait.class).getIntValue().isPresent()) { + throw new SourceException( + "intEnum members MUST have the enumValue trait with the `int` member set", + getSourceLocation()); + } + members.get().put(member.getMemberName(), member); + + return this; + } + + /** + * Adds a member to the builder. + * + * @param memberName Member name to add. + * @param enumValue The value of the enum. + * @return Returns the builder. + */ + public Builder addMember(String memberName, int enumValue) { + return addMember(memberName, enumValue, null); + } + + /** + * Adds a member to the builder. + * + * @param memberName Member name to add. + * @param enumValue The value of the enum. + * @param memberUpdater Consumer that can update the created member shape. + * @return Returns the builder. + */ + public Builder addMember(String memberName, int enumValue, Consumer memberUpdater) { + if (getId() == null) { + throw new IllegalStateException("An id must be set before setting a member with a target"); + } + + MemberShape.Builder builder = MemberShape.builder() + .target(UnitTypeTrait.UNIT) + .id(getId().withMember(memberName)) + .addTrait(EnumValueTrait.builder().intValue(enumValue).build()); + + if (memberUpdater != null) { + memberUpdater.accept(builder); + } + + return addMember(builder.build()); + } + + /** + * Removes a member by name. + * + *

Note that removing a member that was added by a mixin results in + * an inconsistent model. It's best to use ModelTransform to ensure + * that the model remains consistent when removing members. + * + * @param member Member name to remove. + * @return Returns the builder. + */ + public Builder removeMember(String member) { + if (members.hasValue()) { + members.get().remove(member); + } + return this; + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntegerShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntegerShape.java index 414a5d5d660..54239c5da5f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntegerShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntegerShape.java @@ -21,9 +21,9 @@ /** * Represents an {@code integer} shape. */ -public final class IntegerShape extends NumberShape implements ToSmithyBuilder { +public class IntegerShape extends NumberShape implements ToSmithyBuilder { - private IntegerShape(Builder builder) { + IntegerShape(Builder builder) { super(builder); } @@ -54,7 +54,7 @@ public ShapeType getType() { /** * Builder used to create a {@link IntegerShape}. */ - public static final class Builder extends AbstractShapeBuilder { + public static class Builder extends AbstractShapeBuilder { @Override public IntegerShape build() { return new IntegerShape(this); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java index 645af62aebf..a07ca8b7beb 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/Shape.java @@ -402,6 +402,13 @@ public Optional asIntegerShape() { return Optional.empty(); } + /** + * @return Optionally returns the shape as a {@link IntEnumShape}. + */ + public Optional asIntEnumShape() { + return Optional.empty(); + } + /** * @return Optionally returns the shape as a {@link ListShape}. */ @@ -574,7 +581,14 @@ public final boolean isSetShape() { * @return Returns true if the shape is a {@link IntegerShape} shape. */ public final boolean isIntegerShape() { - return getType() == ShapeType.INTEGER; + return getType() == ShapeType.INTEGER || getType() == ShapeType.INT_ENUM; + } + + /** + * @return Returns true if the shape is a {@link IntEnumShape} shape. + */ + public final boolean isIntEnumShape() { + return getType() == ShapeType.INT_ENUM; } /** diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeToBuilder.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeToBuilder.java index 8194a1500ed..cbb75b9ce17 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeToBuilder.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeToBuilder.java @@ -65,6 +65,11 @@ public AbstractShapeBuilder integerShape(IntegerShape shape) { return shape.toBuilder(); } + @Override + public AbstractShapeBuilder intEnumShape(IntEnumShape shape) { + return shape.toBuilder(); + } + @Override public AbstractShapeBuilder longShape(LongShape shape) { return shape.toBuilder(); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java index f962e5290ba..ad6306e6934 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java @@ -34,6 +34,7 @@ public enum ShapeType { BIG_DECIMAL("bigDecimal", BigDecimalShape.class, Category.SIMPLE), BIG_INTEGER("bigInteger", BigIntegerShape.class, Category.SIMPLE), ENUM("enum", EnumShape.class, Category.SIMPLE), + INT_ENUM("intEnum", IntEnumShape.class, Category.SIMPLE), LIST("list", ListShape.class, Category.AGGREGATE), SET("set", SetShape.class, Category.AGGREGATE), @@ -121,6 +122,8 @@ public static Optional fromString(String text) { return ShortShape.builder(); case INTEGER: return IntegerShape.builder(); + case INT_ENUM: + return IntEnumShape.builder(); case LONG: return LongShape.builder(); case FLOAT: diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeVisitor.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeVisitor.java index 8f6b46d4d15..94cdee35b55 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeVisitor.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeVisitor.java @@ -39,6 +39,10 @@ public interface ShapeVisitor { R integerShape(IntegerShape shape); + default R intEnumShape(IntEnumShape shape) { + return integerShape(shape); + } + R longShape(LongShape shape); R floatShape(FloatShape shape); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnitTypeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnitTypeValidator.java index 8878ab3be47..bbcac3d6110 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnitTypeValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/UnitTypeValidator.java @@ -54,10 +54,13 @@ public List validate(Model model) { .asMemberShape() .map(MemberShape::getContainer) .flatMap(model::getShape) - .filter(shape -> !(shape.getType() == ShapeType.UNION || shape.getType() == ShapeType.ENUM)) + .filter(shape -> !(shape.getType() == ShapeType.UNION + || shape.getType() == ShapeType.ENUM + || shape.getType() == ShapeType.INT_ENUM)) .ifPresent(container -> { - events.add(error(relationship.getShape(), - "Only members of a union can reference smithy.api#Unit")); + events.add(error( + relationship.getShape(), + "Only members of a union, enum, or intEnum can reference smithy.api#Unit")); }); break; default: diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index a13d0c4a9eb..c95e2814352 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -435,7 +435,7 @@ structure EnumDefinition { string EnumConstantBodyName /// Defines the value of an enum entry. -@trait(selector: "enum > member") +@trait(selector: ":is(enum, intEnum) > member") union enumValue { /// The value for the enum entry if it is a string. string: String diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java new file mode 100644 index 00000000000..9ee7e911416 --- /dev/null +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.shapes; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.traits.UnitTypeTrait; + +public class IntEnumShapeTest { + @Test + public void returnsAppropriateType() { + IntEnumShape shape = (IntEnumShape) IntEnumShape.builder().id("ns.foo#bar").build(); + + assertEquals(shape.getType(), ShapeType.INT_ENUM); + assertTrue(shape.isIntEnumShape()); + assertTrue(shape.isIntegerShape()); + assertTrue(shape.asIntEnumShape().isPresent()); + assertTrue(shape.asIntegerShape().isPresent()); + } + + @Test + public void mustNotContainMembersInShapeId() { + Assertions.assertThrows(SourceException.class, () -> { + IntEnumShape.builder().id("ns.foo#bar$baz").build(); + }); + } + + @Test + public void addMember() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + IntEnumShape shape = builder.addMember("foo", 1).build(); + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().intValue(1).build()) + .build()); + } + + @Test + public void addMemberShape() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + MemberShape member = MemberShape.builder() + .id("ns.foo#bar$foo") + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().intValue(1).build()) + .build(); + IntEnumShape shape = builder.addMember(member).build(); + assertEquals(shape.getMember("foo").get(), member); + } + + @Test + public void addMultipleMembers() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + IntEnumShape shape = builder + .addMember("foo", 1) + .addMember("bar", 2) + .build(); + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().intValue(1).build()) + .build()); + assertEquals(shape.getMember("bar").get(), + MemberShape.builder() + .id(shape.getId().withMember("bar")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().intValue(2).build()) + .build()); + } + + @Test + public void removeMember() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + IntEnumShape shape = builder + .addMember("foo", 1) + .removeMember("foo") + .addMember("bar", 2) + .build(); + assertFalse(shape.getMember("foo").isPresent()); + assertEquals(shape.getMember("bar").get(), + MemberShape.builder() + .id(shape.getId().withMember("bar")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().intValue(2).build()) + .build()); + } + + @Test + public void clearMembers() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + IntEnumShape shape = builder + .addMember("foo", 1) + .clearMembers() + .addMember("bar", 2) + .build(); + assertFalse(shape.getMember("foo").isPresent()); + assertEquals(shape.getMember("bar").get(), + MemberShape.builder() + .id(shape.getId().withMember("bar")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().intValue(2).build()) + .build()); + } + + @Test + public void idMustBeSetFirst() { + Assertions.assertThrows(IllegalStateException.class, () -> { + IntEnumShape.builder().addMember("foo", 1).build(); + }); + } + + @Test + public void membersMustHaveEnumValue() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + MemberShape member = MemberShape.builder() + .id("ns.foo#bar$foo") + .target(UnitTypeTrait.UNIT) + .build(); + Assertions.assertThrows(SourceException.class, () -> { + builder.addMember(member); + }); + } + + @Test + public void membersMustHaveEnumValueWithIntSet() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + MemberShape member = MemberShape.builder() + .id("ns.foo#bar$foo") + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build(); + Assertions.assertThrows(SourceException.class, () -> { + builder.addMember(member); + }); + } + + @Test + public void membersMustTargetUnit() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + MemberShape member = MemberShape.builder() + .id("ns.foo#bar$foo") + .target("smithy.api#Integer") + .addTrait(EnumValueTrait.builder().intValue(1).build()) + .build(); + Assertions.assertThrows(SourceException.class, () -> { + builder.addMember(member); + }); + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unit/invalid-members.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unit/invalid-members.errors index 38508fd0c98..cebb7dda433 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unit/invalid-members.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/unit/invalid-members.errors @@ -1,4 +1,4 @@ -[ERROR] smithy.example#Foo$bar: Only members of a union can reference smithy.api#Unit | UnitType -[ERROR] smithy.example#Baz$member: Only members of a union can reference smithy.api#Unit | UnitType -[ERROR] smithy.example#Boo$member: Only members of a union can reference smithy.api#Unit | UnitType -[ERROR] smithy.example#Bam$value: Only members of a union can reference smithy.api#Unit | UnitType +[ERROR] smithy.example#Foo$bar: Only members of a union, enum, or intEnum can reference smithy.api#Unit | UnitType +[ERROR] smithy.example#Baz$member: Only members of a union, enum, or intEnum can reference smithy.api#Unit | UnitType +[ERROR] smithy.example#Boo$member: Only members of a union, enum, or intEnum can reference smithy.api#Unit | UnitType +[ERROR] smithy.example#Bam$value: Only members of a union, enum, or intEnum can reference smithy.api#Unit | UnitType From 573ce2b4d7783d144163bef3a00c45d6193fe417 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 10 Feb 2022 13:06:49 +0100 Subject: [PATCH 03/45] Add enum and intEnum relationships --- .../model/neighbor/NeighborVisitor.java | 20 ++++++++++ .../model/neighbor/RelationshipType.java | 14 +++++++ .../model/neighbor/NeighborVisitorTest.java | 38 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java index d7fe7fe6303..5097a633490 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/NeighborVisitor.java @@ -19,6 +19,8 @@ import java.util.List; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.EntityShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; @@ -171,6 +173,24 @@ public List memberShape(MemberShape shape) { return result; } + @Override + public List enumShape(EnumShape shape) { + List result = new ArrayList<>(); + for (MemberShape member : shape.getAllMembers().values()) { + result.add(relationship(shape, RelationshipType.ENUM_MEMBER, member)); + } + return result; + } + + @Override + public List intEnumShape(IntEnumShape shape) { + List result = new ArrayList<>(); + for (MemberShape member : shape.getAllMembers().values()) { + result.add(relationship(shape, RelationshipType.INT_ENUM_MEMBER, member)); + } + return result; + } + @Override public List listShape(ListShape shape) { return ListUtils.of(relationship(shape, RelationshipType.LIST_MEMBER, shape.getMember())); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java index e210f9912f0..75290a6d41f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/neighbor/RelationshipType.java @@ -18,6 +18,8 @@ import java.util.Optional; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.selector.Selector; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; @@ -152,6 +154,18 @@ public enum RelationshipType { */ ERROR("error", RelationshipDirection.DIRECTED), + /** + * Relationships that exist on {@link EnumShape enum} shapes to their + * {@link MemberShape member shapes}. + */ + ENUM_MEMBER("member", RelationshipDirection.DIRECTED), + + /** + * Relationships that exist on {@link IntEnumShape intEnum} shapes to their + * {@link MemberShape member shapes}. + */ + INT_ENUM_MEMBER("member", RelationshipDirection.DIRECTED), + /** * Relationships that exist on {@link ListShape list} shapes to their * {@link MemberShape member shapes}. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java index 8241bde9b19..4ecaccfbcf5 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/neighbor/NeighborVisitorTest.java @@ -25,6 +25,8 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.BlobShape; import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.MapShape; import software.amazon.smithy.model.shapes.MemberShape; @@ -76,6 +78,42 @@ public void stringShape() { assertThat(relationships, empty()); } + @Test + public void enumShape() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#name"); + EnumShape shape = builder + .addMember("foo", "bar") + .addMember("baz", "bam") + .build(); + MemberShape member1Target = shape.getMember("foo").get(); + MemberShape member2Target = shape.getMember("baz").get(); + Model model = Model.builder().addShape(shape).build(); + NeighborVisitor neighborVisitor = new NeighborVisitor(model); + List relationships = shape.accept(neighborVisitor); + + assertThat(relationships, containsInAnyOrder( + Relationship.create(shape, RelationshipType.ENUM_MEMBER, member1Target), + Relationship.create(shape, RelationshipType.ENUM_MEMBER, member2Target))); + } + + @Test + public void intEnumShape() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#name"); + IntEnumShape shape = builder + .addMember("foo", 1) + .addMember("baz", 2) + .build(); + MemberShape member1Target = shape.getMember("foo").get(); + MemberShape member2Target = shape.getMember("baz").get(); + Model model = Model.builder().addShape(shape).build(); + NeighborVisitor neighborVisitor = new NeighborVisitor(model); + List relationships = shape.accept(neighborVisitor); + + assertThat(relationships, containsInAnyOrder( + Relationship.create(shape, RelationshipType.INT_ENUM_MEMBER, member1Target), + Relationship.create(shape, RelationshipType.INT_ENUM_MEMBER, member2Target))); + } + @Test public void timestampShape() { Shape shape = TimestampShape.builder().id("ns.foo#name").build(); From 91878bcac3cddfe9a7ab8776569273d207ae54fb Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 10 Feb 2022 14:11:39 +0100 Subject: [PATCH 04/45] Make enum shape's enum trait synthetic --- .../amazon/smithy/model/shapes/EnumShape.java | 27 ++++++--- .../amazon/smithy/model/traits/EnumTrait.java | 14 +++-- .../traits/synthetic/SyntheticEnumTrait.java | 58 +++++++++++++++++++ .../smithy/model/shapes/EnumShapeTest.java | 5 +- 4 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index 95987bc6f73..0539147d14b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -26,6 +26,7 @@ import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.traits.UnitTypeTrait; +import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; import software.amazon.smithy.utils.BuilderRef; import software.amazon.smithy.utils.ListUtils; @@ -75,6 +76,14 @@ public boolean equals(Object other) { return getMemberNames().equals(b.getMemberNames()) && members.equals(b.members); } + @Override + public Optional findTrait(ShapeId id) { + if (id.equals(EnumTrait.ID)) { + return super.findTrait(SyntheticEnumTrait.ID); + } + return super.findTrait(id); + } + /** * Get a specific member by name. * @@ -164,12 +173,13 @@ public Builder members(EnumTrait trait) { throw new IllegalStateException("An id must be set before adding a named enum trait to a string."); } clearMembers(); - super.addTrait(trait); + SyntheticEnumTrait.Builder traitBuilder = SyntheticEnumTrait.builder(); for (EnumDefinition definition : trait.getValues()) { Optional member = definition.asMember(getId()); if (member.isPresent()) { addMember(member.get(), false); + traitBuilder.addEnum(definition); } else { throw new IllegalStateException(String.format( "Unable to convert enum trait entry with name: `%s` and value `%s` to an enum member.", @@ -177,6 +187,7 @@ public Builder members(EnumTrait trait) { )); } } + super.addTrait(traitBuilder.build()); return this; } @@ -202,7 +213,7 @@ public Builder members(Collection members) { */ public Builder clearMembers() { members.clear(); - super.removeTrait(EnumTrait.ID); + super.removeTrait(SyntheticEnumTrait.ID); return this; } @@ -226,11 +237,11 @@ private Builder addMember(MemberShape member, boolean updateEnumTrait) { members.get().put(member.getMemberName(), member); if (updateEnumTrait) { - EnumTrait.Builder builder; - if (getTraits().containsKey(EnumTrait.ID)) { - builder = ((EnumTrait) getTraits().get(EnumTrait.ID)).toBuilder(); + SyntheticEnumTrait.Builder builder; + if (getTraits().containsKey(SyntheticEnumTrait.ID)) { + builder = ((SyntheticEnumTrait) getTraits().get(SyntheticEnumTrait.ID)).toBuilder(); } else { - builder = EnumTrait.builder(); + builder = SyntheticEnumTrait.builder(); } builder.addEnum(EnumDefinition.fromMember(member)); super.addTrait(builder.build()); @@ -288,7 +299,7 @@ public Builder addMember(String memberName, String enumValue, Consumer { +public class EnumTrait extends AbstractTrait implements ToSmithyBuilder { public static final ShapeId ID = ShapeId.from("smithy.api#enum"); - private final List definitions; + protected final List definitions; - private EnumTrait(Builder builder) { - super(ID, builder.sourceLocation); + protected EnumTrait(ShapeId id, Builder builder) { + super(id, builder.sourceLocation); this.definitions = builder.definitions.copy(); if (definitions.isEmpty()) { throw new SourceException("enum must have at least one entry", getSourceLocation()); } } + private EnumTrait(Builder builder) { + this(ID, builder); + } + /** * Gets the enum value to body. * @@ -93,7 +97,7 @@ public static Builder builder() { /** * Builder used to create the enum trait. */ - public static final class Builder extends AbstractTraitBuilder { + public static class Builder extends AbstractTraitBuilder { private final BuilderRef> definitions = BuilderRef.forList(); public Builder addEnum(EnumDefinition value) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java new file mode 100644 index 00000000000..b850999a8cb --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.traits.synthetic; + +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.EnumTrait; + +/** + * A synthetic copy of the {@link EnumTrait} for use in the {@link EnumShape}. + */ +public final class SyntheticEnumTrait extends EnumTrait { + + public static final ShapeId ID = ShapeId.from("smithy.synthetic#enum"); + + private SyntheticEnumTrait(Builder builder) { + super(ID, builder); + } + + @Override + public boolean isSynthetic() { + return true; + } + + @Override + public Builder toBuilder() { + Builder builder = (Builder) builder().sourceLocation(getSourceLocation()); + definitions.forEach(builder::addEnum); + return builder; + } + + /** + * @return Returns a synthetic enum trait builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends EnumTrait.Builder { + @Override + public SyntheticEnumTrait build() { + return new SyntheticEnumTrait(this); + } + } +} diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index d17d385bad2..059b0b850ba 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -25,6 +25,7 @@ import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.UnitTypeTrait; +import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.SetUtils; @@ -246,7 +247,7 @@ public void cannotDirectlyAddEnumTrait() { public void cannotDirectlyRemoveEnumTrait() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); Assertions.assertThrows(SourceException.class, () -> { - builder.removeTrait(EnumTrait.ID).build(); + builder.removeTrait(SyntheticEnumTrait.ID).build(); }); } @@ -302,7 +303,7 @@ public void canConvertBaseString() { .build(); Optional optionalEnum = EnumShape.fromStringShape(string); assertTrue(optionalEnum.isPresent()); - assertEquals(trait, optionalEnum.get().expectTrait(EnumTrait.class)); + assertEquals(trait.getValues(), optionalEnum.get().expectTrait(SyntheticEnumTrait.class).getValues()); assertEquals(optionalEnum.get().getMember("foo").get(), MemberShape.builder() From f5c9f8c03020ddeddef45f5bcb6c2ef50c3bcd77 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 10 Feb 2022 14:23:14 +0100 Subject: [PATCH 05/45] Support loading enums in ast --- .../smithy/model/loader/AstModelLoader.java | 35 ++++++++------ .../smithy/model/shapes/ModelSerializer.java | 16 +++++-- .../shapes/ast-serialization/cases/enums.json | 48 +++++++++++++++++++ 3 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/ast-serialization/cases/enums.json diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AstModelLoader.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/AstModelLoader.java index b5481390f3a..ad605df033c 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AstModelLoader.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/AstModelLoader.java @@ -35,7 +35,9 @@ import software.amazon.smithy.model.shapes.CollectionShape; import software.amazon.smithy.model.shapes.DocumentShape; import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.LongShape; @@ -75,7 +77,7 @@ enum AstModelLoader { private static final List TOP_LEVEL_PROPERTIES = ListUtils.of("smithy", SHAPES, METADATA); private static final List APPLY_PROPERTIES = ListUtils.of(TYPE, TRAITS); private static final List SIMPLE_PROPERTY_NAMES = ListUtils.of(TYPE, TRAITS); - private static final List STRUCTURE_AND_UNION_PROPERTY_NAMES = ListUtils.of(TYPE, MEMBERS, TRAITS, MIXINS); + private static final List NAMED_MEMBER_SHAPE_PROPERTY_NAMES = ListUtils.of(TYPE, MEMBERS, TRAITS, MIXINS); private static final List COLLECTION_PROPERTY_NAMES = ListUtils.of(TYPE, "member", TRAITS); private static final List MAP_PROPERTY_NAMES = ListUtils.of(TYPE, "key", "value", TRAITS); private static final Set MEMBER_PROPERTIES = SetUtils.of(TARGET, TRAITS); @@ -143,6 +145,9 @@ private void loadShape(ShapeId id, String type, ObjectNode value, FullyResolvedM case "integer": loadSimpleShape(id, value, IntegerShape.builder(), modelFile); break; + case "intEnum": + loadNamedMemberShape(id, value, IntEnumShape.builder(), modelFile); + break; case "long": loadSimpleShape(id, value, LongShape.builder(), modelFile); break; @@ -164,6 +169,9 @@ private void loadShape(ShapeId id, String type, ObjectNode value, FullyResolvedM case "string": loadSimpleShape(id, value, StringShape.builder(), modelFile); break; + case "enum": + loadNamedMemberShape(id, value, EnumShape.builder(), modelFile); + break; case "timestamp": loadSimpleShape(id, value, TimestampShape.builder(), modelFile); break; @@ -183,10 +191,10 @@ private void loadShape(ShapeId id, String type, ObjectNode value, FullyResolvedM loadService(id, value, modelFile); break; case "structure": - loadStructure(id, value, modelFile); + loadNamedMemberShape(id, value, StructureShape.builder(), modelFile); break; case "union": - loadUnion(id, value, modelFile); + loadNamedMemberShape(id, value, UnionShape.builder(), modelFile); break; case "operation": loadOperation(id, value, modelFile); @@ -320,19 +328,18 @@ private void loadSimpleShape( addMixins(id, node, modelFile); } - private void loadStructure(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { - LoaderUtils.checkForAdditionalProperties(node, id, STRUCTURE_AND_UNION_PROPERTY_NAMES, modelFile.events()); - modelFile.onShape(StructureShape.builder().id(id).source(node.getSourceLocation())); - finishLoadingStructOrUnionMembers(id, node, modelFile); - } - - private void loadUnion(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { - LoaderUtils.checkForAdditionalProperties(node, id, STRUCTURE_AND_UNION_PROPERTY_NAMES, modelFile.events()); - modelFile.onShape(UnionShape.builder().id(id).source(node.getSourceLocation())); - finishLoadingStructOrUnionMembers(id, node, modelFile); + private void loadNamedMemberShape( + ShapeId id, + ObjectNode node, + AbstractShapeBuilder builder, + FullyResolvedModelFile modelFile + ) { + LoaderUtils.checkForAdditionalProperties(node, id, NAMED_MEMBER_SHAPE_PROPERTY_NAMES, modelFile.events()); + modelFile.onShape(builder.id(id).source(node.getSourceLocation())); + finishLoadingNamedMemberShapeMembers(id, node, modelFile); } - private void finishLoadingStructOrUnionMembers(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { + private void finishLoadingNamedMemberShapeMembers(ShapeId id, ObjectNode node, FullyResolvedModelFile modelFile) { applyShapeTraits(id, node, modelFile); ObjectNode memberObject = node.getObjectMember(MEMBERS).orElse(Node.objectNode()); for (Map.Entry entry : memberObject.getStringMap().entrySet()) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ModelSerializer.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ModelSerializer.java index 0c31c645932..a497f80b208 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ModelSerializer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ModelSerializer.java @@ -229,6 +229,16 @@ protected ObjectNode getDefault(Shape shape) { return serializeAllTraits(shape, createTypedBuilder(shape)).build(); } + @Override + public Node enumShape(EnumShape shape) { + return createNamedMemberShape(shape, shape.getAllMembers()); + } + + @Override + public Node intEnumShape(IntEnumShape shape) { + return createNamedMemberShape(shape, shape.getAllMembers()); + } + @Override public Node listShape(ListShape shape) { return collectionShape(shape); @@ -332,15 +342,15 @@ private Optional createOptionalIdList(Collection list) { @Override public Node structureShape(StructureShape shape) { - return createStructureAndUnion(shape, shape.getAllMembers()); + return createNamedMemberShape(shape, shape.getAllMembers()); } @Override public Node unionShape(UnionShape shape) { - return createStructureAndUnion(shape, shape.getAllMembers()); + return createNamedMemberShape(shape, shape.getAllMembers()); } - private ObjectNode createStructureAndUnion(Shape shape, Map members) { + private ObjectNode createNamedMemberShape(Shape shape, Map members) { ObjectNode.Builder result = createTypedBuilder(shape); ObjectNode.Builder membersBuilder = ObjectNode.objectNodeBuilder(); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/ast-serialization/cases/enums.json b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/ast-serialization/cases/enums.json new file mode 100644 index 00000000000..ebb136ec501 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/ast-serialization/cases/enums.json @@ -0,0 +1,48 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#StringEnum": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "foo" + } + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "bar" + } + } + } + }, + "traits": {} + }, + "smithy.example#IntEnum": { + "type": "intEnum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "int": 1 + } + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "int": 2 + } + } + } + } + } + } +} From a37837960f1b1fea75675900caeed7c1bde75a4c Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 10 Feb 2022 14:55:58 +0100 Subject: [PATCH 06/45] Set enum value trait automatically --- .../amazon/smithy/model/shapes/EnumShape.java | 7 ++-- .../smithy/model/shapes/EnumShapeTest.java | 35 ++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index 0539147d14b..a0ea7ca3278 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -228,8 +228,11 @@ private Builder addMember(MemberShape member, boolean updateEnumTrait) { "Enum members may only target `smithy.api#Unit`, but found `%s`", member.getTarget() ), getSourceLocation()); } - if (!member.hasTrait(EnumValueTrait.ID) - || !member.expectTrait(EnumValueTrait.class).getStringValue().isPresent()) { + if (!member.hasTrait(EnumValueTrait.ID)) { + member = member.toBuilder() + .addTrait(EnumValueTrait.builder().stringValue(member.getMemberName()).build()) + .build(); + } else if (!member.expectTrait(EnumValueTrait.class).getStringValue().isPresent()) { throw new SourceException( "Enum members MUST have the enumValue trait with the `string` member set", getSourceLocation()); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index 059b0b850ba..ae09097ef81 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -88,6 +88,29 @@ public void addMemberShape() { )); } + @Test + public void memberValueIsAppliedIfNotPresent() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + MemberShape member = MemberShape.builder() + .id("ns.foo#bar$foo") + .target(UnitTypeTrait.UNIT) + .build(); + EnumShape shape = builder.addMember(member).build(); + + MemberShape expected = member.toBuilder() + .addTrait(EnumValueTrait.builder().stringValue("foo").build()) + .build(); + assertEquals(shape.getMember("foo").get(), expected); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of( + EnumDefinition.builder() + .name("foo") + .value("foo") + .build() + )); + } + @Test public void addMemberFromEnumTrait() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); @@ -251,18 +274,6 @@ public void cannotDirectlyRemoveEnumTrait() { }); } - @Test - public void membersMustHaveEnumValue() { - EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); - MemberShape member = MemberShape.builder() - .id("ns.foo#bar$foo") - .target(UnitTypeTrait.UNIT) - .build(); - Assertions.assertThrows(SourceException.class, () -> { - builder.addMember(member); - }); - } - @Test public void membersMustHaveEnumValueWithStringSet() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); From 00aa523a1216a0ef7368e1b8620746f72ddbe4fe Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 10 Feb 2022 16:07:32 +0100 Subject: [PATCH 07/45] Support loading enums in idl --- .../smithy/model/loader/IdlModelParser.java | 53 ++++++++++++++----- .../shapes/SmithyIdlModelSerializer.java | 44 ++++++++++++++- .../idl-serialization/cases/enums.smithy | 30 +++++++++++ 3 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index 098ac7efd74..eabb534d838 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -43,7 +43,9 @@ import software.amazon.smithy.model.shapes.CollectionShape; import software.amazon.smithy.model.shapes.DocumentShape; import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.LongShape; @@ -64,6 +66,7 @@ import software.amazon.smithy.model.traits.InputTrait; import software.amazon.smithy.model.traits.OutputTrait; import software.amazon.smithy.model.traits.TraitFactory; +import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.model.validation.Severity; import software.amazon.smithy.model.validation.ValidationEvent; import software.amazon.smithy.model.validation.Validator; @@ -403,6 +406,9 @@ private void parseShape(List traits) { case "string": parseSimpleShape(id, location, StringShape.builder()); break; + case "enum": + parseEnumShape(id, location, EnumShape.builder()); + break; case "blob": parseSimpleShape(id, location, BlobShape.builder()); break; @@ -415,6 +421,9 @@ private void parseShape(List traits) { case "integer": parseSimpleShape(id, location, IntegerShape.builder()); break; + case "intEnum": + parseEnumShape(id, location, IntEnumShape.builder()); + break; case "long": parseSimpleShape(id, location, LongShape.builder()); break; @@ -455,6 +464,13 @@ private void parseSimpleShape(ShapeId id, SourceLocation location, AbstractShape parseMixins(id); } + private void parseEnumShape(ShapeId id, SourceLocation location, AbstractShapeBuilder builder) { + modelFile.onShape(builder.id(id).source(location)); + parseMixins(id); + parseMembers(id, Collections.emptySet(), true); + clearPendingDocs(); + } + // See parseMap for information on why members are parsed before the // list/set is registered with the ModelFile. private void parseCollection(ShapeId id, SourceLocation location, CollectionShape.Builder builder) { @@ -466,6 +482,10 @@ private void parseCollection(ShapeId id, SourceLocation location, CollectionShap } private void parseMembers(ShapeId id, Set requiredMembers) { + parseMembers(id, requiredMembers, false); + } + + private void parseMembers(ShapeId id, Set requiredMembers, boolean targetsUnion) { Set definedMembers = new HashSet<>(); ws(); @@ -477,7 +497,7 @@ private void parseMembers(ShapeId id, Set requiredMembers) { break; } - parseMember(id, requiredMembers, definedMembers); + parseMember(id, requiredMembers, definedMembers, targetsUnion); // Clears out any previously captured documentation // comments that may have been found when parsing the member. @@ -493,7 +513,7 @@ private void parseMembers(ShapeId id, Set requiredMembers) { expect('}'); } - private void parseMember(ShapeId parent, Set allowed, Set defined) { + private void parseMember(ShapeId parent, Set allowed, Set defined, boolean targetsUnion) { // Parse optional member traits. List memberTraits = parseDocsAndTraits(); SourceLocation memberLocation = currentLocation(); @@ -511,21 +531,28 @@ private void parseMember(ShapeId parent, Set allowed, Set define throw syntax(parent, "Unexpected member of " + parent + ": '" + memberName + '\''); } - ws(); - expect(':'); - - if (peek() == '=') { - throw syntax("Defining structures inline with the `:=` syntax may only be used when " - + "defining operation input and output shapes."); - } - - ws(); ShapeId memberId = parent.withMember(memberName); MemberShape.Builder memberBuilder = MemberShape.builder().id(memberId).source(memberLocation); - String target = ParserUtils.parseShapeId(this); modelFile.onShape(memberBuilder); + String target; + + ws(); + + if (!targetsUnion) { + expect(':'); + + if (peek() == '=') { + throw syntax("Defining structures inline with the `:=` syntax may only be used when " + + "defining operation input and output shapes."); + } + + ws(); + target = ParserUtils.parseShapeId(this); + } else { + target = UnitTypeTrait.UNIT.toString(); + } modelFile.addForwardReference(target, memberBuilder::target); - addTraits(memberId, memberTraits); + addTraits(parent.withMember(memberName), memberTraits); } private void parseMapStatement(ShapeId id, SourceLocation location) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java index 29fed1bd877..313e5187a3d 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java @@ -24,9 +24,11 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; @@ -42,6 +44,7 @@ import software.amazon.smithy.model.node.StringNode; import software.amazon.smithy.model.traits.AnnotationTrait; import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.IdRefTrait; import software.amazon.smithy.model.traits.InputTrait; import software.amazon.smithy.model.traits.OutputTrait; @@ -396,6 +399,10 @@ protected Void getDefault(Shape shape) { } private void shapeWithMembers(Shape shape, List members) { + shapeWithMembers(shape, members, false); + } + + private void shapeWithMembers(Shape shape, List members, boolean isEnum) { List nonMixinMembers = new ArrayList<>(); List mixinMembers = new ArrayList<>(); for (MemberShape member : members) { @@ -410,7 +417,11 @@ private void shapeWithMembers(Shape shape, List members) { codeWriter.writeInline("$L $L ", shape.getType(), shape.getId().getName()); writeMixins(shape); - writeShapeMembers(nonMixinMembers); + if (isEnum) { + writeEnumMembers(nonMixinMembers); + } else { + writeShapeMembers(nonMixinMembers); + } codeWriter.write(""); applyIntroducedTraits(mixinMembers); } @@ -442,6 +453,25 @@ private void writeShapeMembers(Collection members) { } } + private void writeEnumMembers(Collection members) { + if (members.isEmpty()) { + codeWriter.writeInline("{}").write(""); + return; + } + + codeWriter.openBlock("{", "}", () -> { + for (MemberShape member : members) { + Map traits = new LinkedHashMap<>(member.getAllTraits()); + Optional stringValue = member.expectTrait(EnumValueTrait.class).getStringValue(); + if (stringValue.isPresent() && member.getMemberName().equals(stringValue.get())) { + traits.remove(EnumValueTrait.ID); + } + serializeTraits(traits); + codeWriter.write("$L", member.getMemberName()); + } + }); + } + private void applyIntroducedTraits(Collection members) { for (MemberShape member : members) { // Use short form for a single trait, and block form for multiple traits. @@ -511,6 +541,18 @@ private boolean isEmptyStructure(Node node, Shape shape) { return !shape.isDocumentShape() && node.asObjectNode().map(ObjectNode::isEmpty).orElse(false); } + @Override + public Void enumShape(EnumShape shape) { + shapeWithMembers(shape, new ArrayList<>(shape.members()), true); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + shapeWithMembers(shape, new ArrayList<>(shape.members()), true); + return null; + } + @Override public Void listShape(ListShape shape) { shapeWithMembers(shape, Collections.singletonList(shape.getMember())); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy new file mode 100644 index 00000000000..c784a86eeb0 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy @@ -0,0 +1,30 @@ +$version: "2.0" + +namespace ns.foo + +intEnum IntEnum { + @enumValue( + int: 1 + ) + FOO + @enumValue( + int: 2 + ) + BAR +} + +enum StringEnum { + FOO + BAR +} + +enum StringEnumWithExplicitValues { + @enumValue( + string: "foo" + ) + FOO + @enumValue( + string: "bar" + ) + BAR +} From fd10fc5cbeda7e2ab38c78ee2be6ada51d8d1a8d Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 10 Feb 2022 16:34:34 +0100 Subject: [PATCH 08/45] Add more loader tests --- .../amazon/smithy/model/shapes/EnumShape.java | 4 + .../smithy/model/shapes/IntEnumShape.java | 4 + .../smithy/model/shapes/EnumShapeTest.java | 3 +- .../smithy/model/shapes/IntEnumShapeTest.java | 3 +- .../loader/invalid/enum-without-member.smithy | 7 ++ .../invalid/int-enum-without-member.smithy | 7 ++ .../smithy/model/loader/valid/enums.json | 118 ++++++++++++++++++ .../smithy/model/loader/valid/enums.smithy | 41 ++++++ 8 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-without-member.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/int-enum-without-member.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index a0ea7ca3278..956b4c4b5d9 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -38,6 +38,10 @@ public final class EnumShape extends StringShape { private EnumShape(Builder builder) { super(builder); members = builder.members.get(); + validateMemberShapeIds(); + if (members.size() < 1) { + throw new SourceException("enum shapes must have at least one member", getSourceLocation()); + } } /** diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java index 1daa7b70567..8d3aefc55a8 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -34,6 +34,10 @@ public final class IntEnumShape extends IntegerShape { private IntEnumShape(Builder builder) { super(builder); members = builder.members.get(); + validateMemberShapeIds(); + if (members.size() < 1) { + throw new SourceException("intEnum shapes must have at least one member", getSourceLocation()); + } } /** diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index ae09097ef81..ac73c0f9140 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -32,7 +32,8 @@ public class EnumShapeTest { @Test public void returnsAppropriateType() { - EnumShape shape = (EnumShape) EnumShape.builder().id("ns.foo#bar").build(); + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumShape shape = builder.addMember("foo", "bar").build(); assertEquals(shape.getType(), ShapeType.ENUM); assertTrue(shape.isEnumShape()); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java index 9ee7e911416..469e86a26a7 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java @@ -26,7 +26,8 @@ public class IntEnumShapeTest { @Test public void returnsAppropriateType() { - IntEnumShape shape = (IntEnumShape) IntEnumShape.builder().id("ns.foo#bar").build(); + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + IntEnumShape shape = builder.addMember("foo", 1).build(); assertEquals(shape.getType(), ShapeType.INT_ENUM); assertTrue(shape.isIntEnumShape()); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-without-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-without-member.smithy new file mode 100644 index 00000000000..e77cc3aa05d --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/enum-without-member.smithy @@ -0,0 +1,7 @@ +// enum must have at least one entry + +$version: "2.0" + +namespace smithy.example + +enum Enum {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/int-enum-without-member.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/int-enum-without-member.smithy new file mode 100644 index 00000000000..8c95a8d58a2 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/int-enum-without-member.smithy @@ -0,0 +1,7 @@ +// intEnum shapes must have at least one member + +$version: "2.0" + +namespace smithy.example + +intEnum IntEnum {} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json new file mode 100644 index 00000000000..34edff79374 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json @@ -0,0 +1,118 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#EnumWithoutValueTraits": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "FOO" + } + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "BAR" + } + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "BAZ" + } + } + } + } + }, + "smithy.example#EnumWithValueTraits": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "foo" + } + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "bar" + } + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "baz" + } + } + } + } + }, + "smithy.example#EnumWithDefaultBound": { + "type": "enum", + "members": { + "DEFAULT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "" + } + } + } + } + }, + "smithy.example#IntEnum": { + "type": "intEnum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "int": 1 + } + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "int": 2 + } + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "int": 3 + } + } + } + } + }, + "smithy.example#IntEnumWithDefaultBound": { + "type": "intEnum", + "members": { + "DEFAULT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "int": 0 + } + } + } + } + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy new file mode 100644 index 00000000000..03993e67f40 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy @@ -0,0 +1,41 @@ +$version: "2.0" + +namespace smithy.example + +enum EnumWithoutValueTraits { + FOO + BAR + BAZ +} + +enum EnumWithValueTraits { + @enumValue(string: "foo") + FOO + + @enumValue(string: "bar") + BAR + + @enumValue(string: "baz") + BAZ +} + +enum EnumWithDefaultBound { + @enumValue(string: "") + DEFAULT +} + +intEnum IntEnum { + @enumValue(int: 1) + FOO + + @enumValue(int: 2) + BAR + + @enumValue(int: 3) + BAZ +} + +intEnum IntEnumWithDefaultBound { + @enumValue(int: 0) + DEFAULT +} From 3f6beaeba7d6eaf35cf918359e146969f35c9952 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 10 Feb 2022 16:37:20 +0100 Subject: [PATCH 09/45] Change enum shape category to new enum category --- .../software/amazon/smithy/model/shapes/ShapeType.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java index ad6306e6934..8448ae9b88b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java @@ -33,8 +33,9 @@ public enum ShapeType { DOUBLE("double", DoubleShape.class, Category.SIMPLE), BIG_DECIMAL("bigDecimal", BigDecimalShape.class, Category.SIMPLE), BIG_INTEGER("bigInteger", BigIntegerShape.class, Category.SIMPLE), - ENUM("enum", EnumShape.class, Category.SIMPLE), - INT_ENUM("intEnum", IntEnumShape.class, Category.SIMPLE), + + ENUM("enum", EnumShape.class, Category.ENUM), + INT_ENUM("intEnum", IntEnumShape.class, Category.ENUM), LIST("list", ListShape.class, Category.AGGREGATE), SET("set", SetShape.class, Category.AGGREGATE), @@ -47,7 +48,7 @@ public enum ShapeType { RESOURCE("resource", ResourceShape.class, Category.SERVICE), OPERATION("operation", OperationShape.class, Category.SERVICE); - public enum Category { SIMPLE, AGGREGATE, SERVICE } + public enum Category { SIMPLE, AGGREGATE, SERVICE, ENUM } private final String stringValue; private final Class shapeClass; From 64435472c39a600c43abaab1e13ea845688b460f Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 10 Feb 2022 17:36:11 +0100 Subject: [PATCH 10/45] Support model transformer conversion for enums --- .../amazon/smithy/model/shapes/EnumShape.java | 40 +----- .../model/transform/ChangeShapeType.java | 66 +++++++-- .../smithy/model/shapes/EnumShapeTest.java | 21 --- .../model/transform/ChangeShapeTypeTest.java | 134 +++++++++++++++++- 4 files changed, 189 insertions(+), 72 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index 956b4c4b5d9..e9d59038583 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -155,6 +155,11 @@ public static final class Builder extends StringShape.Builder { @Override public EnumShape build() { + SyntheticEnumTrait.Builder builder = SyntheticEnumTrait.builder(); + for (MemberShape member : members.get().values()) { + builder.addEnum(EnumDefinition.fromMember(member)); + } + addTrait(builder.build()); return new EnumShape(this); } @@ -177,13 +182,11 @@ public Builder members(EnumTrait trait) { throw new IllegalStateException("An id must be set before adding a named enum trait to a string."); } clearMembers(); - SyntheticEnumTrait.Builder traitBuilder = SyntheticEnumTrait.builder(); for (EnumDefinition definition : trait.getValues()) { Optional member = definition.asMember(getId()); if (member.isPresent()) { addMember(member.get(), false); - traitBuilder.addEnum(definition); } else { throw new IllegalStateException(String.format( "Unable to convert enum trait entry with name: `%s` and value `%s` to an enum member.", @@ -191,7 +194,6 @@ public Builder members(EnumTrait trait) { )); } } - super.addTrait(traitBuilder.build()); return this; } @@ -217,7 +219,6 @@ public Builder members(Collection members) { */ public Builder clearMembers() { members.clear(); - super.removeTrait(SyntheticEnumTrait.ID); return this; } @@ -243,17 +244,6 @@ private Builder addMember(MemberShape member, boolean updateEnumTrait) { } members.get().put(member.getMemberName(), member); - if (updateEnumTrait) { - SyntheticEnumTrait.Builder builder; - if (getTraits().containsKey(SyntheticEnumTrait.ID)) { - builder = ((SyntheticEnumTrait) getTraits().get(SyntheticEnumTrait.ID)).toBuilder(); - } else { - builder = SyntheticEnumTrait.builder(); - } - builder.addEnum(EnumDefinition.fromMember(member)); - super.addTrait(builder.build()); - } - return this; } @@ -306,28 +296,8 @@ public Builder addMember(String memberName, String enumValue, Consumer shapeBuilder = to.createBuilderForType(); + copySharedParts(shape, shapeBuilder); + return shapeBuilder.build(); + } + @Override public Shape longShape(LongShape shape) { return copyToSimpleShape(to, shape); @@ -128,9 +144,36 @@ public Shape bigDecimalShape(BigDecimalShape shape) { @Override public Shape stringShape(StringShape shape) { + if (to == ShapeType.ENUM) { + Optional enumShape = EnumShape.fromStringShape(shape); + if (enumShape.isPresent()) { + return enumShape.get(); + } + throw invalidType(shape, to, "Strings can only be converted to enums if they have an enum " + + "trait where each enum definition has a name."); + } return copyToSimpleShape(to, shape); } + @Override + public Shape enumShape(EnumShape shape) { + if (to.getCategory() != ShapeType.Category.SIMPLE) { + throw invalidType(shape, to, "Enum types can only be converted to simple types."); + } + + AbstractShapeBuilder shapeBuilder = to.createBuilderForType(); + copySharedParts(shape, shapeBuilder); + shapeBuilder.removeTrait(SyntheticEnumTrait.ID); + + if (to == ShapeType.STRING) { + EnumTrait.Builder traitBuilder = EnumTrait.builder(); + shape.expectTrait(SyntheticEnumTrait.class).getValues().forEach(traitBuilder::addEnum); + shapeBuilder.addTrait(traitBuilder.build()); + } + + return shapeBuilder.build(); + } + @Override public Shape timestampShape(TimestampShape shape) { return copyToSimpleShape(to, shape); @@ -142,7 +185,7 @@ public Shape listShape(ListShape shape) { throw invalidType(shape, to, "Lists can only be converted to sets."); } SetShape.Builder builder = SetShape.builder(); - copySharedPartsToShape(shape, builder); + copySharedPartsAndMembers(shape, builder); return builder.build(); } @@ -152,7 +195,7 @@ public Shape setShape(SetShape shape) { throw invalidType(shape, to, "Sets can only be converted to lists."); } ListShape.Builder builder = ListShape.builder(); - copySharedPartsToShape(shape, builder); + copySharedPartsAndMembers(shape, builder); return builder.build(); } @@ -162,7 +205,7 @@ public Shape structureShape(StructureShape shape) { throw invalidType(shape, to, "Structures can only be converted to unions."); } UnionShape.Builder builder = UnionShape.builder(); - copySharedPartsToShape(shape, builder); + copySharedPartsAndMembers(shape, builder); return builder.build(); } @@ -172,27 +215,30 @@ public Shape unionShape(UnionShape shape) { throw invalidType(shape, to, "Unions can only be converted to structures."); } StructureShape.Builder builder = StructureShape.builder(); - copySharedPartsToShape(shape, builder); + copySharedPartsAndMembers(shape, builder); return builder.build(); } - private void copySharedPartsToShape(Shape source, AbstractShapeBuilder builder) { - builder.traits(source.getAllTraits().values()); - builder.id(source.getId()); - builder.source(source.getSourceLocation()); - + private void copySharedPartsAndMembers(Shape source, AbstractShapeBuilder builder) { + copySharedParts(source, builder); for (MemberShape member : source.members()) { builder.addMember(member); } } + private void copySharedParts(Shape source, AbstractShapeBuilder builder) { + builder.traits(source.getAllTraits().values()); + builder.id(source.getId()); + builder.source(source.getSourceLocation()); + } + private Shape copyToSimpleShape(ShapeType to, Shape shape) { if (to.getCategory() != ShapeType.Category.SIMPLE) { throw invalidType(shape, to, "Simple types can only be converted to other simple types."); } AbstractShapeBuilder shapeBuilder = to.createBuilderForType(); - copySharedPartsToShape(shape, shapeBuilder); + copySharedPartsAndMembers(shape, shapeBuilder); return shapeBuilder.build(); } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index ac73c0f9140..909de8c5c51 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -254,27 +254,6 @@ public void clearMembers() { )); } - @Test - public void cannotDirectlyAddEnumTrait() { - EnumTrait trait = EnumTrait.builder() - .addEnum(EnumDefinition.builder() - .name("foo") - .value("bar") - .build()) - .build(); - Assertions.assertThrows(SourceException.class, () -> { - EnumShape.builder().addTrait(trait).id("ns.foo#bar").build(); - }); - } - - @Test - public void cannotDirectlyRemoveEnumTrait() { - EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); - Assertions.assertThrows(SourceException.class, () -> { - builder.removeTrait(SyntheticEnumTrait.ID).build(); - }); - } - @Test public void membersMustHaveEnumValueWithStringSet() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java index 6f51866b0f3..7b7cfd6b9ca 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java @@ -16,9 +16,12 @@ package software.amazon.smithy.model.transform; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.TreeSet; import org.hamcrest.Matchers; @@ -29,14 +32,24 @@ import org.junit.jupiter.params.provider.MethodSource; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.SetShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.shapes.StringShape; 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.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.traits.UnitTypeTrait; +import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; +import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.MapUtils; public class ChangeShapeTypeTest { @@ -46,11 +59,28 @@ public void changesSimpleShapeTypes(ShapeType start, ShapeType dest, boolean suc DocumentationTrait docTrait = new DocumentationTrait("Hi"); ShapeId id = ShapeId.from("smithy.example#Test"); SourceLocation source = new SourceLocation("/foo", 1, 1); - Shape startShape = start.createBuilderForType() - .addTrait(docTrait) - .id(id) - .source(source) - .build(); + Shape startShape; + if (start == ShapeType.ENUM) { + startShape = EnumShape.builder() + .id(id) + .addMember("FOO", "foo") + .addTrait(docTrait) + .source(source) + .build(); + } else if (start == ShapeType.INT_ENUM) { + startShape = IntEnumShape.builder() + .id(id) + .addMember("FOO", 1) + .addTrait(docTrait) + .source(source) + .build(); + } else { + startShape = start.createBuilderForType() + .addTrait(docTrait) + .id(id) + .source(source) + .build(); + } Model model = Model.builder().addShape(startShape).build(); try { @@ -71,7 +101,7 @@ public void changesSimpleShapeTypes(ShapeType start, ShapeType dest, boolean suc private static List simpleTypeTransforms() { Set simpleTypes = new TreeSet<>(); for (ShapeType type : ShapeType.values()) { - if (type.getCategory() == ShapeType.Category.SIMPLE) { + if (type.getCategory() == ShapeType.Category.SIMPLE || type.getCategory() == ShapeType.Category.ENUM) { simpleTypes.add(type); } } @@ -215,4 +245,96 @@ public void cannotConvertUnionToAnythingButStructure() { ModelTransformer.create().changeShapeType(model, MapUtils.of(startShape.getId(), ShapeType.STRING)); }); } + + @Test + public void canConvertStringToEnum() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .name("foo") + .value("bar") + .build()) + .build(); + SourceLocation source = new SourceLocation("/foo", 1, 1); + ShapeId id = ShapeId.fromParts("ns.foo", "bar"); + StringShape startShape = StringShape.builder() + .id(id) + .addTrait(trait) + .source(source) + .build(); + Model model = Model.assembler().addShape(startShape).assemble().unwrap(); + Model result = ModelTransformer.create().changeShapeType(model, MapUtils.of(id, ShapeType.ENUM)); + + assertThat(result.expectShape(id).getType(), Matchers.is(ShapeType.ENUM)); + assertThat(result.expectShape(id).getSourceLocation(), Matchers.equalTo(source)); + assertThat(result.expectShape(id).members(), Matchers.hasSize(1)); + assertThat(result.expectShape(id).members().iterator().next(), Matchers.equalTo(MemberShape.builder() + .id(id.withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build())); + } + + @Test + public void cantConvertBaseStringWithoutEnumTrait() { + SourceLocation source = new SourceLocation("/foo", 1, 1); + ShapeId id = ShapeId.fromParts("ns.foo", "bar"); + StringShape startShape = StringShape.builder() + .id(id) + .source(source) + .build(); + Model model = Model.assembler().addShape(startShape).assemble().unwrap(); + + Assertions.assertThrows(IllegalArgumentException.class, () -> { + ModelTransformer.create().changeShapeType(model, MapUtils.of(startShape.getId(), ShapeType.ENUM)); + }); + } + + @Test + public void cantConvertBaseStringWithNamelessEnumTrait() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("bar") + .build()) + .build(); + SourceLocation source = new SourceLocation("/foo", 1, 1); + ShapeId id = ShapeId.fromParts("ns.foo", "bar"); + StringShape startShape = StringShape.builder() + .id(id) + .addTrait(trait) + .source(source) + .build(); + Model model = Model.assembler().addShape(startShape).assemble().unwrap(); + + Assertions.assertThrows(IllegalArgumentException.class, () -> { + ModelTransformer.create().changeShapeType(model, MapUtils.of(startShape.getId(), ShapeType.ENUM)); + }); + } + + @Test + public void canConvertEnumToString() { + SourceLocation source = new SourceLocation("/foo", 1, 1); + ShapeId id = ShapeId.fromParts("ns.foo", "bar"); + Shape startShape = EnumShape.builder() + .id(id) + .addMember("FOO", "foo") + .source(source) + .build(); + + Model model = Model.assembler().addShape(startShape).assemble().unwrap(); + Model result = ModelTransformer.create().changeShapeType(model, MapUtils.of(id, ShapeType.STRING)); + + assertThat(result.expectShape(id).getType(), Matchers.is(ShapeType.STRING)); + assertThat(result.expectShape(id).getSourceLocation(), Matchers.equalTo(source)); + assertTrue(result.expectShape(id).hasTrait(EnumTrait.ID)); + + EnumTrait trait = result.expectShape(id).expectTrait(EnumTrait.class); + assertFalse(trait instanceof SyntheticEnumTrait); + + assertThat(trait.getValues(), Matchers.equalTo(ListUtils.of( + EnumDefinition.builder() + .name("FOO") + .value("foo") + .build() + ))); + } } From 9071fd3a44678d9a1fb5de0f86cb3ea1e72e1f51 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 14 Feb 2022 16:21:41 +0100 Subject: [PATCH 11/45] Add EnumValidator This adds a validator for the enum shapes that ensures their members are corretcly set, that there's no duplicate values, and so on. In doing this a few changes had to be made in the enum classes themselves. --- .../software/amazon/smithy/model/Model.java | 40 +++++++ .../amazon/smithy/model/shapes/EnumShape.java | 20 ++-- .../smithy/model/shapes/IntEnumShape.java | 6 -- .../validators/EnumShapeValidator.java | 101 ++++++++++++++++++ ...e.amazon.smithy.model.validation.Validator | 1 + .../smithy/model/shapes/EnumShapeTest.java | 13 --- .../smithy/model/shapes/IntEnumShapeTest.java | 25 ----- .../errorfiles/validators/enum-shapes.errors | 7 ++ .../errorfiles/validators/enum-shapes.smithy | 40 +++++++ 9 files changed, 203 insertions(+), 50 deletions(-) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/Model.java b/smithy-model/src/main/java/software/amazon/smithy/model/Model.java index ccfb69368dc..4f063dc607d 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/Model.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/Model.java @@ -40,7 +40,9 @@ import software.amazon.smithy.model.shapes.ByteShape; import software.amazon.smithy.model.shapes.DocumentShape; import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.LongShape; @@ -384,6 +386,25 @@ public Set getIntegerShapesWithTrait(Class trait) return new ShapeTypeFilteredSet<>(getShapesWithTrait(trait), IntegerShape.class); } + /** + * Gets an immutable set of all intEnums in the Model. + * + * @return Returns the Set of {@code intEnum}s. + */ + public Set getIntEnumShapes() { + return toSet(IntEnumShape.class); + } + + /** + * Gets an immutable set of all intEnums in the Model that have a specific trait. + * + * @param trait The exact trait class to look for on shapes. + * @return Returns the set of {@code intEnum}s that have a specific trait. + */ + public Set getIntEnumShapesWithTrait(Class trait) { + return new ShapeTypeFilteredSet<>(getShapesWithTrait(trait), IntEnumShape.class); + } + /** * Gets an immutable set of all lists in the Model. * @@ -574,6 +595,25 @@ public Set getStringShapesWithTrait(Class trait) { return new ShapeTypeFilteredSet<>(getShapesWithTrait(trait), StringShape.class); } + /** + * Gets an immutable set of all enums in the Model. + * + * @return Returns the Set of {@code enum}s. + */ + public Set getEnumShapes() { + return toSet(EnumShape.class); + } + + /** + * Gets an immutable set of all enums in the Model that have a specific trait. + * + * @param trait The exact trait class to look for on shapes. + * @return Returns the set of {@code enum}s that have a specific trait. + */ + public Set getEnumShapesWithTrait(Class trait) { + return new ShapeTypeFilteredSet<>(getShapesWithTrait(trait), EnumShape.class); + } + /** * Gets an immutable set of all structures in the Model. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index e9d59038583..75798c7157b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -155,12 +155,24 @@ public static final class Builder extends StringShape.Builder { @Override public EnumShape build() { + addSyntheticEnumTrait(); + return new EnumShape(this); + } + + private void addSyntheticEnumTrait() { SyntheticEnumTrait.Builder builder = SyntheticEnumTrait.builder(); for (MemberShape member : members.get().values()) { - builder.addEnum(EnumDefinition.fromMember(member)); + try { + builder.addEnum(EnumDefinition.fromMember(member)); + } catch (IllegalStateException e) { + // This can happen if the enum value trait is using something other + // than a string value. Rather than letting the exception propagate + // here, we let the shape validator handle it because it will give + // a much better error. + return; + } } addTrait(builder.build()); - return new EnumShape(this); } @Override @@ -237,10 +249,6 @@ private Builder addMember(MemberShape member, boolean updateEnumTrait) { member = member.toBuilder() .addTrait(EnumValueTrait.builder().stringValue(member.getMemberName()).build()) .build(); - } else if (!member.expectTrait(EnumValueTrait.class).getStringValue().isPresent()) { - throw new SourceException( - "Enum members MUST have the enumValue trait with the `string` member set", - getSourceLocation()); } members.get().put(member.getMemberName(), member); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java index 8d3aefc55a8..d2f50877715 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -171,12 +171,6 @@ public Builder addMember(MemberShape member) { "intEnum members may only target `smithy.api#Unit`, but found `%s`", member.getTarget() ), getSourceLocation()); } - if (!member.hasTrait(EnumValueTrait.ID) - || !member.expectTrait(EnumValueTrait.class).getIntValue().isPresent()) { - throw new SourceException( - "intEnum members MUST have the enumValue trait with the `int` member set", - getSourceLocation()); - } members.get().put(member.getMemberName(), member); return this; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java new file mode 100644 index 00000000000..a42c83201af --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java @@ -0,0 +1,101 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.validation.validators; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.validation.AbstractValidator; +import software.amazon.smithy.model.validation.ValidationEvent; + +public class EnumShapeValidator extends AbstractValidator { + private static final Pattern RECOMMENDED_NAME_PATTERN = Pattern.compile("^[A-Z]+[A-Z_0-9]*$"); + + @Override + public List validate(Model model) { + List events = new ArrayList<>(); + + for (EnumShape shape : model.getEnumShapes()) { + validateEnumShape(events, shape); + } + + for (IntEnumShape shape : model.getIntEnumShapes()) { + validateIntEnumShape(events, shape); + } + + return events; + } + + private void validateEnumShape(List events, EnumShape shape) { + Set values = new HashSet<>(); + for (MemberShape member : shape.members()) { + Optional value = member.expectTrait(EnumValueTrait.class).getStringValue(); + if (!value.isPresent()) { + events.add(error(member, member.expectTrait(EnumValueTrait.class), + "The enumValue trait may only use the string option when applied to enum shapes.")); + } else if (!values.add(value.get())) { + events.add(error(member, String.format( + "Multiple enum members found with duplicate value `%s`", + value.get() + ))); + } + validateEnumMemberName(events, member); + } + } + + private void validateIntEnumShape(List events, IntEnumShape shape) { + Set values = new HashSet<>(); + for (MemberShape member : shape.members()) { + if (!member.hasTrait(EnumValueTrait.ID)) { + events.add(missingIntEnumValue(member, member)); + } else if (!member.expectTrait(EnumValueTrait.class).getIntValue().isPresent()) { + events.add(missingIntEnumValue(member, member.expectTrait(EnumValueTrait.class))); + } else { + int value = member.expectTrait(EnumValueTrait.class).getIntValue().get(); + if (!values.add(value)) { + events.add(error(member, String.format( + "Multiple enum members found with duplicate value `%s`", + value + ))); + } + } + validateEnumMemberName(events, member); + } + } + + private ValidationEvent missingIntEnumValue(Shape shape, FromSourceLocation sourceLocation) { + return error(shape, sourceLocation, "intEnum members must have the enumValue trait with the `int` member set"); + } + + private void validateEnumMemberName(List events, MemberShape member) { + if (!RECOMMENDED_NAME_PATTERN.matcher(member.getMemberName()).find()) { + events.add(warning(member, String.format( + "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.", + member.getMemberName()))); + } + } +} diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator index 23691265e64..c9e6d250cee 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator @@ -1,6 +1,7 @@ software.amazon.smithy.model.validation.validators.AuthTraitValidator software.amazon.smithy.model.validation.validators.DefaultValueInUpdateValidator software.amazon.smithy.model.validation.validators.DeprecatedTraitValidator +software.amazon.smithy.model.validation.validators.EnumShapeValidator software.amazon.smithy.model.validation.validators.EnumTraitValidator software.amazon.smithy.model.validation.validators.EventPayloadTraitValidator software.amazon.smithy.model.validation.validators.ExamplesTraitValidator diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index 909de8c5c51..736236fb961 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -254,19 +254,6 @@ public void clearMembers() { )); } - @Test - public void membersMustHaveEnumValueWithStringSet() { - EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); - MemberShape member = MemberShape.builder() - .id("ns.foo#bar$foo") - .target(UnitTypeTrait.UNIT) - .addTrait(EnumValueTrait.builder().intValue(1).build()) - .build(); - Assertions.assertThrows(SourceException.class, () -> { - builder.addMember(member); - }); - } - @Test public void membersMustTargetUnit() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java index 469e86a26a7..e7d0f5c75ea 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java @@ -129,31 +129,6 @@ public void idMustBeSetFirst() { }); } - @Test - public void membersMustHaveEnumValue() { - IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); - MemberShape member = MemberShape.builder() - .id("ns.foo#bar$foo") - .target(UnitTypeTrait.UNIT) - .build(); - Assertions.assertThrows(SourceException.class, () -> { - builder.addMember(member); - }); - } - - @Test - public void membersMustHaveEnumValueWithIntSet() { - IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); - MemberShape member = MemberShape.builder() - .id("ns.foo#bar$foo") - .target(UnitTypeTrait.UNIT) - .addTrait(EnumValueTrait.builder().stringValue("bar").build()) - .build(); - Assertions.assertThrows(SourceException.class, () -> { - builder.addMember(member); - }); - } - @Test public void membersMustTargetUnit() { IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors new file mode 100644 index 00000000000..1ea3db2c8cd --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors @@ -0,0 +1,7 @@ +[ERROR] ns.foo#StringEnum$INT_VALUE: The enumValue trait may only use the string option when applied to enum shapes. | EnumShape +[ERROR] ns.foo#StringEnum$DUPLICATE_VALUE: Multiple enum members found with duplicate value `explicit` | EnumShape +[WARNING] ns.foo#StringEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape +[ERROR] ns.foo#IntEnum$IMPLICIT_VALUE: intEnum members must have the enumValue trait with the `int` member set | EnumShape +[ERROR] ns.foo#IntEnum$STRING_VALUE: intEnum members must have the enumValue trait with the `int` member set | EnumShape +[ERROR] ns.foo#IntEnum$DUPLICATE_VALUE: Multiple enum members found with duplicate value `1` | EnumShape +[WARNING] ns.foo#IntEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy new file mode 100644 index 00000000000..139561c94f7 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy @@ -0,0 +1,40 @@ +$version: "2.0" + +namespace ns.foo + +enum StringEnum { + IMPLICIT_VALUE + + @enumValue(string: "explicit") + EXPLICIT_VALUE + + @enumValue(string: "") + DEFAULT_VALUE + + @enumValue(int: 1) + INT_VALUE + + @enumValue(string: "explicit") + DUPLICATE_VALUE + + undesirableName +} + +intEnum IntEnum { + IMPLICIT_VALUE + + @enumValue(int: 1) + EXPLICIT_VALUE + + @enumValue(int: 0) + DEFAULT_VALUE + + @enumValue(string: "foo") + STRING_VALUE + + @enumValue(int: 1) + DUPLICATE_VALUE + + @enumValue(int: 99) + undesirableName +} From 4b3ba2ad3f210a3677b5da38a320526c895b5e08 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 14 Feb 2022 16:55:58 +0100 Subject: [PATCH 12/45] Add a simple transformer from string to enum --- .../ChangeStringEnumsToEnumShapes.java | 32 +++++++ ....amazon.smithy.build.ProjectionTransformer | 2 + .../ChangeStringEnumsToEnumShapesTest.java | 85 +++++++++++++++++++ .../model/transform/ChangeShapeType.java | 12 +++ .../model/transform/ModelTransformer.java | 12 +++ .../model/transform/ChangeShapeTypeTest.java | 44 ++++++++++ 6 files changed, 187 insertions(+) create mode 100644 smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapes.java create mode 100644 smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapes.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapes.java new file mode 100644 index 00000000000..66d2deeb89e --- /dev/null +++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapes.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.build.transforms; + +import software.amazon.smithy.build.ProjectionTransformer; +import software.amazon.smithy.build.TransformContext; +import software.amazon.smithy.model.Model; + +public final class ChangeStringEnumsToEnumShapes implements ProjectionTransformer { + @Override + public String getName() { + return "changeStringEnumsToEnumShapes"; + } + + @Override + public Model transform(TransformContext context) { + return context.getTransformer().changeStringEnumsToEnumShapes(context.getModel()); + } +} diff --git a/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer b/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer index bca84cd9797..df49430f02c 100644 --- a/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer +++ b/smithy-build/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer @@ -1,4 +1,6 @@ software.amazon.smithy.build.transforms.Apply +software.amazon.smithy.build.transforms.ChangeStringEnumsToEnumShapes +software.amazon.smithy.build.transforms.ChangeTypes software.amazon.smithy.build.transforms.ExcludeMetadata software.amazon.smithy.build.transforms.ExcludeShapesByTag software.amazon.smithy.build.transforms.ExcludeShapesByTrait diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java new file mode 100644 index 00000000000..6ea5045fb41 --- /dev/null +++ b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.build.transforms; + +import static org.hamcrest.MatcherAssert.assertThat; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.TransformContext; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.traits.UnitTypeTrait; + +public class ChangeStringEnumsToEnumShapesTest { + + + @Test + public void canFindEnumsToConvert() { + EnumTrait compatibleTrait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .name("foo") + .value("bar") + .build()) + .build(); + ShapeId compatibleStringId = ShapeId.fromParts("ns.foo", "CompatibleString"); + StringShape compatibleString = StringShape.builder() + .id(compatibleStringId) + .addTrait(compatibleTrait) + .build(); + + EnumTrait incompatibleTrait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("bar") + .build()) + .build(); + ShapeId incompatibleStringId = ShapeId.fromParts("ns.foo", "IncompatibleString"); + StringShape incompatibleString = StringShape.builder() + .id(incompatibleStringId) + .addTrait(incompatibleTrait) + .build(); + + Model model = Model.assembler() + .addShape(compatibleString) + .addShape(incompatibleString) + .assemble().unwrap(); + + TransformContext context = TransformContext.builder() + .model(model) + .settings(Node.objectNode().withMember("changeStringEnumsToEnumShapes", Node.objectNode())) + .build(); + + Model result = new ChangeStringEnumsToEnumShapes().transform(context); + + assertThat(result.expectShape(compatibleStringId).getType(), Matchers.is(ShapeType.ENUM)); + assertThat(result.expectShape(compatibleStringId).members(), Matchers.hasSize(1)); + assertThat(result.expectShape(compatibleStringId).members().iterator().next(), Matchers.equalTo(MemberShape.builder() + .id(compatibleStringId.withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build())); + + assertThat(result.expectShape(incompatibleStringId).getType(), Matchers.is(ShapeType.STRING)); + assertThat(result.expectShape(incompatibleStringId).members(), Matchers.hasSize(0)); + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java index 6263eaad9f4..d3d5dd24c67 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java @@ -15,6 +15,7 @@ package software.amazon.smithy.model.transform; +import java.util.HashMap; import java.util.Map; import java.util.Optional; import software.amazon.smithy.model.Model; @@ -54,6 +55,17 @@ final class ChangeShapeType { this.shapeToType = shapeToType; } + static ChangeShapeType upgradeEnums(Model model) { + Map toUpdate = new HashMap<>(); + for (StringShape shape: model.getStringShapesWithTrait(EnumTrait.class)) { + EnumTrait trait = shape.expectTrait(EnumTrait.class); + if (trait.getValues().iterator().next().getName().isPresent()) { + toUpdate.put(shape.getId(), ShapeType.ENUM); + } + } + return new ChangeShapeType(toUpdate); + } + Model transform(ModelTransformer transformer, Model model) { return transformer.mapShapes(model, shape -> { if (shapeToType.containsKey(shape.getId())) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java index 74e483a5473..dd0ed4edbc3 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java @@ -481,6 +481,18 @@ public Model changeShapeType(Model model, Map shapeToType) { return new ChangeShapeType(shapeToType).transform(this, model); } + /** + * Changes each compatible string shape with the enum trait to an enum shape. + * + *

Strings with enum traits that don't define names are not converted. + * + * @param model Model to transform. + * @return Returns the transformed model. + */ + public Model changeStringEnumsToEnumShapes(Model model) { + return ChangeShapeType.upgradeEnums(model).transform(this, model); + } + /** * Copies the errors defined on the given service onto each operation bound to the * service, effectively flattening service error inheritance. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java index 7b7cfd6b9ca..84f7e9db4ff 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java @@ -337,4 +337,48 @@ public void canConvertEnumToString() { .build() ))); } + + @Test + public void canFindEnumsToConvert() { + EnumTrait compatibleTrait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .name("foo") + .value("bar") + .build()) + .build(); + ShapeId compatibleStringId = ShapeId.fromParts("ns.foo", "CompatibleString"); + StringShape compatibleString = StringShape.builder() + .id(compatibleStringId) + .addTrait(compatibleTrait) + .build(); + + EnumTrait incompatibleTrait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("bar") + .build()) + .build(); + ShapeId incompatibleStringId = ShapeId.fromParts("ns.foo", "IncompatibleString"); + StringShape incompatibleString = StringShape.builder() + .id(incompatibleStringId) + .addTrait(incompatibleTrait) + .build(); + + + Model model = Model.assembler() + .addShape(compatibleString) + .addShape(incompatibleString) + .assemble().unwrap(); + Model result = ModelTransformer.create().changeStringEnumsToEnumShapes(model); + + assertThat(result.expectShape(compatibleStringId).getType(), Matchers.is(ShapeType.ENUM)); + assertThat(result.expectShape(compatibleStringId).members(), Matchers.hasSize(1)); + assertThat(result.expectShape(compatibleStringId).members().iterator().next(), Matchers.equalTo(MemberShape.builder() + .id(compatibleStringId.withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .build())); + + assertThat(result.expectShape(incompatibleStringId).getType(), Matchers.is(ShapeType.STRING)); + assertThat(result.expectShape(incompatibleStringId).members(), Matchers.hasSize(0)); + } } From d1606487c280e64c1ed14d19cd997f9bbdb1e87b Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 14 Feb 2022 18:12:12 +0100 Subject: [PATCH 13/45] Let model assembler set shapes version --- .../model/loader/FullyResolvedModelFile.java | 21 +++++++++++++++++-- .../smithy/model/loader/ModelAssembler.java | 17 ++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/FullyResolvedModelFile.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/FullyResolvedModelFile.java index dc4e300ba81..c4a0ef37e1b 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/FullyResolvedModelFile.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/FullyResolvedModelFile.java @@ -52,8 +52,25 @@ final class FullyResolvedModelFile extends AbstractMutableModelFile { * @return Returns the create {@code FullyResolvedModelFile} containing the shapes. */ static FullyResolvedModelFile fromShapes(TraitFactory traitFactory, Collection shapes) { - FullyResolvedModelFile modelFile = new FullyResolvedModelFile(SourceLocation.none().getFilename(), - traitFactory); + return fromShapes(traitFactory, shapes, null); + } + + + /** + * Create a {@code FullyResolvedModelFile} from already built shapes. + * + * @param traitFactory Factory used to create traits when merging traits. + * @param shapes Shapes to convert into builders and treat as a ModelFile. + * @return Returns the create {@code FullyResolvedModelFile} containing the shapes. + */ + static FullyResolvedModelFile fromShapes(TraitFactory traitFactory, Collection shapes, Version version) { + FullyResolvedModelFile modelFile = new FullyResolvedModelFile( + SourceLocation.none().getFilename(), traitFactory); + + if (version != null) { + modelFile.setVersion(version); + } + for (Shape shape : shapes) { // Convert the shape to a builder and remove all the traits. // These traits are added to the trait container so that they diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java index e49bf3275d1..6ffd2e475e0 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java @@ -105,6 +105,7 @@ public final class ModelAssembler { private final Map properties = new HashMap<>(); private boolean disablePrelude; private Consumer validationEventListener = DEFAULT_EVENT_LISTENER; + private Version parsedShapesVersion; // Lazy initialization holder class idiom to hold a default trait factory. static final class LazyTraitFactoryHolder { @@ -367,6 +368,18 @@ public ModelAssembler addShapes(Shape... shapes) { return this; } + /** + * Sets the Smithy version to use for parsed shapes added directly to the + * assembler. + * + * @param version A Smithy IDL version. + * @return Returns the assembler. + */ + public ModelAssembler setParsedShapesVersion(String version) { + this.parsedShapesVersion = Version.fromString(version); + return this; + } + /** * Explicitly adds a trait to a shape in the assembled model. * @@ -617,7 +630,9 @@ private List createModelFiles() { } // A modelFile is created for the assembler to capture anything that was manually added. - FullyResolvedModelFile assemblerModelFile = FullyResolvedModelFile.fromShapes(traitFactory, shapes); + FullyResolvedModelFile assemblerModelFile = FullyResolvedModelFile.fromShapes( + traitFactory, shapes, parsedShapesVersion); + modelFiles.add(assemblerModelFile); metadata.forEach(assemblerModelFile::putMetadata); for (Pair pendingTrait : pendingTraits) { From 031a3d2932ca645e6734120fef38f986f82fb21c Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 14 Feb 2022 18:13:02 +0100 Subject: [PATCH 14/45] Restrict enum shapes to 2.0 --- .../model/loader/AbstractMutableModelFile.java | 7 +++++++ .../amazon/smithy/model/loader/Version.java | 13 ++++++------- .../smithy/model/transform/ChangeShapeTypeTest.java | 7 +++++-- .../model/loader/invalid/idl-enums-in-v1.smithy | 9 +++++++++ .../model/loader/invalid/idl-int-enums-in-v1.smithy | 12 ++++++++++++ 5 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-enums-in-v1.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-int-enums-in-v1.smithy diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java index 5862ce8dd72..833ac65e2ad 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java @@ -83,6 +83,13 @@ public final Version getVersion() { void onShape(AbstractShapeBuilder builder) { allShapeIds.add(builder.getId()); + if ((builder.getShapeType() == ShapeType.ENUM || builder.getShapeType() == ShapeType.INT_ENUM) + && !getVersion().supportsEnumShapes()) { + throw new SourceException(String.format( + "%s shapes may only be used with Smithy version 2 or later.", builder.getShapeType().toString()), + builder.getSourceLocation()); + } + if (builder instanceof MemberShape.Builder) { String memberName = builder.getId().getMember().get(); ShapeId containerId = builder.getId().withoutMember(); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java index 9db110c9b79..a0f9d0aa570 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java @@ -70,21 +70,20 @@ boolean supportsMixins() { } /** - * Returns true if this version of the IDL supports using "!" as - * syntactic sugar for applying the required trait. + * Checks if this version of the IDL supports inlined operation IO shapes. * - * @return Returns true if this version of the IDL supports "!" sugar. + * @return Returns true if this version supports inlined operation IO shapes. */ - boolean supportsRequiredSugar() { + boolean supportsInlineOperationIO() { return this == VERSION_2_0; } /** - * Checks if this version of the IDL supports inlined operation IO shapes. + * Checks if this version supports enum and intEnum shapes. * - * @return Returns true if this version supports inlined operation IO shapes. + * @return Returns true if this version supports the enum and intEnum shapes. */ - boolean supportsInlineOperationIO() { + boolean supportsEnumShapes() { return this == VERSION_2_0; } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java index 84f7e9db4ff..79ffc7e4169 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.TreeSet; import org.hamcrest.Matchers; @@ -320,7 +319,11 @@ public void canConvertEnumToString() { .source(source) .build(); - Model model = Model.assembler().addShape(startShape).assemble().unwrap(); + Model model = Model.assembler() + .setParsedShapesVersion("2.0") + .addShape(startShape) + .assemble() + .unwrap(); Model result = ModelTransformer.create().changeShapeType(model, MapUtils.of(id, ShapeType.STRING)); assertThat(result.expectShape(id).getType(), Matchers.is(ShapeType.STRING)); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-enums-in-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-enums-in-v1.smithy new file mode 100644 index 00000000000..b13d74974c2 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-enums-in-v1.smithy @@ -0,0 +1,9 @@ +// enum shapes may only be used with Smithy version 2 or later. +$version: "1.0" + +namespace ns.foo + +enum Enum { + FOO + BAR +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-int-enums-in-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-int-enums-in-v1.smithy new file mode 100644 index 00000000000..812147c74eb --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-int-enums-in-v1.smithy @@ -0,0 +1,12 @@ +// intEnum shapes may only be used with Smithy version 2 or later. +$version: "1.0" + +namespace ns.foo + +intEnum IntEnum { + @enumValue(int: 1) + FOO + + @enumValue(int: 2) + BAR +} From 5edfb1769c1a648934a3196c25e81846f33ee65c Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 14 Feb 2022 19:12:13 +0100 Subject: [PATCH 15/45] Make enums be selectable by parent types --- .../software/amazon/smithy/model/Model.java | 2 +- .../model/selector/ShapeTypeSelector.java | 4 +- .../model/selector/ShapeTypeSelectorTest.java | 53 +++++++++++++++++++ .../model/selector/shape-type-test.smithy | 16 ++++++ 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeSelectorTest.java create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/selector/shape-type-test.smithy diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/Model.java b/smithy-model/src/main/java/software/amazon/smithy/model/Model.java index 4f063dc607d..fb723882c64 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/Model.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/Model.java @@ -772,7 +772,7 @@ public Set toSet(Class shapeType) { return (Set) cachedTypes.computeIfAbsent(shapeType, t -> { Set result = new HashSet<>(); for (Shape shape : shapeMap.values()) { - if (shape.getClass() == shapeType) { + if (shapeType.isAssignableFrom(shape.getClass())) { result.add((T) shape); } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java index 228f7f5962a..aa37c7dae40 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java @@ -31,7 +31,9 @@ final class ShapeTypeSelector implements InternalSelector { @Override public boolean push(Context ctx, Shape shape, Receiver next) { - if (shape.getType() == shapeType) { + if (shape.getType() == shapeType + || (shapeType == ShapeType.STRING && shape.getType() == ShapeType.ENUM) + || (shapeType == ShapeType.INTEGER && shape.getType() == ShapeType.INT_ENUM)) { return next.apply(ctx, shape); } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeSelectorTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeSelectorTest.java new file mode 100644 index 00000000000..01637972259 --- /dev/null +++ b/smithy-model/src/test/java/software/amazon/smithy/model/selector/ShapeTypeSelectorTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.selector; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.in; + +import java.util.Set; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; + +public class ShapeTypeSelectorTest { + + private static Model model; + + @BeforeAll + public static void before() { + model = Model.assembler() + .addImport(SelectorTest.class.getResource("shape-type-test.smithy")) + .assemble() + .unwrap(); + } + + @Test + public void stringSelectsEnum() { + Set ids = SelectorTest.ids(model, "string"); + assertThat("smithy.example#String", in(ids)); + assertThat("smithy.example#Enum", in(ids)); + } + + @Test + public void integerSelectsIntEnum() { + Set ids = SelectorTest.ids(model, "integer"); + assertThat("smithy.example#Integer", in(ids)); + assertThat("smithy.example#IntEnum", in(ids)); + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/shape-type-test.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/shape-type-test.smithy new file mode 100644 index 00000000000..faede0bb4e4 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/shape-type-test.smithy @@ -0,0 +1,16 @@ +$version: "2.0" + +namespace smithy.example + +enum Enum { + FOO +} + +string String + +intEnum IntEnum { + @enumValue(int: 1) + FOO +} + +integer Integer From deccfe7b4f24960b3ed891f8c0051b843c7fce68 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 14 Feb 2022 19:25:45 +0100 Subject: [PATCH 16/45] Add unit test for toSet with subclassing --- .../java/software/amazon/smithy/model/ModelTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/ModelTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/ModelTest.java index 786443ee0d7..0a780d5fd03 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/ModelTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/ModelTest.java @@ -38,6 +38,7 @@ import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.node.ExpectationNotMetException; import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.EnumShape; import software.amazon.smithy.model.shapes.IntegerShape; import software.amazon.smithy.model.shapes.ListShape; import software.amazon.smithy.model.shapes.Shape; @@ -205,6 +206,16 @@ public void convertsToSet() { assertThat(model.toSet(), containsInAnyOrder(a, b)); } + @Test + public void toSetWithTypeRespectsSubclassing() { + StringShape a = StringShape.builder().id("ns.foo#a").build(); + EnumShape.Builder enumBuilder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#b"); + EnumShape b = enumBuilder.addMember("FOO", "foo").build(); + Model model = Model.builder().addShapes(a, b).build(); + + assertThat(model.toSet(StringShape.class), containsInAnyOrder(a, b)); + } + @Test public void addsMembersAutomatically() { StringShape string = StringShape.builder().id("ns.foo#a").build(); From 6e4208f75b8ccec078fdcef34bbeea2ee05d53af Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 15 Feb 2022 17:28:19 +0100 Subject: [PATCH 17/45] Deprecate enum trait This deprecates the enum trait and updates all the internal enums to be enum shapes. It also fixes a few bugs along the way. --- .../META-INF/smithy/aws.apigateway.json | 147 +++++++++------ .../META-INF/smithy/aws.cloudformation.smithy | 82 ++++----- .../resources/META-INF/smithy/aws.iam.json | 170 +++++++++++++++--- .../model/aws-config.smithy | 50 +++--- .../services/machinelearning.smithy | 18 +- .../restJson1/http-string-payload.smithy | 6 +- .../restJson1/services/apigateway.smithy | 36 ++-- .../validation/malformed-enum.smithy | 9 +- .../validation/recursive-structures.smithy | 9 +- .../model/restXml/services/s3.smithy | 76 +++----- .../model/shared-types.smithy | 39 ++-- .../ProtocolHttpPayloadValidator.java | 3 +- .../resources/META-INF/smithy/aws.api.json | 76 +++++--- .../META-INF/smithy/aws.protocols.json | 54 ++++-- .../http-checksum-header-conflicts.smithy | 37 ++-- .../http-checksum-member-enums.smithy | 63 ++----- .../errorfiles/http-checksum-trait.smithy | 37 +--- .../smithy/model/loader/ModelAssembler.java | 6 +- .../amazon/smithy/model/loader/prelude.smithy | 95 +++++----- .../enum-can-be-deprecated-and-tagged.errors | 1 + .../validators/enum-trait-validation.errors | 7 + .../mqtt/traits/errorfiles/job-service.smithy | 112 ++++-------- .../META-INF/smithy/smithy.test.smithy | 22 +-- .../traits/test-with-appliesto.smithy | 1 + .../resources/META-INF/smithy/waiters.smithy | 80 ++++----- 25 files changed, 629 insertions(+), 607 deletions(-) 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 ef0e05fc8b1..c6279734a57 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 @@ -216,26 +216,40 @@ } }, "aws.apigateway#IntegrationType": { - "type": "string", - "traits": { - "smithy.api#enum": [ - { - "value": "aws", - "name": "AWS" - }, - { - "value": "aws_proxy", - "name": "AWS_PROXY" - }, - { - "value": "http", - "name": "HTTP" - }, - { - "value": "http_proxy", - "name": "HTTP_PROXY" + "type": "enum", + "members": { + "AWS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "aws" + } + } + }, + "AWS_PROXY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "aws_proxy" + } + } + }, + "HTTP": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "http" + } + } + }, + "HTTP_PROXY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "http_proxy" + } } - ] + } } }, "aws.apigateway#IamRoleArn": { @@ -283,50 +297,81 @@ } }, "aws.apigateway#ConnectionType": { - "type": "string", - "traits": { - "smithy.api#private": {}, - "smithy.api#enum": [ - { - "name": "INTERNET", - "value": "INTERNET" - }, - { - "name": "VPC_LINK", - "value": "VPC_LINK" + "type": "enum", + "members": { + "INTERNET": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "INTERNET" + } } - ] + }, + "VPC_LINK": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "VPC_LINK" + } + } + } } }, "aws.apigateway#PassThroughBehavior": { - "type": "string", + "type": "enum", + "members": { + "WHEN_NO_TEMPLATES": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "when_no_templates" + } + } + }, + "WHEN_NO_MATCH": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "when_no_match" + } + } + }, + "NEVER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "never" + } + } + } + }, "traits": { "smithy.api#documentation": "Defines the passThroughBehavior for the integration", - "smithy.api#enum": [ - { - "value": "when_no_templates", - "name": "WHEN_NO_TEMPLATES" - }, - { - "value": "when_no_match", - "name": "WHEN_NO_MATCH" - }, - { - "value": "never", - "name": "NEVER" - } - ], "smithy.api#private": {} } }, "aws.apigateway#ContentHandling": { - "type": "string", + "type": "enum", + "members": { + "CONVERT_TO_TEXT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "CONVERT_TO_TEXT" + } + } + }, + "CONVERT_TO_BINARY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "CONVERT_TO_BINARY" + } + } + } + }, "traits": { "smithy.api#documentation": "Defines the contentHandling for the integration", - "smithy.api#enum": [ - {"value": "CONVERT_TO_TEXT", "name": "CONVERT_TO_TEXT"}, - {"value": "CONVERT_TO_BINARY", "name": "CONVERT_TO_BINARY"} - ], "smithy.api#private": {} } }, diff --git a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy index 3cdeb65b2c9..e35175096fb 100644 --- a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy +++ b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy @@ -39,52 +39,42 @@ structure cfnExcludeProperty {} selector: "structure > member", conflicts: [cfnExcludeProperty] ) -@enum([ - { - value: "full", - name: "FULL", - documentation: """ - Indicates that the CloudFormation property generated from this - member does not have any mutability restrictions, meaning that it - can be specified by the user and returned in a `read` or `list` - request.""", - }, - { - value: "create-and-read", - name: "CREATE_AND_READ", - documentation: """ - Indicates that the CloudFormation property generated from this - member can be specified only during resource creation and can be - returned in a `read` or `list` request.""", - }, - { - value: "create", - name: "CREATE", - documentation: """ - Indicates that the CloudFormation property generated from this - member can be specified only during resource creation and cannot - be returned in a `read` or `list` request. MUST NOT be set if the - member is also marked with the `@additionalIdentifier` trait.""", - }, - { - value: "read", - name: "READ", - documentation: """ - Indicates that the CloudFormation property generated from this - member can be returned by a `read` or `list` request, but - cannot be set by the user.""", - }, - { - value: "write", - name: "WRITE", - documentation: """ - Indicates that the CloudFormation property generated from this - member can be specified by the user, but cannot be returned by a - `read` or `list` request. MUST NOT be set if the member is also - marked with the `@additionalIdentifier` trait.""", - } -]) -string cfnMutability +enum cfnMutability { + /// Indicates that the CloudFormation property generated from this + /// member does not have any mutability restrictions, meaning that it + /// can be specified by the user and returned in a `read` or `list` + /// request. + @enumValue(string: "full") + FULL + + /// Indicates that the CloudFormation property generated from this + /// member can be specified only during resource creation and can be + /// returned in a `read` or `list` request. + @enumValue(string: "create-and-read") + CREATE_AND_READ + + /// Indicates that the CloudFormation property generated from this + /// member can be specified only during resource creation and cannot + /// be returned in a `read` or `list` request. MUST NOT be set if the + /// member is also marked with the `@additionalIdentifier` trait. + @enumValue(string: "create") + CREATE + + + /// Indicates that the CloudFormation property generated from this + /// member can be returned by a `read` or `list` request, but + /// cannot be set by the user. + @enumValue(string: "read") + READ + + + /// Indicates that the CloudFormation property generated from this + /// member can be specified by the user, but cannot be returned by a + /// `read` or `list` request. MUST NOT be set if the member is also + /// marked with the `@additionalIdentifier` trait. + @enumValue(string: "write") + WRITE +} /// Indicates that a Smithy resource is a CloudFormation resource. @unstable 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 ca5222ad5f6..2e25b1efb16 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 @@ -122,39 +122,157 @@ } }, "aws.iam#ConditionKeyType": { - "type": "string", + "type": "enum", + "members": { + "ARN": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "ARN" + } + } + }, + "ARRAY_OF_ARN": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "ArrayOfARN" + } + } + }, + "BINARY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "Binary" + } + } + }, + "ARRAY_OF_BINARY": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "ArrayOfBinary" + } + } + }, + "STRING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "String" + } + } + }, + "ARRAY_OF_STRING": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "ArrayOfString" + } + } + }, + "NUMERIC": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "Numeric" + } + } + }, + "DATE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "Date" + } + } + }, + "ARRAY_OF_DATE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "ArrayOfDate" + } + } + }, + "BOOL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "Bool" + } + } + }, + "ARRAY_OF_BOOL": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "ArrayOfBool" + } + } + }, + "IP_ADDRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "IPAddress" + } + } + }, + "ARRAY_OF_IP_ADDRESS": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "ArrayOfIPAddress" + } + } + } + }, "traits": { "smithy.api#private": {}, - "smithy.api#documentation": "The IAM policy type of the value that will supplied for this context key", - "smithy.api#enum": [ - {"value": "ARN", "name": "ARN"}, - {"value": "ArrayOfARN", "name": "ARRAY_OF_ARN"}, - {"value": "Binary", "name": "BINARY"}, - {"value": "ArrayOfBinary", "name": "ARRAY_OF_BINARY"}, - {"value": "String", "name": "STRING"}, - {"value": "ArrayOfString", "name": "ARRAY_OF_STRING"}, - {"value": "Numeric", "name": "NUMERIC"}, - {"value": "ArrayOfNumeric", "name": "ARRAY_OF_NUMERIC"}, - {"value": "Date", "name": "DATE"}, - {"value": "ArrayOfDate", "name": "ARRAY_OF_DATE"}, - {"value": "Bool", "name": "BOOL"}, - {"value": "ArrayOfBool", "name": "ARRAY_OF_BOOL"}, - {"value": "IPAddress", "name": "IP_ADDRESS"}, - {"value": "ArrayOfIPAddress", "name": "ARRAY_OF_IP_ADDRESS"} - ] + "smithy.api#documentation": "The IAM policy type of the value that will supplied for this context key" } }, "aws.iam#PrincipalType": { - "type": "string", + "type": "enum", + "members": { + "ROOT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "Root" + } + } + }, + "IAM_USER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "IAMUser" + } + } + }, + "IAM_ROLE": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "IAMRole" + } + } + }, + "FEDERATED_USER": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "FederatedUser" + } + } + } + }, "traits": { "smithy.api#private": {}, - "smithy.api#documentation": "An IAM policy principal type.", - "smithy.api#enum": [ - {"value": "Root", "name": "ROOT"}, - {"value": "IAMUser", "name": "IAM_USER"}, - {"value": "IAMRole", "name": "IAM_ROLE"}, - {"value": "FederatedUser", "name": "FEDERATED_USER"} - ] + "smithy.api#documentation": "An IAM policy principal type." } } } diff --git a/smithy-aws-protocol-tests/model/aws-config.smithy b/smithy-aws-protocol-tests/model/aws-config.smithy index 1f4ce8ac830..9dbbf43ebae 100644 --- a/smithy-aws-protocol-tests/model/aws-config.smithy +++ b/smithy-aws-protocol-tests/model/aws-config.smithy @@ -97,35 +97,25 @@ structure RetryConfig { } /// Controls the S3 addressing bucket style. -@enum([ - { - value: "auto", - name: "AUTO", - }, - { - value: "path", - name: "PATH", - }, - { - value: "virtual", - name: "VIRTUAL", - } -]) -string S3AddressingStyle +enum S3AddressingStyle { + @enumValue(string: "auto") + AUTO + + @enumValue(string: "path") + PATH + + @enumValue(string: "virtual") + VIRTUAL +} /// Controls the strategy used for retries. -@enum([ - { - value: "legacy", - name: "LEGACY", - }, - { - value: "standard", - name: "STANDARD", - }, - { - value: "adaptive", - name: "ADAPTIVE", - } -]) -string RetryMode +enum RetryMode { + @enumValue(string: "legacy") + LEGACY + + @enumValue(string: "standard") + STANDARD + + @enumValue(string: "adaptive") + ADAPTIVE +} diff --git a/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy b/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy index b03f5bfb416..d11c13fec42 100644 --- a/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy +++ b/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy @@ -135,17 +135,13 @@ map ScoreValuePerLabelMap { value: ScoreValue, } -@enum([ - { - value: "PredictiveModelType", - name: "PREDICTIVE_MODEL_TYPE", - }, - { - value: "Algorithm", - name: "ALGORITHM", - }, -]) -string DetailsAttributes +enum DetailsAttributes { + @enumValue(string: "PredictiveModelType") + PREDICTIVE_MODEL_TYPE + + @enumValue(string: "Algorithm") + ALGORITHM +} @length( min: 1, diff --git a/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy b/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy index 176949e4228..c3280440838 100644 --- a/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy @@ -35,8 +35,10 @@ structure EnumPayloadInput { payload: StringEnum } -@enum([{"value": "enumvalue", "name": "V"}]) -string StringEnum +enum StringEnum { + @enumValue(string: "enumvalue") + V +} @http(uri: "/StringPayload", method: "POST") @httpRequestTests([ diff --git a/smithy-aws-protocol-tests/model/restJson1/services/apigateway.smithy b/smithy-aws-protocol-tests/model/restJson1/services/apigateway.smithy index 0ab1d47a90e..4366e6c68a3 100644 --- a/smithy-aws-protocol-tests/model/restJson1/services/apigateway.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/services/apigateway.smithy @@ -137,34 +137,18 @@ map MapOfStringToString { value: String, } -@enum([ - { - value: "HEADER", - name: "HEADER", - }, - { - value: "AUTHORIZER", - name: "AUTHORIZER", - }, -]) -string ApiKeySourceType +enum ApiKeySourceType { + HEADER + AUTHORIZER +} boolean Boolean -@enum([ - { - value: "REGIONAL", - name: "REGIONAL", - }, - { - value: "EDGE", - name: "EDGE", - }, - { - value: "PRIVATE", - name: "PRIVATE", - }, -]) -string EndpointType + +enum EndpointType { + REGIONAL + EDGE + PRIVATE +} @box integer NullableInteger diff --git a/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy b/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy index 4d278d5bac9..e73bfd62c4e 100644 --- a/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy @@ -191,8 +191,13 @@ structure MalformedEnumInput { union: EnumUnion } -@enum([{value: "abc", name: "ABC"}, {value: "def", name: "DEF"}]) -string EnumString +enum EnumString { + @enumValue(string: "abc") + ABC + + @enumValue(string: "def") + DEF +} list EnumList { member: EnumString diff --git a/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy b/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy index 81d4ffe4a7a..eb2c0556d5c 100644 --- a/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy @@ -85,8 +85,13 @@ structure RecursiveStructuresInput { union: RecursiveUnionOne } -@enum([{value: "abc", name: "ABC"}, {value: "def", name: "DEF"}]) -string RecursiveEnumString +enum RecursiveEnumString { + @enumValue(string: "abc") + ABC + + @enumValue(string: "def") + DEF +} union RecursiveUnionOne { string: RecursiveEnumString, diff --git a/smithy-aws-protocol-tests/model/restXml/services/s3.smithy b/smithy-aws-protocol-tests/model/restXml/services/s3.smithy index 4639c0c8317..f4107c0ddb3 100644 --- a/smithy-aws-protocol-tests/model/restXml/services/s3.smithy +++ b/smithy-aws-protocol-tests/model/restXml/services/s3.smithy @@ -408,13 +408,10 @@ string Delimiter string DisplayName -@enum([ - { - value: "url", - name: "url", - }, -]) -string EncodingType +enum EncodingType { + @suppress(["EnumShape"]) + url +} string ETag @@ -437,51 +434,23 @@ string NextToken ) string ObjectKey -@enum([ - { - value: "STANDARD", - name: "STANDARD", - }, - { - value: "REDUCED_REDUNDANCY", - name: "REDUCED_REDUNDANCY", - }, - { - value: "GLACIER", - name: "GLACIER", - }, - { - value: "STANDARD_IA", - name: "STANDARD_IA", - }, - { - value: "ONEZONE_IA", - name: "ONEZONE_IA", - }, - { - value: "INTELLIGENT_TIERING", - name: "INTELLIGENT_TIERING", - }, - { - value: "DEEP_ARCHIVE", - name: "DEEP_ARCHIVE", - }, - { - value: "OUTPOSTS", - name: "OUTPOSTS", - }, -]) -string ObjectStorageClass +enum ObjectStorageClass { + STANDARD + REDUCED_REDUNDANCY + GLACIER + STANDARD_IA + ONEZONE_IA + INTELLIGENT_TIERING + DEEP_ARCHIVE + OUTPOSTS +} string Prefix -@enum([ - { - value: "requester", - name: "requester", - }, -]) -string RequestPayer +enum RequestPayer { + @suppress(["EnumShape"]) + requester +} integer Size @@ -489,7 +458,8 @@ string StartAfter string Token -@enum([ - { value: "us-west-2", name: "us_west_2" } -]) -string BucketLocationConstraint +enum BucketLocationConstraint { + @suppress(["EnumShape"]) + @enumValue(string: "us-west-2") + us_west_2 +} diff --git a/smithy-aws-protocol-tests/model/shared-types.smithy b/smithy-aws-protocol-tests/model/shared-types.smithy index a28c7a64fc2..f75adbfd7d9 100644 --- a/smithy-aws-protocol-tests/model/shared-types.smithy +++ b/smithy-aws-protocol-tests/model/shared-types.smithy @@ -74,29 +74,22 @@ list TimestampList { member: Timestamp, } -@enum([ - { - name: "FOO", - value: "Foo", - }, - { - name: "BAZ", - value: "Baz", - }, - { - name: "BAR", - value: "Bar", - }, - { - name: "ONE", - value: "1", - }, - { - name: "ZERO", - value: "0", - }, -]) -string FooEnum +enum FooEnum { + @enumValue(string: "Foo") + FOO + + @enumValue(string: "Baz") + BAZ + + @enumValue(string: "Bar") + BAR + + @enumValue(string: "1") + ONE + + @enumValue(string: "0") + ZERO +} list FooEnumList { member: FooEnum, diff --git a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/ProtocolHttpPayloadValidator.java b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/ProtocolHttpPayloadValidator.java index 6529073473c..65cc7acda8e 100644 --- a/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/ProtocolHttpPayloadValidator.java +++ b/smithy-aws-traits/src/main/java/software/amazon/smithy/aws/traits/protocols/ProtocolHttpPayloadValidator.java @@ -45,7 +45,8 @@ @SmithyInternalApi public final class ProtocolHttpPayloadValidator extends AbstractValidator { private static final Set VALID_HTTP_PAYLOAD_TYPES = SetUtils.of( - ShapeType.STRUCTURE, ShapeType.UNION, ShapeType.DOCUMENT, ShapeType.BLOB, ShapeType.STRING + ShapeType.STRUCTURE, ShapeType.UNION, ShapeType.DOCUMENT, ShapeType.BLOB, ShapeType.STRING, + ShapeType.ENUM ); @Override 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 acee2da4785..23b05c44286 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,38 +79,58 @@ } }, "aws.api#data": { - "type": "string", + "type": "enum", + "members": { + "CUSTOMER_CONTENT": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "content" + }, + "smithy.api#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." + } + }, + "CUSTOMER_ACCOUNT_INFORMATION": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "account" + }, + "smithy.api#documentation": "Account information means information about customers that customers provide to AWS in connection with the creation or administration of customers\u2019 accounts." + } + }, + "SERVICE_ATTRIBUTES": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "usage" + }, + "smithy.api#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." + } + }, + "TAG_DATA": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "tagging" + }, + "smithy.api#documentation": "Designates metadata tags applied to AWS resources." + } + }, + "PERMISSIONS_DATA": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "permissions" + }, + "smithy.api#documentation": "Designates security and access roles, rules, usage policies, and permissions." + } + } + }, "traits": { "smithy.api#trait": { "selector": ":test(simpleType, collection, structure, union, member)" }, - "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." - }, - { - "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." - }, - { - "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." - }, - { - "value": "tagging", - "name": "TAG_DATA", - "documentation": "Designates metadata tags applied to AWS resources." - }, - { - "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-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json index 8ae03d046c2..87140120afa 100644 --- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json +++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json @@ -276,26 +276,42 @@ } }, "aws.protocols#ChecksumAlgorithm": { - "type": "string", - "traits": { - "smithy.api#enum": [ - { - "value": "CRC32C", - "name": "CRC32C" - }, - { - "value": "CRC32", - "name": "CRC32" - }, - { - "value": "SHA1", - "name": "SHA1" - }, - { - "value": "SHA256", - "name": "SHA256" + "type": "enum", + "members": { + "CRC32C": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "CRC32C" + } + } + }, + "CRC32": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "CRC32" + } + } + }, + "SHA1": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "SHA1" + } + } + }, + "SHA256": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "SHA256" + } } - ], + } + }, + "traits": { "smithy.api#private": {} } } diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-header-conflicts.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-header-conflicts.smithy index 946050258a6..8ec5b700ff6 100644 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-header-conflicts.smithy +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-header-conflicts.smithy @@ -131,33 +131,16 @@ structure NoConflictError { noConflictHeaders: StringMap, } -@enum([ - { - value: "CRC32C", - name: "CRC32C" - }, - { - value: "CRC32", - name: "CRC32" - }, - { - value: "SHA1", - name: "SHA1" - }, - { - value: "SHA256", - name: "SHA256" - } -]) -string ChecksumAlgorithm - -@enum([ - { - value: "ENABLED", - name: "ENABLED" - } -]) -string ValidationMode +enum ChecksumAlgorithm { + CRC32C + CRC32 + SHA1 + SHA256 +} + +enum ValidationMode { + ENABLED +} map StringMap { key: String, diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-member-enums.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-member-enums.smithy index 972af27f567..046d016f255 100644 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-member-enums.smithy +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-member-enums.smithy @@ -82,50 +82,25 @@ structure NoMemberInput {} @output structure NoMemberOutput {} -@enum([ - { - value: "CRC32C", - name: "CRC32C" - }, - { - value: "CRC32", - name: "CRC32" - }, - { - value: "SHA1", - name: "SHA1" - }, - { - value: "SHA256", - name: "SHA256" - } -]) -string ChecksumAlgorithm - -@enum([ - { - value: "ENABLED", - name: "ENABLED" - } -]) -string ValidationMode - - -@enum([ - { - value: "SHA2", - name: "SHA2" - } -]) -string BadChecksumAlgorithm - -@enum([ - { - value: "DISABLED", - name: "DISABLED" - } -]) -string BadValidationMode +enum ChecksumAlgorithm { + CRC32C + CRC32 + SHA1 + SHA256 +} + +enum ValidationMode { + ENABLED +} + + +enum BadChecksumAlgorithm { + SHA2 +} + +enum BadValidationMode { + DISABLED +} map StringMap { key: String, diff --git a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.smithy b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.smithy index 55a4457da36..800cb375ace 100644 --- a/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.smithy +++ b/smithy-aws-traits/src/test/resources/software/amazon/smithy/aws/traits/errorfiles/http-checksum-trait.smithy @@ -54,32 +54,13 @@ structure NoOutputForResponseInput { validationMode: ValidationMode, } -@enum([ - { - value: "CRC32C", - name: "CRC32C" - }, - { - value: "CRC32", - name: "CRC32" - }, - { - value: "SHA1", - name: "SHA1" - }, - { - value: "SHA256", - name: "SHA256" - } -]) -string ChecksumAlgorithm - -@enum([ - { - value: "ENABLED", - name: "ENABLED" - } -]) -string ValidationMode - +enum ChecksumAlgorithm { + CRC32C + CRC32 + SHA1 + SHA256 +} +enum ValidationMode { + ENABLED +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java index 6ffd2e475e0..16fd24f121f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java @@ -646,7 +646,11 @@ private List createModelFiles() { List nonPrelude = model.shapes() .filter(FunctionalUtils.not(Prelude::isPreludeShape)) .collect(Collectors.toList()); - FullyResolvedModelFile resolvedFile = FullyResolvedModelFile.fromShapes(traitFactory, nonPrelude); + // Since we're pulling from a loaded model, we know that it has been converted to the latest + // supported version. We include that here to ensure we don't hit any validation issues from + // using new features. + FullyResolvedModelFile resolvedFile = FullyResolvedModelFile.fromShapes( + traitFactory, nonPrelude, Version.fromString(Model.MODEL_VERSION)); model.getMetadata().forEach(resolvedFile::putMetadata); modelFiles.add(resolvedFile); } diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index c95e2814352..88d9c584492 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -46,19 +46,15 @@ structure trait { } @private -@enum([ - { - name: "MEMBER", - value: "member", - documentation: "Only a single member of a structure can be marked with the trait." - }, - { - name: "TARGET", - value: "target", - documentation: "Only a single member of a structure can target a shape marked with this trait." - } -]) -string StructurallyExclusive +enum StructurallyExclusive { + /// Only a single member of a structure can be marked with the trait. + @enumValue(string: "member") + MEMBER + + /// Only a single member of a structure can target a shape marked with this trait. + @enumValue(string: "target") + TARGET +} /// Marks a shape or member as deprecated. @trait @@ -176,17 +172,13 @@ structure httpApiKeyAuth { structure default {} @private -@enum([ - { - name: "HEADER", - value: "header", - }, - { - name: "QUERY", - value: "query", - }, -]) -string HttpApiKeyLocations +enum HttpApiKeyLocations { + @enumValue(string: "header") + HEADER + + @enumValue(string: "query") + QUERY +} /// Indicates that an operation can be called without authentication. @trait(selector: "operation") @@ -226,10 +218,13 @@ structure ExampleError { /// targeted with this trait. @trait(selector: "structure", conflicts: [trait]) @tags(["diff.error.const"]) -@enum([ - {value: "client", name: "CLIENT"}, - {value: "server", name: "SERVER"}]) -string error +enum error { + @enumValue(string: "client") + CLIENT + + @enumValue(string: "server") + SERVER +} /// Indicates that an error MAY be retried by the client. @trait(selector: "structure[trait|error]") @@ -402,6 +397,7 @@ string title @trait(selector: "string") @tags(["diff.error.add", "diff.error.remove"]) @length(min: 1) +@deprecated list enum { member: EnumDefinition } @@ -695,31 +691,24 @@ structure idRef { @trait(selector: ":test(timestamp, member > timestamp)") @tags(["diff.error.const"]) -@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).""" - }, - { - 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).""" - }, - { - 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 +enum timestampFormat { + + /// 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). + @enumValue(string: "date-time") + DATE_TIME + + /// 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). + @enumValue(string: "epoch-seconds") + EPOCH_SECONDS + + /// 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). + @enumValue(string: "http-date") + HTTP_DATE +} /// Configures a custom operation endpoint. @trait(selector: "operation") diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/enum-can-be-deprecated-and-tagged.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/enum-can-be-deprecated-and-tagged.errors index e69de29bb2d..39a746c3cac 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/enum-can-be-deprecated-and-tagged.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/loader/enum-can-be-deprecated-and-tagged.errors @@ -0,0 +1 @@ +[WARNING] smithy.example#MyString: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait 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 b6bf1525082..223b3e82099 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 @@ -9,3 +9,10 @@ [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 [ERROR] ns.foo#Invalid4: Duplicate enum trait values found with the same `value` property of 'a' | EnumTrait +[WARNING] ns.foo#Invalid1: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait +[WARNING] ns.foo#Invalid2: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait +[WARNING] ns.foo#Invalid3: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait +[WARNING] ns.foo#Invalid4: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait +[WARNING] ns.foo#ValidFullDefinition: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait +[WARNING] ns.foo#ValidNoName: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait +[WARNING] ns.foo#Warn1: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait 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 630c5ab25dc..e19919ab71d 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 @@ -51,45 +51,34 @@ structure RejectedError { executionState: JobExecutionState, } -@enum([ - { - name: "INVALID_TOPIC", - value: "InvalidTopic", - }, - { - name: "INVALID_JSON", - value: "InvalidJson", - }, - { - name: "INVALID_REQUEST", - value: "InvalidRequest", - }, - { - name: "INVALID_STATE_TRANSITION", - value: "InvalidStateTransition", - }, - { - name: "RESOURCE_NOT_FOUND", - value: "ResourceNotFound", - }, - { - name: "VERSION_MISMATCH", - value: "VersionMismatch", - }, - { - name: "INTERNAL_ERROR", - value: "InternalError", - }, - { - name: "REQUEST_THROTTLED", - value: "RequestThrottled", - }, - { - name: "TERMINAL_STATE_REACHED", - value: "TerminalStateReached", - }, -]) -string RejectedErrorCode +enum RejectedErrorCode { + @enumValue(string: "InvalidTopic") + INVALID_TOPIC + + @enumValue(string: "InvalidJson") + INVALID_JSON + + @enumValue(string: "InvalidRequest") + INVALID_REQUEST + + @enumValue(string: "InvalidStateTransition") + INVALID_STATE_TRANSITION + + @enumValue(string: "ResourceNotFound") + RESOURCE_NOT_FOUND + + @enumValue(string: "VersionMismatch") + VERSION_MISMATCH + + @enumValue(string: "InternalError") + INTERNAL_ERROR + + @enumValue(string: "RequestThrottled") + REQUEST_THROTTLED + + @enumValue(string: "TerminalStateReached") + TERMINAL_STATE_REACHED +} // ------ GetPendingJobExecutions ------- @@ -231,41 +220,16 @@ structure JobExecutionData { executionNumber: smithy.api#Long, } -@enum([ - { - name: "QUEUED", - value: "QUEUED", - }, - { - name: "IN_PROGRESS", - value: "IN_PROGRESS", - }, - { - name: "TIMED_OUT", - value: "TIMED_OUT", - }, - { - name: "FAILED", - value: "FAILED", - }, - { - name: "SUCCEEDED", - value: "SUCCEEDED", - }, - { - name: "CANCELED", - value: "CANCELED", - }, - { - name: "REJECTED", - value: "REJECTED", - }, - { - name: "REMOVED", - value: "REMOVED", - }, -]) -string JobStatus +enum JobStatus { + QUEUED + IN_PROGRESS + TIMED_OUT + FAILED + SUCCEEDED + CANCELED + REJECTED + REMOVED +} // ------- DescribeJobExecution ---------- diff --git a/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy index 15f65dbd08d..2f403de9962 100644 --- a/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy +++ b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy @@ -272,19 +272,15 @@ list NonEmptyStringList { string NonEmptyString @private -@enum([ - { - value: "client", - name: "CLIENT", - documentation: "The test only applies to client implementations." - }, - { - value: "server", - name: "SERVER", - documentation: "The test only applies to server implementations." - }, -]) -string AppliesTo +enum AppliesTo { + /// The test only applies to client implementations. + @enumValue(string: "client") + CLIENT + + /// The test only applies to server implementations. + @enumValue(string: "server") + SERVER +} /// Define how a malformed HTTP request is rejected by a server given a specific protocol @trait(selector: "operation") diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/test-with-appliesto.smithy b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/test-with-appliesto.smithy index 56f56dca66c..de6d6adcf60 100644 --- a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/test-with-appliesto.smithy +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/test-with-appliesto.smithy @@ -1,3 +1,4 @@ + $version: "2.0" namespace smithy.example diff --git a/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy b/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy index 0bd7b7481c3..bde71cc4920 100644 --- a/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy +++ b/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy @@ -69,30 +69,22 @@ structure Acceptor { /// The transition state of a waiter. @private -@enum([ - { - "name": "SUCCESS", - "value": "success", - "documentation": """ - The waiter successfully finished waiting. This is a terminal - state that causes the waiter to stop.""" - }, - { - "name": "FAILURE", - "value": "failure", - "documentation": """ - The waiter failed to enter into the desired state. This is a - terminal state that causes the waiter to stop.""" - }, - { - "name": "RETRY", - "value": "retry", - "documentation": """ - The waiter will retry the operation. This state transition is - implicit if no accepter causes a state transition.""" - }, -]) -string AcceptorState +enum AcceptorState { + /// The waiter successfully finished waiting. This is a terminal + /// state that causes the waiter to stop. + @enumValue(string: "success") + SUCCESS + + /// The waiter failed to enter into the desired state. This is a + /// terminal state that causes the waiter to stop. + @enumValue(string: "failure") + FAILURE + + /// The waiter will retry the operation. This state transition is + /// implicit if no accepter causes a state transition. + @enumValue(string: "retry") + RETRY +} /// Defines how an acceptor determines if it matches the current state of /// a resource. @@ -141,30 +133,24 @@ structure PathMatcher { } /// Defines a comparison to perform in a PathMatcher. -@enum([ - { - "name": "STRING_EQUALS", - "value": "stringEquals", - "documentation": "Matches if the return value is a string that is equal to the expected string." - }, - { - "name": "BOOLEAN_EQUALS", - "value": "booleanEquals", - "documentation": "Matches if the return value is a boolean that is equal to the string literal 'true' or 'false'." - }, - { - "name": "ALL_STRING_EQUALS", - "value": "allStringEquals", - "documentation": "Matches if all values in the list matches the expected string." - }, - { - "name": "ANY_STRING_EQUALS", - "value": "anyStringEquals", - "documentation": "Matches if any value in the list matches the expected string." - } -]) @private -string PathComparator +enum PathComparator { + /// Matches if the return value is a string that is equal to the expected string. + @enumValue(string: "stringEquals") + STRING_EQUALS + + /// Matches if the return value is a boolean that is equal to the string literal 'true' or 'false'. + @enumValue(string: "booleanEquals") + BOOLEAN_EQUALS + + /// Matches if all values in the list matches the expected string. + @enumValue(string: "allStringEquals") + ALL_STRING_EQUALS + + /// Matches if any value in the list matches the expected string. + @enumValue(string: "anyStringEquals") + ANY_STRING_EQUALS +} @private list NonEmptyStringList { From 5dca4c7c42f111ebe710764a501c86dda96080a3 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 15 Feb 2022 17:36:22 +0100 Subject: [PATCH 18/45] Disallow applying enum traits to enum shapes --- .../software/amazon/smithy/model/loader/prelude.smithy | 2 +- .../smithy/model/errorfiles/validators/enum-shapes.errors | 2 ++ .../smithy/model/errorfiles/validators/enum-shapes.smithy | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index 88d9c584492..8be654f2c7d 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -394,7 +394,7 @@ string title /// Constrains the acceptable values of a string to a fixed set /// of constant values. -@trait(selector: "string") +@trait(selector: "string :not(enum)") @tags(["diff.error.add", "diff.error.remove"]) @length(min: 1) @deprecated diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors index 1ea3db2c8cd..7ddcb7d7977 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors @@ -5,3 +5,5 @@ [ERROR] ns.foo#IntEnum$STRING_VALUE: intEnum members must have the enumValue trait with the `int` member set | EnumShape [ERROR] ns.foo#IntEnum$DUPLICATE_VALUE: Multiple enum members found with duplicate value `1` | EnumShape [WARNING] ns.foo#IntEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape +[WARNING] ns.foo#EnumWithEnumTrait: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait +[ERROR] ns.foo#EnumWithEnumTrait: Trait `enum` cannot be applied to `ns.foo#EnumWithEnumTrait`. This trait may only be applied to shapes that match the following selector: string :not(enum) | TraitTarget diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy index 139561c94f7..a861b8d8a3d 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy @@ -20,6 +20,14 @@ enum StringEnum { undesirableName } +@enum([{ + name: "FOO" + value: "FOO" +}]) +enum EnumWithEnumTrait { + BAR +} + intEnum IntEnum { IMPLICIT_VALUE From 72f8f62a89196c051f7c1d1f7f8fe6e48dbd24a8 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 15 Feb 2022 17:48:05 +0100 Subject: [PATCH 19/45] Add more enum trait conversion tests --- .../smithy/model/shapes/EnumShapeTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index 736236fb961..fc8dc0cec6f 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -21,9 +21,12 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.traits.DeprecatedTrait; +import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.traits.TagsTrait; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; import software.amazon.smithy.utils.ListUtils; @@ -132,6 +135,74 @@ public void addMemberFromEnumTrait() { assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of(enumDefinition)); } + @Test + public void convertsDocsFromEnumTrait() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + String docs = "docs"; + EnumDefinition enumDefinition = EnumDefinition.builder() + .name("foo") + .value("bar") + .documentation(docs) + .build(); + EnumShape shape = builder.members(EnumTrait.builder().addEnum(enumDefinition).build()).build(); + + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .addTrait(new DocumentationTrait(docs)) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of(enumDefinition)); + } + + @Test + public void convertsTagsFromEnumTrait() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + String tag = "tag"; + EnumDefinition enumDefinition = EnumDefinition.builder() + .name("foo") + .value("bar") + .addTag(tag) + .build(); + EnumShape shape = builder.members(EnumTrait.builder().addEnum(enumDefinition).build()).build(); + + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .addTrait(TagsTrait.builder().addValue(tag).build()) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of(enumDefinition)); + } + + @Test + public void convertsDeprecatedFromEnumTrait() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumDefinition enumDefinition = EnumDefinition.builder() + .name("foo") + .value("bar") + .deprecated(true) + .build(); + EnumShape shape = builder.members(EnumTrait.builder().addEnum(enumDefinition).build()).build(); + + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("bar").build()) + .addTrait(DeprecatedTrait.builder().build()) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of(enumDefinition)); + } + @Test public void givenEnumTraitMustUseNames() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); From ba6eeb0f3360e75f358acd91fb1bebd3083c7ebb Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Wed, 16 Feb 2022 17:06:58 +0100 Subject: [PATCH 20/45] Support mixins for enum shapes --- .../smithy/model/loader/IdlModelParser.java | 3 +- .../amazon/smithy/model/shapes/EnumShape.java | 37 ++++- .../smithy/model/shapes/IntEnumShape.java | 29 +++- .../smithy/model/shapes/NamedMemberUtils.java | 148 ++++++++++++++++++ .../smithy/model/shapes/NamedMembers.java | 48 ++++++ .../model/shapes/NamedMembersShape.java | 115 +------------- .../model/transform/ModelTransformerTest.java | 3 +- .../valid/mixins/enum-mixins.flattened.smithy | 28 ++++ .../loader/valid/mixins/enum-mixins.json | 111 +++++++++++++ .../loader/valid/mixins/enum-mixins.smithy | 43 +++++ 10 files changed, 445 insertions(+), 120 deletions(-) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMemberUtils.java create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.flattened.smithy create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.json create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.smithy diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index eabb534d838..50f7aaaf36e 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -536,9 +536,8 @@ private void parseMember(ShapeId parent, Set allowed, Set define modelFile.onShape(memberBuilder); String target; - ws(); - if (!targetsUnion) { + ws(); expect(':'); if (peek() == '=') { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index 75798c7157b..6f4bd11b924 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -30,14 +30,15 @@ import software.amazon.smithy.utils.BuilderRef; import software.amazon.smithy.utils.ListUtils; -public final class EnumShape extends StringShape { +public final class EnumShape extends StringShape implements NamedMembers { private final Map members; private volatile List memberNames; private EnumShape(Builder builder) { super(builder); - members = builder.members.get(); + members = NamedMemberUtils.computeMixinMembers( + builder.getMixins(), builder.members, getId(), getSourceLocation()); validateMemberShapeIds(); if (members.size() < 1) { throw new SourceException("enum shapes must have at least one member", getSourceLocation()); @@ -198,7 +199,7 @@ public Builder members(EnumTrait trait) { for (EnumDefinition definition : trait.getValues()) { Optional member = definition.asMember(getId()); if (member.isPresent()) { - addMember(member.get(), false); + addMember(member.get()); } else { throw new IllegalStateException(String.format( "Unable to convert enum trait entry with name: `%s` and value `%s` to an enum member.", @@ -236,10 +237,6 @@ public Builder clearMembers() { @Override public Builder addMember(MemberShape member) { - return addMember(member, true); - } - - private Builder addMember(MemberShape member, boolean updateEnumTrait) { if (!member.getTarget().equals(UnitTypeTrait.UNIT)) { throw new SourceException(String.format( "Enum members may only target `smithy.api#Unit`, but found `%s`", member.getTarget() @@ -307,5 +304,31 @@ public Builder removeMember(String member) { } return this; } + + @Override + public Builder addMixin(Shape shape) { + if (getId() == null) { + throw new IllegalStateException("An id must be set before adding a mixin"); + } + super.addMixin(shape); + NamedMemberUtils.cleanMixins(shape, members.get()); + return this; + } + + @Override + public Builder removeMixin(ToShapeId shape) { + super.removeMixin(shape); + NamedMemberUtils.removeMixin(shape, members.get()); + return this; + } + + @Override + public Builder flattenMixins() { + if (getMixins().isEmpty()) { + return this; + } + members(NamedMemberUtils.flattenMixins(members.get(), getMixins(), getId(), getSourceLocation())); + return (Builder) super.flattenMixins(); + } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java index d2f50877715..dbe4745b9b6 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -33,7 +33,8 @@ public final class IntEnumShape extends IntegerShape { private IntEnumShape(Builder builder) { super(builder); - members = builder.members.get(); + members = NamedMemberUtils.computeMixinMembers( + builder.getMixins(), builder.members, getId(), getSourceLocation()); validateMemberShapeIds(); if (members.size() < 1) { throw new SourceException("intEnum shapes must have at least one member", getSourceLocation()); @@ -228,5 +229,31 @@ public Builder removeMember(String member) { } return this; } + + @Override + public Builder addMixin(Shape shape) { + if (getId() == null) { + throw new IllegalStateException("An id must be set before adding a mixin"); + } + super.addMixin(shape); + NamedMemberUtils.cleanMixins(shape, members.get()); + return this; + } + + @Override + public Builder removeMixin(ToShapeId shape) { + super.removeMixin(shape); + NamedMemberUtils.removeMixin(shape, members.get()); + return this; + } + + @Override + public Builder flattenMixins() { + if (getMixins().isEmpty()) { + return this; + } + members(NamedMemberUtils.flattenMixins(members.get(), getMixins(), getId(), getSourceLocation())); + return (Builder) super.flattenMixins(); + } } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMemberUtils.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMemberUtils.java new file mode 100644 index 00000000000..891b13cdf33 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMemberUtils.java @@ -0,0 +1,148 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.shapes; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.BuilderRef; + +final class NamedMemberUtils { + + private NamedMemberUtils() { + } + + static Map computeMixinMembers( + Map mixins, + BuilderRef> members, + ShapeId shapeId, + SourceLocation sourceLocation + ) { + if (mixins.isEmpty()) { + return members.copy(); + } + + // Compute mixin members of this shape that inherit traits from mixin members. + Map computedMembers = new LinkedHashMap<>(); + for (Shape shape : mixins.values()) { + for (MemberShape member : shape.members()) { + String name = member.getMemberName(); + if (members.get().containsKey(name)) { + MemberShape localMember = members.get().get(name); + // Rebuild the member with the proper inherited mixin if needed. + // This catches errant cases where a member is added to a structure/union + // but omits the mixin members of parent shapes. Arguably, that's way too + // nuanced and error-prone to _not_ try to smooth over. + if (localMember.getMixins().isEmpty() || !mixins.containsKey(member.getId())) { + localMember = localMember.toBuilder().clearMixins().addMixin(member).build(); + } + computedMembers.put(name, localMember); + } else { + computedMembers.put(name, MemberShape.builder() + .id(shapeId.withMember(name)) + .target(member.getTarget()) + .source(sourceLocation) + .addMixin(member) + .build()); + } + } + } + + // Add members local to the structure after inherited members. + for (MemberShape member : members.get().values()) { + if (!computedMembers.containsKey(member.getMemberName())) { + computedMembers.put(member.getMemberName(), member); + } + } + + return Collections.unmodifiableMap(computedMembers); + } + + static Set flattenMixins( + Map members, + Map mixins, + ShapeId shapeId, + SourceLocation sourceLocation + ) { + // Ensure that the members are ordered, mixin members first, followed by local members. + Set orderedMembers = new LinkedHashSet<>(); + + // Copy members from mixins onto the shape. + for (Shape mixin : mixins.values()) { + for (MemberShape member : mixin.members()) { + SourceLocation location = sourceLocation; + Collection localTraits = Collections.emptyList(); + MemberShape existing = members.remove(member.getMemberName()); + if (existing != null) { + localTraits = existing.getIntroducedTraits().values(); + location = existing.getSourceLocation(); + } + orderedMembers.add(MemberShape.builder() + .id(shapeId.withMember(member.getMemberName())) + .target(member.getTarget()) + .addTraits(member.getAllTraits().values()) + .addTraits(localTraits) + .source(location) + .build()); + } + } + + // Add any local members _after_ mixin members. LinkedHashSet will keep insertion + // order, so no need to check for non-mixin members first. + orderedMembers.addAll(members.values()); + return orderedMembers; + } + + static void cleanMixins(Shape newMixin, Map members) { + // Clean up members that were previously mixed in by the given shape but + // are no longer present on the given shape. + members.values().removeIf(member -> { + if (!isMemberMixedInFromShape(newMixin.getId(), member)) { + return false; + } + for (MemberShape mixinMember : newMixin.members()) { + if (mixinMember.getMemberName().equals(member.getMemberName())) { + return false; + } + } + return true; + }); + } + + static void removeMixin(ToShapeId mixin, Map members) { + ShapeId id = mixin.toShapeId(); + // Remove members that have a mixin where the ID equals the given ID or + // the mixin ID without a member equals the given ID. + members.values().removeIf(member -> isMemberMixedInFromShape(id, member)); + } + + private static boolean isMemberMixedInFromShape(ShapeId check, MemberShape member) { + if (member.getMixins().contains(check)) { + return true; + } + for (ShapeId memberMixin : member.getMixins()) { + if (memberMixin.withoutMember().equals(check)) { + return true; + } + } + return false; + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java new file mode 100644 index 00000000000..c3f4ad7b47c --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.shapes; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +interface NamedMembers { + + /** + * Gets the members of the shape, including mixin members. + * + * @return Returns the immutable member map. + */ + Map getAllMembers(); + + /** + * Returns an ordered list of member names based on the order they are + * defined in the model, including mixin members. + * + * @return Returns an immutable list of member names. + */ + List getMemberNames(); + + /** + * Get a specific member by name. + * + * @param name Name of the member to retrieve. + * @return Returns the optional member. + */ + default Optional getMember(String name) { + return Optional.ofNullable(getAllMembers().get(name)); + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShape.java index ee0748ef657..b8f0c94bde4 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShape.java @@ -16,16 +16,10 @@ package software.amazon.smithy.model.shapes; import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.function.Consumer; -import software.amazon.smithy.model.SourceLocation; -import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.utils.BuilderRef; import software.amazon.smithy.utils.ListUtils; @@ -35,54 +29,15 @@ *

The order of members in structures and unions are the same as the * order that they are defined in the model. */ -public abstract class NamedMembersShape extends Shape { +public abstract class NamedMembersShape extends Shape implements NamedMembers { private final Map members; private volatile List memberNames; NamedMembersShape(NamedMembersShape.Builder builder) { super(builder, false); - - if (getMixins().isEmpty()) { - members = builder.members.copy(); - } else { - // Compute mixin members of this shape that inherit traits from mixin members. - Map computedMembers = new LinkedHashMap<>(); - for (Shape shape : builder.getMixins().values()) { - for (MemberShape member : shape.members()) { - String name = member.getMemberName(); - // TODO: hasValue - if (builder.members.get().containsKey(name)) { - MemberShape localMember = builder.members.get().get(name); - // Rebuild the member with the proper inherited mixin if needed. - // This catches errant cases where a member is added to a structure/union - // but omits the mixin members of parent shapes. Arguably, that's way too - // nuanced and error-prone to _not_ try to smooth over. - if (localMember.getMixins().isEmpty() || !builder.getMixins().containsKey(member.getId())) { - localMember = localMember.toBuilder().clearMixins().addMixin(member).build(); - } - computedMembers.put(name, localMember); - } else { - computedMembers.put(name, MemberShape.builder() - .id(getId().withMember(name)) - .target(member.getTarget()) - .source(getSourceLocation()) - .addMixin(member) - .build()); - } - } - } - - // Add members local to the structure after inherited members. - for (MemberShape member : builder.members.get().values()) { - if (!computedMembers.containsKey(member.getMemberName())) { - computedMembers.put(member.getMemberName(), member); - } - } - - members = Collections.unmodifiableMap(computedMembers); - } - + members = NamedMemberUtils.computeMixinMembers( + builder.getMixins(), builder.members, getId(), getSourceLocation()); validateMemberShapeIds(); } @@ -246,23 +201,8 @@ public B addMixin(Shape shape) { if (getId() == null) { throw new IllegalStateException("An id must be set before adding a mixin"); } - super.addMixin(shape); - - // Clean up members that were previously mixed in by the given shape but - // are no longer present on the given shape. - members.get().values().removeIf(member -> { - if (!isMemberMixedInFromShape(shape.getId(), member)) { - return false; - } - for (MemberShape mixinMember : shape.members()) { - if (mixinMember.getMemberName().equals(member.getMemberName())) { - return false; - } - } - return true; - }); - + NamedMemberUtils.cleanMixins(shape, members.get()); return (B) this; } @@ -270,10 +210,7 @@ public B addMixin(Shape shape) { @SuppressWarnings("unchecked") public B removeMixin(ToShapeId shape) { super.removeMixin(shape); - ShapeId id = shape.toShapeId(); - // Remove members that have a mixin where the ID equals the given ID or - // the mixin ID without a member equals the given ID. - members.get().values().removeIf(member -> isMemberMixedInFromShape(id, member)); + NamedMemberUtils.removeMixin(shape, members.get()); return (B) this; } @@ -283,48 +220,8 @@ public B flattenMixins() { if (getMixins().isEmpty()) { return (B) this; } - - // Ensure that the members are ordered, mixin members first, followed by local members. - Set orderedMembers = new LinkedHashSet<>(); - - // Copy members from mixins onto the shape. - for (Shape mixin : getMixins().values()) { - for (MemberShape member : mixin.members()) { - SourceLocation location = getSourceLocation(); - Collection localTraits = Collections.emptyList(); - MemberShape existing = members.get().remove(member.getMemberName()); - if (existing != null) { - localTraits = existing.getIntroducedTraits().values(); - location = existing.getSourceLocation(); - } - orderedMembers.add(MemberShape.builder() - .id(getId().withMember(member.getMemberName())) - .target(member.getTarget()) - .addTraits(member.getAllTraits().values()) - .addTraits(localTraits) - .source(location) - .build()); - } - } - - // Add any local members _after_ mixin members. LinkedHashSet will keep insertion - // order, so no need to check for non-mixin members first. - orderedMembers.addAll(members.get().values()); - members(orderedMembers); - + members(NamedMemberUtils.flattenMixins(members.get(), getMixins(), getId(), getSourceLocation())); return super.flattenMixins(); } - - private boolean isMemberMixedInFromShape(ShapeId check, MemberShape member) { - if (member.getMixins().contains(check)) { - return true; - } - for (ShapeId memberMixin : member.getMixins()) { - if (memberMixin.withoutMember().equals(check)) { - return true; - } - } - return false; - } } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ModelTransformerTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ModelTransformerTest.java index 974b1e35d70..2800169d6a0 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ModelTransformerTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ModelTransformerTest.java @@ -173,7 +173,8 @@ public static String[] flattenShapesData() { "operations", "resources", "services", - "idl-mixins-redefine-member" + "idl-mixins-redefine-member", + "enum-mixins" }; } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.flattened.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.flattened.smithy new file mode 100644 index 00000000000..68d84b1ae36 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.flattened.smithy @@ -0,0 +1,28 @@ +$version: "2.0" + +namespace smithy.example + +/// Mixed enum +@sensitive +@private +enum MixedEnum { + FOO + /// Docs + BAR + BAZ +} + +/// Mixed int enum +@sensitive +@private +intEnum MixedIntEnum { + @enumValue(int: 1) + FOO + + /// Docs + @enumValue(int: 2) + BAR + + @enumValue(int: 3) + BAZ +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.json new file mode 100644 index 00000000000..8a88a5073b6 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.json @@ -0,0 +1,111 @@ +{ + "smithy": "2.0", + "shapes": { + "smithy.example#BaseEnum": { + "type": "enum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "FOO" + } + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "BAR" + }, + "smithy.api#documentation": "Documentation" + } + } + }, + "traits": { + "smithy.api#mixin": {}, + "smithy.api#private": {}, + "smithy.api#documentation": "Base enum" + } + }, + "smithy.example#MixedEnum": { + "type": "enum", + "members": { + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "Docs" + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "string": "BAZ" + } + } + } + }, + "traits": { + "smithy.api#sensitive": {}, + "smithy.api#documentation": "Mixed enum" + }, + "mixins": [{ + "target": "smithy.example#BaseEnum" + }] + }, + "smithy.example#BaseIntEnum": { + "type": "intEnum", + "members": { + "FOO": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "int": 1 + } + } + }, + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "int": 2 + }, + "smithy.api#documentation": "Documentation" + } + } + }, + "traits": { + "smithy.api#mixin": {}, + "smithy.api#private": {}, + "smithy.api#documentation": "Base int enum" + } + }, + "smithy.example#MixedIntEnum": { + "type": "intEnum", + "members": { + "BAR": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#documentation": "Docs" + } + }, + "BAZ": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": { + "int": 3 + } + } + } + }, + "traits": { + "smithy.api#sensitive": {}, + "smithy.api#documentation": "Mixed int enum" + }, + "mixins": [{ + "target": "smithy.example#BaseIntEnum" + }] + } + } +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.smithy new file mode 100644 index 00000000000..9d2c1b7587b --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.smithy @@ -0,0 +1,43 @@ +$version: "2.0" + +namespace smithy.example + +/// Base enum +@mixin +@private +enum BaseEnum { + FOO + + /// Documentation + BAR +} + +/// Mixed enum +@sensitive +enum MixedEnum with [BaseEnum] { + /// Docs + BAR + BAZ +} + +/// Base int enum +@mixin +@private +intEnum BaseIntEnum { + @enumValue(int: 1) + FOO + + /// Documentation + @enumValue(int: 2) + BAR +} + +/// Mixed int enum +@sensitive +intEnum MixedIntEnum with [BaseIntEnum] { + /// Docs + BAR + + @enumValue(int: 3) + BAZ +} From 0c3cd5ee854ecdc0fb3d696294747b06d7c06048 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Wed, 16 Feb 2022 17:24:12 +0100 Subject: [PATCH 21/45] Add enum design doc --- designs/enum-shapes.md | 259 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 designs/enum-shapes.md diff --git a/designs/enum-shapes.md b/designs/enum-shapes.md new file mode 100644 index 00000000000..b0f6548b1c7 --- /dev/null +++ b/designs/enum-shapes.md @@ -0,0 +1,259 @@ +# Enum shapes in Smithy + +* **Author**: Michael Dowling +* **Created**: 2022-02-14 +* **Last updated**: 2022-02-14 + +## Abstract + +This proposal introduces an `enum` shape and `intEnum` shape to Smithy to +expand the existing `@enum` trait capabilities. + +## Motivation + +There are two primary motivations: + +1. Smithy does not provide the ability to define an enumerated integer, and + many serialization formats serialize enums as integers rather than strings. +2. Enum values are similar to members, but defined in a less flexible way that + requires special casing. Enums in Smithy have enum values that are very + similar to members, but cannot be targeted by traits or filtered like other + members. For example, an `@enum` trait supports the value, name, + documentation, tags, and deprecated properties, of which documentation, + tags, and deprecated provide functionality that is exactly equivalent to + similarly named traits. In order to filter enums out of a model, model + transformations need to write special-cased code to deal with enum traits + rather than relying on the existing logic used to remove member shapes. + +## Proposal + +Two new simple shapes will be introduced in Smithy 2.0: + +* `enum`: An enumerated type that is represented using string values +* `intEnum`: An enumerated type that is represented using integer values. + +### Enum shape + +The enum shape is used to represent a fixed set of string values. The following +example defines an enum shape: + +``` +enum Suit { + DIAMOND + CLUB + HEART + SPADE +} +``` + +Each value listed in the enum is a member that implicitly targets +`smithy.api#Unit`. The string representation of an enum member defaults to the +member name. The string representation can be customized by applying the +`@enumValue` trait. + +``` +enum Suit { + @enumValue(string: "diamond") + DIAMOND + + @enumValue(string: "club") + CLUB + + @enumValue(string: "heart") + HEART + + @enumValue(string: "spade") + SPADE +} +``` + +Enums do not support aliasing; all values MUST be unique. + +#### enum default values + +The default value of the enum shape is an empty string "", regardless of if +the enum defines a member with a value of "". + +``` +structure GetFooInput { + @default + suit: Suit +} +``` + +To account for this, enums MAY define a default member by making the +`@enumValue` of the member "": + +``` +enum Suit { + @enumValue(string: "") + UNKNOWN + + @enumValue(string: "diamond") + DIAMOND + + @enumValue(string: "club") + CLUB + + @enumValue(string: "heart") + HEART + + @enumValue(string: "spade") + SPADE +} +``` + +#### enum is a specialization of string + +Enums are considered open, meaning it is a backward compatible change to add +new members. Previously generated clients MUST NOT fail when they encounter an +unknown enum value. Client implementations MUST provide the capability of +sending and receiving unknown enum values. + +To facilitate this behavior, the enum type is a subclass of the string type. +Any selector that accepts a string implicitly accepts an enum. For example, an +enum can be used in an `@httpHeader` or `@httpLabel`. + +``` +structure GetFooInput { + @httpLabel + suit: Suit +} +``` + +### intEnum shape + +An intEnum is used to represent an enumerated set of integer values. The +members of intEnum MUST be marked with the @enumValue trait set to a unique +integer value. The following example defines an intEnum shape: + +``` +intEnum FaceCard { + @enumValue(int: 1) + JACK + + @enumValue(int: 2) + QUEEN + + @enumValue(int: 3) + KING + + @enumValue(int: 4) + ACE + + @enumValue(int: 5) + JOKER +} +``` + +#### intEnum default values + +The default value of the intEnum shape is 0, regardless of if the enum defines +a member with a value of 0. + +``` +structure GetFooInput { + @default + suit: Suit +} +``` + +intEnums MAY define a member to represent the default value of the shape by +making the `@enumValue` of the member 0: + +``` +intEnum FaceCard { + @enumValue(0) + UNKNOWN + + @enumValue(1) + JACK + + @enumValue(2) + QUEEN + + @enumValue(3) + KING + + @enumValue(4) + ACE + + @enumValue(5) + JOKER +} +``` + +#### intEnum is a specialization of integer + +Like enums, intEnums are considered open, meaning it is a backward compatible +change to add new members. Previously generated clients MUST NOT fail when +they encounter an unknown intEnum value. Client implementations SHOULD provide +the capability of sending and receiving unknown intEnum values. + +To facilitate this behavior, the intEnum type is a subclass of the integer +type. Any selector that accepts an integer implicitly accepts an intEnum. + +Implementation note: In Smithy’s Java implementation, shape visitors will by +default dispatch to the integer shape when an intEnum is encountered. This +both makes adding enums backward compatible, and allows implementations that +do not support enums at all to ignore them. + +### Smithy taxonomy updates + +Both enum and intEnum are considered simple shapes though they have members. + +The current definition of aggregate shapes is: + +> Aggregate types define shapes that are composed of other shapes. Aggregate +> shapes reference other shapes using members. + +This will require updating the description of Aggregate Types to become: + +> Aggregate types are shapes that can contain more than one value. + +The member type in Smithy is currently classified as an aggregate type. Because +members will now be present in simple shapes, we will add a new shape type +called “member” to go alongside simple, aggregate, and service types. + +## Smithy 1.0 → 2.0 + +Since the enum trait does not require names, it is not possible to +automatically convert string shapes using the `@enum` trait into enum traits. +Instead, a transformer will be added that upgrades those shapes that can +be upgraded. To make this change backward compatible for existing code +generators, enum shapes in IDL 2.0 will always contain an enum trait with +properties that correspond to the members of the enum. To prevent the +`@enum` trait from being serialized, a synthetic variant will be used. +Additionally, visitors will call their parent shape's visitor method by +default. + +## FAQ + +### Why does the enum type dictate its serialization? + +RPC systems require and open-world in order for them to evolve without +breaking end users. Previously generated clients need a way to handle +unknown enum values. Discarding unknown values when a client encounters a newly +introduced enum would be a regression in functionality. By making a distinct +intEnum and enum shape, client implementations can reliably receive and +round-trip unknown enum values because the unknown value is wire-compatible +with an integer or enum. + +### Why do enums contain members but you can’t define the shape they target? + +Every value of an enum and intEnum is considered a member so that they have a +shape ID and can have traits, but the shape targeted by each enum member is +meaningless. Because of this, members of an enum defined in the IDL implicitly +target Smithy’s unit type, `smithy.api#Unit`. + +When defined in the JSON AST, members MUST be defined to target +`smithy.api#Unit`. + +### Why does intEnum require an explicit enumValue on every member? + +The order and numbers assigned to enum values are extremely important, and +deriving their ordinals implicitly would not allow filtering members based on +the target audience of the model. It is very common in Smithy models to filter +out members from models based on the target audience (for example, internal +clients know about all enum members, private beta customers might know about +specific members, etc). From 0a314dc12ea37a4b4e34bf8a16b9764a7b42b312 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Wed, 16 Feb 2022 17:35:30 +0100 Subject: [PATCH 22/45] Change shape type categories to match design --- .../amazon/smithy/model/shapes/ShapeType.java | 10 +++++----- .../smithy/model/transform/ChangeShapeType.java | 2 ++ .../smithy/model/transform/ChangeShapeTypeTest.java | 11 +++++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java index 8448ae9b88b..47adfbfcdef 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java @@ -33,22 +33,22 @@ public enum ShapeType { DOUBLE("double", DoubleShape.class, Category.SIMPLE), BIG_DECIMAL("bigDecimal", BigDecimalShape.class, Category.SIMPLE), BIG_INTEGER("bigInteger", BigIntegerShape.class, Category.SIMPLE), - - ENUM("enum", EnumShape.class, Category.ENUM), - INT_ENUM("intEnum", IntEnumShape.class, Category.ENUM), + ENUM("enum", EnumShape.class, Category.SIMPLE), + INT_ENUM("intEnum", IntEnumShape.class, Category.SIMPLE), LIST("list", ListShape.class, Category.AGGREGATE), SET("set", SetShape.class, Category.AGGREGATE), MAP("map", MapShape.class, Category.AGGREGATE), STRUCTURE("structure", StructureShape.class, Category.AGGREGATE), UNION("union", UnionShape.class, Category.AGGREGATE), - MEMBER("member", MemberShape.class, Category.AGGREGATE), + + MEMBER("member", MemberShape.class, Category.MEMBER), SERVICE("service", ServiceShape.class, Category.SERVICE), RESOURCE("resource", ResourceShape.class, Category.SERVICE), OPERATION("operation", OperationShape.class, Category.SERVICE); - public enum Category { SIMPLE, AGGREGATE, SERVICE, ENUM } + public enum Category { SIMPLE, AGGREGATE, SERVICE, MEMBER } private final String stringValue; private final Class shapeClass; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java index d3d5dd24c67..deb3da8362a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java @@ -247,6 +247,8 @@ private void copySharedParts(Shape source, AbstractShapeBuilder builder) { private Shape copyToSimpleShape(ShapeType to, Shape shape) { if (to.getCategory() != ShapeType.Category.SIMPLE) { throw invalidType(shape, to, "Simple types can only be converted to other simple types."); + } else if (to == ShapeType.ENUM || to == ShapeType.INT_ENUM) { + throw invalidType(shape, to, "Simple types cannot be converted to enum types."); } AbstractShapeBuilder shapeBuilder = to.createBuilderForType(); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java index 79ffc7e4169..0644132ef22 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java @@ -100,7 +100,7 @@ public void changesSimpleShapeTypes(ShapeType start, ShapeType dest, boolean suc private static List simpleTypeTransforms() { Set simpleTypes = new TreeSet<>(); for (ShapeType type : ShapeType.values()) { - if (type.getCategory() == ShapeType.Category.SIMPLE || type.getCategory() == ShapeType.Category.ENUM) { + if (type.getCategory() == ShapeType.Category.SIMPLE) { simpleTypes.add(type); } } @@ -109,7 +109,7 @@ private static List simpleTypeTransforms() { for (ShapeType start : simpleTypes) { for (ShapeType dest : ShapeType.values()) { if (start != dest) { - result.add(Arguments.of(start, dest, dest.getCategory() == ShapeType.Category.SIMPLE)); + result.add(Arguments.of(start, dest, expectedResult(dest))); } } } @@ -117,6 +117,13 @@ private static List simpleTypeTransforms() { return result; } + private static boolean expectedResult(ShapeType dest) { + if (dest == ShapeType.ENUM || dest == ShapeType.INT_ENUM) { + return false; + } + return dest.getCategory() == ShapeType.Category.SIMPLE; + } + @Test public void changesListToSet() { DocumentationTrait docTrait = new DocumentationTrait("Hi"); From 52bb6ef5ec38d29039cc0aae89304650ffd153f2 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 18 Feb 2022 16:00:54 +0100 Subject: [PATCH 23/45] Update enum design doc --- designs/enum-shapes.md | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/designs/enum-shapes.md b/designs/enum-shapes.md index b0f6548b1c7..25e68b47c61 100644 --- a/designs/enum-shapes.md +++ b/designs/enum-shapes.md @@ -1,6 +1,6 @@ # Enum shapes in Smithy -* **Author**: Michael Dowling +* **Authors**: Michael Dowling, Jordon Phillips * **Created**: 2022-02-14 * **Last updated**: 2022-02-14 @@ -163,22 +163,22 @@ making the `@enumValue` of the member 0: ``` intEnum FaceCard { - @enumValue(0) + @enumValue(int: 0) UNKNOWN - @enumValue(1) + @enumValue(int: 1) JACK - @enumValue(2) + @enumValue(int: 2) QUEEN - @enumValue(3) + @enumValue(int: 3) KING - @enumValue(4) + @enumValue(int: 4) ACE - @enumValue(5) + @enumValue(int: 5) JOKER } ``` @@ -187,7 +187,7 @@ intEnum FaceCard { Like enums, intEnums are considered open, meaning it is a backward compatible change to add new members. Previously generated clients MUST NOT fail when -they encounter an unknown intEnum value. Client implementations SHOULD provide +they encounter an unknown intEnum value. Client implementations MUST provide the capability of sending and receiving unknown intEnum values. To facilitate this behavior, the intEnum type is a subclass of the integer @@ -217,15 +217,20 @@ called “member” to go alongside simple, aggregate, and service types. ## Smithy 1.0 → 2.0 -Since the enum trait does not require names, it is not possible to -automatically convert string shapes using the `@enum` trait into enum traits. -Instead, a transformer will be added that upgrades those shapes that can -be upgraded. To make this change backward compatible for existing code -generators, enum shapes in IDL 2.0 will always contain an enum trait with -properties that correspond to the members of the enum. To prevent the -`@enum` trait from being serialized, a synthetic variant will be used. -Additionally, visitors will call their parent shape's visitor method by -default. +The `@enum` trait will be deprecated in IDL 2.0, but not removed. This is to +make it easier to migrate to IDL 2.0. + +The reference implementation will not automatically upgrade strings bearing +the `@enum` trait into enum shapes since it isn't possible to convert enum +traits that don't set the name property. Instead, a transformer will be added +that upgrades those shapes that can be upgraded. + +Additionally, to make this change backwards compatible for exising code +generators, visitors in the reference implementation will call their parent +shape's visitor methods by default. `enum` shapes in the reference +implementation will always contain an `@enum` trait with properties filled +out based on the enum's members and their traits. To prevent this trait +from being serialized, a synthetic variant will be used. ## FAQ From 8e59464a99eb3050175eff4fe1466e47a5f028d9 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 18 Feb 2022 17:01:04 +0100 Subject: [PATCH 24/45] Update enum error messages --- .../transforms/ChangeStringEnumsToEnumShapesTest.java | 2 -- .../smithy/model/traits/synthetic/SyntheticEnumTrait.java | 7 +++++++ .../amazon/smithy/model/transform/ChangeShapeType.java | 2 +- .../model/validation/validators/EnumShapeValidator.java | 4 ++-- .../smithy/model/errorfiles/validators/enum-shapes.errors | 2 +- .../smithy/protocoltests/traits/test-with-appliesto.smithy | 1 - 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java index 6ea5045fb41..aaf2418845b 100644 --- a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java +++ b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java @@ -32,8 +32,6 @@ import software.amazon.smithy.model.traits.UnitTypeTrait; public class ChangeStringEnumsToEnumShapesTest { - - @Test public void canFindEnumsToConvert() { EnumTrait compatibleTrait = EnumTrait.builder() diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java index b850999a8cb..a2f448e75d5 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java @@ -21,6 +21,13 @@ /** * A synthetic copy of the {@link EnumTrait} for use in the {@link EnumShape}. + * + * This exists only to bridge compatibility between IDL 1.0 and 2.0. This + * synthetic trait will be applied to enum shapes so that code generators + * can treat enum shapes as string shapes with the enum trait. We set synthetic + * to true so that it won't get serialized. We change the shape id so that + * it doesn't trip up selector validation for the enum trait, which does + * not allow targeting enum shapes. */ public final class SyntheticEnumTrait extends EnumTrait { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java index deb3da8362a..1a73983c222 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java @@ -248,7 +248,7 @@ private Shape copyToSimpleShape(ShapeType to, Shape shape) { if (to.getCategory() != ShapeType.Category.SIMPLE) { throw invalidType(shape, to, "Simple types can only be converted to other simple types."); } else if (to == ShapeType.ENUM || to == ShapeType.INT_ENUM) { - throw invalidType(shape, to, "Simple types cannot be converted to enum types."); + throw invalidType(shape, to, "This simple type cannot be converted to an enum type."); } AbstractShapeBuilder shapeBuilder = to.createBuilderForType(); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java index a42c83201af..2e6f781d98f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java @@ -31,7 +31,7 @@ import software.amazon.smithy.model.validation.AbstractValidator; import software.amazon.smithy.model.validation.ValidationEvent; -public class EnumShapeValidator extends AbstractValidator { +public final class EnumShapeValidator extends AbstractValidator { private static final Pattern RECOMMENDED_NAME_PATTERN = Pattern.compile("^[A-Z]+[A-Z_0-9]*$"); @Override @@ -55,7 +55,7 @@ private void validateEnumShape(List events, EnumShape shape) { Optional value = member.expectTrait(EnumValueTrait.class).getStringValue(); if (!value.isPresent()) { events.add(error(member, member.expectTrait(EnumValueTrait.class), - "The enumValue trait may only use the string option when applied to enum shapes.")); + "The enumValue trait must use the string option when applied to enum shapes.")); } else if (!values.add(value.get())) { events.add(error(member, String.format( "Multiple enum members found with duplicate value `%s`", diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors index 7ddcb7d7977..7108c09e9f9 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors @@ -1,4 +1,4 @@ -[ERROR] ns.foo#StringEnum$INT_VALUE: The enumValue trait may only use the string option when applied to enum shapes. | EnumShape +[ERROR] ns.foo#StringEnum$INT_VALUE: The enumValue trait must use the string option when applied to enum shapes. | EnumShape [ERROR] ns.foo#StringEnum$DUPLICATE_VALUE: Multiple enum members found with duplicate value `explicit` | EnumShape [WARNING] ns.foo#StringEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape [ERROR] ns.foo#IntEnum$IMPLICIT_VALUE: intEnum members must have the enumValue trait with the `int` member set | EnumShape diff --git a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/test-with-appliesto.smithy b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/test-with-appliesto.smithy index de6d6adcf60..56f56dca66c 100644 --- a/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/test-with-appliesto.smithy +++ b/smithy-protocol-test-traits/src/test/resources/software/amazon/smithy/protocoltests/traits/test-with-appliesto.smithy @@ -1,4 +1,3 @@ - $version: "2.0" namespace smithy.example From 35c18029d6f2e52da8f0baf49f85a0fc6e79a6fa Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 12:15:13 +0100 Subject: [PATCH 25/45] Add ability to synthesize enum names --- .../ChangeStringEnumsToEnumShapes.java | 37 ++++++++++++++-- .../smithy/build/transforms/ChangeTypes.java | 17 +++++++- .../ChangeStringEnumsToEnumShapesTest.java | 36 ++++++++++++++++ .../build/transforms/ChangeTypesTest.java | 43 +++++++++++++++++++ .../amazon/smithy/model/shapes/EnumShape.java | 26 +++++++++-- .../smithy/model/traits/EnumDefinition.java | 40 +++++++++++++++-- .../model/transform/ChangeShapeType.java | 37 +++++++++++++--- .../model/transform/ModelTransformer.java | 36 +++++++++++++++- .../smithy/model/shapes/EnumShapeTest.java | 40 +++++++++++++++++ .../model/transform/ChangeShapeTypeTest.java | 27 ++++++++++++ 10 files changed, 318 insertions(+), 21 deletions(-) diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapes.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapes.java index 66d2deeb89e..eb33129ef23 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapes.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapes.java @@ -15,18 +15,47 @@ package software.amazon.smithy.build.transforms; -import software.amazon.smithy.build.ProjectionTransformer; import software.amazon.smithy.build.TransformContext; import software.amazon.smithy.model.Model; -public final class ChangeStringEnumsToEnumShapes implements ProjectionTransformer { +/** + * {@code changeStringEnumsToEnumShapes} is used to change string shapes + * bearing the enum trait into enum shapes. + */ +public final class ChangeStringEnumsToEnumShapes + extends ConfigurableProjectionTransformer { + + /** + * {@code changeStringEnumsToEnumShapes} configuration settings. + */ + public static final class Config { + private boolean synthesizeNames = false; + + /** + * @param synthesizeNames Whether enums without names should have names synthesized if possible. + */ + public void setSynthesizeNames(boolean synthesizeNames) { + this.synthesizeNames = synthesizeNames; + } + + public boolean getSynthesizeNames() { + return synthesizeNames; + } + } + + @Override + public Class getConfigType() { + return Config.class; + } + @Override public String getName() { return "changeStringEnumsToEnumShapes"; } @Override - public Model transform(TransformContext context) { - return context.getTransformer().changeStringEnumsToEnumShapes(context.getModel()); + protected Model transformWithConfig(TransformContext context, Config config) { + return context.getTransformer().changeStringEnumsToEnumShapes( + context.getModel(), config.getSynthesizeNames()); } } diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java index 38560c68a2e..7297a5a1164 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java @@ -34,6 +34,7 @@ public final class ChangeTypes extends ConfigurableProjectionTransformer shapeTypes = new LinkedHashMap<>(); + private boolean synthesizeEnumNames = false; /** * Sets the map of shape IDs to shape types to set. @@ -48,6 +49,19 @@ public void setShapeTypes(Map shapeTypes) { public Map getShapeTypes() { return shapeTypes; } + + /** + * Sets whether to synthesize names for enums that don't already have them. + * + * @param synthesizeEnumNames Whether to synthesize enum names. + */ + public void setSynthesizeEnumNames(boolean synthesizeEnumNames) { + this.synthesizeEnumNames = synthesizeEnumNames; + } + + public boolean getSynthesizeEnumNames() { + return synthesizeEnumNames; + } } @Override @@ -66,6 +80,7 @@ protected Model transformWithConfig(TransformContext context, Config config) { throw new SmithyBuildException(getName() + ": shapeTypes must not be empty"); } - return context.getTransformer().changeShapeType(context.getModel(), config.getShapeTypes()); + return context.getTransformer().changeShapeType( + context.getModel(), config.getShapeTypes(), config.getSynthesizeEnumNames()); } } diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java index aaf2418845b..443be0b8b39 100644 --- a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java +++ b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeStringEnumsToEnumShapesTest.java @@ -22,6 +22,7 @@ import software.amazon.smithy.build.TransformContext; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; @@ -46,6 +47,7 @@ public void canFindEnumsToConvert() { .addTrait(compatibleTrait) .build(); + // This won't have a name synthesized because that setting is disabled EnumTrait incompatibleTrait = EnumTrait.builder() .addEnum(EnumDefinition.builder() .value("bar") @@ -80,4 +82,38 @@ public void canFindEnumsToConvert() { assertThat(result.expectShape(incompatibleStringId).getType(), Matchers.is(ShapeType.STRING)); assertThat(result.expectShape(incompatibleStringId).members(), Matchers.hasSize(0)); } + + @Test + public void canSynthesizeNames() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("foo:bar") + .build()) + .build(); + ShapeId shapeId = ShapeId.fromParts("ns.foo", "ConvertableShape"); + StringShape initialShape = StringShape.builder() + .id(shapeId) + .addTrait(trait) + .build(); + + Model model = Model.assembler() + .addShape(initialShape) + .assemble().unwrap(); + + ObjectNode config = Node.parse("{\"synthesizeNames\": true}").expectObjectNode(); + TransformContext context = TransformContext.builder() + .model(model) + .settings(config) + .build(); + + Model result = new ChangeStringEnumsToEnumShapes().transform(context); + + assertThat(result.expectShape(shapeId).getType(), Matchers.is(ShapeType.ENUM)); + assertThat(result.expectShape(shapeId).members(), Matchers.hasSize(1)); + assertThat(result.expectShape(shapeId).members().iterator().next(), Matchers.equalTo(MemberShape.builder() + .id(shapeId.withMember("foo_bar")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("foo:bar").build()) + .build())); + } } diff --git a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeTypesTest.java b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeTypesTest.java index cc668253328..65a6722de08 100644 --- a/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeTypesTest.java +++ b/smithy-build/src/test/java/software/amazon/smithy/build/transforms/ChangeTypesTest.java @@ -3,14 +3,21 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import software.amazon.smithy.build.TransformContext; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.shapes.StringShape; import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.EnumDefinition; +import software.amazon.smithy.model.traits.EnumTrait; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.traits.UnitTypeTrait; public class ChangeTypesTest { @Test @@ -29,4 +36,40 @@ public void changesShapeTypes() { assertThat(result.expectShape(ShapeId.from("ns.foo#a")).getType(), is(ShapeType.BOOLEAN)); assertThat(result.expectShape(ShapeId.from("ns.foo#b")).getType(), is(ShapeType.UNION)); } + + @Test + public void canSynthesizeEnumNames() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("foo:bar") + .build()) + .build(); + ShapeId shapeId = ShapeId.fromParts("ns.foo", "ConvertableShape"); + StringShape initialShape = StringShape.builder() + .id(shapeId) + .addTrait(trait) + .build(); + + Model model = Model.assembler() + .addShape(initialShape) + .assemble().unwrap(); + + ObjectNode settings = Node.objectNode() + .withMember("shapeTypes", Node.objectNode().withMember(shapeId.toString(), "enum")) + .withMember("synthesizeEnumNames", true); + TransformContext context = TransformContext.builder() + .model(model) + .settings(settings) + .build(); + + Model result = new ChangeTypes().transform(context); + + assertThat(result.expectShape(shapeId).getType(), Matchers.is(ShapeType.ENUM)); + assertThat(result.expectShape(shapeId).members(), Matchers.hasSize(1)); + assertThat(result.expectShape(shapeId).members().iterator().next(), Matchers.equalTo(MemberShape.builder() + .id(shapeId.withMember("foo_bar")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("foo:bar").build()) + .build())); + } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index 6f4bd11b924..fba187d7396 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -130,9 +130,10 @@ public Optional asEnumShape() { * or if the enum definitions don't have names. * * @param shape A base {@link StringShape} to convert. + * @param synthesizeNames Whether names should be synthesized if possible. * @return Optionally returns an {@link EnumShape} equivalent of the given shape. */ - public static Optional fromStringShape(StringShape shape) { + public static Optional fromStringShape(StringShape shape, boolean synthesizeNames) { if (!shape.hasTrait(EnumTrait.ID)) { return Optional.empty(); } @@ -140,12 +141,25 @@ public static Optional fromStringShape(StringShape shape) { Builder enumBuilder = EnumShape.builder(); stringWithoutEnumTrait.updateBuilder(enumBuilder); try { - return Optional.of(enumBuilder.members(shape.expectTrait(EnumTrait.class)).build()); + return Optional.of(enumBuilder.members(shape.expectTrait(EnumTrait.class), synthesizeNames).build()); } catch (IllegalStateException e) { return Optional.empty(); } } + /** + * Converts a base {@link StringShape} to an {@link EnumShape} if possible. + * + * The result will be empty if the given shape doesn't have the {@link EnumTrait} + * or if the enum definitions don't have names. + * + * @param shape A base {@link StringShape} to convert. + * @return Optionally returns an {@link EnumShape} equivalent of the given shape. + */ + public static Optional fromStringShape(StringShape shape) { + return fromStringShape(shape, false); + } + @Override public ShapeType getType() { return ShapeType.ENUM; @@ -190,14 +204,14 @@ public Builder id(ShapeId shapeId) { return this; } - public Builder members(EnumTrait trait) { + public Builder members(EnumTrait trait, boolean synthesizeNames) { if (getId() == null) { throw new IllegalStateException("An id must be set before adding a named enum trait to a string."); } clearMembers(); for (EnumDefinition definition : trait.getValues()) { - Optional member = definition.asMember(getId()); + Optional member = definition.asMember(getId(), synthesizeNames); if (member.isPresent()) { addMember(member.get()); } else { @@ -211,6 +225,10 @@ public Builder members(EnumTrait trait) { return this; } + public Builder members(EnumTrait trait) { + return members(trait, false); + } + /** * Replaces the members of the builder. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java index 974463d7fe6..73216429dca 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.regex.Pattern; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.node.StringNode; @@ -42,6 +43,8 @@ public final class EnumDefinition implements ToNode, ToSmithyBuilder tags; @@ -114,15 +117,21 @@ public static EnumDefinition fromNode(Node node) { /** * Converts an enum definition to the equivalent enum member shape. * - * This is only possible if the enum definition has a name. - * * @param parentId The {@link ShapeId} of the enum shape. + * @param synthesizeName Whether to synthesize a name if possible. * @return An optional member shape representing the enum definition, * or empty if conversion is impossible. */ - public Optional asMember(ShapeId parentId) { + public Optional asMember(ShapeId parentId, boolean synthesizeName) { + String name; if (!getName().isPresent()) { - return Optional.empty(); + if (canConvertToMember(synthesizeName)) { + name = getValue().replaceAll("[-.:/\\s]+", "_"); + } else { + return Optional.empty(); + } + } else { + name = getName().get(); } try { @@ -145,6 +154,29 @@ public Optional asMember(ShapeId parentId) { } } + /** + * Converts an enum definition to the equivalent enum member shape. + * + * This is only possible if the enum definition has a name. + * + * @param parentId The {@link ShapeId} of the enum shape. + * @return An optional member shape representing the enum definition, + * or empty if conversion is impossible. + */ + public Optional asMember(ShapeId parentId) { + return asMember(parentId, false); + } + + /** + * Determines whether the definition can be converted to a member. + * + * @param withSynthesizedNames Whether to account for name synthesization. + * @return Returns true if the definition can be converted. + */ + public boolean canConvertToMember(boolean withSynthesizedNames) { + return getName().isPresent() || (withSynthesizedNames && CONVERTABLE_VALUE.matcher(getValue()).find()); + } + /** * Converts an enum member into an equivalent enum definition object. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java index 1a73983c222..81c11cdb8b2 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java @@ -44,32 +44,53 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; final class ChangeShapeType { private final Map shapeToType; + private final boolean synthesizeEnumNames; - ChangeShapeType(Map shapeToType) { + ChangeShapeType(Map shapeToType, boolean synthesizeEnumNames) { this.shapeToType = shapeToType; + this.synthesizeEnumNames = synthesizeEnumNames; + } + + ChangeShapeType(Map shapeToType) { + this(shapeToType, false); } - static ChangeShapeType upgradeEnums(Model model) { + static ChangeShapeType upgradeEnums(Model model, boolean synthesizeEnumNames) { Map toUpdate = new HashMap<>(); for (StringShape shape: model.getStringShapesWithTrait(EnumTrait.class)) { EnumTrait trait = shape.expectTrait(EnumTrait.class); - if (trait.getValues().iterator().next().getName().isPresent()) { + if (canUpgradeEnum(trait, synthesizeEnumNames)) { toUpdate.put(shape.getId(), ShapeType.ENUM); } } - return new ChangeShapeType(toUpdate); + return new ChangeShapeType(toUpdate, synthesizeEnumNames); + } + + private static boolean canUpgradeEnum(EnumTrait trait, boolean synthesizeEnumNames) { + if (!synthesizeEnumNames && trait.getValues().iterator().next().getName().isPresent()) { + return true; + } + + for (EnumDefinition definition : trait.getValues()) { + if (!definition.canConvertToMember(synthesizeEnumNames)) { + return false; + } + } + + return true; } Model transform(ModelTransformer transformer, Model model) { return transformer.mapShapes(model, shape -> { if (shapeToType.containsKey(shape.getId())) { - return shape.accept(new Retype(shapeToType.get(shape.getId()))); + return shape.accept(new Retype(shapeToType.get(shape.getId()), synthesizeEnumNames)); } else { return shape; } @@ -78,9 +99,11 @@ Model transform(ModelTransformer transformer, Model model) { private static final class Retype extends ShapeVisitor.Default { private final ShapeType to; + private final boolean synthesizeEnumNames; - Retype(ShapeType to) { + Retype(ShapeType to, boolean synthesizeEnumNames) { this.to = to; + this.synthesizeEnumNames = synthesizeEnumNames; } @Override @@ -157,7 +180,7 @@ public Shape bigDecimalShape(BigDecimalShape shape) { @Override public Shape stringShape(StringShape shape) { if (to == ShapeType.ENUM) { - Optional enumShape = EnumShape.fromStringShape(shape); + Optional enumShape = EnumShape.fromStringShape(shape, synthesizeEnumNames); if (enumShape.isPresent()) { return enumShape.get(); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java index dd0ed4edbc3..747d7ab87fe 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java @@ -481,6 +481,40 @@ public Model changeShapeType(Model model, Map shapeToType) { return new ChangeShapeType(shapeToType).transform(this, model); } + /** + * Changes the type of each given shape. + * + *

The following transformations are permitted: + * + *

    + *
  • Any simple type to any simple type
  • + *
  • List to set
  • + *
  • Set to list
  • + *
  • Structure to union
  • + *
  • Union to structure
  • + *
+ * + * @param model Model to transform. + * @param shapeToType Map of shape IDs to the new type to use for the shape. + * @param synthesizeEnumNames Whether enums without names should have names synthesized if possible. + * @return Returns the transformed model. + * @throws ModelTransformException if an incompatible type transform is attempted. + */ + public Model changeShapeType(Model model, Map shapeToType, boolean synthesizeEnumNames) { + return new ChangeShapeType(shapeToType, synthesizeEnumNames).transform(this, model); + } + + /** + * Changes each compatible string shape with the enum trait to an enum shape. + * + * @param model Model to transform. + * @param synthesizeEnumNames Whether enums without names should have names synthesized if possible. + * @return Returns the transformed model. + */ + public Model changeStringEnumsToEnumShapes(Model model, boolean synthesizeEnumNames) { + return ChangeShapeType.upgradeEnums(model, synthesizeEnumNames).transform(this, model); + } + /** * Changes each compatible string shape with the enum trait to an enum shape. * @@ -490,7 +524,7 @@ public Model changeShapeType(Model model, Map shapeToType) { * @return Returns the transformed model. */ public Model changeStringEnumsToEnumShapes(Model model) { - return ChangeShapeType.upgradeEnums(model).transform(this, model); + return ChangeShapeType.upgradeEnums(model, false).transform(this, model); } /** diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index fc8dc0cec6f..95babdb9bb6 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -217,6 +217,46 @@ public void givenEnumTraitMustUseNames() { }); } + @Test + public void givenEnumTraitMaySynthesizeNames() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("foo:bar") + .build()) + .build(); + EnumShape shape = builder.members(trait, true).build(); + + assertEquals(shape.getMember("foo_bar").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo_bar")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("foo:bar").build()) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + + EnumDefinition expectedDefinition = EnumDefinition.builder() + .name("foo_bar") + .value("foo:bar") + .build(); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of(expectedDefinition)); + } + + @Test + public void givenEnumTraitMayOnlySynthesizeNamesFromValidValues() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("foo&bar") + .build()) + .build(); + + Assertions.assertThrows(IllegalStateException.class, () -> { + builder.members(trait, true); + }); + } + @Test public void addMultipleMembers() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java index 0644132ef22..ba775535baf 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java @@ -391,4 +391,31 @@ public void canFindEnumsToConvert() { assertThat(result.expectShape(incompatibleStringId).getType(), Matchers.is(ShapeType.STRING)); assertThat(result.expectShape(incompatibleStringId).members(), Matchers.hasSize(0)); } + + @Test + public void canSynthesizeEnumNames() { + EnumTrait trait = EnumTrait.builder() + .addEnum(EnumDefinition.builder() + .value("foo:bar") + .build()) + .build(); + ShapeId shapeId = ShapeId.fromParts("ns.foo", "ConvertableShape"); + StringShape initialShape = StringShape.builder() + .id(shapeId) + .addTrait(trait) + .build(); + + Model model = Model.assembler() + .addShape(initialShape) + .assemble().unwrap(); + Model result = ModelTransformer.create().changeStringEnumsToEnumShapes(model, true); + + assertThat(result.expectShape(shapeId).getType(), Matchers.is(ShapeType.ENUM)); + assertThat(result.expectShape(shapeId).members(), Matchers.hasSize(1)); + assertThat(result.expectShape(shapeId).members().iterator().next(), Matchers.equalTo(MemberShape.builder() + .id(shapeId.withMember("foo_bar")) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue("foo:bar").build()) + .build())); + } } From d26d24dc6614ad50536977b4c48a9061aeb83a31 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 12:29:33 +0100 Subject: [PATCH 26/45] Warn when a string can't be converted to enum --- .../model/transform/ChangeShapeType.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java index 81c11cdb8b2..cd9e9aca760 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.logging.Logger; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.AbstractShapeBuilder; import software.amazon.smithy.model.shapes.BigDecimalShape; @@ -50,6 +51,8 @@ final class ChangeShapeType { + private static final Logger LOGGER = Logger.getLogger(ChangeShapeType.class.getName()); + private final Map shapeToType; private final boolean synthesizeEnumNames; @@ -65,21 +68,31 @@ final class ChangeShapeType { static ChangeShapeType upgradeEnums(Model model, boolean synthesizeEnumNames) { Map toUpdate = new HashMap<>(); for (StringShape shape: model.getStringShapesWithTrait(EnumTrait.class)) { - EnumTrait trait = shape.expectTrait(EnumTrait.class); - if (canUpgradeEnum(trait, synthesizeEnumNames)) { + if (canUpgradeEnum(shape, synthesizeEnumNames)) { toUpdate.put(shape.getId(), ShapeType.ENUM); } } return new ChangeShapeType(toUpdate, synthesizeEnumNames); } - private static boolean canUpgradeEnum(EnumTrait trait, boolean synthesizeEnumNames) { + private static boolean canUpgradeEnum(StringShape shape, boolean synthesizeEnumNames) { + EnumTrait trait = shape.expectTrait(EnumTrait.class); if (!synthesizeEnumNames && trait.getValues().iterator().next().getName().isPresent()) { + LOGGER.warning(String.format( + "Unable to convert string shape `%s` to enum shape because it doesn't define names. The " + + "`synthesizeNames` option may be able to synthesize the names for you.", + shape.getId() + )); return true; } for (EnumDefinition definition : trait.getValues()) { if (!definition.canConvertToMember(synthesizeEnumNames)) { + LOGGER.warning(String.format( + "Unable to convert string shape `%s` to enum shape because it has at least one value which " + + "cannot be safely synthesized into a name: %s", + shape.getId(), definition.getValue() + )); return false; } } From a374018dea3e76fc700736873e795d1ebe261620 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 12:49:45 +0100 Subject: [PATCH 27/45] Add isShapeType method to ShapeType --- .../model/selector/ShapeTypeSelector.java | 4 +-- .../amazon/smithy/model/shapes/ShapeType.java | 33 ++++++++++++++++--- .../smithy/model/shapes/ShapeTypeTest.java | 11 +++++++ 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java index aa37c7dae40..7f6b34e8c96 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/selector/ShapeTypeSelector.java @@ -31,9 +31,7 @@ final class ShapeTypeSelector implements InternalSelector { @Override public boolean push(Context ctx, Shape shape, Receiver next) { - if (shape.getType() == shapeType - || (shapeType == ShapeType.STRING && shape.getType() == ShapeType.ENUM) - || (shapeType == ShapeType.INTEGER && shape.getType() == ShapeType.INT_ENUM)) { + if (shape.getType().isShapeType(shapeType)) { return next.apply(ctx, shape); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java index 47adfbfcdef..5dd656b6021 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java @@ -16,6 +16,7 @@ package software.amazon.smithy.model.shapes; import java.util.Optional; +import java.util.function.BiFunction; /** An enumeration of the different types in a model. */ public enum ShapeType { @@ -33,8 +34,9 @@ public enum ShapeType { DOUBLE("double", DoubleShape.class, Category.SIMPLE), BIG_DECIMAL("bigDecimal", BigDecimalShape.class, Category.SIMPLE), BIG_INTEGER("bigInteger", BigIntegerShape.class, Category.SIMPLE), - ENUM("enum", EnumShape.class, Category.SIMPLE), - INT_ENUM("intEnum", IntEnumShape.class, Category.SIMPLE), + + ENUM("enum", EnumShape.class, Category.SIMPLE, (a, b) -> b.equals(a) || b.equals(STRING)), + INT_ENUM("intEnum", IntEnumShape.class, Category.SIMPLE, (a, b) -> b.equals(a) || b.equals(INTEGER)), LIST("list", ListShape.class, Category.AGGREGATE), SET("set", SetShape.class, Category.AGGREGATE), @@ -53,11 +55,22 @@ public enum Category { SIMPLE, AGGREGATE, SERVICE, MEMBER } private final String stringValue; private final Class shapeClass; private final Category category; + private final BiFunction isShapeType; + + ShapeType(String stringValue, Class shapeClass, Category category) { + this(stringValue, shapeClass, category, Enum::equals); + } - ShapeType(String stringValue, Class shapeClass, Category categry) { + ShapeType( + String stringValue, + Class shapeClass, + Category category, + BiFunction isShapeType + ) { this.stringValue = stringValue; this.shapeClass = shapeClass; - this.category = categry; + this.category = category; + this.isShapeType = isShapeType; } @Override @@ -84,6 +97,18 @@ public Category getCategory() { return category; } + /** + * Returns whether this shape type is equivalent to the given shape type. + * + * This accounts for things like enums being considered specializations of strings. + * + * @param other The other shape type to compare against. + * @return Returns true if the shape types are equivalent. + */ + public boolean isShapeType(ShapeType other) { + return isShapeType.apply(this, other); + } + /** * Create a new Shape.Type from a string. * diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/ShapeTypeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/ShapeTypeTest.java index 38b7856b72e..6e9307f49ef 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/ShapeTypeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/ShapeTypeTest.java @@ -1,6 +1,8 @@ package software.amazon.smithy.model.shapes; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; @@ -20,4 +22,13 @@ public void hasCategory() { assertThat(ShapeType.LIST.getCategory(), Matchers.is(ShapeType.Category.AGGREGATE)); assertThat(ShapeType.SERVICE.getCategory(), Matchers.is(ShapeType.Category.SERVICE)); } + + @Test + public void enumEquivalence() { + assertTrue(ShapeType.ENUM.isShapeType(ShapeType.STRING)); + assertFalse(ShapeType.STRING.isShapeType(ShapeType.ENUM)); + + assertTrue(ShapeType.INT_ENUM.isShapeType(ShapeType.INTEGER)); + assertFalse(ShapeType.INTEGER.isShapeType(ShapeType.INT_ENUM)); + } } From f8f6efa9652abcef51685754f39bcd4b0f01600c Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 12:57:04 +0100 Subject: [PATCH 28/45] Add isShapeTypeSupported to Version --- .../model/loader/AbstractMutableModelFile.java | 3 +-- .../amazon/smithy/model/loader/Version.java | 13 +++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java index 833ac65e2ad..be05547cc64 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/AbstractMutableModelFile.java @@ -83,8 +83,7 @@ public final Version getVersion() { void onShape(AbstractShapeBuilder builder) { allShapeIds.add(builder.getId()); - if ((builder.getShapeType() == ShapeType.ENUM || builder.getShapeType() == ShapeType.INT_ENUM) - && !getVersion().supportsEnumShapes()) { + if (!getVersion().isShapeTypeSupported(builder.getShapeType())) { throw new SourceException(String.format( "%s shapes may only be used with Smithy version 2 or later.", builder.getShapeType().toString()), builder.getSourceLocation()); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java index a0f9d0aa570..954ba946e4e 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/Version.java @@ -17,6 +17,7 @@ import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeType; import software.amazon.smithy.model.traits.MixinTrait; /** @@ -79,12 +80,16 @@ boolean supportsInlineOperationIO() { } /** - * Checks if this version supports enum and intEnum shapes. + * Checks if the given shape type is supported in this version. * - * @return Returns true if this version supports the enum and intEnum shapes. + * @param shapeType The shape type to check. + * @return Returns true if the shape type is supported in this version. */ - boolean supportsEnumShapes() { - return this == VERSION_2_0; + boolean isShapeTypeSupported(ShapeType shapeType) { + if (this == VERSION_2_0) { + return true; + } + return shapeType != ShapeType.ENUM && shapeType != ShapeType.INT_ENUM; } /** From d0f6af2e42837cdd7074dbc32253b98ed3f57662 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 13:07:39 +0100 Subject: [PATCH 29/45] Fix typo: targetsUnion -> targetsUnit --- .../amazon/smithy/model/loader/IdlModelParser.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java index 50f7aaaf36e..9b42b42ad9d 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/IdlModelParser.java @@ -485,7 +485,7 @@ private void parseMembers(ShapeId id, Set requiredMembers) { parseMembers(id, requiredMembers, false); } - private void parseMembers(ShapeId id, Set requiredMembers, boolean targetsUnion) { + private void parseMembers(ShapeId id, Set requiredMembers, boolean targetsUnit) { Set definedMembers = new HashSet<>(); ws(); @@ -497,7 +497,7 @@ private void parseMembers(ShapeId id, Set requiredMembers, boolean targe break; } - parseMember(id, requiredMembers, definedMembers, targetsUnion); + parseMember(id, requiredMembers, definedMembers, targetsUnit); // Clears out any previously captured documentation // comments that may have been found when parsing the member. @@ -513,7 +513,7 @@ private void parseMembers(ShapeId id, Set requiredMembers, boolean targe expect('}'); } - private void parseMember(ShapeId parent, Set allowed, Set defined, boolean targetsUnion) { + private void parseMember(ShapeId parent, Set allowed, Set defined, boolean targetsUnit) { // Parse optional member traits. List memberTraits = parseDocsAndTraits(); SourceLocation memberLocation = currentLocation(); @@ -536,7 +536,7 @@ private void parseMember(ShapeId parent, Set allowed, Set define modelFile.onShape(memberBuilder); String target; - if (!targetsUnion) { + if (!targetsUnit) { ws(); expect(':'); From dc67dafeb272d1fdbbf6580f9fe2a5d7c2f13f7a Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 13:14:09 +0100 Subject: [PATCH 30/45] Update ModelAssembler docs for unparsed shapes --- .../software/amazon/smithy/model/loader/ModelAssembler.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java index 16fd24f121f..ff86c10def0 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/loader/ModelAssembler.java @@ -372,6 +372,8 @@ public ModelAssembler addShapes(Shape... shapes) { * Sets the Smithy version to use for parsed shapes added directly to the * assembler. * + * If unset, the default version of 1.0 will be assumed. + * * @param version A Smithy IDL version. * @return Returns the assembler. */ From 3725c1f039256c6cbb899dfb85e855be60e0ad92 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 13:18:32 +0100 Subject: [PATCH 31/45] Remove unnecessary members duplication in idl ser --- .../model/shapes/SmithyIdlModelSerializer.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java index 313e5187a3d..d0e667dda5c 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/SmithyIdlModelSerializer.java @@ -398,11 +398,11 @@ protected Void getDefault(Shape shape) { return null; } - private void shapeWithMembers(Shape shape, List members) { + private void shapeWithMembers(Shape shape, Collection members) { shapeWithMembers(shape, members, false); } - private void shapeWithMembers(Shape shape, List members, boolean isEnum) { + private void shapeWithMembers(Shape shape, Collection members, boolean isEnum) { List nonMixinMembers = new ArrayList<>(); List mixinMembers = new ArrayList<>(); for (MemberShape member : members) { @@ -543,13 +543,13 @@ private boolean isEmptyStructure(Node node, Shape shape) { @Override public Void enumShape(EnumShape shape) { - shapeWithMembers(shape, new ArrayList<>(shape.members()), true); + shapeWithMembers(shape, shape.members(), true); return null; } @Override public Void intEnumShape(IntEnumShape shape) { - shapeWithMembers(shape, new ArrayList<>(shape.members()), true); + shapeWithMembers(shape, shape.members(), true); return null; } @@ -573,13 +573,13 @@ public Void mapShape(MapShape shape) { @Override public Void structureShape(StructureShape shape) { - shapeWithMembers(shape, new ArrayList<>(shape.getAllMembers().values())); + shapeWithMembers(shape, shape.members()); return null; } @Override public Void unionShape(UnionShape shape) { - shapeWithMembers(shape, new ArrayList<>(shape.getAllMembers().values())); + shapeWithMembers(shape, shape.members()); return null; } From 879e2bbd5225d3b0cdd7cbe997a41954fe246d73 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 13:27:04 +0100 Subject: [PATCH 32/45] Re-privatize enum definitions --- .../amazon/smithy/model/traits/EnumTrait.java | 11 ++++++++++- .../model/traits/synthetic/SyntheticEnumTrait.java | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) 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 61a03b05a8b..f8a8a18e1b2 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 @@ -21,17 +21,26 @@ 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.EnumShape; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; import software.amazon.smithy.utils.BuilderRef; import software.amazon.smithy.utils.ToSmithyBuilder; /** * Constrains string values to one of the predefined enum constants. + * + * This trait is deprecated, use an {@link EnumShape} instead. + * + * There is also the {@link SyntheticEnumTrait}, which is a synthetic variant of this + * trait used exclusively to assist in making {@link EnumShape} as backwards compatible + * as possible. */ +@Deprecated public class EnumTrait extends AbstractTrait implements ToSmithyBuilder { public static final ShapeId ID = ShapeId.from("smithy.api#enum"); - protected final List definitions; + private final List definitions; protected EnumTrait(ShapeId id, Builder builder) { super(id, builder.sourceLocation); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java index a2f448e75d5..080c6912151 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/synthetic/SyntheticEnumTrait.java @@ -45,7 +45,7 @@ public boolean isSynthetic() { @Override public Builder toBuilder() { Builder builder = (Builder) builder().sourceLocation(getSourceLocation()); - definitions.forEach(builder::addEnum); + getValues().forEach(builder::addEnum); return builder; } From 4d38efd4567c0aa337e6ca1501e64b2d42675a95 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 13:30:55 +0100 Subject: [PATCH 33/45] Add override annotations for NamedMembers methods --- .../java/software/amazon/smithy/model/shapes/EnumShape.java | 3 +++ .../software/amazon/smithy/model/shapes/IntEnumShape.java | 5 ++++- .../software/amazon/smithy/model/shapes/NamedMembers.java | 4 +--- .../amazon/smithy/model/shapes/NamedMembersShape.java | 3 +++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index fba187d7396..d4124ad09d1 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -50,6 +50,7 @@ private EnumShape(Builder builder) { * * @return Returns the immutable member map. */ + @Override public Map getAllMembers() { return members; } @@ -60,6 +61,7 @@ public Map getAllMembers() { * * @return Returns an immutable list of member names. */ + @Override public List getMemberNames() { List names = memberNames; if (names == null) { @@ -95,6 +97,7 @@ public Optional findTrait(ShapeId id) { * @param name Name of the member to retrieve. * @return Returns the optional member. */ + @Override public Optional getMember(String name) { return Optional.ofNullable(members.get(name)); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java index dbe4745b9b6..e3b74e42039 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -26,7 +26,7 @@ import software.amazon.smithy.utils.BuilderRef; import software.amazon.smithy.utils.ListUtils; -public final class IntEnumShape extends IntegerShape { +public final class IntEnumShape extends IntegerShape implements NamedMembers { private final Map members; private volatile List memberNames; @@ -46,6 +46,7 @@ private IntEnumShape(Builder builder) { * * @return Returns the immutable member map. */ + @Override public Map getAllMembers() { return members; } @@ -56,6 +57,7 @@ public Map getAllMembers() { * * @return Returns an immutable list of member names. */ + @Override public List getMemberNames() { List names = memberNames; if (names == null) { @@ -83,6 +85,7 @@ public boolean equals(Object other) { * @param name Name of the member to retrieve. * @return Returns the optional member. */ + @Override public Optional getMember(String name) { return Optional.ofNullable(members.get(name)); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java index c3f4ad7b47c..59a82f5898c 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java @@ -42,7 +42,5 @@ interface NamedMembers { * @param name Name of the member to retrieve. * @return Returns the optional member. */ - default Optional getMember(String name) { - return Optional.ofNullable(getAllMembers().get(name)); - } + Optional getMember(String name); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShape.java index b8f0c94bde4..20d2240fbb9 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembersShape.java @@ -46,6 +46,7 @@ public abstract class NamedMembersShape extends Shape implements NamedMembers { * * @return Returns the immutable member map. */ + @Override public Map getAllMembers() { return members; } @@ -56,6 +57,7 @@ public Map getAllMembers() { * * @return Returns an immutable list of member names. */ + @Override public List getMemberNames() { List names = memberNames; if (names == null) { @@ -72,6 +74,7 @@ public List getMemberNames() { * @param name Name of the member to retrieve. * @return Returns the optional member. */ + @Override public Optional getMember(String name) { return Optional.ofNullable(members.get(name)); } From 2dc22b8cada081e493fd5c80645ad9e2af03c291 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 13:42:15 +0100 Subject: [PATCH 34/45] Add getEnumValues methods to enum shapes This make it a bit easier when all you care about is the name value pair. --- .../amazon/smithy/model/shapes/EnumShape.java | 19 +++++++++++++++++++ .../smithy/model/shapes/IntEnumShape.java | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index d4124ad09d1..5521ce60250 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -16,6 +16,7 @@ package software.amazon.smithy.model.shapes; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -29,11 +30,13 @@ import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; import software.amazon.smithy.utils.BuilderRef; import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; public final class EnumShape extends StringShape implements NamedMembers { private final Map members; private volatile List memberNames; + private volatile Map enumValues; private EnumShape(Builder builder) { super(builder); @@ -45,6 +48,22 @@ private EnumShape(Builder builder) { } } + /** + * Gets a map of enum member names to their corresponding values. + * + * @return A map of member names to enum values. + */ + public Map getEnumValues() { + if (enumValues == null) { + Map values = new LinkedHashMap<>(members.size()); + for (MemberShape member : members()) { + values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).getStringValue().get()); + } + enumValues = MapUtils.orderedCopyOf(values); + } + return enumValues; + } + /** * Gets the members of the shape, including mixin members. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java index e3b74e42039..eb37e850a7f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -16,6 +16,7 @@ package software.amazon.smithy.model.shapes; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -25,11 +26,13 @@ import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.utils.BuilderRef; import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; public final class IntEnumShape extends IntegerShape implements NamedMembers { private final Map members; private volatile List memberNames; + private volatile Map enumValues; private IntEnumShape(Builder builder) { super(builder); @@ -41,6 +44,22 @@ private IntEnumShape(Builder builder) { } } + /** + * Gets a map of enum member names to their corresponding values. + * + * @return A map of member names to enum values. + */ + public Map getEnumValues() { + if (enumValues == null) { + Map values = new LinkedHashMap<>(members.size()); + for (MemberShape member : members()) { + values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).getIntValue().get()); + } + enumValues = MapUtils.orderedCopyOf(values); + } + return enumValues; + } + /** * Gets the members of the shape, including mixin members. * From 5dbad8b760edc489e00d631a5ca406d2a0ecd66f Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 16:45:35 +0100 Subject: [PATCH 35/45] Add enumDefault trait --- designs/enum-shapes.md | 18 +++++-- .../amazon/smithy/model/shapes/EnumShape.java | 43 ++++++++++++++- .../smithy/model/shapes/IntEnumShape.java | 41 ++++++++++++++- .../smithy/model/shapes/NamedMembers.java | 2 +- .../smithy/model/traits/EnumDefaultTrait.java | 52 +++++++++++++++++++ .../smithy/model/traits/EnumDefinition.java | 8 +-- .../smithy/model/traits/EnumValueTrait.java | 38 ++++++++++++++ .../validators/EnumShapeValidator.java | 29 +++++++++-- ...xclusiveStructureMemberTraitValidator.java | 16 +++--- ...re.amazon.smithy.model.traits.TraitService | 1 + .../amazon/smithy/model/loader/prelude.smithy | 16 ++++-- .../smithy/model/shapes/EnumShapeTest.java | 35 +++++++++++++ .../smithy/model/shapes/IntEnumShapeTest.java | 27 ++++++++++ .../errorfiles/validators/enum-shapes.errors | 4 ++ .../errorfiles/validators/enum-shapes.smithy | 20 +++++++ .../event-payload-validation.errors | 4 +- ...e-structure-member-traits-validator.errors | 6 +-- .../http-request-response-validator.errors | 2 +- .../validators/request-token.errors | 2 +- .../smithy/model/loader/valid/enums.json | 8 +-- .../smithy/model/loader/valid/enums.smithy | 4 +- 21 files changed, 332 insertions(+), 44 deletions(-) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java diff --git a/designs/enum-shapes.md b/designs/enum-shapes.md index 25e68b47c61..b057127ee4f 100644 --- a/designs/enum-shapes.md +++ b/designs/enum-shapes.md @@ -81,12 +81,12 @@ structure GetFooInput { } ``` -To account for this, enums MAY define a default member by making the -`@enumValue` of the member "": +To account for this, enums MAY define a default member by setting the +`@enumDefault` trait on a member: ``` enum Suit { - @enumValue(string: "") + @enumDefault UNKNOWN @enumValue(string: "diamond") @@ -121,6 +121,10 @@ structure GetFooInput { } ``` +#### enums must define at least one member + +Every enum shape MUST define at least one member. + ### intEnum shape An intEnum is used to represent an enumerated set of integer values. The @@ -159,11 +163,11 @@ structure GetFooInput { ``` intEnums MAY define a member to represent the default value of the shape by -making the `@enumValue` of the member 0: +setting the `@enumDefault` trait on a member: ``` intEnum FaceCard { - @enumValue(int: 0) + @enumDefault UNKNOWN @enumValue(int: 1) @@ -198,6 +202,10 @@ default dispatch to the integer shape when an intEnum is encountered. This both makes adding enums backward compatible, and allows implementations that do not support enums at all to ignore them. +#### intEnums must define at least one member + +Every intEnum shape MUST define at least one member. + ### Smithy taxonomy updates Both enum and intEnum are considered simple shapes though they have members. diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index 5521ce60250..a814022315c 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.function.Consumer; import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.EnumValueTrait; @@ -57,7 +58,11 @@ public Map getEnumValues() { if (enumValues == null) { Map values = new LinkedHashMap<>(members.size()); for (MemberShape member : members()) { - values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).getStringValue().get()); + if (member.hasTrait(EnumDefaultTrait.ID)) { + values.put(member.getMemberName(), ""); + continue; + } + values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).expectStringValue()); } enumValues = MapUtils.orderedCopyOf(values); } @@ -282,7 +287,7 @@ public Builder addMember(MemberShape member) { "Enum members may only target `smithy.api#Unit`, but found `%s`", member.getTarget() ), getSourceLocation()); } - if (!member.hasTrait(EnumValueTrait.ID)) { + if (!member.hasTrait(EnumValueTrait.ID) && !member.hasTrait(EnumDefaultTrait.ID)) { member = member.toBuilder() .addTrait(EnumValueTrait.builder().stringValue(member.getMemberName()).build()) .build(); @@ -328,6 +333,40 @@ public Builder addMember(String memberName, String enumValue, Consumer memberUpdater) { + if (getId() == null) { + throw new IllegalStateException("An id must be set before setting a member with a target"); + } + + MemberShape.Builder builder = MemberShape.builder() + .target(UnitTypeTrait.UNIT) + .id(getId().withMember(memberName)) + .addTrait(new EnumDefaultTrait()); + + if (memberUpdater != null) { + memberUpdater.accept(builder); + } + + return addMember(builder.build()); + } + /** * Removes a member by name. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java index eb37e850a7f..293b54d6d0f 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.function.Consumer; import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.utils.BuilderRef; @@ -53,7 +54,11 @@ public Map getEnumValues() { if (enumValues == null) { Map values = new LinkedHashMap<>(members.size()); for (MemberShape member : members()) { - values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).getIntValue().get()); + if (member.hasTrait(EnumDefaultTrait.ID)) { + values.put(member.getMemberName(), 0); + continue; + } + values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).expectIntValue()); } enumValues = MapUtils.orderedCopyOf(values); } @@ -235,6 +240,40 @@ public Builder addMember(String memberName, int enumValue, Consumer memberUpdater) { + if (getId() == null) { + throw new IllegalStateException("An id must be set before setting a member with a target"); + } + + MemberShape.Builder builder = MemberShape.builder() + .target(UnitTypeTrait.UNIT) + .id(getId().withMember(memberName)) + .addTrait(new EnumDefaultTrait()); + + if (memberUpdater != null) { + memberUpdater.accept(builder); + } + + return addMember(builder.build()); + } + /** * Removes a member by name. * diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java index 59a82f5898c..f5ca4b954c3 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java @@ -19,7 +19,7 @@ import java.util.Map; import java.util.Optional; -interface NamedMembers { +public interface NamedMembers { /** * Gets the members of the shape, including mixin members. diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java new file mode 100644 index 00000000000..22372178306 --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.traits; + +import java.util.Collections; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.ShapeId; + +/** + * Indicates that an enum member is the default value member. + * + * On an {@link IntEnumShape} this implies the enum value is 0. On an + * {@link EnumShape} this implies the enum value is an empty string. + */ +public class EnumDefaultTrait extends AnnotationTrait { + public static final ShapeId ID = ShapeId.from("smithy.api#enumDefault"); + + public EnumDefaultTrait(ObjectNode node) { + super(ID, node); + } + + public EnumDefaultTrait(SourceLocation location) { + this(new ObjectNode(Collections.emptyMap(), location)); + } + + public EnumDefaultTrait() { + this(Node.objectNode()); + } + + public static final class Provider extends AnnotationTrait.Provider { + public Provider() { + super(ID, EnumDefaultTrait::new); + } + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java index 73216429dca..c97ed7684d0 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java @@ -186,9 +186,11 @@ public boolean canConvertToMember(boolean withSynthesizedNames) { public static EnumDefinition fromMember(MemberShape member) { EnumDefinition.Builder builder = EnumDefinition.builder().name(member.getMemberName()); - EnumValueTrait valueTrait = member.expectTrait(EnumValueTrait.class); - if (valueTrait.getStringValue().isPresent()) { - builder.value(valueTrait.getStringValue().get()); + Optional traitValue = member.getTrait(EnumValueTrait.class).flatMap(EnumValueTrait::getStringValue); + if (member.hasTrait(EnumDefaultTrait.class)) { + builder.value(""); + } else if (traitValue.isPresent()) { + builder.value(traitValue.get()); } else { throw new IllegalStateException("Enum definitions can only be made for string enums."); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java index d2489d4069b..9ab3a454449 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java @@ -17,6 +17,7 @@ import java.util.Optional; import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.node.ExpectationNotMetException; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.NumberNode; import software.amazon.smithy.model.node.ObjectNode; @@ -25,6 +26,9 @@ import software.amazon.smithy.utils.SmithyBuilder; import software.amazon.smithy.utils.ToSmithyBuilder; +/** + * Sets the value for an enum member. + */ public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuilder { public static final ShapeId ID = ShapeId.from("smithy.api#enumValue"); @@ -43,14 +47,48 @@ private EnumValueTrait(Builder builder) { } } + /** + * Gets the string value if a string value was set. + * + * @return Optionally returns the string value. + */ public Optional getStringValue() { return Optional.ofNullable(string); } + /** + * Gets the string value. + * + * @return Returns the string value. + * @throws ExpectationNotMetException if the string value was not set. + */ + public String expectStringValue() { + return getStringValue().orElseThrow(() -> new ExpectationNotMetException( + "Expected string value was not set.", this + )); + } + + /** + * Gets the int value if an int value was set. + * + * @return Returns the set int value. + */ public Optional getIntValue() { return Optional.ofNullable(integer); } + /** + * Gets the int value. + * + * @return Returns the int value. + * @throws ExpectationNotMetException if the int value was not set. + */ + public int expectIntValue() { + return getIntValue().orElseThrow(() -> new ExpectationNotMetException( + "Expected integer value was not set.", this + )); + } + public static final class Provider extends AbstractTrait.Provider { public Provider() { super(ID); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java index 2e6f781d98f..b4037fc1df3 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java @@ -27,6 +27,7 @@ import software.amazon.smithy.model.shapes.IntEnumShape; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.validation.AbstractValidator; import software.amazon.smithy.model.validation.ValidationEvent; @@ -52,15 +53,25 @@ public List validate(Model model) { private void validateEnumShape(List events, EnumShape shape) { Set values = new HashSet<>(); for (MemberShape member : shape.members()) { + if (member.hasTrait(EnumDefaultTrait.ID)) { + continue; + } Optional value = member.expectTrait(EnumValueTrait.class).getStringValue(); if (!value.isPresent()) { events.add(error(member, member.expectTrait(EnumValueTrait.class), "The enumValue trait must use the string option when applied to enum shapes.")); - } else if (!values.add(value.get())) { - events.add(error(member, String.format( - "Multiple enum members found with duplicate value `%s`", - value.get() - ))); + } else { + if (!values.add(value.get())) { + events.add(error(member, String.format( + "Multiple enum members found with duplicate value `%s`", + value.get() + ))); + } + if (value.get().equals("")) { + events.add(error(member, "enum values may not be empty because an empty string is the " + + "default value of enum shapes. Instead, use `smithy.api#enumDefault` to set an " + + "explicit name for the default value.")); + } } validateEnumMemberName(events, member); } @@ -69,6 +80,9 @@ private void validateEnumShape(List events, EnumShape shape) { private void validateIntEnumShape(List events, IntEnumShape shape) { Set values = new HashSet<>(); for (MemberShape member : shape.members()) { + if (member.hasTrait(EnumDefaultTrait.ID)) { + continue; + } if (!member.hasTrait(EnumValueTrait.ID)) { events.add(missingIntEnumValue(member, member)); } else if (!member.expectTrait(EnumValueTrait.class).getIntValue().isPresent()) { @@ -81,6 +95,11 @@ private void validateIntEnumShape(List events, IntEnumShape sha value ))); } + if (value == 0) { + events.add(error(member, "intEnum values may not be set to 0 because 0 is the " + + "default value of intEnum shapes. Instead, use `smithy.api#enumDefault` to set an " + + "explicit name for the default value.")); + } } validateEnumMemberName(events, member); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExclusiveStructureMemberTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExclusiveStructureMemberTraitValidator.java index f0ee2e34e9b..35ba9d5785e 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExclusiveStructureMemberTraitValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExclusiveStructureMemberTraitValidator.java @@ -21,9 +21,9 @@ import java.util.Set; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.NamedMembers; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.traits.TraitDefinition; import software.amazon.smithy.model.validation.AbstractValidator; @@ -51,22 +51,22 @@ public List validate(Model model) { } List events = new ArrayList<>(); - for (StructureShape shape : model.getStructureShapes()) { + model.shapes().filter(shape -> shape instanceof NamedMembers).forEach(shape -> { validateExclusiveMembers(shape, exclusiveMemberTraits, events); validateExclusiveTargets(model, shape, exclusiveTargetTraits, events); - } + }); return events; } private void validateExclusiveMembers( - StructureShape shape, + Shape shape, Set exclusiveMemberTraits, List events ) { for (ShapeId traitId : exclusiveMemberTraits) { List matches = new ArrayList<>(); - for (MemberShape member : shape.getAllMembers().values()) { + for (MemberShape member : shape.members()) { if (member.findTrait(traitId).isPresent()) { matches.add(member.getMemberName()); } @@ -74,7 +74,7 @@ private void validateExclusiveMembers( if (matches.size() > 1) { events.add(error(shape, String.format( - "The `%s` trait can be applied to only a single member of a structure, but it was found on " + "The `%s` trait can be applied to only a single member of a shape, but it was found on " + "the following members: %s", Trait.getIdiomaticTraitName(traitId), ValidationUtils.tickedList(matches)))); @@ -84,14 +84,14 @@ private void validateExclusiveMembers( private void validateExclusiveTargets( Model model, - StructureShape shape, + Shape shape, Set exclusiveTargets, List events ) { // Find all member targets that violate the exclusion rule (e.g., streaming trait). for (ShapeId id : exclusiveTargets) { List matches = new ArrayList<>(); - for (MemberShape member : shape.getAllMembers().values()) { + for (MemberShape member : shape.members()) { if (memberTargetHasTrait(model, member, id)) { matches.add(member.getMemberName()); } diff --git a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService index 9be2fc12f34..3b8293a30bb 100644 --- a/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService +++ b/smithy-model/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService @@ -6,6 +6,7 @@ software.amazon.smithy.model.traits.DefaultTrait$Provider software.amazon.smithy.model.traits.DeprecatedTrait$Provider software.amazon.smithy.model.traits.DocumentationTrait$Provider software.amazon.smithy.model.traits.EndpointTrait$Provider +software.amazon.smithy.model.traits.EnumDefaultTrait$Provider software.amazon.smithy.model.traits.EnumTrait$Provider software.amazon.smithy.model.traits.EnumValueTrait$Provider software.amazon.smithy.model.traits.ErrorTrait$Provider diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index 8be654f2c7d..9f5f2963760 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -38,7 +38,7 @@ structure trait { /// The valid places in a model that the trait can be applied. selector: String, - /// Whether or not only a single member in a structure can have this trait. + /// Whether or not only a single member in a shape can have this trait. structurallyExclusive: StructurallyExclusive, /// The traits that this trait conflicts with. @@ -47,11 +47,11 @@ structure trait { @private enum StructurallyExclusive { - /// Only a single member of a structure can be marked with the trait. + /// Only a single member of a shape can be marked with the trait. @enumValue(string: "member") MEMBER - /// Only a single member of a structure can target a shape marked with this trait. + /// Only a single member of a shape can target a shape marked with this trait. @enumValue(string: "target") TARGET } @@ -430,7 +430,7 @@ structure EnumDefinition { @pattern("^[a-zA-Z_]+[a-zA-Z_0-9]*$") string EnumConstantBodyName -/// Defines the value of an enum entry. +/// Defines the value of an enum member. @trait(selector: ":is(enum, intEnum) > member") union enumValue { /// The value for the enum entry if it is a string. @@ -440,6 +440,14 @@ union enumValue { int: Integer } +/// Sets an enum member as the default value member. +@trait( + selector: ":is(enum, intEnum) > member" + structurallyExclusive: "member" + conflicts: [enumValue] +) +structure enumDefault {} + /// Constrains a shape to minimum and maximum number of elements or size. @trait(selector: ":test(collection, map, string, blob, member > :is(collection, map, string, blob))") structure length { diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index 95babdb9bb6..fe557e355c5 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -17,12 +17,14 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.EnumValueTrait; @@ -30,6 +32,7 @@ import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; import software.amazon.smithy.utils.SetUtils; public class EnumShapeTest { @@ -72,6 +75,26 @@ public void addMember() { )); } + @Test + public void addDefaultMember() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumShape shape = builder.addDefaultMember("foo").build(); + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(new EnumDefaultTrait()) + .build()); + + assertTrue(shape.hasTrait(EnumTrait.class)); + assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of( + EnumDefinition.builder() + .name("foo") + .value("") + .build() + )); + } + @Test public void addMemberShape() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); @@ -425,4 +448,16 @@ public void cantConvertBaseStringWithNamelessEnumTrait() { Optional optionalEnum = EnumShape.fromStringShape(string); assertFalse(optionalEnum.isPresent()); } + + @Test + public void getEnumValues() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumShape shape = builder.addDefaultMember("FOO").addMember("BAR", "bar").build(); + + Map expected = MapUtils.of( + "FOO", "", + "BAR", "bar" + ); + assertEquals(expected, shape.getEnumValues()); + } } diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java index e7d0f5c75ea..bb0f27a597f 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/IntEnumShapeTest.java @@ -17,11 +17,14 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumValueTrait; import software.amazon.smithy.model.traits.UnitTypeTrait; +import software.amazon.smithy.utils.MapUtils; public class IntEnumShapeTest { @Test @@ -55,6 +58,18 @@ public void addMember() { .build()); } + @Test + public void addDefaultMember() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + IntEnumShape shape = builder.addDefaultMember("foo").build(); + assertEquals(shape.getMember("foo").get(), + MemberShape.builder() + .id(shape.getId().withMember("foo")) + .target(UnitTypeTrait.UNIT) + .addTrait(new EnumDefaultTrait()) + .build()); + } + @Test public void addMemberShape() { IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); @@ -141,4 +156,16 @@ public void membersMustTargetUnit() { builder.addMember(member); }); } + + @Test + public void getEnumValues() { + IntEnumShape.Builder builder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#bar"); + IntEnumShape shape = builder.addDefaultMember("FOO").addMember("BAR", 1).build(); + + Map expected = MapUtils.of( + "FOO", 0, + "BAR", 1 + ); + assertEquals(expected, shape.getEnumValues()); + } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors index 7108c09e9f9..29cf8916b9b 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.errors @@ -7,3 +7,7 @@ [WARNING] ns.foo#IntEnum$undesirableName: The name `undesirableName` does not match the recommended enum name format of beginning with an uppercase letter, followed by any number of uppercase letters, numbers, or underscores. | EnumShape [WARNING] ns.foo#EnumWithEnumTrait: This shape applies a trait that is deprecated: smithy.api#enum | DeprecatedTrait [ERROR] ns.foo#EnumWithEnumTrait: Trait `enum` cannot be applied to `ns.foo#EnumWithEnumTrait`. This trait may only be applied to shapes that match the following selector: string :not(enum) | TraitTarget +[ERROR] ns.foo#StringEnum$EMPTY_STRING: enum values may not be empty because an empty string is the default value of enum shapes. Instead, use `smithy.api#enumDefault` to set an explicit name for the default value. | EnumShape +[ERROR] ns.foo#MultipleDefaults: The `enumDefault` trait can be applied to only a single member of a shape, but it was found on the following members: `DEFAULT1`, `DEFAULT2` | ExclusiveStructureMemberTrait +[ERROR] ns.foo#DefaultWithExplicitValue$DEFAULT: Found conflicting traits on member shape: `enumDefault` conflicts with `enumValue` | TraitConflict +[ERROR] ns.foo#IntEnum$ZERO: intEnum values may not be set to 0 because 0 is the default value of intEnum shapes. Instead, use `smithy.api#enumDefault` to set an explicit name for the default value. | EnumShape diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy index a861b8d8a3d..32c45121ce4 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy @@ -9,6 +9,9 @@ enum StringEnum { EXPLICIT_VALUE @enumValue(string: "") + EMPTY_STRING + + @enumDefault DEFAULT_VALUE @enumValue(int: 1) @@ -28,6 +31,20 @@ enum EnumWithEnumTrait { BAR } +enum MultipleDefaults { + @enumDefault + DEFAULT1 + + @enumDefault + DEFAULT2 +} + +enum DefaultWithExplicitValue { + @enumDefault + @enumValue(string: "foo") + DEFAULT +} + intEnum IntEnum { IMPLICIT_VALUE @@ -35,6 +52,9 @@ intEnum IntEnum { EXPLICIT_VALUE @enumValue(int: 0) + ZERO + + @enumDefault DEFAULT_VALUE @enumValue(string: "foo") diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/event-payload-validation.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/event-payload-validation.errors index 361fce3e69d..48ba0f1e1fc 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/event-payload-validation.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/event-payload-validation.errors @@ -1,6 +1,6 @@ [ERROR] ns.foo#InvalidEvent1: This event structure contains a member marked with the `eventPayload` trait, so all other members must be marked with the `eventHeader` trait. However, the following member(s) are not marked with the eventHeader trait: `baz` | EventPayloadTrait -[ERROR] ns.foo#InvalidEvent2: The `eventPayload` trait can be applied to only a single member of a structure, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait -[ERROR] ns.foo#InvalidEvent2: The `eventPayload` trait can be applied to only a single member of a structure, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait +[ERROR] ns.foo#InvalidEvent2: The `eventPayload` trait can be applied to only a single member of a shape, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait +[ERROR] ns.foo#InvalidEvent2: The `eventPayload` trait can be applied to only a single member of a shape, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait [ERROR] ns.foo#InvalidEvent2$foo: Trait `eventPayload` cannot be applied to `ns.foo#InvalidEvent2$foo`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(blob, string, structure, union)) | TraitTarget [ERROR] ns.foo#InvalidEvent4$foo: Trait `eventPayload` cannot be applied to `ns.foo#InvalidEvent4$foo`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(blob, string, structure, union)) | TraitTarget [ERROR] ns.foo#InvalidEvent3$foo: Trait `eventHeader` cannot be applied to `ns.foo#InvalidEvent3$foo`. This trait may only be applied to shapes that match the following selector: structure > :test(member > :test(boolean, byte, short, integer, long, blob, string, timestamp)) | TraitTarget diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/exclusive-structure-member-traits-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/exclusive-structure-member-traits-validator.errors index c76fe9102fa..f2e8a675b8e 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/exclusive-structure-member-traits-validator.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/exclusive-structure-member-traits-validator.errors @@ -1,3 +1,3 @@ -[ERROR] ns.foo#Invalid1: The `eventPayload` trait can be applied to only a single member of a structure, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait -[ERROR] ns.foo#Invalid2: The `idempotencyToken` trait can be applied to only a single member of a structure, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait -[ERROR] ns.foo#Invalid3: The `httpPayload` trait can be applied to only a single member of a structure, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait +[ERROR] ns.foo#Invalid1: The `eventPayload` trait can be applied to only a single member of a shape, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait +[ERROR] ns.foo#Invalid2: The `idempotencyToken` trait can be applied to only a single member of a shape, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait +[ERROR] ns.foo#Invalid3: The `httpPayload` trait can be applied to only a single member of a shape, but it was found on the following members: `baz`, `foo` | ExclusiveStructureMemberTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors index 77c8c3c6443..c12f171e718 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/http-request-response-validator.errors @@ -13,7 +13,7 @@ [ERROR] ns.foo#GInput$b: Found conflicting traits on member shape: `httpHeader` conflicts with `httpPrefixHeaders`, `httpPrefixHeaders` conflicts with `httpHeader` | TraitConflict [ERROR] ns.foo#GOutput$a: Found conflicting traits on member shape: `httpHeader` conflicts with `httpPayload`, `httpPayload` conflicts with `httpHeader` | TraitConflict [ERROR] ns.foo#GOutput$b: Found conflicting traits on member shape: `httpHeader` conflicts with `httpPrefixHeaders`, `httpPrefixHeaders` conflicts with `httpHeader` | TraitConflict -[ERROR] ns.foo#JInput: The `httpPrefixHeaders` trait can be applied to only a single member of a structure, but it was found on the following members: `a`, `b` | ExclusiveStructureMemberTrait +[ERROR] ns.foo#JInput: The `httpPrefixHeaders` trait can be applied to only a single member of a shape, but it was found on the following members: `a`, `b` | ExclusiveStructureMemberTrait [ERROR] ns.foo#K: Operation URI, `/k`, conflicts with other operation URIs in the same service: [`ns.foo#L` (/k)] | HttpUriConflict [ERROR] ns.foo#KInput: `httpHeader` field name binding conflicts found for the `x-foo` header in the following structure members: `a`, `b` | HttpHeaderTrait [ERROR] ns.foo#KInput: `httpQuery` parameter name binding conflicts found for the `foo` parameter in the following structure members: `c`, `d` | HttpQueryTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/request-token.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/request-token.errors index 34fb908697a..871239fc85b 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/request-token.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/request-token.errors @@ -1 +1 @@ -[ERROR] ns.foo#InvalidInput: The `idempotencyToken` trait can be applied to only a single member of a structure, but it was found on the following members: `anotherToken`, `token` | ExclusiveStructureMemberTrait +[ERROR] ns.foo#InvalidInput: The `idempotencyToken` trait can be applied to only a single member of a shape, but it was found on the following members: `anotherToken`, `token` | ExclusiveStructureMemberTrait diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json index 34edff79374..f7112058e2c 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json @@ -65,9 +65,7 @@ "DEFAULT": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "" - } + "smithy.api#enumDefault": {} } } } @@ -107,9 +105,7 @@ "DEFAULT": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "int": 0 - } + "smithy.api#enumDefault": {} } } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy index 03993e67f40..be84491ad17 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy @@ -20,7 +20,7 @@ enum EnumWithValueTraits { } enum EnumWithDefaultBound { - @enumValue(string: "") + @enumDefault DEFAULT } @@ -36,6 +36,6 @@ intEnum IntEnum { } intEnum IntEnumWithDefaultBound { - @enumValue(int: 0) + @enumDefault DEFAULT } From 51a2d95101eb1ca8fa4d6f50858e7045514a7571 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 17:24:43 +0100 Subject: [PATCH 36/45] Set node cache on enum value trait --- .../software/amazon/smithy/model/traits/EnumValueTrait.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java index 9ab3a454449..095bf45bd58 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java @@ -104,7 +104,9 @@ public Trait createTrait(ShapeId target, Node value) { objectNode.getMember("int") .map(v -> v.expectNumberNode().getValue().intValue()) .ifPresent(builder::intValue); - return builder.build(); + EnumValueTrait result = builder.build(); + result.setNodeCache(value); + return result; } } From c167f42da00b0d379fbe989b46ce721457dd6734 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 22 Feb 2022 17:38:57 +0100 Subject: [PATCH 37/45] Add transformer to downgrade enums --- .../model/transform/ChangeShapeType.java | 11 ++++++++ .../model/transform/ModelTransformer.java | 10 +++++++ .../model/transform/ChangeShapeTypeTest.java | 27 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java index cd9e9aca760..10ed48d36b8 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java @@ -100,6 +100,17 @@ private static boolean canUpgradeEnum(StringShape shape, boolean synthesizeEnumN return true; } + static ChangeShapeType downgradeEnums(Model model) { + Map toUpdate = new HashMap<>(); + for (EnumShape shape : model.getEnumShapes()) { + toUpdate.put(shape.getId(), ShapeType.STRING); + } + for (IntEnumShape shape : model.getIntEnumShapes()) { + toUpdate.put(shape.getId(), ShapeType.INTEGER); + } + return new ChangeShapeType(toUpdate); + } + Model transform(ModelTransformer transformer, Model model) { return transformer.mapShapes(model, shape -> { if (shapeToType.containsKey(shape.getId())) { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java index 747d7ab87fe..aab4e40d94d 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java @@ -527,6 +527,16 @@ public Model changeStringEnumsToEnumShapes(Model model) { return ChangeShapeType.upgradeEnums(model, false).transform(this, model); } + /** + * Changes each enum shape to a string shape and each intEnum to an integer. + * + * @param model Model to transform. + * @return Returns the transformed model. + */ + public Model changeEnumsToBaseShapeTypes(Model model) { + return ChangeShapeType.downgradeEnums(model).transform(this, model); + } + /** * Copies the errors defined on the given service onto each operation bound to the * service, effectively flattening service error inheritance. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java index ba775535baf..f4c6a1d5907 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java @@ -418,4 +418,31 @@ public void canSynthesizeEnumNames() { .addTrait(EnumValueTrait.builder().stringValue("foo:bar").build()) .build())); } + + @Test + public void canDowngradeEnums() { + EnumShape.Builder stringEnumBuilder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#Enum"); + EnumShape stringEnum = stringEnumBuilder.addMember("FOO", "foo").build(); + + IntEnumShape.Builder intEnumBuilder = (IntEnumShape.Builder) IntEnumShape.builder().id("ns.foo#IntEnum"); + IntEnumShape intEnum = intEnumBuilder.addMember("FOO", 1).build(); + + Model model = Model.assembler() + .setParsedShapesVersion("2.0") + .addShapes(stringEnum, intEnum) + .assemble().unwrap(); + Model result = ModelTransformer.create().changeEnumsToBaseShapeTypes(model); + + assertThat(result.expectShape(stringEnum.getId()).getType(), Matchers.is(ShapeType.STRING)); + assertThat(result.expectShape(intEnum.getId()).getType(), Matchers.is(ShapeType.INTEGER)); + + EnumTrait trait = result.expectShape(stringEnum.getId()).expectTrait(EnumTrait.class); + assertFalse(trait instanceof SyntheticEnumTrait); + assertThat(trait.getValues(), Matchers.equalTo(ListUtils.of( + EnumDefinition.builder() + .name("FOO") + .value("foo") + .build() + ))); + } } From e1deac443a1edf8695af2d349d23fb0cb0ea5315 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 24 Feb 2022 16:03:15 +0100 Subject: [PATCH 38/45] Convert enumValue to a document trait --- designs/enum-shapes.md | 36 +++++----- .../META-INF/smithy/aws.apigateway.json | 44 +++--------- .../META-INF/smithy/aws.cloudformation.smithy | 10 +-- .../resources/META-INF/smithy/aws.iam.json | 68 +++++-------------- .../model/aws-config.smithy | 12 ++-- .../services/machinelearning.smithy | 4 +- .../restJson1/http-string-payload.smithy | 2 +- .../validation/malformed-enum.smithy | 4 +- .../validation/recursive-structures.smithy | 4 +- .../model/restXml/services/s3.smithy | 2 +- .../model/shared-types.smithy | 10 +-- .../resources/META-INF/smithy/aws.api.json | 20 ++---- .../META-INF/smithy/aws.protocols.json | 16 ++--- .../smithy/model/traits/EnumValueTrait.java | 25 ++++--- .../amazon/smithy/model/loader/prelude.smithy | 26 +++---- .../errorfiles/validators/enum-shapes.smithy | 20 +++--- .../validators/enum-value-invalid-type.errors | 5 ++ .../validators/enum-value-invalid-type.smithy | 20 ++++++ .../loader/invalid/idl-int-enums-in-v1.smithy | 4 +- .../smithy/model/loader/valid/enums.json | 36 +++------- .../smithy/model/loader/valid/enums.smithy | 12 ++-- .../valid/mixins/enum-mixins.flattened.smithy | 6 +- .../loader/valid/mixins/enum-mixins.json | 24 ++----- .../loader/valid/mixins/enum-mixins.smithy | 6 +- .../model/selector/shape-type-test.smithy | 2 +- .../shapes/ast-serialization/cases/enums.json | 16 ++--- .../idl-serialization/cases/enums.smithy | 16 ++--- .../mqtt/traits/errorfiles/job-service.smithy | 18 ++--- .../META-INF/smithy/smithy.test.smithy | 4 +- .../resources/META-INF/smithy/waiters.smithy | 14 ++-- 30 files changed, 192 insertions(+), 294 deletions(-) create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors create mode 100644 smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.smithy diff --git a/designs/enum-shapes.md b/designs/enum-shapes.md index b057127ee4f..8bb5ea54916 100644 --- a/designs/enum-shapes.md +++ b/designs/enum-shapes.md @@ -53,16 +53,16 @@ member name. The string representation can be customized by applying the ``` enum Suit { - @enumValue(string: "diamond") + @enumValue("diamond") DIAMOND - @enumValue(string: "club") + @enumValue("club") CLUB - @enumValue(string: "heart") + @enumValue("heart") HEART - @enumValue(string: "spade") + @enumValue("spade") SPADE } ``` @@ -89,16 +89,16 @@ enum Suit { @enumDefault UNKNOWN - @enumValue(string: "diamond") + @enumValue("diamond") DIAMOND - @enumValue(string: "club") + @enumValue("club") CLUB - @enumValue(string: "heart") + @enumValue("heart") HEART - @enumValue(string: "spade") + @enumValue("spade") SPADE } ``` @@ -133,19 +133,19 @@ integer value. The following example defines an intEnum shape: ``` intEnum FaceCard { - @enumValue(int: 1) + @enumValue(1) JACK - @enumValue(int: 2) + @enumValue(2) QUEEN - @enumValue(int: 3) + @enumValue(3) KING - @enumValue(int: 4) + @enumValue(4) ACE - @enumValue(int: 5) + @enumValue(5) JOKER } ``` @@ -170,19 +170,19 @@ intEnum FaceCard { @enumDefault UNKNOWN - @enumValue(int: 1) + @enumValue(1) JACK - @enumValue(int: 2) + @enumValue(2) QUEEN - @enumValue(int: 3) + @enumValue(3) KING - @enumValue(int: 4) + @enumValue(4) ACE - @enumValue(int: 5) + @enumValue(5) JOKER } ``` 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 c6279734a57..284fa618d9d 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 @@ -221,33 +221,25 @@ "AWS": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "aws" - } + "smithy.api#enumValue": "aws" } }, "AWS_PROXY": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "aws_proxy" - } + "smithy.api#enumValue": "aws_proxy" } }, "HTTP": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "http" - } + "smithy.api#enumValue": "http" } }, "HTTP_PROXY": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "http_proxy" - } + "smithy.api#enumValue": "http_proxy" } } } @@ -302,17 +294,13 @@ "INTERNET": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "INTERNET" - } + "smithy.api#enumValue": "INTERNET" } }, "VPC_LINK": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "VPC_LINK" - } + "smithy.api#enumValue": "VPC_LINK" } } } @@ -323,25 +311,19 @@ "WHEN_NO_TEMPLATES": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "when_no_templates" - } + "smithy.api#enumValue": "when_no_templates" } }, "WHEN_NO_MATCH": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "when_no_match" - } + "smithy.api#enumValue": "when_no_match" } }, "NEVER": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "never" - } + "smithy.api#enumValue": "never" } } }, @@ -356,17 +338,13 @@ "CONVERT_TO_TEXT": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "CONVERT_TO_TEXT" - } + "smithy.api#enumValue": "CONVERT_TO_TEXT" } }, "CONVERT_TO_BINARY": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "CONVERT_TO_BINARY" - } + "smithy.api#enumValue": "CONVERT_TO_BINARY" } } }, diff --git a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy index e35175096fb..bc3ec819f36 100644 --- a/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy +++ b/smithy-aws-cloudformation-traits/src/main/resources/META-INF/smithy/aws.cloudformation.smithy @@ -44,27 +44,27 @@ enum cfnMutability { /// member does not have any mutability restrictions, meaning that it /// can be specified by the user and returned in a `read` or `list` /// request. - @enumValue(string: "full") + @enumValue("full") FULL /// Indicates that the CloudFormation property generated from this /// member can be specified only during resource creation and can be /// returned in a `read` or `list` request. - @enumValue(string: "create-and-read") + @enumValue("create-and-read") CREATE_AND_READ /// Indicates that the CloudFormation property generated from this /// member can be specified only during resource creation and cannot /// be returned in a `read` or `list` request. MUST NOT be set if the /// member is also marked with the `@additionalIdentifier` trait. - @enumValue(string: "create") + @enumValue("create") CREATE /// Indicates that the CloudFormation property generated from this /// member can be returned by a `read` or `list` request, but /// cannot be set by the user. - @enumValue(string: "read") + @enumValue("read") READ @@ -72,7 +72,7 @@ enum cfnMutability { /// member can be specified by the user, but cannot be returned by a /// `read` or `list` request. MUST NOT be set if the member is also /// marked with the `@additionalIdentifier` trait. - @enumValue(string: "write") + @enumValue("write") WRITE } 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 2e25b1efb16..2910c4538ec 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 @@ -127,105 +127,79 @@ "ARN": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "ARN" - } + "smithy.api#enumValue": "ARN" } }, "ARRAY_OF_ARN": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "ArrayOfARN" - } + "smithy.api#enumValue": "ArrayOfARN" } }, "BINARY": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "Binary" - } + "smithy.api#enumValue": "Binary" } }, "ARRAY_OF_BINARY": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "ArrayOfBinary" - } + "smithy.api#enumValue": "ArrayOfBinary" } }, "STRING": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "String" - } + "smithy.api#enumValue": "String" } }, "ARRAY_OF_STRING": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "ArrayOfString" - } + "smithy.api#enumValue": "ArrayOfString" } }, "NUMERIC": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "Numeric" - } + "smithy.api#enumValue": "Numeric" } }, "DATE": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "Date" - } + "smithy.api#enumValue": "Date" } }, "ARRAY_OF_DATE": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "ArrayOfDate" - } + "smithy.api#enumValue": "ArrayOfDate" } }, "BOOL": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "Bool" - } + "smithy.api#enumValue": "Bool" } }, "ARRAY_OF_BOOL": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "ArrayOfBool" - } + "smithy.api#enumValue": "ArrayOfBool" } }, "IP_ADDRESS": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "IPAddress" - } + "smithy.api#enumValue": "IPAddress" } }, "ARRAY_OF_IP_ADDRESS": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "ArrayOfIPAddress" - } + "smithy.api#enumValue": "ArrayOfIPAddress" } } }, @@ -240,33 +214,25 @@ "ROOT": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "Root" - } + "smithy.api#enumValue": "Root" } }, "IAM_USER": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "IAMUser" - } + "smithy.api#enumValue": "IAMUser" } }, "IAM_ROLE": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "IAMRole" - } + "smithy.api#enumValue": "IAMRole" } }, "FEDERATED_USER": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "FederatedUser" - } + "smithy.api#enumValue": "FederatedUser" } } }, diff --git a/smithy-aws-protocol-tests/model/aws-config.smithy b/smithy-aws-protocol-tests/model/aws-config.smithy index 9dbbf43ebae..ff3a9f9d703 100644 --- a/smithy-aws-protocol-tests/model/aws-config.smithy +++ b/smithy-aws-protocol-tests/model/aws-config.smithy @@ -98,24 +98,24 @@ structure RetryConfig { /// Controls the S3 addressing bucket style. enum S3AddressingStyle { - @enumValue(string: "auto") + @enumValue("auto") AUTO - @enumValue(string: "path") + @enumValue("path") PATH - @enumValue(string: "virtual") + @enumValue("virtual") VIRTUAL } /// Controls the strategy used for retries. enum RetryMode { - @enumValue(string: "legacy") + @enumValue("legacy") LEGACY - @enumValue(string: "standard") + @enumValue("standard") STANDARD - @enumValue(string: "adaptive") + @enumValue("adaptive") ADAPTIVE } diff --git a/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy b/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy index d11c13fec42..119da283de3 100644 --- a/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy +++ b/smithy-aws-protocol-tests/model/awsJson1_1/services/machinelearning.smithy @@ -136,10 +136,10 @@ map ScoreValuePerLabelMap { } enum DetailsAttributes { - @enumValue(string: "PredictiveModelType") + @enumValue("PredictiveModelType") PREDICTIVE_MODEL_TYPE - @enumValue(string: "Algorithm") + @enumValue("Algorithm") ALGORITHM } diff --git a/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy b/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy index c3280440838..9f362d0960d 100644 --- a/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/http-string-payload.smithy @@ -36,7 +36,7 @@ structure EnumPayloadInput { } enum StringEnum { - @enumValue(string: "enumvalue") + @enumValue("enumvalue") V } diff --git a/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy b/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy index e73bfd62c4e..1bb391b4a20 100644 --- a/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/validation/malformed-enum.smithy @@ -192,10 +192,10 @@ structure MalformedEnumInput { } enum EnumString { - @enumValue(string: "abc") + @enumValue("abc") ABC - @enumValue(string: "def") + @enumValue("def") DEF } diff --git a/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy b/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy index eb2c0556d5c..9c40305e03b 100644 --- a/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy +++ b/smithy-aws-protocol-tests/model/restJson1/validation/recursive-structures.smithy @@ -86,10 +86,10 @@ structure RecursiveStructuresInput { } enum RecursiveEnumString { - @enumValue(string: "abc") + @enumValue("abc") ABC - @enumValue(string: "def") + @enumValue("def") DEF } diff --git a/smithy-aws-protocol-tests/model/restXml/services/s3.smithy b/smithy-aws-protocol-tests/model/restXml/services/s3.smithy index f4107c0ddb3..b0faf8efb2c 100644 --- a/smithy-aws-protocol-tests/model/restXml/services/s3.smithy +++ b/smithy-aws-protocol-tests/model/restXml/services/s3.smithy @@ -460,6 +460,6 @@ string Token enum BucketLocationConstraint { @suppress(["EnumShape"]) - @enumValue(string: "us-west-2") + @enumValue("us-west-2") us_west_2 } diff --git a/smithy-aws-protocol-tests/model/shared-types.smithy b/smithy-aws-protocol-tests/model/shared-types.smithy index f75adbfd7d9..d3d011176b6 100644 --- a/smithy-aws-protocol-tests/model/shared-types.smithy +++ b/smithy-aws-protocol-tests/model/shared-types.smithy @@ -75,19 +75,19 @@ list TimestampList { } enum FooEnum { - @enumValue(string: "Foo") + @enumValue("Foo") FOO - @enumValue(string: "Baz") + @enumValue("Baz") BAZ - @enumValue(string: "Bar") + @enumValue("Bar") BAR - @enumValue(string: "1") + @enumValue("1") ONE - @enumValue(string: "0") + @enumValue("0") ZERO } 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 23b05c44286..5a2fe0f61a3 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 @@ -84,45 +84,35 @@ "CUSTOMER_CONTENT": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "content" - }, + "smithy.api#enumValue": "content", "smithy.api#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." } }, "CUSTOMER_ACCOUNT_INFORMATION": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "account" - }, + "smithy.api#enumValue": "account", "smithy.api#documentation": "Account information means information about customers that customers provide to AWS in connection with the creation or administration of customers\u2019 accounts." } }, "SERVICE_ATTRIBUTES": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "usage" - }, + "smithy.api#enumValue": "usage", "smithy.api#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." } }, "TAG_DATA": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "tagging" - }, + "smithy.api#enumValue": "tagging", "smithy.api#documentation": "Designates metadata tags applied to AWS resources." } }, "PERMISSIONS_DATA": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "permissions" - }, + "smithy.api#enumValue": "permissions", "smithy.api#documentation": "Designates security and access roles, rules, usage policies, and permissions." } } diff --git a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json index 87140120afa..eeec19a8e0f 100644 --- a/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json +++ b/smithy-aws-traits/src/main/resources/META-INF/smithy/aws.protocols.json @@ -281,33 +281,25 @@ "CRC32C": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "CRC32C" - } + "smithy.api#enumValue": "CRC32C" } }, "CRC32": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "CRC32" - } + "smithy.api#enumValue": "CRC32" } }, "SHA1": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "SHA1" - } + "smithy.api#enumValue": "SHA1" } }, "SHA256": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "SHA256" - } + "smithy.api#enumValue": "SHA256" } } }, diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java index 095bf45bd58..dddd12452bb 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java @@ -20,7 +20,6 @@ import software.amazon.smithy.model.node.ExpectationNotMetException; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.NumberNode; -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.SmithyBuilder; @@ -97,13 +96,12 @@ public Provider() { @Override public Trait createTrait(ShapeId target, Node value) { Builder builder = builder().sourceLocation(value); - ObjectNode objectNode = value.expectObjectNode(); - objectNode.getMember("string") - .map(v -> v.expectStringNode().getValue()) - .ifPresent(builder::stringValue); - objectNode.getMember("int") - .map(v -> v.expectNumberNode().getValue().intValue()) - .ifPresent(builder::intValue); + value.asStringNode().ifPresent(node -> builder.stringValue(node.getValue())); + value.asNumberNode().ifPresent(node -> { + if (node.isNaturalNumber()) { + builder.intValue(node.getValue().intValue()); + } + }); EnumValueTrait result = builder.build(); result.setNodeCache(value); return result; @@ -112,11 +110,12 @@ public Trait createTrait(ShapeId target, Node value) { @Override protected Node createNode() { - return ObjectNode.builder() - .sourceLocation(getSourceLocation()) - .withOptionalMember("string", getStringValue().map(StringNode::from)) - .withOptionalMember("int", getIntValue().map(NumberNode::from)) - .build(); + if (getIntValue().isPresent()) { + return new NumberNode(integer, getSourceLocation()); + } else { + return new StringNode(string, getSourceLocation()); + + } } @Override diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index 9f5f2963760..7456f896369 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -48,11 +48,11 @@ structure trait { @private enum StructurallyExclusive { /// Only a single member of a shape can be marked with the trait. - @enumValue(string: "member") + @enumValue("member") MEMBER /// Only a single member of a shape can target a shape marked with this trait. - @enumValue(string: "target") + @enumValue("target") TARGET } @@ -173,10 +173,10 @@ structure default {} @private enum HttpApiKeyLocations { - @enumValue(string: "header") + @enumValue("header") HEADER - @enumValue(string: "query") + @enumValue("query") QUERY } @@ -219,10 +219,10 @@ structure ExampleError { @trait(selector: "structure", conflicts: [trait]) @tags(["diff.error.const"]) enum error { - @enumValue(string: "client") + @enumValue("client") CLIENT - @enumValue(string: "server") + @enumValue("server") SERVER } @@ -432,13 +432,7 @@ string EnumConstantBodyName /// Defines the value of an enum member. @trait(selector: ":is(enum, intEnum) > member") -union enumValue { - /// The value for the enum entry if it is a string. - string: String - - /// The value for the enum entry if it is an integer. - int: Integer -} +document enumValue /// Sets an enum member as the default value member. @trait( @@ -703,18 +697,18 @@ enum timestampFormat { /// 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). - @enumValue(string: "date-time") + @enumValue("date-time") DATE_TIME /// 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). - @enumValue(string: "epoch-seconds") + @enumValue("epoch-seconds") EPOCH_SECONDS /// 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). - @enumValue(string: "http-date") + @enumValue("http-date") HTTP_DATE } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy index 32c45121ce4..c771b0cf1c7 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-shapes.smithy @@ -5,19 +5,19 @@ namespace ns.foo enum StringEnum { IMPLICIT_VALUE - @enumValue(string: "explicit") + @enumValue("explicit") EXPLICIT_VALUE - @enumValue(string: "") + @enumValue("") EMPTY_STRING @enumDefault DEFAULT_VALUE - @enumValue(int: 1) + @enumValue(1) INT_VALUE - @enumValue(string: "explicit") + @enumValue("explicit") DUPLICATE_VALUE undesirableName @@ -41,28 +41,28 @@ enum MultipleDefaults { enum DefaultWithExplicitValue { @enumDefault - @enumValue(string: "foo") + @enumValue("foo") DEFAULT } intEnum IntEnum { IMPLICIT_VALUE - @enumValue(int: 1) + @enumValue(1) EXPLICIT_VALUE - @enumValue(int: 0) + @enumValue(0) ZERO @enumDefault DEFAULT_VALUE - @enumValue(string: "foo") + @enumValue("foo") STRING_VALUE - @enumValue(int: 1) + @enumValue(1) DUPLICATE_VALUE - @enumValue(int: 99) + @enumValue(99) undesirableName } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors new file mode 100644 index 00000000000..39add152c59 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors @@ -0,0 +1,5 @@ +[ERROR] smithy.example#IntEnum$FLOAT: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model +[ERROR] smithy.example#IntEnum$ARRAY: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model +[ERROR] smithy.example#IntEnum$MAP: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model +[ERROR] smithy.example#IntEnum$NULL: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model +[ERROR] smithy.example#IntEnum$BOOLEAN: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.smithy new file mode 100644 index 00000000000..8ce6d95c6a0 --- /dev/null +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.smithy @@ -0,0 +1,20 @@ +$version: "2.0" + +namespace smithy.example + +intEnum IntEnum { + @enumValue(1.1) + FLOAT + + @enumValue([1]) + ARRAY + + @enumValue({"foo": "bar"}) + MAP + + @enumValue(null) + NULL + + @enumValue(true) + BOOLEAN +} diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-int-enums-in-v1.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-int-enums-in-v1.smithy index 812147c74eb..301f8b87e41 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-int-enums-in-v1.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/invalid/idl-int-enums-in-v1.smithy @@ -4,9 +4,9 @@ $version: "1.0" namespace ns.foo intEnum IntEnum { - @enumValue(int: 1) + @enumValue(1) FOO - @enumValue(int: 2) + @enumValue(2) BAR } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json index f7112058e2c..9ac47eaf261 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.json @@ -7,25 +7,19 @@ "FOO": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "FOO" - } + "smithy.api#enumValue": "FOO" } }, "BAR": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "BAR" - } + "smithy.api#enumValue": "BAR" } }, "BAZ": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "BAZ" - } + "smithy.api#enumValue": "BAZ" } } } @@ -36,25 +30,19 @@ "FOO": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "foo" - } + "smithy.api#enumValue": "foo" } }, "BAR": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "bar" - } + "smithy.api#enumValue": "bar" } }, "BAZ": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "baz" - } + "smithy.api#enumValue": "baz" } } } @@ -76,25 +64,19 @@ "FOO": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "int": 1 - } + "smithy.api#enumValue": 1 } }, "BAR": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "int": 2 - } + "smithy.api#enumValue": 2 } }, "BAZ": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "int": 3 - } + "smithy.api#enumValue": 3 } } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy index be84491ad17..38e6ef50ac1 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/enums.smithy @@ -9,13 +9,13 @@ enum EnumWithoutValueTraits { } enum EnumWithValueTraits { - @enumValue(string: "foo") + @enumValue("foo") FOO - @enumValue(string: "bar") + @enumValue("bar") BAR - @enumValue(string: "baz") + @enumValue("baz") BAZ } @@ -25,13 +25,13 @@ enum EnumWithDefaultBound { } intEnum IntEnum { - @enumValue(int: 1) + @enumValue(1) FOO - @enumValue(int: 2) + @enumValue(2) BAR - @enumValue(int: 3) + @enumValue(3) BAZ } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.flattened.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.flattened.smithy index 68d84b1ae36..b4ca4dc8f7c 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.flattened.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.flattened.smithy @@ -16,13 +16,13 @@ enum MixedEnum { @sensitive @private intEnum MixedIntEnum { - @enumValue(int: 1) + @enumValue(1) FOO /// Docs - @enumValue(int: 2) + @enumValue(2) BAR - @enumValue(int: 3) + @enumValue(3) BAZ } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.json b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.json index 8a88a5073b6..aeb9862b96a 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.json @@ -7,17 +7,13 @@ "FOO": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "FOO" - } + "smithy.api#enumValue": "FOO" } }, "BAR": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "BAR" - }, + "smithy.api#enumValue": "BAR", "smithy.api#documentation": "Documentation" } } @@ -40,9 +36,7 @@ "BAZ": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "BAZ" - } + "smithy.api#enumValue": "BAZ" } } }, @@ -60,17 +54,13 @@ "FOO": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "int": 1 - } + "smithy.api#enumValue": 1 } }, "BAR": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "int": 2 - }, + "smithy.api#enumValue": 2, "smithy.api#documentation": "Documentation" } } @@ -93,9 +83,7 @@ "BAZ": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "int": 3 - } + "smithy.api#enumValue": 3 } } }, diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.smithy index 9d2c1b7587b..0e56af6ed50 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/loader/valid/mixins/enum-mixins.smithy @@ -24,11 +24,11 @@ enum MixedEnum with [BaseEnum] { @mixin @private intEnum BaseIntEnum { - @enumValue(int: 1) + @enumValue(1) FOO /// Documentation - @enumValue(int: 2) + @enumValue(2) BAR } @@ -38,6 +38,6 @@ intEnum MixedIntEnum with [BaseIntEnum] { /// Docs BAR - @enumValue(int: 3) + @enumValue(3) BAZ } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/shape-type-test.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/shape-type-test.smithy index faede0bb4e4..430243111af 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/selector/shape-type-test.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/selector/shape-type-test.smithy @@ -9,7 +9,7 @@ enum Enum { string String intEnum IntEnum { - @enumValue(int: 1) + @enumValue(1) FOO } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/ast-serialization/cases/enums.json b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/ast-serialization/cases/enums.json index ebb136ec501..7be45c6d02b 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/ast-serialization/cases/enums.json +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/ast-serialization/cases/enums.json @@ -7,17 +7,13 @@ "FOO": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "foo" - } + "smithy.api#enumValue": "foo" } }, "BAR": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "string": "bar" - } + "smithy.api#enumValue": "bar" } } }, @@ -29,17 +25,13 @@ "FOO": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "int": 1 - } + "smithy.api#enumValue": 1 } }, "BAR": { "target": "smithy.api#Unit", "traits": { - "smithy.api#enumValue": { - "int": 2 - } + "smithy.api#enumValue": 2 } } } diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy index c784a86eeb0..b49611fb907 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/shapes/idl-serialization/cases/enums.smithy @@ -3,13 +3,9 @@ $version: "2.0" namespace ns.foo intEnum IntEnum { - @enumValue( - int: 1 - ) + @enumValue(1) FOO - @enumValue( - int: 2 - ) + @enumValue(2) BAR } @@ -19,12 +15,8 @@ enum StringEnum { } enum StringEnumWithExplicitValues { - @enumValue( - string: "foo" - ) + @enumValue("foo") FOO - @enumValue( - string: "bar" - ) + @enumValue("bar") BAR } 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 e19919ab71d..7e83cf576fe 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 @@ -52,31 +52,31 @@ structure RejectedError { } enum RejectedErrorCode { - @enumValue(string: "InvalidTopic") + @enumValue("InvalidTopic") INVALID_TOPIC - @enumValue(string: "InvalidJson") + @enumValue("InvalidJson") INVALID_JSON - @enumValue(string: "InvalidRequest") + @enumValue("InvalidRequest") INVALID_REQUEST - @enumValue(string: "InvalidStateTransition") + @enumValue("InvalidStateTransition") INVALID_STATE_TRANSITION - @enumValue(string: "ResourceNotFound") + @enumValue("ResourceNotFound") RESOURCE_NOT_FOUND - @enumValue(string: "VersionMismatch") + @enumValue("VersionMismatch") VERSION_MISMATCH - @enumValue(string: "InternalError") + @enumValue("InternalError") INTERNAL_ERROR - @enumValue(string: "RequestThrottled") + @enumValue("RequestThrottled") REQUEST_THROTTLED - @enumValue(string: "TerminalStateReached") + @enumValue("TerminalStateReached") TERMINAL_STATE_REACHED } diff --git a/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy index 2f403de9962..51c97b38f37 100644 --- a/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy +++ b/smithy-protocol-test-traits/src/main/resources/META-INF/smithy/smithy.test.smithy @@ -274,11 +274,11 @@ string NonEmptyString @private enum AppliesTo { /// The test only applies to client implementations. - @enumValue(string: "client") + @enumValue("client") CLIENT /// The test only applies to server implementations. - @enumValue(string: "server") + @enumValue("server") SERVER } diff --git a/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy b/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy index bde71cc4920..97ade1f0481 100644 --- a/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy +++ b/smithy-waiters/src/main/resources/META-INF/smithy/waiters.smithy @@ -72,17 +72,17 @@ structure Acceptor { enum AcceptorState { /// The waiter successfully finished waiting. This is a terminal /// state that causes the waiter to stop. - @enumValue(string: "success") + @enumValue("success") SUCCESS /// The waiter failed to enter into the desired state. This is a /// terminal state that causes the waiter to stop. - @enumValue(string: "failure") + @enumValue("failure") FAILURE /// The waiter will retry the operation. This state transition is /// implicit if no accepter causes a state transition. - @enumValue(string: "retry") + @enumValue("retry") RETRY } @@ -136,19 +136,19 @@ structure PathMatcher { @private enum PathComparator { /// Matches if the return value is a string that is equal to the expected string. - @enumValue(string: "stringEquals") + @enumValue("stringEquals") STRING_EQUALS /// Matches if the return value is a boolean that is equal to the string literal 'true' or 'false'. - @enumValue(string: "booleanEquals") + @enumValue("booleanEquals") BOOLEAN_EQUALS /// Matches if all values in the list matches the expected string. - @enumValue(string: "allStringEquals") + @enumValue("allStringEquals") ALL_STRING_EQUALS /// Matches if any value in the list matches the expected string. - @enumValue(string: "anyStringEquals") + @enumValue("anyStringEquals") ANY_STRING_EQUALS } From 46fbfd529d67ad9ef38123443103bd8848b70b3b Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 24 Feb 2022 19:03:02 +0100 Subject: [PATCH 39/45] Implement pr feedback --- .../amazon/smithy/model/shapes/EnumShape.java | 62 ++++++++++++++++--- .../smithy/model/shapes/IntEnumShape.java | 4 +- .../amazon/smithy/model/shapes/ShapeType.java | 4 +- .../smithy/model/traits/EnumDefaultTrait.java | 2 +- .../amazon/smithy/model/traits/EnumTrait.java | 4 +- .../model/transform/ModelTransformer.java | 7 ++- .../validators/EnumShapeValidator.java | 9 +++ ...xclusiveStructureMemberTraitValidator.java | 10 +-- .../amazon/smithy/model/loader/prelude.smithy | 2 + .../smithy/model/shapes/EnumShapeTest.java | 19 +++--- .../model/transform/ChangeShapeTypeTest.java | 2 +- 11 files changed, 92 insertions(+), 33 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index a814022315c..429be551947 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -16,11 +16,13 @@ package software.amazon.smithy.model.shapes; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; +import java.util.logging.Logger; import software.amazon.smithy.model.SourceException; import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumDefinition; @@ -31,10 +33,11 @@ import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; import software.amazon.smithy.utils.BuilderRef; import software.amazon.smithy.utils.ListUtils; -import software.amazon.smithy.utils.MapUtils; public final class EnumShape extends StringShape implements NamedMembers { + private static final Logger LOGGER = Logger.getLogger(EnumShape.class.getName()); + private final Map members; private volatile List memberNames; private volatile Map enumValues; @@ -64,7 +67,7 @@ public Map getEnumValues() { } values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).expectStringValue()); } - enumValues = MapUtils.orderedCopyOf(values); + enumValues = Collections.unmodifiableMap(values); } return enumValues; } @@ -153,8 +156,9 @@ public Optional asEnumShape() { /** * Converts a base {@link StringShape} to an {@link EnumShape} if possible. * - * The result will be empty if the given shape doesn't have the {@link EnumTrait} - * or if the enum definitions don't have names. + * The result will be empty if the given shape doesn't have the {@link EnumTrait}, + * if the enum doesn't have names and name synthesization is disabled, or if a name + * cannot be synthesized. * * @param shape A base {@link StringShape} to convert. * @param synthesizeNames Whether names should be synthesized if possible. @@ -168,8 +172,11 @@ public static Optional fromStringShape(StringShape shape, boolean syn Builder enumBuilder = EnumShape.builder(); stringWithoutEnumTrait.updateBuilder(enumBuilder); try { - return Optional.of(enumBuilder.members(shape.expectTrait(EnumTrait.class), synthesizeNames).build()); + return Optional.of(enumBuilder + .setMembersFromEnumTrait(shape.expectTrait(EnumTrait.class), synthesizeNames) + .build()); } catch (IllegalStateException e) { + LOGGER.info(String.format("Unable to convert `%s` to an enum: %s", shape.getId(), e)); return Optional.empty(); } } @@ -201,6 +208,13 @@ public EnumShape build() { return new EnumShape(this); } + /** + * Adds a synthetic version of the enum trait. + * + *

This allows the enum shape to be used as if it were a string shape with + * the enum trait, without having to manually add the trait or risk that it + * gets serialized. + */ private void addSyntheticEnumTrait() { SyntheticEnumTrait.Builder builder = SyntheticEnumTrait.builder(); for (MemberShape member : members.get().values()) { @@ -231,7 +245,16 @@ public Builder id(ShapeId shapeId) { return this; } - public Builder members(EnumTrait trait, boolean synthesizeNames) { + /** + * Sets enum members from an {@link EnumTrait}. + * + *

This is primarily useful when converting from string shapes to enums. + * + * @param trait The {@link EnumTrait} whose values should be converted to members. + * @param synthesizeNames Whether to synthesize names if they aren't present in the enum trait. + * @return Returns the builder. + */ + public Builder setMembersFromEnumTrait(EnumTrait trait, boolean synthesizeNames) { if (getId() == null) { throw new IllegalStateException("An id must be set before adding a named enum trait to a string."); } @@ -252,8 +275,16 @@ public Builder members(EnumTrait trait, boolean synthesizeNames) { return this; } - public Builder members(EnumTrait trait) { - return members(trait, false); + /** + * Sets enum members from an {@link EnumTrait}. + * + *

This is primarily useful when converting from string shapes to enums. + * + * @param trait The {@link EnumTrait} whose values should be converted to members. + * @return Returns the builder. + */ + public Builder setMembersFromEnumTrait(EnumTrait trait) { + return setMembersFromEnumTrait(trait, false); } /** @@ -262,7 +293,7 @@ public Builder members(EnumTrait trait) { * @param members Members to add to the builder. * @return Returns the builder. */ - public Builder members(Collection members) { + public Builder setMembersFromEnumTrait(Collection members) { clearMembers(); for (MemberShape member : members) { addMember(member); @@ -280,6 +311,16 @@ public Builder clearMembers() { return this; } + /** + * Adds a member to the shape. + * + *

If the member does not already have an {@link EnumValueTrait}, one will + * be generated with the value being equal to the member name. + * + * @param member Member to add to the shape. + * @return Returns the model assembler. + * @throws UnsupportedOperationException if the shape does not support members. + */ @Override public Builder addMember(MemberShape member) { if (!member.getTarget().equals(UnitTypeTrait.UNIT)) { @@ -406,7 +447,8 @@ public Builder flattenMixins() { if (getMixins().isEmpty()) { return this; } - members(NamedMemberUtils.flattenMixins(members.get(), getMixins(), getId(), getSourceLocation())); + setMembersFromEnumTrait(NamedMemberUtils.flattenMixins( + members.get(), getMixins(), getId(), getSourceLocation())); return (Builder) super.flattenMixins(); } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java index 293b54d6d0f..ef7b799d930 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java @@ -16,6 +16,7 @@ package software.amazon.smithy.model.shapes; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -27,7 +28,6 @@ import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.utils.BuilderRef; import software.amazon.smithy.utils.ListUtils; -import software.amazon.smithy.utils.MapUtils; public final class IntEnumShape extends IntegerShape implements NamedMembers { @@ -60,7 +60,7 @@ public Map getEnumValues() { } values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).expectIntValue()); } - enumValues = MapUtils.orderedCopyOf(values); + enumValues = Collections.unmodifiableMap(values); } return enumValues; } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java index 5dd656b6021..a6d4f805383 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/ShapeType.java @@ -35,8 +35,8 @@ public enum ShapeType { BIG_DECIMAL("bigDecimal", BigDecimalShape.class, Category.SIMPLE), BIG_INTEGER("bigInteger", BigIntegerShape.class, Category.SIMPLE), - ENUM("enum", EnumShape.class, Category.SIMPLE, (a, b) -> b.equals(a) || b.equals(STRING)), - INT_ENUM("intEnum", IntEnumShape.class, Category.SIMPLE, (a, b) -> b.equals(a) || b.equals(INTEGER)), + ENUM("enum", EnumShape.class, Category.SIMPLE, (a, b) -> b == a || b == STRING), + INT_ENUM("intEnum", IntEnumShape.class, Category.SIMPLE, (a, b) -> b == a || b == INTEGER), LIST("list", ListShape.class, Category.AGGREGATE), SET("set", SetShape.class, Category.AGGREGATE), diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java index 22372178306..c4142879a02 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefaultTrait.java @@ -29,7 +29,7 @@ * On an {@link IntEnumShape} this implies the enum value is 0. On an * {@link EnumShape} this implies the enum value is an empty string. */ -public class EnumDefaultTrait extends AnnotationTrait { +public final class EnumDefaultTrait extends AnnotationTrait { public static final ShapeId ID = ShapeId.from("smithy.api#enumDefault"); public EnumDefaultTrait(ObjectNode node) { 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 f8a8a18e1b2..a248e3042df 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 @@ -30,9 +30,9 @@ /** * Constrains string values to one of the predefined enum constants. * - * This trait is deprecated, use an {@link EnumShape} instead. + *

This trait is deprecated, use an {@link EnumShape} instead. * - * There is also the {@link SyntheticEnumTrait}, which is a synthetic variant of this + *

There is also the {@link SyntheticEnumTrait}, which is a synthetic variant of this * trait used exclusively to assist in making {@link EnumShape} as backwards compatible * as possible. */ diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java index aab4e40d94d..75cf0ac1d37 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java @@ -37,6 +37,7 @@ import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.traits.TraitDefinition; import software.amazon.smithy.model.traits.synthetic.OriginalShapeIdTrait; @@ -507,6 +508,8 @@ public Model changeShapeType(Model model, Map shapeToType, b /** * Changes each compatible string shape with the enum trait to an enum shape. * + *

A member will be created on the shape for each entry in the {@link EnumTrait}. + * * @param model Model to transform. * @param synthesizeEnumNames Whether enums without names should have names synthesized if possible. * @return Returns the transformed model. @@ -518,6 +521,8 @@ public Model changeStringEnumsToEnumShapes(Model model, boolean synthesizeEnumNa /** * Changes each compatible string shape with the enum trait to an enum shape. * + *

A member will be created on the shape for each entry in the {@link EnumTrait}. + * *

Strings with enum traits that don't define names are not converted. * * @param model Model to transform. @@ -533,7 +538,7 @@ public Model changeStringEnumsToEnumShapes(Model model) { * @param model Model to transform. * @return Returns the transformed model. */ - public Model changeEnumsToBaseShapeTypes(Model model) { + public Model downgradeEnums(Model model) { return ChangeShapeType.downgradeEnums(model).transform(this, model); } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java index b4037fc1df3..da134af6739 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java @@ -32,6 +32,15 @@ import software.amazon.smithy.model.validation.AbstractValidator; import software.amazon.smithy.model.validation.ValidationEvent; +/** + * Emits an error validation event if an enum member's enumValue trait has the wrong type, + * if there are any duplicate values in a single enum, if the enum's default value is + * set using the enumValue trait, or if an intEnum member lacks an enumValue / enumDefault + * trait. + * + *

Additionally, emits warning events when enum member names don't follow the recommended + * naming convention of all upper case letters separated by underscores. + */ public final class EnumShapeValidator extends AbstractValidator { private static final Pattern RECOMMENDED_NAME_PATTERN = Pattern.compile("^[A-Z]+[A-Z_0-9]*$"); diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExclusiveStructureMemberTraitValidator.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExclusiveStructureMemberTraitValidator.java index 35ba9d5785e..47152b1a651 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExclusiveStructureMemberTraitValidator.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/ExclusiveStructureMemberTraitValidator.java @@ -51,10 +51,12 @@ public List validate(Model model) { } List events = new ArrayList<>(); - model.shapes().filter(shape -> shape instanceof NamedMembers).forEach(shape -> { - validateExclusiveMembers(shape, exclusiveMemberTraits, events); - validateExclusiveTargets(model, shape, exclusiveTargetTraits, events); - }); + for (Shape shape : model.toSet()) { + if (shape instanceof NamedMembers) { + validateExclusiveMembers(shape, exclusiveMemberTraits, events); + validateExclusiveTargets(model, shape, exclusiveTargetTraits, events); + } + } return events; } diff --git a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy index 7456f896369..3c393395604 100644 --- a/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy +++ b/smithy-model/src/main/resources/software/amazon/smithy/model/loader/prelude.smithy @@ -432,6 +432,7 @@ string EnumConstantBodyName /// Defines the value of an enum member. @trait(selector: ":is(enum, intEnum) > member") +@tags(["diff.error.const"]) document enumValue /// Sets an enum member as the default value member. @@ -440,6 +441,7 @@ document enumValue structurallyExclusive: "member" conflicts: [enumValue] ) +@tags(["diff.error.const"]) structure enumDefault {} /// Constrains a shape to minimum and maximum number of elements or size. diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index fe557e355c5..6d6e8b277af 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -33,7 +33,6 @@ import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.MapUtils; -import software.amazon.smithy.utils.SetUtils; public class EnumShapeTest { @Test @@ -145,7 +144,7 @@ public void addMemberFromEnumTrait() { .name("foo") .value("bar") .build(); - EnumShape shape = builder.members(EnumTrait.builder().addEnum(enumDefinition).build()).build(); + EnumShape shape = builder.setMembersFromEnumTrait(EnumTrait.builder().addEnum(enumDefinition).build()).build(); assertEquals(shape.getMember("foo").get(), MemberShape.builder() @@ -167,7 +166,7 @@ public void convertsDocsFromEnumTrait() { .value("bar") .documentation(docs) .build(); - EnumShape shape = builder.members(EnumTrait.builder().addEnum(enumDefinition).build()).build(); + EnumShape shape = builder.setMembersFromEnumTrait(EnumTrait.builder().addEnum(enumDefinition).build()).build(); assertEquals(shape.getMember("foo").get(), MemberShape.builder() @@ -190,7 +189,7 @@ public void convertsTagsFromEnumTrait() { .value("bar") .addTag(tag) .build(); - EnumShape shape = builder.members(EnumTrait.builder().addEnum(enumDefinition).build()).build(); + EnumShape shape = builder.setMembersFromEnumTrait(EnumTrait.builder().addEnum(enumDefinition).build()).build(); assertEquals(shape.getMember("foo").get(), MemberShape.builder() @@ -212,7 +211,7 @@ public void convertsDeprecatedFromEnumTrait() { .value("bar") .deprecated(true) .build(); - EnumShape shape = builder.members(EnumTrait.builder().addEnum(enumDefinition).build()).build(); + EnumShape shape = builder.setMembersFromEnumTrait(EnumTrait.builder().addEnum(enumDefinition).build()).build(); assertEquals(shape.getMember("foo").get(), MemberShape.builder() @@ -236,7 +235,7 @@ public void givenEnumTraitMustUseNames() { .build(); Assertions.assertThrows(IllegalStateException.class, () -> { - builder.members(trait); + builder.setMembersFromEnumTrait(trait); }); } @@ -248,7 +247,7 @@ public void givenEnumTraitMaySynthesizeNames() { .value("foo:bar") .build()) .build(); - EnumShape shape = builder.members(trait, true).build(); + EnumShape shape = builder.setMembersFromEnumTrait(trait, true).build(); assertEquals(shape.getMember("foo_bar").get(), MemberShape.builder() @@ -276,7 +275,7 @@ public void givenEnumTraitMayOnlySynthesizeNamesFromValidValues() { .build(); Assertions.assertThrows(IllegalStateException.class, () -> { - builder.members(trait, true); + builder.setMembersFromEnumTrait(trait, true); }); } @@ -284,7 +283,7 @@ public void givenEnumTraitMayOnlySynthesizeNamesFromValidValues() { public void addMultipleMembers() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); - EnumShape shape = builder.members(ListUtils.of( + EnumShape shape = builder.setMembersFromEnumTrait(ListUtils.of( MemberShape.builder() .id("ns.foo#bar$foo") .target(UnitTypeTrait.UNIT) @@ -328,7 +327,7 @@ public void addMultipleMembers() { public void removeMember() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); - builder.members(ListUtils.of( + builder.setMembersFromEnumTrait(ListUtils.of( MemberShape.builder() .id("ns.foo#bar$foo") .target(UnitTypeTrait.UNIT) diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java index f4c6a1d5907..60d636f42ba 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/transform/ChangeShapeTypeTest.java @@ -431,7 +431,7 @@ public void canDowngradeEnums() { .setParsedShapesVersion("2.0") .addShapes(stringEnum, intEnum) .assemble().unwrap(); - Model result = ModelTransformer.create().changeEnumsToBaseShapeTypes(model); + Model result = ModelTransformer.create().downgradeEnums(model); assertThat(result.expectShape(stringEnum.getId()).getType(), Matchers.is(ShapeType.STRING)); assertThat(result.expectShape(intEnum.getId()).getType(), Matchers.is(ShapeType.INTEGER)); From a8965b7ce0db0a407d162beb1b29d1bb365c0865 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 25 Feb 2022 16:36:34 +0100 Subject: [PATCH 40/45] Use feature array for changeShapeType --- .../smithy/build/transforms/ChangeTypes.java | 9 +++++- .../model/transform/ModelTransformer.java | 31 +++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java index 7297a5a1164..ad33f61d1d9 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java @@ -22,6 +22,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.transform.ModelTransformer; /** * {@code changeType} is used to change the type of one or more shapes. @@ -80,7 +81,13 @@ protected Model transformWithConfig(TransformContext context, Config config) { throw new SmithyBuildException(getName() + ": shapeTypes must not be empty"); } + if (config.getSynthesizeEnumNames()) { + return context.getTransformer().changeShapeType( + context.getModel(), config.getShapeTypes(), + ModelTransformer.ChangeShapeTypeOption.SYNTHESIZE_ENUM_NAMES); + } + return context.getTransformer().changeShapeType( - context.getModel(), config.getShapeTypes(), config.getSynthesizeEnumNames()); + context.getModel(), config.getShapeTypes()); } } diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java index 75cf0ac1d37..b75e539767c 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java @@ -497,12 +497,17 @@ public Model changeShapeType(Model model, Map shapeToType) { * * @param model Model to transform. * @param shapeToType Map of shape IDs to the new type to use for the shape. - * @param synthesizeEnumNames Whether enums without names should have names synthesized if possible. + * @param changeShapeTypeOptions An array of options to enable when changing types. * @return Returns the transformed model. * @throws ModelTransformException if an incompatible type transform is attempted. */ - public Model changeShapeType(Model model, Map shapeToType, boolean synthesizeEnumNames) { - return new ChangeShapeType(shapeToType, synthesizeEnumNames).transform(this, model); + public Model changeShapeType( + Model model, + Map shapeToType, + ChangeShapeTypeOption... changeShapeTypeOptions + ) { + boolean synthesizeNames = ChangeShapeTypeOption.SYNTHESIZE_ENUM_NAMES.hasFeature(changeShapeTypeOptions); + return new ChangeShapeType(shapeToType, synthesizeNames).transform(this, model); } /** @@ -617,4 +622,24 @@ public Model createDedicatedInputAndOutput(Model model, String inputSuffix, Stri public Model flattenAndRemoveMixins(Model model) { return new FlattenAndRemoveMixins().transform(this, model); } + + /** + * Options that can be enabled when changing shape types. + */ + public enum ChangeShapeTypeOption { + /** + * Enables synthesizing enum names when changing a string shape to an enum shape and the + * string shape's {@link EnumTrait} doesn't have names. + */ + SYNTHESIZE_ENUM_NAMES; + + boolean hasFeature(ChangeShapeTypeOption[] haystack) { + for (ChangeShapeTypeOption feature : haystack) { + if (feature == this) { + return true; + } + } + return false; + } + } } From d0664f5f7be15c277e11c3f4f5220dbc04f34b03 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 25 Feb 2022 17:25:15 +0100 Subject: [PATCH 41/45] Hide away enum conversion guts --- .../amazon/smithy/model/shapes/EnumShape.java | 136 +++++++++++++++++- .../smithy/model/traits/EnumDefinition.java | 93 ------------ .../model/transform/ChangeShapeType.java | 31 +--- .../smithy/model/shapes/EnumShapeTest.java | 8 ++ 4 files changed, 143 insertions(+), 125 deletions(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java index 429be551947..35d0df0411a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java @@ -23,11 +23,15 @@ import java.util.Optional; import java.util.function.Consumer; import java.util.logging.Logger; +import java.util.regex.Pattern; import software.amazon.smithy.model.SourceException; +import software.amazon.smithy.model.traits.DeprecatedTrait; +import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.EnumDefaultTrait; import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.model.traits.TagsTrait; import software.amazon.smithy.model.traits.Trait; import software.amazon.smithy.model.traits.UnitTypeTrait; import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; @@ -36,6 +40,7 @@ public final class EnumShape extends StringShape implements NamedMembers { + private static final Pattern CONVERTABLE_VALUE = Pattern.compile("^[a-zA-Z-_.:/\\s]+[a-zA-Z_0-9-.:/\\s]*$"); private static final Logger LOGGER = Logger.getLogger(EnumShape.class.getName()); private final Map members; @@ -165,6 +170,10 @@ public Optional asEnumShape() { * @return Optionally returns an {@link EnumShape} equivalent of the given shape. */ public static Optional fromStringShape(StringShape shape, boolean synthesizeNames) { + if (shape.isEnumShape()) { + return Optional.of((EnumShape) shape); + } + if (!shape.hasTrait(EnumTrait.ID)) { return Optional.empty(); } @@ -194,6 +203,129 @@ public static Optional fromStringShape(StringShape shape) { return fromStringShape(shape, false); } + /** + * Determines whether a given string shape can be converted to an enum shape. + * + * @param shape The string shape to be converted. + * @param synthesizeEnumNames Whether synthesizing enum names should be accounted for. + * @return Returns true if the string shape can be converted to an enum shape. + */ + public static boolean canConvertToEnum(StringShape shape, boolean synthesizeEnumNames) { + if (shape.isEnumShape()) { + return true; + } + + if (!shape.hasTrait(EnumTrait.class)) { + LOGGER.info(String.format( + "Unable to convert string shape `%s` to enum shape because it doesn't have an enum trait.", + shape.getId() + )); + return false; + } + + EnumTrait trait = shape.expectTrait(EnumTrait.class); + if (!synthesizeEnumNames && trait.getValues().iterator().next().getName().isPresent()) { + LOGGER.info(String.format( + "Unable to convert string shape `%s` to enum shape because it doesn't define names. The " + + "`synthesizeNames` option may be able to synthesize the names for you.", + shape.getId() + )); + return true; + } + + for (EnumDefinition definition : trait.getValues()) { + if (!canConvertEnumDefinitionToMember(definition, synthesizeEnumNames)) { + LOGGER.info(String.format( + "Unable to convert string shape `%s` to enum shape because it has at least one value which " + + "cannot be safely synthesized into a name: %s", + shape.getId(), definition.getValue() + )); + return false; + } + } + + return true; + } + + /** + * Converts an enum definition to the equivalent enum member shape. + * + * @param parentId The {@link ShapeId} of the enum shape. + * @param synthesizeName Whether to synthesize a name if possible. + * @return An optional member shape representing the enum definition, + * or empty if conversion is impossible. + */ + static Optional memberFromEnumDefinition( + EnumDefinition definition, + ShapeId parentId, + boolean synthesizeName + ) { + String name; + if (!definition.getName().isPresent()) { + if (canConvertEnumDefinitionToMember(definition, synthesizeName)) { + name = definition.getValue().replaceAll("[-.:/\\s]+", "_"); + } else { + return Optional.empty(); + } + } else { + name = definition.getName().get(); + } + + try { + MemberShape.Builder builder = MemberShape.builder() + .id(parentId.withMember(name)) + .target(UnitTypeTrait.UNIT) + .addTrait(EnumValueTrait.builder().stringValue(definition.getValue()).build()); + + definition.getDocumentation().ifPresent(docs -> builder.addTrait(new DocumentationTrait(docs))); + if (!definition.getTags().isEmpty()) { + builder.addTrait(TagsTrait.builder().values(definition.getTags()).build()); + } + if (definition.isDeprecated()) { + builder.addTrait(DeprecatedTrait.builder().build()); + } + + return Optional.of(builder.build()); + } catch (ShapeIdSyntaxException e) { + return Optional.empty(); + } + } + + /** + * Determines whether the definition can be converted to a member. + * + * @param withSynthesizedNames Whether to account for name synthesization. + * @return Returns true if the definition can be converted. + */ + static boolean canConvertEnumDefinitionToMember(EnumDefinition definition, boolean withSynthesizedNames) { + return definition.getName().isPresent() + || (withSynthesizedNames && CONVERTABLE_VALUE.matcher(definition.getValue()).find()); + } + + /** + * Converts an enum member into an equivalent enum definition object. + * + * @param member The enum member to convert. + * @return An {@link EnumDefinition} representing the given member. + */ + static EnumDefinition enumDefinitionFromMember(MemberShape member) { + EnumDefinition.Builder builder = EnumDefinition.builder().name(member.getMemberName()); + + Optional traitValue = member.getTrait(EnumValueTrait.class).flatMap(EnumValueTrait::getStringValue); + if (member.hasTrait(EnumDefaultTrait.class)) { + builder.value(""); + } else if (traitValue.isPresent()) { + builder.value(traitValue.get()); + } else { + throw new IllegalStateException("Enum definitions can only be made for string enums."); + } + + member.getTrait(DocumentationTrait.class).ifPresent(docTrait -> builder.documentation(docTrait.getValue())); + member.getTrait(TagsTrait.class).ifPresent(tagsTrait -> builder.tags(tagsTrait.getValues())); + member.getTrait(DeprecatedTrait.class).ifPresent(deprecatedTrait -> builder.deprecated(true)); + return builder.build(); + } + @Override public ShapeType getType() { return ShapeType.ENUM; @@ -219,7 +351,7 @@ private void addSyntheticEnumTrait() { SyntheticEnumTrait.Builder builder = SyntheticEnumTrait.builder(); for (MemberShape member : members.get().values()) { try { - builder.addEnum(EnumDefinition.fromMember(member)); + builder.addEnum(EnumShape.enumDefinitionFromMember(member)); } catch (IllegalStateException e) { // This can happen if the enum value trait is using something other // than a string value. Rather than letting the exception propagate @@ -261,7 +393,7 @@ public Builder setMembersFromEnumTrait(EnumTrait trait, boolean synthesizeNames) clearMembers(); for (EnumDefinition definition : trait.getValues()) { - Optional member = definition.asMember(getId(), synthesizeNames); + Optional member = EnumShape.memberFromEnumDefinition(definition, getId(), synthesizeNames); if (member.isPresent()) { addMember(member.get()); } else { diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java index c97ed7684d0..8463873dcd9 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java @@ -20,14 +20,10 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.regex.Pattern; 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.node.ToNode; -import software.amazon.smithy.model.shapes.MemberShape; -import software.amazon.smithy.model.shapes.ShapeId; -import software.amazon.smithy.model.shapes.ShapeIdSyntaxException; import software.amazon.smithy.utils.SmithyBuilder; import software.amazon.smithy.utils.Tagged; import software.amazon.smithy.utils.ToSmithyBuilder; @@ -43,8 +39,6 @@ public final class EnumDefinition implements ToNode, ToSmithyBuilder tags; @@ -114,93 +108,6 @@ public static EnumDefinition fromNode(Node node) { return builder.build(); } - /** - * Converts an enum definition to the equivalent enum member shape. - * - * @param parentId The {@link ShapeId} of the enum shape. - * @param synthesizeName Whether to synthesize a name if possible. - * @return An optional member shape representing the enum definition, - * or empty if conversion is impossible. - */ - public Optional asMember(ShapeId parentId, boolean synthesizeName) { - String name; - if (!getName().isPresent()) { - if (canConvertToMember(synthesizeName)) { - name = getValue().replaceAll("[-.:/\\s]+", "_"); - } else { - return Optional.empty(); - } - } else { - name = getName().get(); - } - - try { - MemberShape.Builder builder = MemberShape.builder() - .id(parentId.withMember(name)) - .target(UnitTypeTrait.UNIT) - .addTrait(EnumValueTrait.builder().stringValue(value).build()); - - getDocumentation().ifPresent(docs -> builder.addTrait(new DocumentationTrait(docs))); - if (!tags.isEmpty()) { - builder.addTrait(TagsTrait.builder().values(tags).build()); - } - if (deprecated) { - builder.addTrait(DeprecatedTrait.builder().build()); - } - - return Optional.of(builder.build()); - } catch (ShapeIdSyntaxException e) { - return Optional.empty(); - } - } - - /** - * Converts an enum definition to the equivalent enum member shape. - * - * This is only possible if the enum definition has a name. - * - * @param parentId The {@link ShapeId} of the enum shape. - * @return An optional member shape representing the enum definition, - * or empty if conversion is impossible. - */ - public Optional asMember(ShapeId parentId) { - return asMember(parentId, false); - } - - /** - * Determines whether the definition can be converted to a member. - * - * @param withSynthesizedNames Whether to account for name synthesization. - * @return Returns true if the definition can be converted. - */ - public boolean canConvertToMember(boolean withSynthesizedNames) { - return getName().isPresent() || (withSynthesizedNames && CONVERTABLE_VALUE.matcher(getValue()).find()); - } - - /** - * Converts an enum member into an equivalent enum definition object. - * - * @param member The enum member to convert. - * @return An {@link EnumDefinition} representing the given member. - */ - public static EnumDefinition fromMember(MemberShape member) { - EnumDefinition.Builder builder = EnumDefinition.builder().name(member.getMemberName()); - - Optional traitValue = member.getTrait(EnumValueTrait.class).flatMap(EnumValueTrait::getStringValue); - if (member.hasTrait(EnumDefaultTrait.class)) { - builder.value(""); - } else if (traitValue.isPresent()) { - builder.value(traitValue.get()); - } else { - throw new IllegalStateException("Enum definitions can only be made for string enums."); - } - - member.getTrait(DocumentationTrait.class).ifPresent(docTrait -> builder.documentation(docTrait.getValue())); - member.getTrait(TagsTrait.class).ifPresent(tagsTrait -> builder.tags(tagsTrait.getValues())); - member.getTrait(DeprecatedTrait.class).ifPresent(deprecatedTrait -> builder.deprecated(true)); - return builder.build(); - } - @Override public List getTags() { return tags; diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java index 10ed48d36b8..5dda202b45c 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeType.java @@ -18,7 +18,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.logging.Logger; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.AbstractShapeBuilder; import software.amazon.smithy.model.shapes.BigDecimalShape; @@ -45,14 +44,11 @@ import software.amazon.smithy.model.shapes.StructureShape; import software.amazon.smithy.model.shapes.TimestampShape; import software.amazon.smithy.model.shapes.UnionShape; -import software.amazon.smithy.model.traits.EnumDefinition; import software.amazon.smithy.model.traits.EnumTrait; import software.amazon.smithy.model.traits.synthetic.SyntheticEnumTrait; final class ChangeShapeType { - private static final Logger LOGGER = Logger.getLogger(ChangeShapeType.class.getName()); - private final Map shapeToType; private final boolean synthesizeEnumNames; @@ -68,38 +64,13 @@ final class ChangeShapeType { static ChangeShapeType upgradeEnums(Model model, boolean synthesizeEnumNames) { Map toUpdate = new HashMap<>(); for (StringShape shape: model.getStringShapesWithTrait(EnumTrait.class)) { - if (canUpgradeEnum(shape, synthesizeEnumNames)) { + if (EnumShape.canConvertToEnum(shape, synthesizeEnumNames)) { toUpdate.put(shape.getId(), ShapeType.ENUM); } } return new ChangeShapeType(toUpdate, synthesizeEnumNames); } - private static boolean canUpgradeEnum(StringShape shape, boolean synthesizeEnumNames) { - EnumTrait trait = shape.expectTrait(EnumTrait.class); - if (!synthesizeEnumNames && trait.getValues().iterator().next().getName().isPresent()) { - LOGGER.warning(String.format( - "Unable to convert string shape `%s` to enum shape because it doesn't define names. The " - + "`synthesizeNames` option may be able to synthesize the names for you.", - shape.getId() - )); - return true; - } - - for (EnumDefinition definition : trait.getValues()) { - if (!definition.canConvertToMember(synthesizeEnumNames)) { - LOGGER.warning(String.format( - "Unable to convert string shape `%s` to enum shape because it has at least one value which " - + "cannot be safely synthesized into a name: %s", - shape.getId(), definition.getValue() - )); - return false; - } - } - - return true; - } - static ChangeShapeType downgradeEnums(Model model) { Map toUpdate = new HashMap<>(); for (EnumShape shape : model.getEnumShapes()) { diff --git a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java index 6d6e8b277af..e7293735b58 100644 --- a/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java +++ b/smithy-model/src/test/java/software/amazon/smithy/model/shapes/EnumShapeTest.java @@ -225,6 +225,14 @@ public void convertsDeprecatedFromEnumTrait() { assertEquals(shape.expectTrait(EnumTrait.class).getValues(), ListUtils.of(enumDefinition)); } + @Test + public void convertsEnumUnchanged() { + EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); + EnumShape shape = builder.addMember("foo", "bar").build(); + Optional converted = EnumShape.fromStringShape(shape); + assertEquals(shape, converted.get()); + } + @Test public void givenEnumTraitMustUseNames() { EnumShape.Builder builder = (EnumShape.Builder) EnumShape.builder().id("ns.foo#bar"); From c7a1aafbff2e8bf28a430213b4f460ce40293580 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 25 Feb 2022 17:36:41 +0100 Subject: [PATCH 42/45] Call out enumValue defaulting in design --- designs/enum-shapes.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/designs/enum-shapes.md b/designs/enum-shapes.md index 8bb5ea54916..22bd57af930 100644 --- a/designs/enum-shapes.md +++ b/designs/enum-shapes.md @@ -125,6 +125,41 @@ structure GetFooInput { Every enum shape MUST define at least one member. +#### enum members always have a value + +Enum members that have neither the `@enumValue` nor the `@enumDefault` trait +are indistinguishable from members that have an `@enumValue` trait whose trait +value is equal to the enum member's name. + +The following model: + +``` +enum Suit { + DIAMOND + CLUB + HEART + SPADE +} +``` + +Is equivalent to: + +``` +enum Suit { + @enumValue("DIAMOND") + DIAMOND + + @enumValue("CLUB") + CLUB + + @enumValue("HEART") + HEART + + @enumValue("SPADE") + SPADE +} +``` + ### intEnum shape An intEnum is used to represent an enumerated set of integer values. The From f27baad863cad093ab54600045ac92bb0031bacb Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 25 Feb 2022 22:10:31 +0100 Subject: [PATCH 43/45] Give specific error for fractions in enumValue --- .../software/amazon/smithy/model/traits/EnumValueTrait.java | 5 +++++ .../errorfiles/validators/enum-value-invalid-type.errors | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java index dddd12452bb..ffd7230f449 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java @@ -100,6 +100,11 @@ public Trait createTrait(ShapeId target, Node value) { value.asNumberNode().ifPresent(node -> { if (node.isNaturalNumber()) { builder.intValue(node.getValue().intValue()); + } else { + throw new SourceException( + "Enum values may not use fractional numbers.", + value.getSourceLocation() + ); } }); EnumValueTrait result = builder.build(); diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors index 39add152c59..49dd9239c0c 100644 --- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors +++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/enum-value-invalid-type.errors @@ -1,4 +1,4 @@ -[ERROR] smithy.example#IntEnum$FLOAT: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model +[ERROR] smithy.example#IntEnum$FLOAT: Error creating trait `enumValue`: Enum values may not use fractional numbers. | Model [ERROR] smithy.example#IntEnum$ARRAY: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model [ERROR] smithy.example#IntEnum$MAP: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model [ERROR] smithy.example#IntEnum$NULL: Error creating trait `enumValue`: Either a string value or an integer value must be set for the enumValue trait. | Model From bb1d7cff70c03867e8cb2732a1f6c67fc89ae6d6 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 25 Feb 2022 22:12:42 +0100 Subject: [PATCH 44/45] Move ChangeShapeTypeOption out of ModelTransformer --- .../smithy/build/transforms/ChangeTypes.java | 4 +- .../transform/ChangeShapeTypeOption.java | 38 +++++++++++++++++++ .../model/transform/ModelTransformer.java | 19 ---------- 3 files changed, 40 insertions(+), 21 deletions(-) create mode 100644 smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeTypeOption.java diff --git a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java index ad33f61d1d9..9b47c0cb645 100644 --- a/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java +++ b/smithy-build/src/main/java/software/amazon/smithy/build/transforms/ChangeTypes.java @@ -22,7 +22,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeType; -import software.amazon.smithy.model.transform.ModelTransformer; +import software.amazon.smithy.model.transform.ChangeShapeTypeOption; /** * {@code changeType} is used to change the type of one or more shapes. @@ -84,7 +84,7 @@ protected Model transformWithConfig(TransformContext context, Config config) { if (config.getSynthesizeEnumNames()) { return context.getTransformer().changeShapeType( context.getModel(), config.getShapeTypes(), - ModelTransformer.ChangeShapeTypeOption.SYNTHESIZE_ENUM_NAMES); + ChangeShapeTypeOption.SYNTHESIZE_ENUM_NAMES); } return context.getTransformer().changeShapeType( diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeTypeOption.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeTypeOption.java new file mode 100644 index 00000000000..ff8e3ba1fac --- /dev/null +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ChangeShapeTypeOption.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.model.transform; + +import software.amazon.smithy.model.traits.EnumTrait; + +/** + * Options that can be enabled when changing shape types. + */ +public enum ChangeShapeTypeOption { + /** + * Enables synthesizing enum names when changing a string shape to an enum shape and the + * string shape's {@link EnumTrait} doesn't have names. + */ + SYNTHESIZE_ENUM_NAMES; + + boolean hasFeature(ChangeShapeTypeOption[] haystack) { + for (ChangeShapeTypeOption feature : haystack) { + if (feature == this) { + return true; + } + } + return false; + } +} diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java index b75e539767c..1ee00c6d92a 100644 --- a/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java +++ b/smithy-model/src/main/java/software/amazon/smithy/model/transform/ModelTransformer.java @@ -623,23 +623,4 @@ public Model flattenAndRemoveMixins(Model model) { return new FlattenAndRemoveMixins().transform(this, model); } - /** - * Options that can be enabled when changing shape types. - */ - public enum ChangeShapeTypeOption { - /** - * Enables synthesizing enum names when changing a string shape to an enum shape and the - * string shape's {@link EnumTrait} doesn't have names. - */ - SYNTHESIZE_ENUM_NAMES; - - boolean hasFeature(ChangeShapeTypeOption[] haystack) { - for (ChangeShapeTypeOption feature : haystack) { - if (feature == this) { - return true; - } - } - return false; - } - } } From f574098f5fc98adfaa71b426adbc7b8a176e2513 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 25 Feb 2022 22:16:37 +0100 Subject: [PATCH 45/45] Clarify enum value defaulting --- designs/enum-shapes.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/designs/enum-shapes.md b/designs/enum-shapes.md index 22bd57af930..8f16a86828a 100644 --- a/designs/enum-shapes.md +++ b/designs/enum-shapes.md @@ -127,9 +127,11 @@ Every enum shape MUST define at least one member. #### enum members always have a value -Enum members that have neither the `@enumValue` nor the `@enumDefault` trait -are indistinguishable from members that have an `@enumValue` trait whose trait -value is equal to the enum member's name. +If an enum member doesn't have an explicit `@enumValue` or `@enumDefault` trait, +an `@enumValue` trait will be automatically added to the member where the trait +value is the member's name. This means that enum members that have neither the +`@enumValue` nor the `@enumDefault` trait are indistinguishable from enum members +that have the `@enumValue` trait explicitly set. The following model: