Skip to content

Commit

Permalink
Add support for default trait on members (smithy-lang#2267)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hpmellema authored May 1, 2024
1 parent cbf76e2 commit 3565832
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;
Expand Down Expand Up @@ -136,7 +137,9 @@ static Stream<Arguments> createTraitTests() {
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())
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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;
Expand Down Expand Up @@ -210,7 +211,34 @@ static Stream<Arguments> loadsModelTests() {
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")))
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
$version: "2.0"

namespace test.smithy.traitcodegen

use test.smithy.traitcodegen.defaults#StructDefaults

@StructDefaults
structure myStruct {
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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;
Expand Down Expand Up @@ -134,4 +135,17 @@ public static String mapNamespace(String rootSmithyNamespace,
}
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -172,6 +193,13 @@ private void writeProperty(MemberShape shape) {
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));
Expand Down Expand Up @@ -305,4 +333,161 @@ public Void memberShape(MemberShape shape) {
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,11 @@ public Void stringShape(StringShape shape) {
@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;
Expand All @@ -300,13 +300,17 @@ public Void timestampShape(TimestampShape shape) {
}

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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ private final class MemberGenerator extends ShapeVisitor.DataShapeVisitor<Void>
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,7 @@ public Void structureShape(StructureShape shape) {
// 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);",
Expand All @@ -140,6 +133,12 @@ public Void structureShape(StructureShape shape) {
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();
}
Expand Down
Loading

0 comments on commit 3565832

Please sign in to comment.