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:
parent
cbf76e22a7
commit
35658328a8
|
@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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")))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
$version: "2.0"
|
||||
|
||||
namespace test.smithy.traitcodegen
|
||||
|
||||
use test.smithy.traitcodegen.defaults#StructDefaults
|
||||
|
||||
@StructDefaults
|
||||
structure myStruct {
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();");
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue