Skip to content

Commit 5dbad8b

Browse files
Add enumDefault trait
1 parent 2dc22b8 commit 5dbad8b

File tree

21 files changed

+332
-44
lines changed

21 files changed

+332
-44
lines changed

designs/enum-shapes.md

+13-5
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,12 @@ structure GetFooInput {
8181
}
8282
```
8383

84-
To account for this, enums MAY define a default member by making the
85-
`@enumValue` of the member "":
84+
To account for this, enums MAY define a default member by setting the
85+
`@enumDefault` trait on a member:
8686

8787
```
8888
enum Suit {
89-
@enumValue(string: "")
89+
@enumDefault
9090
UNKNOWN
9191
9292
@enumValue(string: "diamond")
@@ -121,6 +121,10 @@ structure GetFooInput {
121121
}
122122
```
123123

124+
#### enums must define at least one member
125+
126+
Every enum shape MUST define at least one member.
127+
124128
### intEnum shape
125129

126130
An intEnum is used to represent an enumerated set of integer values. The
@@ -159,11 +163,11 @@ structure GetFooInput {
159163
```
160164

161165
intEnums MAY define a member to represent the default value of the shape by
162-
making the `@enumValue` of the member 0:
166+
setting the `@enumDefault` trait on a member:
163167

164168
```
165169
intEnum FaceCard {
166-
@enumValue(int: 0)
170+
@enumDefault
167171
UNKNOWN
168172
169173
@enumValue(int: 1)
@@ -198,6 +202,10 @@ default dispatch to the integer shape when an intEnum is encountered. This
198202
both makes adding enums backward compatible, and allows implementations that
199203
do not support enums at all to ignore them.
200204

205+
#### intEnums must define at least one member
206+
207+
Every intEnum shape MUST define at least one member.
208+
201209
### Smithy taxonomy updates
202210

203211
Both enum and intEnum are considered simple shapes though they have members.

smithy-model/src/main/java/software/amazon/smithy/model/shapes/EnumShape.java

+41-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Optional;
2323
import java.util.function.Consumer;
2424
import software.amazon.smithy.model.SourceException;
25+
import software.amazon.smithy.model.traits.EnumDefaultTrait;
2526
import software.amazon.smithy.model.traits.EnumDefinition;
2627
import software.amazon.smithy.model.traits.EnumTrait;
2728
import software.amazon.smithy.model.traits.EnumValueTrait;
@@ -57,7 +58,11 @@ public Map<String, String> getEnumValues() {
5758
if (enumValues == null) {
5859
Map<String, String> values = new LinkedHashMap<>(members.size());
5960
for (MemberShape member : members()) {
60-
values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).getStringValue().get());
61+
if (member.hasTrait(EnumDefaultTrait.ID)) {
62+
values.put(member.getMemberName(), "");
63+
continue;
64+
}
65+
values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).expectStringValue());
6166
}
6267
enumValues = MapUtils.orderedCopyOf(values);
6368
}
@@ -282,7 +287,7 @@ public Builder addMember(MemberShape member) {
282287
"Enum members may only target `smithy.api#Unit`, but found `%s`", member.getTarget()
283288
), getSourceLocation());
284289
}
285-
if (!member.hasTrait(EnumValueTrait.ID)) {
290+
if (!member.hasTrait(EnumValueTrait.ID) && !member.hasTrait(EnumDefaultTrait.ID)) {
286291
member = member.toBuilder()
287292
.addTrait(EnumValueTrait.builder().stringValue(member.getMemberName()).build())
288293
.build();
@@ -328,6 +333,40 @@ public Builder addMember(String memberName, String enumValue, Consumer<MemberSha
328333
return addMember(builder.build());
329334
}
330335

336+
/**
337+
* Adds the default member to the builder.
338+
*
339+
* @param memberName Member name to add.
340+
* @return Returns the builder.
341+
*/
342+
public Builder addDefaultMember(String memberName) {
343+
return addDefaultMember(memberName, null);
344+
}
345+
346+
/**
347+
* Adds the default member to the builder.
348+
*
349+
* @param memberName Member name to add.
350+
* @param memberUpdater Consumer that can update the created member shape.
351+
* @return Returns the builder.
352+
*/
353+
public Builder addDefaultMember(String memberName, Consumer<MemberShape.Builder> memberUpdater) {
354+
if (getId() == null) {
355+
throw new IllegalStateException("An id must be set before setting a member with a target");
356+
}
357+
358+
MemberShape.Builder builder = MemberShape.builder()
359+
.target(UnitTypeTrait.UNIT)
360+
.id(getId().withMember(memberName))
361+
.addTrait(new EnumDefaultTrait());
362+
363+
if (memberUpdater != null) {
364+
memberUpdater.accept(builder);
365+
}
366+
367+
return addMember(builder.build());
368+
}
369+
331370
/**
332371
* Removes a member by name.
333372
*

smithy-model/src/main/java/software/amazon/smithy/model/shapes/IntEnumShape.java

+40-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Optional;
2323
import java.util.function.Consumer;
2424
import software.amazon.smithy.model.SourceException;
25+
import software.amazon.smithy.model.traits.EnumDefaultTrait;
2526
import software.amazon.smithy.model.traits.EnumValueTrait;
2627
import software.amazon.smithy.model.traits.UnitTypeTrait;
2728
import software.amazon.smithy.utils.BuilderRef;
@@ -53,7 +54,11 @@ public Map<String, Integer> getEnumValues() {
5354
if (enumValues == null) {
5455
Map<String, Integer> values = new LinkedHashMap<>(members.size());
5556
for (MemberShape member : members()) {
56-
values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).getIntValue().get());
57+
if (member.hasTrait(EnumDefaultTrait.ID)) {
58+
values.put(member.getMemberName(), 0);
59+
continue;
60+
}
61+
values.put(member.getMemberName(), member.expectTrait(EnumValueTrait.class).expectIntValue());
5762
}
5863
enumValues = MapUtils.orderedCopyOf(values);
5964
}
@@ -235,6 +240,40 @@ public Builder addMember(String memberName, int enumValue, Consumer<MemberShape.
235240
return addMember(builder.build());
236241
}
237242

243+
/**
244+
* Adds the default member to the builder.
245+
*
246+
* @param memberName Member name to add.
247+
* @return Returns the builder.
248+
*/
249+
public Builder addDefaultMember(String memberName) {
250+
return addDefaultMember(memberName, null);
251+
}
252+
253+
/**
254+
* Adds the default member to the builder.
255+
*
256+
* @param memberName Member name to add.
257+
* @param memberUpdater Consumer that can update the created member shape.
258+
* @return Returns the builder.
259+
*/
260+
public Builder addDefaultMember(String memberName, Consumer<MemberShape.Builder> memberUpdater) {
261+
if (getId() == null) {
262+
throw new IllegalStateException("An id must be set before setting a member with a target");
263+
}
264+
265+
MemberShape.Builder builder = MemberShape.builder()
266+
.target(UnitTypeTrait.UNIT)
267+
.id(getId().withMember(memberName))
268+
.addTrait(new EnumDefaultTrait());
269+
270+
if (memberUpdater != null) {
271+
memberUpdater.accept(builder);
272+
}
273+
274+
return addMember(builder.build());
275+
}
276+
238277
/**
239278
* Removes a member by name.
240279
*

smithy-model/src/main/java/software/amazon/smithy/model/shapes/NamedMembers.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.util.Map;
2020
import java.util.Optional;
2121

22-
interface NamedMembers {
22+
public interface NamedMembers {
2323

2424
/**
2525
* Gets the members of the shape, including mixin members.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.model.traits;
17+
18+
import java.util.Collections;
19+
import software.amazon.smithy.model.SourceLocation;
20+
import software.amazon.smithy.model.node.Node;
21+
import software.amazon.smithy.model.node.ObjectNode;
22+
import software.amazon.smithy.model.shapes.EnumShape;
23+
import software.amazon.smithy.model.shapes.IntEnumShape;
24+
import software.amazon.smithy.model.shapes.ShapeId;
25+
26+
/**
27+
* Indicates that an enum member is the default value member.
28+
*
29+
* On an {@link IntEnumShape} this implies the enum value is 0. On an
30+
* {@link EnumShape} this implies the enum value is an empty string.
31+
*/
32+
public class EnumDefaultTrait extends AnnotationTrait {
33+
public static final ShapeId ID = ShapeId.from("smithy.api#enumDefault");
34+
35+
public EnumDefaultTrait(ObjectNode node) {
36+
super(ID, node);
37+
}
38+
39+
public EnumDefaultTrait(SourceLocation location) {
40+
this(new ObjectNode(Collections.emptyMap(), location));
41+
}
42+
43+
public EnumDefaultTrait() {
44+
this(Node.objectNode());
45+
}
46+
47+
public static final class Provider extends AnnotationTrait.Provider<EnumDefaultTrait> {
48+
public Provider() {
49+
super(ID, EnumDefaultTrait::new);
50+
}
51+
}
52+
}

smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumDefinition.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,11 @@ public boolean canConvertToMember(boolean withSynthesizedNames) {
186186
public static EnumDefinition fromMember(MemberShape member) {
187187
EnumDefinition.Builder builder = EnumDefinition.builder().name(member.getMemberName());
188188

189-
EnumValueTrait valueTrait = member.expectTrait(EnumValueTrait.class);
190-
if (valueTrait.getStringValue().isPresent()) {
191-
builder.value(valueTrait.getStringValue().get());
189+
Optional<String> traitValue = member.getTrait(EnumValueTrait.class).flatMap(EnumValueTrait::getStringValue);
190+
if (member.hasTrait(EnumDefaultTrait.class)) {
191+
builder.value("");
192+
} else if (traitValue.isPresent()) {
193+
builder.value(traitValue.get());
192194
} else {
193195
throw new IllegalStateException("Enum definitions can only be made for string enums.");
194196
}

smithy-model/src/main/java/software/amazon/smithy/model/traits/EnumValueTrait.java

+38
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Optional;
1919
import software.amazon.smithy.model.SourceException;
20+
import software.amazon.smithy.model.node.ExpectationNotMetException;
2021
import software.amazon.smithy.model.node.Node;
2122
import software.amazon.smithy.model.node.NumberNode;
2223
import software.amazon.smithy.model.node.ObjectNode;
@@ -25,6 +26,9 @@
2526
import software.amazon.smithy.utils.SmithyBuilder;
2627
import software.amazon.smithy.utils.ToSmithyBuilder;
2728

29+
/**
30+
* Sets the value for an enum member.
31+
*/
2832
public final class EnumValueTrait extends AbstractTrait implements ToSmithyBuilder<EnumValueTrait> {
2933
public static final ShapeId ID = ShapeId.from("smithy.api#enumValue");
3034

@@ -43,14 +47,48 @@ private EnumValueTrait(Builder builder) {
4347
}
4448
}
4549

50+
/**
51+
* Gets the string value if a string value was set.
52+
*
53+
* @return Optionally returns the string value.
54+
*/
4655
public Optional<String> getStringValue() {
4756
return Optional.ofNullable(string);
4857
}
4958

59+
/**
60+
* Gets the string value.
61+
*
62+
* @return Returns the string value.
63+
* @throws ExpectationNotMetException if the string value was not set.
64+
*/
65+
public String expectStringValue() {
66+
return getStringValue().orElseThrow(() -> new ExpectationNotMetException(
67+
"Expected string value was not set.", this
68+
));
69+
}
70+
71+
/**
72+
* Gets the int value if an int value was set.
73+
*
74+
* @return Returns the set int value.
75+
*/
5076
public Optional<Integer> getIntValue() {
5177
return Optional.ofNullable(integer);
5278
}
5379

80+
/**
81+
* Gets the int value.
82+
*
83+
* @return Returns the int value.
84+
* @throws ExpectationNotMetException if the int value was not set.
85+
*/
86+
public int expectIntValue() {
87+
return getIntValue().orElseThrow(() -> new ExpectationNotMetException(
88+
"Expected integer value was not set.", this
89+
));
90+
}
91+
5492
public static final class Provider extends AbstractTrait.Provider {
5593
public Provider() {
5694
super(ID);

smithy-model/src/main/java/software/amazon/smithy/model/validation/validators/EnumShapeValidator.java

+24-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import software.amazon.smithy.model.shapes.IntEnumShape;
2828
import software.amazon.smithy.model.shapes.MemberShape;
2929
import software.amazon.smithy.model.shapes.Shape;
30+
import software.amazon.smithy.model.traits.EnumDefaultTrait;
3031
import software.amazon.smithy.model.traits.EnumValueTrait;
3132
import software.amazon.smithy.model.validation.AbstractValidator;
3233
import software.amazon.smithy.model.validation.ValidationEvent;
@@ -52,15 +53,25 @@ public List<ValidationEvent> validate(Model model) {
5253
private void validateEnumShape(List<ValidationEvent> events, EnumShape shape) {
5354
Set<String> values = new HashSet<>();
5455
for (MemberShape member : shape.members()) {
56+
if (member.hasTrait(EnumDefaultTrait.ID)) {
57+
continue;
58+
}
5559
Optional<String> value = member.expectTrait(EnumValueTrait.class).getStringValue();
5660
if (!value.isPresent()) {
5761
events.add(error(member, member.expectTrait(EnumValueTrait.class),
5862
"The enumValue trait must use the string option when applied to enum shapes."));
59-
} else if (!values.add(value.get())) {
60-
events.add(error(member, String.format(
61-
"Multiple enum members found with duplicate value `%s`",
62-
value.get()
63-
)));
63+
} else {
64+
if (!values.add(value.get())) {
65+
events.add(error(member, String.format(
66+
"Multiple enum members found with duplicate value `%s`",
67+
value.get()
68+
)));
69+
}
70+
if (value.get().equals("")) {
71+
events.add(error(member, "enum values may not be empty because an empty string is the "
72+
+ "default value of enum shapes. Instead, use `smithy.api#enumDefault` to set an "
73+
+ "explicit name for the default value."));
74+
}
6475
}
6576
validateEnumMemberName(events, member);
6677
}
@@ -69,6 +80,9 @@ private void validateEnumShape(List<ValidationEvent> events, EnumShape shape) {
6980
private void validateIntEnumShape(List<ValidationEvent> events, IntEnumShape shape) {
7081
Set<Integer> values = new HashSet<>();
7182
for (MemberShape member : shape.members()) {
83+
if (member.hasTrait(EnumDefaultTrait.ID)) {
84+
continue;
85+
}
7286
if (!member.hasTrait(EnumValueTrait.ID)) {
7387
events.add(missingIntEnumValue(member, member));
7488
} else if (!member.expectTrait(EnumValueTrait.class).getIntValue().isPresent()) {
@@ -81,6 +95,11 @@ private void validateIntEnumShape(List<ValidationEvent> events, IntEnumShape sha
8195
value
8296
)));
8397
}
98+
if (value == 0) {
99+
events.add(error(member, "intEnum values may not be set to 0 because 0 is the "
100+
+ "default value of intEnum shapes. Instead, use `smithy.api#enumDefault` to set an "
101+
+ "explicit name for the default value."));
102+
}
84103
}
85104
validateEnumMemberName(events, member);
86105
}

0 commit comments

Comments
 (0)