Skip to content

Commit affd191

Browse files
committed
Use sealed interfaces and records for Unions
1 parent f25066a commit affd191

12 files changed

Lines changed: 907 additions & 188 deletions

File tree

codegen/codegen-core/src/it/java/software/amazon/smithy/java/codegen/test/RecursionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ void multiplyRecursiveUnionWorks() {
106106
var output = builder.build();
107107
assertEquals(recursive.hashCode(), output.hashCode());
108108
assertEquals(recursive, output);
109-
assertNotEquals(AttributeValue.Type.$UNKNOWN, output.type());
109+
assertNotEquals(AttributeValue.$Unknown.class, output.getClass());
110110
}
111111

112112
@Test

codegen/codegen-core/src/it/java/software/amazon/smithy/java/codegen/test/UnionTest.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package software.amazon.smithy.java.codegen.test;
77

88
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
10+
import static org.junit.jupiter.api.Assertions.assertNotNull;
911
import static org.junit.jupiter.api.Assertions.assertThrows;
1012

1113
import java.math.BigDecimal;
@@ -25,14 +27,13 @@
2527
import software.amazon.smithy.java.codegen.test.model.NestedStruct;
2628
import software.amazon.smithy.java.codegen.test.model.NestedUnion;
2729
import software.amazon.smithy.java.codegen.test.model.UnionType;
28-
import software.amazon.smithy.java.core.schema.SerializableShape;
2930
import software.amazon.smithy.java.core.serde.ShapeSerializer;
3031
import software.amazon.smithy.java.core.serde.document.Document;
3132
import software.amazon.smithy.model.shapes.ShapeType;
3233

3334
public class UnionTest {
3435

35-
static Stream<SerializableShape> unionTypes() {
36+
static Stream<UnionType> unionTypes() {
3637
return Stream.of(
3738
new UnionType.BooleanValueMember(true),
3839
new UnionType.ListValueMember(List.of("a", "b")),
@@ -103,7 +104,11 @@ void unknownUnionDeser() {
103104
document.deserializeInto(builder);
104105
var output = builder.build();
105106

106-
assertEquals(UnionType.Type.$UNKNOWN, output.type());
107+
if (output instanceof UnionType.$Unknown(String memberName)) {
108+
assertEquals("UNKNOWN!!!", memberName);
109+
} else {
110+
throw new AssertionError("Expected $Unknown variant");
111+
}
107112
assertEquals("UNKNOWN!!!", output.getValue());
108113
}
109114

@@ -112,4 +117,19 @@ void unknownUnionSerFails() {
112117
var union = UnionType.builder().$unknownMember("foo").build();
113118
assertThrows(UnsupportedOperationException.class, () -> Document.of(union));
114119
}
120+
121+
@Test
122+
void showExhaustiveMatchRequired() {
123+
var union = getNestedUnion();
124+
switch (union) {
125+
case NestedUnion.AMember(var a) -> assertNotNull(a);
126+
case NestedUnion.BMember(var b) -> assertNotEquals(0, b);
127+
case NestedUnion.$Unknown(var memberName) -> assertNotNull(memberName);
128+
default -> throw new IllegalArgumentException("Unexpected member: " + union);
129+
}
130+
}
131+
132+
private static NestedUnion getNestedUnion() {
133+
return NestedUnion.builder().a("a").build();
134+
}
115135
}

codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/BuilderGenerator.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import software.amazon.smithy.java.core.schema.SchemaUtils;
1414
import software.amazon.smithy.java.core.schema.ShapeBuilder;
1515
import software.amazon.smithy.model.Model;
16+
import software.amazon.smithy.model.shapes.MemberShape;
1617
import software.amazon.smithy.model.shapes.ServiceShape;
1718
import software.amazon.smithy.model.shapes.Shape;
1819
import software.amazon.smithy.model.shapes.ShapeType;
@@ -21,7 +22,7 @@
2122
* Generates a static nested {@code Builder} class for a Java class.
2223
*/
2324
abstract class BuilderGenerator implements Runnable {
24-
private final JavaWriter writer;
25+
protected final JavaWriter writer;
2526
protected final Shape shape;
2627
protected final SymbolProvider symbolProvider;
2728
protected final Model model;
@@ -53,14 +54,14 @@ public void run() {
5354
/**
5455
* @return returns a new Builder.
5556
*/
56-
public static Builder builder() {
57+
${^inInterface}public ${/inInterface}static Builder builder() {
5758
return new Builder();
5859
}
5960
6061
/**
6162
* Builder for {@link ${shape:T}}.
6263
*/
63-
public static final class Builder implements ${sdkShapeBuilder:T}<${shape:T}>${?isStaged}, ${#stages}${value:L}${^key.last}, ${/key.last}${/stages}${/isStaged} {
64+
${^inInterface}public static ${/inInterface}final class Builder implements ${sdkShapeBuilder:T}<${shape:T}>${?isStaged}, ${#stages}${value:L}${^key.last}, ${/key.last}${/stages}${/isStaged} {
6465
${builderProperties:C|}
6566
6667
${builderConstructor:C|}
@@ -91,6 +92,7 @@ public static final class Builder implements ${sdkShapeBuilder:T}<${shape:T}>${?
9192
writer.putContext("setMemberValue", writer.consumer(this::generateSetMemberValue));
9293
boolean isStaged = !this.stageInterfaces().isEmpty();
9394
writer.putContext("isStaged", isStaged);
95+
writer.putContext("inInterface", inInterface());
9496
if (isStaged) {
9597
writer.putContext("stages", this.stageInterfaces());
9698
writer.putContext("stageGen", writer.consumer(this::generateStages));
@@ -130,6 +132,14 @@ protected List<String> stageInterfaces() {
130132
return List.of();
131133
}
132134

135+
protected boolean inInterface() {
136+
return false;
137+
}
138+
139+
protected String getMemberSchemaName(MemberShape member) {
140+
return CodegenUtils.toMemberSchemaName(symbolProvider.toMemberName(member));
141+
}
142+
133143
protected void generateSetMemberValue(JavaWriter writer) {
134144
// Don't override the default implementation that throws if there are no members.
135145
if (shape.members().isEmpty() || (shape.getType() == ShapeType.ENUM || shape.getType() == ShapeType.INT_ENUM)) {
@@ -157,7 +167,7 @@ protected void generateMemberValueSetters(JavaWriter writer) {
157167
writer.pushState();
158168
writer.putContext("memberName", symbolProvider.toMemberName(member));
159169
writer.putContext("type", symbolProvider.toSymbol(member));
160-
writer.putContext("memberSchema", CodegenUtils.toMemberSchemaName(symbolProvider.toMemberName(member)));
170+
writer.putContext("memberSchema", getMemberSchemaName(member));
161171
writer.putContext("schemaUtilsClass", SchemaUtils.class);
162172
writer.putContext("isNullable", CodegenUtils.isNullableMember(model, member));
163173
writer.write(

codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/IdStringGenerator.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@
1616
* @param shape Shape to write ID for
1717
*/
1818
@SmithyInternalApi
19-
public record IdStringGenerator(JavaWriter writer, Shape shape) implements Runnable {
19+
public record IdStringGenerator(JavaWriter writer, Shape shape, boolean isInterface) implements Runnable {
20+
21+
public IdStringGenerator(JavaWriter writer, Shape shape) {
22+
this(writer, shape, false);
23+
}
2024

2125
@Override
2226
public void run() {
2327
writer.pushState();
2428
writer.putContext("shapeId", ShapeId.class);
25-
writer.write("public static final ${shapeId:T} $$ID = $$SCHEMA.id();");
29+
var qualifiers = isInterface ? "" : "public static final ";
30+
writer.write(qualifiers + "${shapeId:T} $$ID = $$SCHEMA.id();");
2631
writer.popState();
2732
}
2833
}

codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SchemaFieldGenerator.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,9 @@ public Void structureShape(StructureShape shape) {
9090
public Void unionShape(UnionShape shape) {
9191
writer.pushState();
9292
writer.putContext("name", schemaFieldOrder.getSchemaFieldName(shape, writer));
93-
writer.write("public static final ${schemaClass:T} $$SCHEMA = ${name:L};");
94-
95-
for (var member : shape.members()) {
96-
writeMemberProperty(member);
97-
}
93+
// Unions are sealed interfaces, so $SCHEMA is implicitly public static final
94+
// Member schemas are generated inside each record variant, not at the interface level
95+
writer.write("${schemaClass:T} $$SCHEMA = ${name:L};");
9896

9997
writer.popState();
10098
return null;

0 commit comments

Comments
 (0)