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.
This commit is contained in:
Hunter Mellema 2024-05-01 16:16:52 -06:00 committed by GitHub
parent cbf76e22a7
commit 35658328a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 311 additions and 24 deletions

View File

@ -3,6 +3,7 @@ package software.amazon.smithy.traitcodegen.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.example.traits.StringTrait;
import com.example.traits.defaults.StructDefaultsTrait;
import com.example.traits.documents.DocumentTrait;
import com.example.traits.documents.StructWithNestedDocumentTrait;
import com.example.traits.enums.IntEnumTrait;
@ -136,7 +137,9 @@ public class CreatesTraitTest {
SetMember.builder().a("second").b(2).c("more").build().toNode()
)),
// Strings
Arguments.of(StringTrait.ID, Node.from("SPORKZ SPOONS YAY! Utensils."))
Arguments.of(StringTrait.ID, Node.from("SPORKZ SPOONS YAY! Utensils.")),
// Defaults
Arguments.of(StructDefaultsTrait.ID, Node.objectNode())
);
}

View File

@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import com.example.traits.StringTrait;
import com.example.traits.defaults.StructDefaultsTrait;
import com.example.traits.documents.DocumentTrait;
import com.example.traits.documents.StructWithNestedDocumentTrait;
import com.example.traits.enums.IntEnumTrait;
@ -210,7 +211,34 @@ public class LoadsFromModelTest {
SetMember.builder().a("second").b(2).c("more").build()))),
// Strings
Arguments.of("string-trait.smithy", StringTrait.class,
MapUtils.of("getValue","Testing String Trait"))
MapUtils.of("getValue","Testing String Trait")),
// Defaults
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultList", ListUtils.of())),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultMap", MapUtils.of())),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultBoolean", true)),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultString", "default")),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultByte", (byte) 1)),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultShort", (short) 1)),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultInt", 1)),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultLong", 1L)),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultFloat", 2.2F)),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultDouble", 1.1)),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultBigInt", new BigInteger("100"))),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultBigDecimal", new BigDecimal("100.01"))),
Arguments.of("defaults/defaults.smithy", StructDefaultsTrait.class,
MapUtils.of("getDefaultTimestamp", Instant.parse("1985-04-12T23:20:50.52Z")))
);
}

View File

@ -0,0 +1,9 @@
$version: "2.0"
namespace test.smithy.traitcodegen
use test.smithy.traitcodegen.defaults#StructDefaults
@StructDefaults
structure myStruct {
}

View File

@ -10,6 +10,7 @@ import software.amazon.smithy.codegen.core.ReservedWords;
import software.amazon.smithy.codegen.core.ReservedWordsBuilder;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.traits.UniqueItemsTrait;
import software.amazon.smithy.utils.CaseUtils;
@ -134,4 +135,17 @@ public final class TraitCodegenUtils {
}
return shapeNamespace.replace(rootSmithyNamespace, packageNamespace);
}
/**
* Determines if a given member represents a nullable type.
*
* @see <a href="https://smithy.io/2.0/spec/aggregate-types.html#structure-member-optionality">structure member optionality</a>
*
* @param shape member to check for nullability
*
* @return if the shape is a nullable type
*/
public static boolean isNullableMember(MemberShape shape) {
return !shape.isRequired() && !shape.hasNonNullDefault();
}
}

View File

@ -5,20 +5,41 @@
package software.amazon.smithy.traitcodegen.generators;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Iterator;
import java.util.Optional;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.BigDecimalShape;
import software.amazon.smithy.model.shapes.BigIntegerShape;
import software.amazon.smithy.model.shapes.BlobShape;
import software.amazon.smithy.model.shapes.BooleanShape;
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.FloatShape;
import software.amazon.smithy.model.shapes.IntegerShape;
import software.amazon.smithy.model.shapes.ListShape;
import software.amazon.smithy.model.shapes.LongShape;
import software.amazon.smithy.model.shapes.MapShape;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeType;
import software.amazon.smithy.model.shapes.ShapeVisitor;
import software.amazon.smithy.model.shapes.ShortShape;
import software.amazon.smithy.model.shapes.StringShape;
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.AbstractTraitBuilder;
import software.amazon.smithy.model.traits.DefaultTrait;
import software.amazon.smithy.model.traits.StringListTrait;
import software.amazon.smithy.model.traits.TimestampFormatTrait;
import software.amazon.smithy.model.traits.TraitDefinition;
import software.amazon.smithy.traitcodegen.SymbolProperties;
import software.amazon.smithy.traitcodegen.TraitCodegenUtils;
@ -172,6 +193,13 @@ final class BuilderGenerator implements Runnable {
symbolProvider.toSymbol(shape),
symbolProvider.toMemberName(shape),
builderRefOptional.orElseThrow(RuntimeException::new));
return;
}
if (shape.hasNonNullDefault()) {
writer.write("private $B $L = $C;", symbolProvider.toSymbol(shape),
symbolProvider.toMemberName(shape),
new DefaultInitializerGenerator(writer, model, symbolProvider, shape));
} else {
writer.write("private $B $L;", symbolProvider.toSymbol(shape),
symbolProvider.toMemberName(shape));
@ -305,4 +333,161 @@ final class BuilderGenerator implements Runnable {
return model.expectShape(shape.getTarget()).accept(this);
}
}
/**
* Adds default values to builder properties.
*/
private static final class DefaultInitializerGenerator extends ShapeVisitor.DataShapeVisitor<Void> implements
Runnable {
private final TraitCodegenWriter writer;
private final Model model;
private final SymbolProvider symbolProvider;
private final MemberShape member;
private Node defaultValue;
DefaultInitializerGenerator(
TraitCodegenWriter writer,
Model model,
SymbolProvider symbolProvider, MemberShape member
) {
this.writer = writer;
this.model = model;
this.symbolProvider = symbolProvider;
this.member = member;
}
@Override
public void run() {
if (member.hasNonNullDefault()) {
this.defaultValue = member.expectTrait(DefaultTrait.class).toNode();
member.accept(this);
}
}
@Override
public Void blobShape(BlobShape blobShape) {
throw new UnsupportedOperationException("Blob default value cannot be set.");
}
@Override
public Void booleanShape(BooleanShape booleanShape) {
writer.write("$L", defaultValue.expectBooleanNode().getValue());
return null;
}
@Override
public Void listShape(ListShape listShape) {
throw new UnsupportedOperationException("List default values are not set with DefaultGenerator.");
}
@Override
public Void mapShape(MapShape mapShape) {
throw new UnsupportedOperationException("Map default values are not set with DefaultGenerator.");
}
@Override
public Void byteShape(ByteShape byteShape) {
// Bytes duplicate the integer toString method
writer.write("$L", defaultValue.expectNumberNode().getValue().intValue());
return null;
}
@Override
public Void shortShape(ShortShape shortShape) {
// Shorts duplicate the int toString method
writer.write("$L", defaultValue.expectNumberNode().getValue().intValue());
return null;
}
@Override
public Void integerShape(IntegerShape integerShape) {
writer.write("$L", defaultValue.expectNumberNode().getValue().intValue());
return null;
}
@Override
public Void longShape(LongShape longShape) {
writer.write("$LL", defaultValue.expectNumberNode().getValue().longValue());
return null;
}
@Override
public Void floatShape(FloatShape floatShape) {
writer.write("$Lf", defaultValue.expectNumberNode().getValue().floatValue());
return null;
}
@Override
public Void documentShape(DocumentShape documentShape) {
throw new UnsupportedOperationException("Document shape defaults cannot be set.");
}
@Override
public Void doubleShape(DoubleShape doubleShape) {
writer.write("$L", defaultValue.expectNumberNode().getValue().doubleValue());
return null;
}
@Override
public Void bigIntegerShape(BigIntegerShape bigIntegerShape) {
writer.write("$T.valueOf($L)", BigInteger.class, defaultValue.expectNumberNode().getValue().intValue());
return null;
}
@Override
public Void bigDecimalShape(BigDecimalShape bigDecimalShape) {
writer.write("$T.valueOf($L)", BigDecimal.class, defaultValue.expectNumberNode().getValue().doubleValue());
return null;
}
@Override
public Void stringShape(StringShape stringShape) {
writer.write("$S", defaultValue.expectStringNode().getValue());
return null;
}
@Override
public Void structureShape(StructureShape structureShape) {
throw new UnsupportedOperationException("Structure shape defaults cannot be set.");
}
@Override
public Void unionShape(UnionShape unionShape) {
throw new UnsupportedOperationException("Union shape defaults cannot be set.");
}
@Override
public Void memberShape(MemberShape memberShape) {
return model.expectShape(memberShape.getTarget()).accept(this);
}
@Override
public Void timestampShape(TimestampShape timestampShape) {
if (member.hasTrait(TimestampFormatTrait.class)) {
switch (member.expectTrait(TimestampFormatTrait.class).getFormat()) {
case EPOCH_SECONDS:
writer.writeInline(
"$T.ofEpochSecond($LL)",
Instant.class,
defaultValue.expectNumberNode().getValue().longValue()
);
return null;
case HTTP_DATE:
writer.writeInline(
"$T.from($T.RFC_1123_DATE_TIME.parse($S))",
Instant.class,
DateTimeFormatter.class,
defaultValue.expectStringNode().getValue()
);
return null;
default:
// Fall through on default
break;
}
}
writer.write("$T.parse($S)", Instant.class, defaultValue.expectStringNode().getValue());
return null;
}
}
}

View File

@ -269,11 +269,11 @@ final class ConstructorGenerator extends TraitVisitor<Void> implements Runnable
@Override
public Void structureShape(StructureShape shape) {
for (MemberShape member : shape.members()) {
if (member.isRequired()) {
if (TraitCodegenUtils.isNullableMember(member)) {
writer.write("this.$L = $L;", symbolProvider.toMemberName(member), getBuilderValue(member));
} else {
writer.write("this.$1L = $2T.requiredState($1S, $3L);",
symbolProvider.toMemberName(member), SmithyBuilder.class, getBuilderValue(member));
} else {
writer.write("this.$L = $L;", symbolProvider.toMemberName(member), getBuilderValue(member));
}
}
return null;
@ -300,13 +300,17 @@ final class ConstructorGenerator extends TraitVisitor<Void> implements Runnable
}
private String getBuilderValue(MemberShape member) {
String memberName = symbolProvider.toMemberName(member);
// If the member requires a builderRef we need to copy that builder ref value rather than use it directly.
if (symbolProvider.toSymbol(member).getProperty(SymbolProperties.BUILDER_REF_INITIALIZER).isPresent()) {
return writer.format("builder.$1L.hasValue() ? builder.$1L.copy() : null",
symbolProvider.toMemberName(member));
} else {
return writer.format("builder.$L", symbolProvider.toMemberName(member));
if (TraitCodegenUtils.isNullableMember(member)) {
return writer.format("builder.$1L.hasValue() ? builder.$1L.copy() : null", memberName);
} else {
return writer.format("builder.$1L.copy()", memberName);
}
}
return writer.format("builder.$L", memberName);
}
private void writeValuesInitializer() {

View File

@ -190,7 +190,7 @@ final class FromNodeGenerator extends TraitVisitor<Void> implements Runnable {
private MemberGenerator(MemberShape member) {
this.fieldName = member.getMemberName();
this.memberName = symbolProvider.toMemberName(member);
this.memberPrefix = member.isRequired() ? ".expect" : ".get";
this.memberPrefix = (member.isRequired() && !member.hasNonNullDefault()) ? ".expect" : ".get";
}
@Override

View File

@ -116,14 +116,7 @@ final class GetterGenerator implements Runnable {
// If the member is required or the type does not require an optional wrapper (such as a list or map)
// then do not wrap return in an Optional
writer.pushState(new GetterSection(member));
if (member.isRequired()) {
writer.openBlock("public $T get$U() {", "}",
symbolProvider.toSymbol(member),
symbolProvider.toMemberName(member),
() -> writer.write("return $L;", symbolProvider.toMemberName(member)));
writer.popState();
writer.newLine();
} else {
if (TraitCodegenUtils.isNullableMember(member)) {
writer.openBlock("public $T<$T> get$U() {", "}",
Optional.class, symbolProvider.toSymbol(member), symbolProvider.toMemberName(member),
() -> writer.write("return $T.ofNullable($L);",
@ -140,6 +133,12 @@ final class GetterGenerator implements Runnable {
symbolProvider.toMemberName(member),
() -> writer.write("return $L;", symbolProvider.toMemberName(member)));
}
} else {
writer.openBlock("public $T get$U() {", "}",
symbolProvider.toSymbol(member),
symbolProvider.toMemberName(member),
() -> writer.write("return $L;", symbolProvider.toMemberName(member)));
writer.popState();
}
writer.newLine();
}

View File

@ -139,14 +139,14 @@ final class ToNodeGenerator implements Runnable {
writer.writeInline(".sourceLocation(getSourceLocation())");
}
for (MemberShape mem : shape.members()) {
if (mem.isRequired()) {
writer.write(".withMember($S, $C)",
mem.getMemberName(),
(Runnable) () -> mem.accept(new ToNodeMapperVisitor(symbolProvider.toMemberName(mem))));
} else {
if (TraitCodegenUtils.isNullableMember(mem)) {
writer.write(".withOptionalMember($S, get$U().map(m -> $C))",
mem.getMemberName(), symbolProvider.toMemberName(mem),
(Runnable) () -> mem.accept(new ToNodeMapperVisitor("m")));
} else {
writer.write(".withMember($S, $C)",
mem.getMemberName(),
(Runnable) () -> mem.accept(new ToNodeMapperVisitor(symbolProvider.toMemberName(mem))));
}
}
writer.writeWithNoFormatting(".build();");

View File

@ -28,7 +28,7 @@ import software.amazon.smithy.model.node.ObjectNode;
public class TraitCodegenPluginTest {
private static final int EXPECTED_NUMBER_OF_FILES = 55;
private static final int EXPECTED_NUMBER_OF_FILES = 56;
private MockManifest manifest;
private Model model;

View File

@ -0,0 +1,44 @@
$version: "2"
namespace test.smithy.traitcodegen.defaults
@trait
structure StructDefaults {
@default([])
defaultList: StringList
@default({})
defaultMap: StringMap
@default(true)
defaultBoolean: Boolean
@default("default")
defaultString: String
@default(1)
defaultByte: Byte
@default(1)
defaultShort: Short
@default(1)
defaultInt: Integer
@default(1)
defaultLong: Long
@default(2.2)
defaultFloat: Float
@default(1.1)
defaultDouble: Double
@default(100)
defaultBigInt: BigInteger
@default(100.01)
defaultBigDecimal: BigDecimal
@default("1985-04-12T23:20:50.52Z")
defaultTimestamp: Timestamp
}
@private
list StringList {
member: String
}
@private
map StringMap {
key: String
value: String
}

View File

@ -41,4 +41,5 @@ uniqueitems/number-set-trait.smithy
uniqueitems/string-set-trait.smithy
uniqueitems/struct-set-trait.smithy
filtered-by-tag.smithy
defaults/defaults.smithy