Skip to content

Commit 7cda77f

Browse files
committed
Generate CRTP style builders
1 parent 5520a37 commit 7cda77f

8 files changed

Lines changed: 120 additions & 14 deletions

File tree

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,28 @@ void errorShapeWithInterfaceMixin() {
6767
assertEquals(1, error.getId());
6868
assertInstanceOf(Exception.class, error);
6969
}
70+
71+
@Test
72+
void simpleUserBuilderImplementsMixinBuilder() {
73+
assertInstanceOf(HasName.Builder.class, SimpleUser.builder());
74+
}
75+
76+
@Test
77+
void detailedUserBuilderImplementsChainedMixinBuilders() {
78+
var builder = DetailedUser.builder();
79+
assertInstanceOf(HasFullName.Builder.class, builder);
80+
assertInstanceOf(HasName.Builder.class, builder);
81+
}
82+
83+
@Test
84+
void taggedUserBuilderImplementsMultipleMixinBuilders() {
85+
var builder = TaggedUser.builder();
86+
assertInstanceOf(HasName.Builder.class, builder);
87+
assertInstanceOf(HasTags.Builder.class, builder);
88+
}
89+
90+
@Test
91+
void errorBuilderImplementsMixinBuilder() {
92+
assertInstanceOf(HasName.Builder.class, UserNotFound.builder());
93+
}
7094
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package software.amazon.smithy.java.codegen.generators;
77

88
import java.util.List;
9+
import software.amazon.smithy.codegen.core.Symbol;
910
import software.amazon.smithy.codegen.core.SymbolProvider;
1011
import software.amazon.smithy.java.codegen.CodegenUtils;
1112
import software.amazon.smithy.java.codegen.writer.JavaWriter;
@@ -61,7 +62,7 @@ public void run() {
6162
/**
6263
* Builder for {@link ${shape:T}}.
6364
*/
64-
${^inInterface}public static ${/inInterface}final class Builder implements ${sdkShapeBuilder:T}<${shape:T}>${?isStaged}, ${#stages}${value:L}${^key.last}, ${/key.last}${/stages}${/isStaged} {
65+
${^inInterface}public static ${/inInterface}final class Builder implements ${sdkShapeBuilder:T}<${shape:T}>${?isStaged}, ${#stages}${value:L}${^key.last}, ${/key.last}${/stages}${/isStaged}${?hasMixinBuilders}${#mixinBuilders}, ${value:T}.Builder<Builder>${/mixinBuilders}${/hasMixinBuilders} {
6566
${builderProperties:C|}
6667
6768
${builderConstructor:C|}
@@ -97,6 +98,9 @@ public void run() {
9798
writer.putContext("stages", this.stageInterfaces());
9899
writer.putContext("stageGen", writer.consumer(this::generateStages));
99100
}
101+
var mixinBuilders = mixinBuilderInterfaces();
102+
writer.putContext("hasMixinBuilders", !mixinBuilders.isEmpty());
103+
writer.putContext("mixinBuilders", mixinBuilders);
100104
writer.write(template);
101105
writer.popState();
102106
}
@@ -136,6 +140,10 @@ protected boolean inInterface() {
136140
return false;
137141
}
138142

143+
protected List<Symbol> mixinBuilderInterfaces() {
144+
return List.of();
145+
}
146+
139147
protected String getMemberSchemaName(MemberShape member) {
140148
return CodegenUtils.toMemberSchemaName(symbolProvider.toMemberName(member));
141149
}

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

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,34 @@ public void accept(T directive) {
6060
"""
6161
public interface ${shape:T}${?hasParents} extends ${#parents}${value:T}${^key.last}, ${/key.last}${/parents}${/hasParents} {
6262
${getters:C|}
63+
64+
${builderInterface:C|}
6365
}
6466
""";
6567
writer.putContext("shape", directive.symbol());
6668
writer.putContext("hasParents", !parentInterfaces.isEmpty());
6769
writer.putContext("parents", parentInterfaces);
6870
writer.putContext("getters",
6971
new GetterSignatureGenerator(writer, shape, symbolProvider, model, parentInterfaces));
72+
writer.putContext("builderInterface",
73+
new BuilderInterfaceGenerator(writer, shape, symbolProvider, model, parentInterfaces));
7074
writer.write(template);
7175

7276
writer.popState();
7377
});
7478
}
7579

80+
private static boolean isMemberFromParentInterface(Shape shape, Model model, MemberShape member) {
81+
for (ShapeId mixinId : shape.getMixins()) {
82+
StructureShape mixinShape = model.expectShape(mixinId, StructureShape.class);
83+
if (MixinTrait.isInterfaceMixin(mixinShape)
84+
&& mixinShape.getAllMembers().containsKey(member.getMemberName())) {
85+
return true;
86+
}
87+
}
88+
return false;
89+
}
90+
7691
private record GetterSignatureGenerator(
7792
JavaWriter writer,
7893
Shape shape,
@@ -82,7 +97,7 @@ private record GetterSignatureGenerator(
8297
@Override
8398
public void run() {
8499
for (MemberShape member : shape.members()) {
85-
if (isMemberFromParentInterface(member)) {
100+
if (isMemberFromParentInterface(shape, model, member)) {
86101
continue;
87102
}
88103
writer.pushState();
@@ -108,16 +123,44 @@ public void run() {
108123
writer.popState();
109124
}
110125
}
126+
}
111127

112-
private boolean isMemberFromParentInterface(MemberShape member) {
113-
for (ShapeId mixinId : shape.getMixins()) {
114-
StructureShape mixinShape = model.expectShape(mixinId, StructureShape.class);
115-
if (MixinTrait.isInterfaceMixin(mixinShape)
116-
&& mixinShape.getAllMembers().containsKey(member.getMemberName())) {
117-
return true;
128+
private record BuilderInterfaceGenerator(
129+
JavaWriter writer,
130+
Shape shape,
131+
SymbolProvider symbolProvider,
132+
Model model,
133+
List<Symbol> parentInterfaces) implements Runnable {
134+
@Override
135+
public void run() {
136+
writer.pushState();
137+
writer.putContext("hasParentBuilders", !parentInterfaces.isEmpty());
138+
writer.putContext("parentBuilders", parentInterfaces);
139+
var template =
140+
"""
141+
interface Builder<B extends Builder<B>>${?hasParentBuilders} extends ${#parentBuilders}${value:T}.Builder<B>${^key.last}, ${/key.last}${/parentBuilders}${/hasParentBuilders} {
142+
${setters:C|}
143+
}""";
144+
writer.putContext("setters", writer.consumer(this::generateSetterSignatures));
145+
writer.write(template);
146+
writer.popState();
147+
}
148+
149+
private void generateSetterSignatures(JavaWriter writer) {
150+
for (MemberShape member : shape.members()) {
151+
if (isMemberFromParentInterface(shape, model, member)) {
152+
continue;
118153
}
154+
writer.pushState();
155+
var memberName = symbolProvider.toMemberName(member);
156+
writer.putContext("memberName", memberName);
157+
writer.putContext("memberSymbol", symbolProvider.toSymbol(member));
158+
writer.putContext("isNullable", CodegenUtils.isNullableMember(model, member));
159+
writer.write(
160+
"B ${memberName:L}(${?isNullable}${memberSymbol:B}${/isNullable}${^isNullable}${memberSymbol:N}${/isNullable} ${memberName:L});");
161+
writer.write("");
162+
writer.popState();
119163
}
120-
return false;
121164
}
122165
}
123166
}

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@ public final class ${shape:T} ${^isError}implements ${serializableStruct:T}${#mi
193193
shape,
194194
directive.symbolProvider(),
195195
directive.model(),
196-
directive.service()));
196+
directive.service(),
197+
interfaceMixinSymbols,
198+
interfaceMixinMemberNames));
197199
writer.putContext("getMemberValue", new GetMemberValueGenerator(writer, directive.symbolProvider(), shape));
198200
writer.putContext("toBuilder", new ToBuilderGenerator(writer, shape, directive.symbolProvider()));
199201
writer.write(template);
@@ -543,15 +545,26 @@ public Builder toBuilder() {
543545
}
544546

545547
private static final class StructureBuilderGenerator extends BuilderGenerator {
548+
private final List<Symbol> interfaceMixinSymbols;
549+
private final Set<String> interfaceMixinMemberNames;
546550

547551
StructureBuilderGenerator(
548552
JavaWriter writer,
549553
Shape shape,
550554
SymbolProvider symbolProvider,
551555
Model model,
552-
ServiceShape service
556+
ServiceShape service,
557+
List<Symbol> interfaceMixinSymbols,
558+
Set<String> interfaceMixinMemberNames
553559
) {
554560
super(writer, shape, symbolProvider, model, service);
561+
this.interfaceMixinSymbols = interfaceMixinSymbols;
562+
this.interfaceMixinMemberNames = interfaceMixinMemberNames;
563+
}
564+
565+
@Override
566+
protected List<Symbol> mixinBuilderInterfaces() {
567+
return interfaceMixinSymbols;
555568
}
556569

557570
// Required shapes marked with clientOptional should not be required to create the type. For these shapes,
@@ -794,10 +807,13 @@ protected void generateSetters(JavaWriter writer) {
794807
writer.putContext("check", CodegenUtils.requiresSetterNullCheck(symbolProvider, member));
795808
writer.putContext("isNullable", CodegenUtils.isNullableMember(model, member));
796809
writer.putContext("schemaName", CodegenUtils.toMemberSchemaName(symbolProvider.toMemberName(member)));
810+
writer.putContext("hasOverride",
811+
interfaceMixinMemberNames.contains(member.getMemberName()));
797812

798813
writer.write(
799814
"""
800-
public Builder ${memberName:L}(${?isNullable}${memberSymbol:B}${/isNullable}${^isNullable}${memberSymbol:N}${/isNullable} ${memberName:L}) {
815+
${?hasOverride}@Override
816+
${/hasOverride}public Builder ${memberName:L}(${?isNullable}${memberSymbol:B}${/isNullable}${^isNullable}${memberSymbol:N}${/isNullable} ${memberName:L}) {
801817
this.${memberName:L} = ${?check}${objects:T}.requireNonNull(${/check}${memberName:L}${?check}, "${memberName:L} cannot be null")${/check};${?tracked}
802818
tracker.setMember(${schemaName:L});${/tracked}
803819
return this;

codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/interface-mixin/expected/DetailedUser.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public static Builder builder() {
140140
/**
141141
* Builder for {@link DetailedUser}.
142142
*/
143-
public static final class Builder implements ShapeBuilder<DetailedUser> {
143+
public static final class Builder implements ShapeBuilder<DetailedUser>, HasFullName.Builder<Builder> {
144144
private final PresenceTracker tracker = PresenceTracker.of($SCHEMA);
145145
private String name;
146146
private int id;
@@ -157,6 +157,7 @@ public Schema schema() {
157157
/**
158158
* @return this builder.
159159
*/
160+
@Override
160161
public Builder name(String name) {
161162
this.name = name;
162163
return this;
@@ -166,6 +167,7 @@ public Builder name(String name) {
166167
* <p><strong>Required</strong>
167168
* @return this builder.
168169
*/
170+
@Override
169171
public Builder id(int id) {
170172
this.id = id;
171173
tracker.setMember($SCHEMA_ID);
@@ -175,6 +177,7 @@ public Builder id(int id) {
175177
/**
176178
* @return this builder.
177179
*/
180+
@Override
178181
public Builder lastName(String lastName) {
179182
this.lastName = lastName;
180183
return this;

codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/interface-mixin/expected/HasFullName.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,9 @@
1010
public interface HasFullName extends HasName {
1111
String getLastName();
1212

13+
interface Builder<B extends Builder<B>> extends HasName.Builder<B> {
14+
B lastName(String lastName);
15+
16+
}
1317
}
1418

codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/interface-mixin/expected/HasName.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,11 @@ public interface HasName {
1212

1313
int getId();
1414

15+
interface Builder<B extends Builder<B>> {
16+
B name(String name);
17+
18+
B id(int id);
19+
20+
}
1521
}
1622

codegen/plugins/types-codegen/src/test/resources/software/amazon/smithy/java/codegen/types/test-cases/interface-mixin/expected/SimpleUser.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public static Builder builder() {
126126
/**
127127
* Builder for {@link SimpleUser}.
128128
*/
129-
public static final class Builder implements ShapeBuilder<SimpleUser> {
129+
public static final class Builder implements ShapeBuilder<SimpleUser>, HasName.Builder<Builder> {
130130
private final PresenceTracker tracker = PresenceTracker.of($SCHEMA);
131131
private String name;
132132
private int id;
@@ -142,6 +142,7 @@ public Schema schema() {
142142
/**
143143
* @return this builder.
144144
*/
145+
@Override
145146
public Builder name(String name) {
146147
this.name = name;
147148
return this;
@@ -151,6 +152,7 @@ public Builder name(String name) {
151152
* <p><strong>Required</strong>
152153
* @return this builder.
153154
*/
155+
@Override
154156
public Builder id(int id) {
155157
this.id = id;
156158
tracker.setMember($SCHEMA_ID);

0 commit comments

Comments
 (0)