Skip to content

Commit 3565832

Browse files
authored
Add support for default trait on members (#2267)
Adds support for defaults on trait member values in trait code generation. Applying the default trait to a member will cause it to be treated as non-nullable and will set the value as the initial value for the member within the generated shape's builder.
1 parent cbf76e2 commit 3565832

File tree

12 files changed

+311
-24
lines changed

12 files changed

+311
-24
lines changed

smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.junit.jupiter.api.Assertions.assertEquals;
44

55
import com.example.traits.StringTrait;
6+
import com.example.traits.defaults.StructDefaultsTrait;
67
import com.example.traits.documents.DocumentTrait;
78
import com.example.traits.documents.StructWithNestedDocumentTrait;
89
import com.example.traits.enums.IntEnumTrait;
@@ -136,7 +137,9 @@ static Stream<Arguments> createTraitTests() {
136137
SetMember.builder().a("second").b(2).c("more").build().toNode()
137138
)),
138139
// Strings
139-
Arguments.of(StringTrait.ID, Node.from("SPORKZ SPOONS YAY! Utensils."))
140+
Arguments.of(StringTrait.ID, Node.from("SPORKZ SPOONS YAY! Utensils.")),
141+
// Defaults
142+
Arguments.of(StructDefaultsTrait.ID, Node.objectNode())
140143
);
141144
}
142145

smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
55

66
import com.example.traits.StringTrait;
7+
import com.example.traits.defaults.StructDefaultsTrait;
78
import com.example.traits.documents.DocumentTrait;
89
import com.example.traits.documents.StructWithNestedDocumentTrait;
910
import com.example.traits.enums.IntEnumTrait;
@@ -210,7 +211,34 @@ static Stream<Arguments> loadsModelTests() {
210211
SetMember.builder().a("second").b(2).c("more").build()))),
211212
// Strings
212213
Arguments.of("string-trait.smithy", StringTrait.class,
213-
MapUtils.of("getValue","Testing String Trait"))
214+
MapUtils.of("getValue","Testing String Trait")),
215+
// Defaults
216+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
217+
MapUtils.of("getDefaultList", ListUtils.of())),
218+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
219+
MapUtils.of("getDefaultMap", MapUtils.of())),
220+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
221+
MapUtils.of("getDefaultBoolean", true)),
222+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
223+
MapUtils.of("getDefaultString", "default")),
224+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
225+
MapUtils.of("getDefaultByte", (byte) 1)),
226+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
227+
MapUtils.of("getDefaultShort", (short) 1)),
228+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
229+
MapUtils.of("getDefaultInt", 1)),
230+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
231+
MapUtils.of("getDefaultLong", 1L)),
232+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
233+
MapUtils.of("getDefaultFloat", 2.2F)),
234+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
235+
MapUtils.of("getDefaultDouble", 1.1)),
236+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
237+
MapUtils.of("getDefaultBigInt", new BigInteger("100"))),
238+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
239+
MapUtils.of("getDefaultBigDecimal", new BigDecimal("100.01"))),
240+
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
241+
MapUtils.of("getDefaultTimestamp", Instant.parse("1985-04-12T23:20:50.52Z")))
214242
);
215243
}
216244

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
$version: "2.0"
2+
3+
namespace test.smithy.traitcodegen
4+
5+
use test.smithy.traitcodegen.defaults#StructDefaults
6+
7+
@StructDefaults
8+
structure myStruct {
9+
}

smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenUtils.java

+14
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import software.amazon.smithy.codegen.core.ReservedWordsBuilder;
1111
import software.amazon.smithy.codegen.core.Symbol;
1212
import software.amazon.smithy.codegen.core.SymbolProvider;
13+
import software.amazon.smithy.model.shapes.MemberShape;
1314
import software.amazon.smithy.model.shapes.Shape;
1415
import software.amazon.smithy.model.traits.UniqueItemsTrait;
1516
import software.amazon.smithy.utils.CaseUtils;
@@ -134,4 +135,17 @@ public static String mapNamespace(String rootSmithyNamespace,
134135
}
135136
return shapeNamespace.replace(rootSmithyNamespace, packageNamespace);
136137
}
138+
139+
/**
140+
* Determines if a given member represents a nullable type.
141+
*
142+
* @see <a href="https://smithy.io/2.0/spec/aggregate-types.html#structure-member-optionality">structure member optionality</a>
143+
*
144+
* @param shape member to check for nullability
145+
*
146+
* @return if the shape is a nullable type
147+
*/
148+
public static boolean isNullableMember(MemberShape shape) {
149+
return !shape.isRequired() && !shape.hasNonNullDefault();
150+
}
137151
}

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

+185
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,41 @@
55

66
package software.amazon.smithy.traitcodegen.generators;
77

8+
import java.math.BigDecimal;
9+
import java.math.BigInteger;
10+
import java.time.Instant;
11+
import java.time.format.DateTimeFormatter;
812
import java.util.Iterator;
913
import java.util.Optional;
1014
import software.amazon.smithy.codegen.core.Symbol;
1115
import software.amazon.smithy.codegen.core.SymbolProvider;
1216
import software.amazon.smithy.model.Model;
17+
import software.amazon.smithy.model.node.Node;
18+
import software.amazon.smithy.model.shapes.BigDecimalShape;
19+
import software.amazon.smithy.model.shapes.BigIntegerShape;
20+
import software.amazon.smithy.model.shapes.BlobShape;
21+
import software.amazon.smithy.model.shapes.BooleanShape;
22+
import software.amazon.smithy.model.shapes.ByteShape;
23+
import software.amazon.smithy.model.shapes.DocumentShape;
24+
import software.amazon.smithy.model.shapes.DoubleShape;
25+
import software.amazon.smithy.model.shapes.FloatShape;
26+
import software.amazon.smithy.model.shapes.IntegerShape;
1327
import software.amazon.smithy.model.shapes.ListShape;
28+
import software.amazon.smithy.model.shapes.LongShape;
1429
import software.amazon.smithy.model.shapes.MapShape;
1530
import software.amazon.smithy.model.shapes.MemberShape;
1631
import software.amazon.smithy.model.shapes.Shape;
1732
import software.amazon.smithy.model.shapes.ShapeType;
1833
import software.amazon.smithy.model.shapes.ShapeVisitor;
34+
import software.amazon.smithy.model.shapes.ShortShape;
35+
import software.amazon.smithy.model.shapes.StringShape;
1936
import software.amazon.smithy.model.shapes.StructureShape;
37+
import software.amazon.smithy.model.shapes.TimestampShape;
38+
import software.amazon.smithy.model.shapes.UnionShape;
2039
import software.amazon.smithy.model.traits.AbstractTraitBuilder;
40+
import software.amazon.smithy.model.traits.DefaultTrait;
2141
import software.amazon.smithy.model.traits.StringListTrait;
42+
import software.amazon.smithy.model.traits.TimestampFormatTrait;
2243
import software.amazon.smithy.model.traits.TraitDefinition;
2344
import software.amazon.smithy.traitcodegen.SymbolProperties;
2445
import software.amazon.smithy.traitcodegen.TraitCodegenUtils;
@@ -172,6 +193,13 @@ private void writeProperty(MemberShape shape) {
172193
symbolProvider.toSymbol(shape),
173194
symbolProvider.toMemberName(shape),
174195
builderRefOptional.orElseThrow(RuntimeException::new));
196+
return;
197+
}
198+
199+
if (shape.hasNonNullDefault()) {
200+
writer.write("private $B $L = $C;", symbolProvider.toSymbol(shape),
201+
symbolProvider.toMemberName(shape),
202+
new DefaultInitializerGenerator(writer, model, symbolProvider, shape));
175203
} else {
176204
writer.write("private $B $L;", symbolProvider.toSymbol(shape),
177205
symbolProvider.toMemberName(shape));
@@ -305,4 +333,161 @@ public Void memberShape(MemberShape shape) {
305333
return model.expectShape(shape.getTarget()).accept(this);
306334
}
307335
}
336+
337+
/**
338+
* Adds default values to builder properties.
339+
*/
340+
private static final class DefaultInitializerGenerator extends ShapeVisitor.DataShapeVisitor<Void> implements
341+
Runnable {
342+
private final TraitCodegenWriter writer;
343+
private final Model model;
344+
private final SymbolProvider symbolProvider;
345+
private final MemberShape member;
346+
private Node defaultValue;
347+
348+
DefaultInitializerGenerator(
349+
TraitCodegenWriter writer,
350+
Model model,
351+
SymbolProvider symbolProvider, MemberShape member
352+
) {
353+
this.writer = writer;
354+
this.model = model;
355+
this.symbolProvider = symbolProvider;
356+
this.member = member;
357+
}
358+
359+
@Override
360+
public void run() {
361+
if (member.hasNonNullDefault()) {
362+
this.defaultValue = member.expectTrait(DefaultTrait.class).toNode();
363+
member.accept(this);
364+
}
365+
}
366+
367+
@Override
368+
public Void blobShape(BlobShape blobShape) {
369+
throw new UnsupportedOperationException("Blob default value cannot be set.");
370+
}
371+
372+
@Override
373+
public Void booleanShape(BooleanShape booleanShape) {
374+
writer.write("$L", defaultValue.expectBooleanNode().getValue());
375+
return null;
376+
}
377+
378+
@Override
379+
public Void listShape(ListShape listShape) {
380+
throw new UnsupportedOperationException("List default values are not set with DefaultGenerator.");
381+
}
382+
383+
@Override
384+
public Void mapShape(MapShape mapShape) {
385+
throw new UnsupportedOperationException("Map default values are not set with DefaultGenerator.");
386+
}
387+
388+
@Override
389+
public Void byteShape(ByteShape byteShape) {
390+
// Bytes duplicate the integer toString method
391+
writer.write("$L", defaultValue.expectNumberNode().getValue().intValue());
392+
return null;
393+
}
394+
395+
@Override
396+
public Void shortShape(ShortShape shortShape) {
397+
// Shorts duplicate the int toString method
398+
writer.write("$L", defaultValue.expectNumberNode().getValue().intValue());
399+
return null;
400+
}
401+
402+
@Override
403+
public Void integerShape(IntegerShape integerShape) {
404+
writer.write("$L", defaultValue.expectNumberNode().getValue().intValue());
405+
return null;
406+
}
407+
408+
@Override
409+
public Void longShape(LongShape longShape) {
410+
writer.write("$LL", defaultValue.expectNumberNode().getValue().longValue());
411+
return null;
412+
}
413+
414+
@Override
415+
public Void floatShape(FloatShape floatShape) {
416+
writer.write("$Lf", defaultValue.expectNumberNode().getValue().floatValue());
417+
return null;
418+
}
419+
420+
@Override
421+
public Void documentShape(DocumentShape documentShape) {
422+
throw new UnsupportedOperationException("Document shape defaults cannot be set.");
423+
}
424+
425+
@Override
426+
public Void doubleShape(DoubleShape doubleShape) {
427+
writer.write("$L", defaultValue.expectNumberNode().getValue().doubleValue());
428+
return null;
429+
}
430+
431+
@Override
432+
public Void bigIntegerShape(BigIntegerShape bigIntegerShape) {
433+
writer.write("$T.valueOf($L)", BigInteger.class, defaultValue.expectNumberNode().getValue().intValue());
434+
return null;
435+
}
436+
437+
@Override
438+
public Void bigDecimalShape(BigDecimalShape bigDecimalShape) {
439+
writer.write("$T.valueOf($L)", BigDecimal.class, defaultValue.expectNumberNode().getValue().doubleValue());
440+
return null;
441+
}
442+
443+
@Override
444+
public Void stringShape(StringShape stringShape) {
445+
writer.write("$S", defaultValue.expectStringNode().getValue());
446+
return null;
447+
}
448+
449+
@Override
450+
public Void structureShape(StructureShape structureShape) {
451+
throw new UnsupportedOperationException("Structure shape defaults cannot be set.");
452+
}
453+
454+
@Override
455+
public Void unionShape(UnionShape unionShape) {
456+
throw new UnsupportedOperationException("Union shape defaults cannot be set.");
457+
458+
}
459+
460+
@Override
461+
public Void memberShape(MemberShape memberShape) {
462+
return model.expectShape(memberShape.getTarget()).accept(this);
463+
}
464+
465+
@Override
466+
public Void timestampShape(TimestampShape timestampShape) {
467+
if (member.hasTrait(TimestampFormatTrait.class)) {
468+
switch (member.expectTrait(TimestampFormatTrait.class).getFormat()) {
469+
case EPOCH_SECONDS:
470+
writer.writeInline(
471+
"$T.ofEpochSecond($LL)",
472+
Instant.class,
473+
defaultValue.expectNumberNode().getValue().longValue()
474+
);
475+
return null;
476+
case HTTP_DATE:
477+
writer.writeInline(
478+
"$T.from($T.RFC_1123_DATE_TIME.parse($S))",
479+
Instant.class,
480+
DateTimeFormatter.class,
481+
defaultValue.expectStringNode().getValue()
482+
);
483+
return null;
484+
default:
485+
// Fall through on default
486+
break;
487+
}
488+
}
489+
writer.write("$T.parse($S)", Instant.class, defaultValue.expectStringNode().getValue());
490+
return null;
491+
}
492+
}
308493
}

smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ConstructorGenerator.java

+11-7
Original file line numberDiff line numberDiff line change
@@ -269,11 +269,11 @@ public Void stringShape(StringShape shape) {
269269
@Override
270270
public Void structureShape(StructureShape shape) {
271271
for (MemberShape member : shape.members()) {
272-
if (member.isRequired()) {
272+
if (TraitCodegenUtils.isNullableMember(member)) {
273+
writer.write("this.$L = $L;", symbolProvider.toMemberName(member), getBuilderValue(member));
274+
} else {
273275
writer.write("this.$1L = $2T.requiredState($1S, $3L);",
274276
symbolProvider.toMemberName(member), SmithyBuilder.class, getBuilderValue(member));
275-
} else {
276-
writer.write("this.$L = $L;", symbolProvider.toMemberName(member), getBuilderValue(member));
277277
}
278278
}
279279
return null;
@@ -300,13 +300,17 @@ public Void timestampShape(TimestampShape shape) {
300300
}
301301

302302
private String getBuilderValue(MemberShape member) {
303+
String memberName = symbolProvider.toMemberName(member);
304+
303305
// If the member requires a builderRef we need to copy that builder ref value rather than use it directly.
304306
if (symbolProvider.toSymbol(member).getProperty(SymbolProperties.BUILDER_REF_INITIALIZER).isPresent()) {
305-
return writer.format("builder.$1L.hasValue() ? builder.$1L.copy() : null",
306-
symbolProvider.toMemberName(member));
307-
} else {
308-
return writer.format("builder.$L", symbolProvider.toMemberName(member));
307+
if (TraitCodegenUtils.isNullableMember(member)) {
308+
return writer.format("builder.$1L.hasValue() ? builder.$1L.copy() : null", memberName);
309+
} else {
310+
return writer.format("builder.$1L.copy()", memberName);
311+
}
309312
}
313+
return writer.format("builder.$L", memberName);
310314
}
311315

312316
private void writeValuesInitializer() {

smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ private final class MemberGenerator extends ShapeVisitor.DataShapeVisitor<Void>
190190
private MemberGenerator(MemberShape member) {
191191
this.fieldName = member.getMemberName();
192192
this.memberName = symbolProvider.toMemberName(member);
193-
this.memberPrefix = member.isRequired() ? ".expect" : ".get";
193+
this.memberPrefix = (member.isRequired() && !member.hasNonNullDefault()) ? ".expect" : ".get";
194194
}
195195

196196
@Override

smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java

+7-8
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,7 @@ public Void structureShape(StructureShape shape) {
116116
// If the member is required or the type does not require an optional wrapper (such as a list or map)
117117
// then do not wrap return in an Optional
118118
writer.pushState(new GetterSection(member));
119-
if (member.isRequired()) {
120-
writer.openBlock("public $T get$U() {", "}",
121-
symbolProvider.toSymbol(member),
122-
symbolProvider.toMemberName(member),
123-
() -> writer.write("return $L;", symbolProvider.toMemberName(member)));
124-
writer.popState();
125-
writer.newLine();
126-
} else {
119+
if (TraitCodegenUtils.isNullableMember(member)) {
127120
writer.openBlock("public $T<$T> get$U() {", "}",
128121
Optional.class, symbolProvider.toSymbol(member), symbolProvider.toMemberName(member),
129122
() -> writer.write("return $T.ofNullable($L);",
@@ -140,6 +133,12 @@ public Void structureShape(StructureShape shape) {
140133
symbolProvider.toMemberName(member),
141134
() -> writer.write("return $L;", symbolProvider.toMemberName(member)));
142135
}
136+
} else {
137+
writer.openBlock("public $T get$U() {", "}",
138+
symbolProvider.toSymbol(member),
139+
symbolProvider.toMemberName(member),
140+
() -> writer.write("return $L;", symbolProvider.toMemberName(member)));
141+
writer.popState();
143142
}
144143
writer.newLine();
145144
}

0 commit comments

Comments
 (0)