diff --git a/settings.gradle b/settings.gradle index 9605f4c7e0d..43e27305eb3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,3 +36,4 @@ include ":smithy-aws-endpoints" include ":smithy-aws-smoke-test-model" include ":smithy-protocol-traits" include ":smithy-protocol-tests" +include ":smithy-trait-codegen" diff --git a/smithy-trait-codegen/build.gradle b/smithy-trait-codegen/build.gradle new file mode 100644 index 00000000000..72408d68f02 --- /dev/null +++ b/smithy-trait-codegen/build.gradle @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +description = "Plugin for Generating Trait Code from Smithy Models" + +ext { + displayName = "Smithy :: Trait Code Generation" + moduleName = "software.amazon.smithy.traitcodegen" +} + +dependencies { + implementation project(":smithy-codegen-core") +} + +// Set up Integration testing source sets +sourceSets { + create("it") { + compileClasspath += sourceSets.main.output + configurations["testRuntimeClasspath"] + configurations["testCompileClasspath"] + runtimeClasspath += output + compileClasspath + sourceSets.test.runtimeClasspath + sourceSets.test.output + + // Pull in the generated trait files + java { + srcDir("$buildDir/integ/") + } + // Add generated service provider file to resources + resources { + srcDirs += "$buildDir/generated-resources" + } + } +} + +// Execute building of trait classes using an executable class +// These traits will then be passed in to the integration test (it) +// source set +tasks.register("generateTraits", JavaExec) { + classpath = sourceSets.test.runtimeClasspath + sourceSets.test.output + mainClass = "software.amazon.smithy.traitcodegen.PluginExecutor" +} + +// Copy generated META-INF files to a new generated-resources directory to +// make it easy to include as resource srcDir +def generatedMetaInf = new File("$buildDir/integ/META-INF") +def destResourceDir = new File("$buildDir/generated-resources", "META-INF") +tasks.register("copyGeneratedSrcs", Copy) { + from generatedMetaInf + into destResourceDir + dependsOn("generateTraits") +} + + +// Add the integ test task +tasks.register("integ", Test) { + useJUnitPlatform() + testClassesDirs = sourceSets.it.output.classesDirs + classpath = sourceSets.it.runtimeClasspath +} + +// Do not run checkstyle on generated trait classes +tasks["checkstyleIt"].enabled = false + +// Force correct ordering so generated sources are available +tasks["compileItJava"].dependsOn("generateTraits") +tasks["compileItJava"].dependsOn("copyGeneratedSrcs") +tasks["processItResources"].dependsOn("copyGeneratedSrcs") +tasks["integ"].mustRunAfter("generateTraits") +tasks["integ"].mustRunAfter("copyGeneratedSrcs") + +// Always run integ tests after base tests +tasks["test"].finalizedBy("integ") + +// dont run spotbugs on integ tests +tasks["spotbugsIt"].enabled(false) diff --git a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java new file mode 100644 index 00000000000..5cbb2273362 --- /dev/null +++ b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/CreatesTraitTest.java @@ -0,0 +1,150 @@ +package software.amazon.smithy.traitcodegen.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.example.traits.StringTrait; +import com.example.traits.documents.DocumentTrait; +import com.example.traits.documents.StructWithNestedDocumentTrait; +import com.example.traits.enums.IntEnumTrait; +import com.example.traits.enums.StringEnumTrait; +import com.example.traits.enums.SuitTrait; +import com.example.traits.lists.ListMember; +import com.example.traits.lists.NumberListTrait; +import com.example.traits.lists.StringListTrait; +import com.example.traits.lists.StructureListTrait; +import com.example.traits.maps.MapValue; +import com.example.traits.maps.StringStringMapTrait; +import com.example.traits.maps.StringToStructMapTrait; +import com.example.traits.mixins.StructWithMixinTrait; +import com.example.traits.mixins.StructureListWithMixinMemberTrait; +import com.example.traits.names.SnakeCaseStructureTrait; +import com.example.traits.numbers.BigDecimalTrait; +import com.example.traits.numbers.BigIntegerTrait; +import com.example.traits.numbers.ByteTrait; +import com.example.traits.numbers.DoubleTrait; +import com.example.traits.numbers.FloatTrait; +import com.example.traits.numbers.IntegerTrait; +import com.example.traits.numbers.LongTrait; +import com.example.traits.numbers.ShortTrait; +import com.example.traits.structures.BasicAnnotationTrait; +import com.example.traits.structures.NestedA; +import com.example.traits.structures.NestedB; +import com.example.traits.structures.StructureTrait; +import com.example.traits.timestamps.DateTimeTimestampTrait; +import com.example.traits.timestamps.EpochSecondsTimestampTrait; +import com.example.traits.timestamps.HttpDateTimestampTrait; +import com.example.traits.timestamps.TimestampTrait; +import com.example.traits.uniqueitems.NumberSetTrait; +import com.example.traits.uniqueitems.SetMember; +import com.example.traits.uniqueitems.StringSetTrait; +import com.example.traits.uniqueitems.StructureSetTrait; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.model.SourceLocation; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.model.traits.TraitFactory; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; + +public class CreatesTraitTest { + private static final ShapeId DUMMY_ID = ShapeId.from("ns.foo#foo"); + private final TraitFactory provider = TraitFactory.createServiceFactory(); + + static Stream createTraitTests() { + return Stream.of( + // Document traits + Arguments.of(DocumentTrait.ID, Node.objectNodeBuilder() + .withMember("metadata", "woo") + .withMember("more", "yay") + .build() + ), + Arguments.of(StructWithNestedDocumentTrait.ID, + ObjectNode.objectNodeBuilder().withMember("doc", ObjectNode.builder() + .withMember("foo", "bar").withMember("fizz", "buzz").build()).build()), + // Enums + Arguments.of(StringEnumTrait.ID, Node.from("no")), + Arguments.of(IntEnumTrait.ID, Node.from(2)), + Arguments.of(SuitTrait.ID, Node.from("clubs")), + // Lists + Arguments.of(NumberListTrait.ID, ArrayNode.fromNodes( + Node.from(1), Node.from(2), Node.from(3)) + ), + Arguments.of(StringListTrait.ID, ArrayNode.fromStrings("a", "b", "c")), + Arguments.of(StructureListTrait.ID, ArrayNode.fromNodes( + ListMember.builder().a("first").b(1).c("other").build().toNode(), + ListMember.builder().a("second").b(2).c("more").build().toNode() + )), + // Maps + Arguments.of(StringStringMapTrait.ID, StringStringMapTrait.builder() + .putValues("a", "first").putValues("b", "other").build().toNode() + ), + Arguments.of(StringToStructMapTrait.ID, StringToStructMapTrait.builder() + .putValues("one", MapValue.builder().a("foo").b(2).build()) + .putValues("two", MapValue.builder().a("bar").b(4).build()) + .build().toNode() + ), + // Mixins + Arguments.of(StructureListWithMixinMemberTrait.ID, + ArrayNode.fromNodes(ObjectNode.builder().withMember("a", "a").withMember("d", "d").build())), + Arguments.of(StructWithMixinTrait.ID, StructWithMixinTrait.builder() + .d("d").build().toNode()), + // Naming Conflicts + Arguments.of(SnakeCaseStructureTrait.ID, ObjectNode.builder() + .withMember("snake_case_member", "stuff").build()), + // Numbers + Arguments.of(BigDecimalTrait.ID, Node.from(1)), + Arguments.of(BigIntegerTrait.ID, Node.from(1)), + Arguments.of(ByteTrait.ID, Node.from(1)), + Arguments.of(DoubleTrait.ID, Node.from(1.2)), + Arguments.of(FloatTrait.ID, Node.from(1.2)), + Arguments.of(IntegerTrait.ID, Node.from(1)), + Arguments.of(LongTrait.ID, Node.from(1L)), + Arguments.of(ShortTrait.ID, Node.from(1)), + // Structures + Arguments.of(BasicAnnotationTrait.ID, Node.objectNode()), + Arguments.of(StructureTrait.ID, StructureTrait.builder() + .fieldA("a") + .fieldB(true) + .fieldC(NestedA.builder() + .fieldN("nested") + .fieldQ(false) + .fieldZ(NestedB.B) + .build() + ) + .fieldD(ListUtils.of("a", "b", "c")) + .fieldE(MapUtils.of("a", "one", "b", "two")) + .build().toNode() + ), + // Timestamps + Arguments.of(TimestampTrait.ID, Node.from("1985-04-12T23:20:50.52Z")), + Arguments.of(DateTimeTimestampTrait.ID, Node.from("1985-04-12T23:20:50.52Z")), + Arguments.of(HttpDateTimestampTrait.ID, Node.from("Tue, 29 Apr 2014 18:30:38 GMT")), + Arguments.of(EpochSecondsTimestampTrait.ID, Node.from(1515531081.123)), + // Unique Items (sets) + Arguments.of(NumberSetTrait.ID, ArrayNode.fromNodes( + Node.from(1), Node.from(2), Node.from(3)) + ), + Arguments.of(StringSetTrait.ID, ArrayNode.fromStrings("a", "b", "c")), + Arguments.of(StructureSetTrait.ID, ArrayNode.fromNodes( + SetMember.builder().a("first").b(1).c("other").build().toNode(), + SetMember.builder().a("second").b(2).c("more").build().toNode() + )), + // Strings + Arguments.of(StringTrait.ID, Node.from("SPORKZ SPOONS YAY! Utensils.")) + ); + } + + @ParameterizedTest + @MethodSource("createTraitTests") + void createsTraitFromNode(ShapeId traitId, Node fromNode) { + Trait trait = provider.createTrait(traitId, DUMMY_ID, fromNode).orElseThrow(RuntimeException::new); + assertEquals(SourceLocation.NONE, trait.getSourceLocation()); + assertEquals(trait, provider.createTrait(traitId, DUMMY_ID, trait.toNode()).orElseThrow(RuntimeException::new)); + } +} diff --git a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/DeprecatedStringTest.java b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/DeprecatedStringTest.java new file mode 100644 index 00000000000..497afbcc1a4 --- /dev/null +++ b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/DeprecatedStringTest.java @@ -0,0 +1,14 @@ +package software.amazon.smithy.traitcodegen.test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.example.traits.DeprecatedStringTrait; +import org.junit.jupiter.api.Test; + +class DeprecatedStringTest { + @Test + void checkForDeprecatedAnnotation() { + Deprecated deprecated = DeprecatedStringTrait.class.getAnnotation(Deprecated.class); + assertNotNull(deprecated); + } +} diff --git a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java new file mode 100644 index 00000000000..98eb7dc2e08 --- /dev/null +++ b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/LoadsFromModelTest.java @@ -0,0 +1,250 @@ +package software.amazon.smithy.traitcodegen.test; + +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.documents.DocumentTrait; +import com.example.traits.documents.StructWithNestedDocumentTrait; +import com.example.traits.enums.IntEnumTrait; +import com.example.traits.enums.StringEnumTrait; +import com.example.traits.enums.SuitTrait; +import com.example.traits.idref.IdRefListTrait; +import com.example.traits.idref.IdRefMapTrait; +import com.example.traits.idref.IdRefStringTrait; +import com.example.traits.idref.IdRefStructTrait; +import com.example.traits.idref.IdRefStructWithNestedIdsTrait; +import com.example.traits.idref.NestedIdRefHolder; +import com.example.traits.lists.ListMember; +import com.example.traits.lists.NumberListTrait; +import com.example.traits.lists.StructureListTrait; +import com.example.traits.maps.MapValue; +import com.example.traits.maps.StringStringMapTrait; +import com.example.traits.maps.StringToStructMapTrait; +import com.example.traits.mixins.ListMemberWithMixin; +import com.example.traits.mixins.StructWithMixinTrait; +import com.example.traits.mixins.StructureListWithMixinMemberTrait; +import com.example.traits.names.SnakeCaseStructureTrait; +import com.example.traits.numbers.BigDecimalTrait; +import com.example.traits.numbers.BigIntegerTrait; +import com.example.traits.numbers.ByteTrait; +import com.example.traits.numbers.DoubleTrait; +import com.example.traits.numbers.FloatTrait; +import com.example.traits.numbers.IntegerTrait; +import com.example.traits.numbers.LongTrait; +import com.example.traits.numbers.ShortTrait; +import com.example.traits.structures.BasicAnnotationTrait; +import com.example.traits.structures.NestedA; +import com.example.traits.structures.NestedB; +import com.example.traits.structures.StructureTrait; +import com.example.traits.timestamps.DateTimeTimestampTrait; +import com.example.traits.timestamps.EpochSecondsTimestampTrait; +import com.example.traits.timestamps.HttpDateTimestampTrait; +import com.example.traits.timestamps.StructWithNestedTimestampsTrait; +import com.example.traits.timestamps.TimestampTrait; +import com.example.traits.uniqueitems.NumberSetTrait; +import com.example.traits.uniqueitems.SetMember; +import com.example.traits.uniqueitems.StringSetTrait; +import com.example.traits.uniqueitems.StructureSetTrait; +import java.lang.reflect.InvocationTargetException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.StringListTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SetUtils; + +public class LoadsFromModelTest { + private static final ShapeId ID = ShapeId.from("test.smithy.traitcodegen#myStruct"); + private static final ShapeId TARGET_ONE = ShapeId.from("test.smithy.traitcodegen#IdRefTarget1"); + private static final ShapeId TARGET_TWO = ShapeId.from("test.smithy.traitcodegen#IdRefTarget2"); + + static Stream loadsModelTests() { + return Stream.of( + // Document types + Arguments.of("documents/document-trait.smithy", DocumentTrait.class, + MapUtils.of("getValue", Node.objectNodeBuilder() + .withMember("metadata", "woo") + .withMember("more", "yay") + .build())), + Arguments.of("documents/struct-with-nested-document.smithy", StructWithNestedDocumentTrait.class, + MapUtils.of("getDoc", Optional.of(ObjectNode.builder().withMember("foo", "bar") + .withMember("fizz", "buzz").build()))), + // Enums + Arguments.of("enums/enum-trait.smithy", StringEnumTrait.class, + MapUtils.of("getValue", "yes", "getEnumValue", StringEnumTrait.StringEnum.YES)), + Arguments.of("enums/int-enum-trait.smithy", IntEnumTrait.class, + MapUtils.of("getValue", 1, "getEnumValue", IntEnumTrait.IntEnum.YES)), + Arguments.of("enums/string-enum-compatibility.smithy", SuitTrait.class, + MapUtils.of("getEnumValue", SuitTrait.Suit.CLUB, "getValue", "club")), + // Id Refs + Arguments.of("idref/idref-string.smithy", IdRefStringTrait.class, + MapUtils.of("getValue", TARGET_ONE)), + Arguments.of("idref/idref-list.smithy", IdRefListTrait.class, + MapUtils.of("getValues", ListUtils.of(TARGET_ONE, TARGET_TWO))), + Arguments.of("idref/idref-map.smithy", IdRefMapTrait.class, + MapUtils.of("getValues", MapUtils.of("a", TARGET_ONE, "b", TARGET_TWO))), + Arguments.of("idref/idref-struct.smithy", IdRefStructTrait.class, + MapUtils.of("getFieldA", Optional.of(TARGET_ONE))), + Arguments.of("idref/idref-struct-with-nested-refs.smithy", IdRefStructWithNestedIdsTrait.class, + MapUtils.of("getIdRefHolder", NestedIdRefHolder.builder().id(TARGET_ONE).build(), + "getIdList", Optional.of(ListUtils.of(TARGET_ONE, TARGET_TWO)), + "getIdMap", Optional.of(MapUtils.of("a", TARGET_ONE, "b", TARGET_TWO)))), + // Lists + Arguments.of("lists/number-list-trait.smithy", NumberListTrait.class, + MapUtils.of("getValues", ListUtils.of(1, 2, 3, 4, 5))), + Arguments.of("lists/string-list-trait.smithy", StringListTrait.class, + MapUtils.of("getValues", ListUtils.of("a", "b", "c", "d"))), + Arguments.of("lists/struct-list-trait.smithy", StructureListTrait.class, + MapUtils.of("getValues", ListUtils.of( + ListMember.builder().a("first").b(1).c("other").build(), + ListMember.builder().a("second").b(2).c("more").build()))), + // Maps + Arguments.of("maps/string-string-map-trait.smithy", StringStringMapTrait.class, + MapUtils.of("getValues", MapUtils.of("a", "stuff", + "b", "other", "c", "more!"))), + Arguments.of("maps/string-to-struct-map-trait.smithy", StringToStructMapTrait.class, + MapUtils.of("getValues", MapUtils.of( + "one", MapValue.builder().a("foo").b(2).build(), + "two", MapValue.builder().a("bar").b(4).build()))), + // Mixins + Arguments.of("mixins/struct-with-mixin-member.smithy", StructureListWithMixinMemberTrait.class, + MapUtils.of("getValues", ListUtils.of( + ListMemberWithMixin.builder().a("first").b(1).c("other") + .d("mixed-in").build(), + ListMemberWithMixin.builder().a("second").b(2).c("more") + .d("mixins are cool").build()))), + Arguments.of("mixins/struct-with-only-mixin-member.smithy", StructWithMixinTrait.class, + MapUtils.of("getD", "mixed-in")), + // Naming conflicts + Arguments.of("names/snake-case-struct.smithy", SnakeCaseStructureTrait.class, + MapUtils.of("getSnakeCaseMember", Optional.of("stuff"))), + // Numbers + Arguments.of("numbers/big-decimal-trait.smithy", BigDecimalTrait.class, + MapUtils.of("getValue", new BigDecimal("100.01"))), + Arguments.of("numbers/big-integer-trait.smithy", BigIntegerTrait.class, + MapUtils.of("getValue", new BigInteger("100"))), + Arguments.of("numbers/byte-trait.smithy", ByteTrait.class, + MapUtils.of("getValue", (byte) 1)), + Arguments.of("numbers/double-trait.smithy", DoubleTrait.class, + MapUtils.of("getValue", 100.01)), + Arguments.of("numbers/float-trait.smithy", FloatTrait.class, + MapUtils.of("getValue", 1.1F)), + Arguments.of("numbers/integer-trait.smithy", IntegerTrait.class, + MapUtils.of("getValue", 1)), + Arguments.of("numbers/long-trait.smithy", LongTrait.class, + MapUtils.of("getValue", 1L)), + Arguments.of("numbers/short-trait.smithy", ShortTrait.class, + MapUtils.of("getValue", (short) 1)), + // Structures + Arguments.of("structures/annotation-trait.smithy", BasicAnnotationTrait.class, + Collections.emptyMap()), + Arguments.of("structures/struct-trait.smithy", StructureTrait.class, + MapUtils.of( + "getFieldA", "first", + "getFieldB", Optional.of(false), + "getFieldC", Optional.of(NestedA.builder() + .fieldN("nested") + .fieldQ(true) + .fieldZ(NestedB.A) + .build()), + "getFieldD", Optional.of(ListUtils.of("a", "b", "c")), + "getFieldDOrEmpty", ListUtils.of("a", "b", "c"), + "getFieldE", Optional.of(MapUtils.of("a", "one", "b", "two")), + "getFieldEOrEmpty", MapUtils.of("a", "one", "b", "two"), + "getFieldF", Optional.of(new BigDecimal("100.01")), + "getFieldG", Optional.of(new BigInteger("100")))), + Arguments.of("structures/struct-with-non-existent-collections.smithy", StructureTrait.class, + MapUtils.of( + "getFieldA", "first", + "getFieldB", Optional.of(false), + "getFieldC", Optional.of(NestedA.builder() + .fieldN("nested") + .fieldQ(true) + .fieldZ(NestedB.A) + .build()), + "getFieldD", Optional.empty()), + "getFieldDOrEmpty", null, + "getFieldE", Optional.empty(), + "getFieldEOrEmpty", null), + // Timestamps + Arguments.of("timestamps/struct-with-nested-timestamps.smithy", StructWithNestedTimestampsTrait.class, + MapUtils.of("getBaseTime", Instant.parse("1985-04-12T23:20:50.52Z"), + "getDateTime", Instant.parse("1985-04-12T23:20:50.52Z"), + "getHttpDate", Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse("Tue, 29 Apr 2014 18:30:38 GMT")), + "getEpochSeconds", Instant.ofEpochSecond((long) 1515531081.123))), + Arguments.of("timestamps/timestamp-trait-date-time.smithy", TimestampTrait.class, + MapUtils.of("getValue", Instant.parse("1985-04-12T23:20:50.52Z"))), + Arguments.of("timestamps/timestamp-trait-epoch-sec.smithy", TimestampTrait.class, + MapUtils.of("getValue", Instant.ofEpochSecond((long) 1515531081.123))), + Arguments.of("timestamps/date-time-format-timestamp-trait.smithy", DateTimeTimestampTrait.class, + MapUtils.of("getValue", Instant.parse("1985-04-12T23:20:50.52Z"))), + Arguments.of("timestamps/http-date-format-timestamp-trait.smithy", HttpDateTimestampTrait.class, + MapUtils.of("getValue", Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME + .parse("Tue, 29 Apr 2014 18:30:38 GMT")))), + Arguments.of("timestamps/epoch-seconds-format-timestamp-trait.smithy", EpochSecondsTimestampTrait.class, + MapUtils.of("getValue", Instant.ofEpochSecond((long) 1515531081.123))), + // Uniques items (sets) + Arguments.of("uniqueitems/number-set-trait.smithy", NumberSetTrait.class, + MapUtils.of("getValues", SetUtils.of(1, 2, 3, 4))), + Arguments.of("uniqueitems/string-set-trait.smithy", StringSetTrait.class, + MapUtils.of("getValues", SetUtils.of("a", "b", "c", "d"))), + Arguments.of("uniqueitems/struct-set-trait.smithy", StructureSetTrait.class, + MapUtils.of("getValues", ListUtils.of( + SetMember.builder().a("first").b(1).c("other").build(), + SetMember.builder().a("second").b(2).c("more").build()))), + // Strings + Arguments.of("string-trait.smithy", StringTrait.class, + MapUtils.of("getValue","Testing String Trait")) + ); + } + + @ParameterizedTest + @MethodSource("loadsModelTests") + void executeTests(String resourceFile, + Class traitClass, + Map valueChecks + ) { + Model result = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .addImport(Objects.requireNonNull(getClass().getResource(resourceFile))) + .assemble() + .unwrap(); + T trait = result.expectShape(ID).expectTrait(traitClass); + valueChecks.forEach((k, v) -> checkValue(traitClass, trait, k, v)); + } + + void checkValue(Class traitClass, T trait, String accessor, Object expected) { + try { + Object value = traitClass.getMethod(accessor).invoke(trait); + // Float values need a delta specified for equals checks + if (value instanceof Float) { + assertEquals((Float) expected, (Float) value, 0.0001, + "Value of accessor `" + accessor + "` invalid for " + trait); + } else if (value instanceof Iterable) { + assertIterableEquals((Iterable) expected, (Iterable) value); + } else { + assertEquals(expected, value, "Value of accessor `" + accessor + + "` invalid for " + trait); + } + + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException("Failed to invoke accessor " + accessor + " for " + trait, e); + } + } +} diff --git a/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/TestRunnerTest.java b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/TestRunnerTest.java new file mode 100644 index 00000000000..c9501cb85cb --- /dev/null +++ b/smithy-trait-codegen/src/it/java/software/amazon/smithy/traitcodegen/test/TestRunnerTest.java @@ -0,0 +1,20 @@ +package software.amazon.smithy.traitcodegen.test; + +import java.util.concurrent.Callable; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.model.validation.testrunner.SmithyTestCase; +import software.amazon.smithy.model.validation.testrunner.SmithyTestSuite; + +class TestRunnerTest { + static Stream source() { + return SmithyTestSuite.defaultParameterizedTestSource(TestRunnerTest.class); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("source") + void testRunner(String filename, Callable callable) throws Exception { + callable.call(); + } +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/documents/document-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/documents/document-trait.smithy new file mode 100644 index 00000000000..b750854052c --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/documents/document-trait.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.documents#DocumentTrait + +@DocumentTrait({ + metadata: "woo" + more: "yay" +}) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/documents/struct-with-nested-document.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/documents/struct-with-nested-document.smithy new file mode 100644 index 00000000000..375e3706c48 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/documents/struct-with-nested-document.smithy @@ -0,0 +1,14 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.documents#structWithNestedDocument + +@structWithNestedDocument( + doc: { + foo: "bar" + fizz: "buzz" + } +) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/enum-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/enum-trait.smithy new file mode 100644 index 00000000000..b253f4ad359 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/enum-trait.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.enums#StringEnum + +@StringEnum("yes") +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/int-enum-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/int-enum-trait.smithy new file mode 100644 index 00000000000..a3318308d6f --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/int-enum-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.enums#IntEnum + +@IntEnum(1) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/string-enum-compatibility.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/string-enum-compatibility.smithy new file mode 100644 index 00000000000..943878846ef --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/enums/string-enum-compatibility.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.enums#Suit + +@Suit("club") +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/enum-trait-errors.errors b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/enum-trait-errors.errors new file mode 100644 index 00000000000..1da8c0793a2 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/enum-trait-errors.errors @@ -0,0 +1,2 @@ +[ERROR] test.smithy.traitcodegen.enums#notAValidVariant: Error validating trait `test.smithy.traitcodegen.enums#StringEnum`: String value provided for `test.smithy.traitcodegen.enums#StringEnum` must be one of the following values: `no`, `yes` | TraitValue +[ERROR] test.smithy.traitcodegen.enums#incorrectValueCasing: Error validating trait `test.smithy.traitcodegen.enums#StringEnum`: String value provided for `test.smithy.traitcodegen.enums#StringEnum` must be one of the following values: `no`, `yes` | TraitValue diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/enum-trait-errors.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/enum-trait-errors.smithy new file mode 100644 index 00000000000..256ba9cab1c --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/enum-trait-errors.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.enums + +@StringEnum("bad") +string notAValidVariant + +@StringEnum("YES") +string incorrectValueCasing diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/number-trait-errors.errors b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/number-trait-errors.errors new file mode 100644 index 00000000000..25a392559cb --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/number-trait-errors.errors @@ -0,0 +1,5 @@ +[ERROR] test.smithy.traitcodegen.numbers#structWithInvalidStringInput: Error creating trait `test.smithy.traitcodegen.numbers#IntegerTrait`: Expected number, but found string. | Model +[ERROR] test.smithy.traitcodegen.numbers#structWithInvalidStringInput: Error creating trait `test.smithy.traitcodegen.numbers#FloatTrait`: Expected number, but found string. | Model +[ERROR] test.smithy.traitcodegen.numbers#structWithInvalidStringInput: Error creating trait `test.smithy.traitcodegen.numbers#LongTrait`: Expected number, but found string. | Model +[ERROR] test.smithy.traitcodegen.numbers#structWithInvalidStringInput: Error creating trait `test.smithy.traitcodegen.numbers#ShortTrait`: Expected number, but found string. | Model +[ERROR] test.smithy.traitcodegen.numbers#structWithInvalidStringInput: Error creating trait `test.smithy.traitcodegen.numbers#DoubleTrait`: Expected number, but found string. | Model diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/number-trait-errors.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/number-trait-errors.smithy new file mode 100644 index 00000000000..fc6471fa9b0 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/number-trait-errors.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.numbers + +// Should fail to use string input for number traits +@IntegerTrait("bad") +@FloatTrait("bad") +@LongTrait("bad") +@ShortTrait("bad") +@DoubleTrait("bad") +structure structWithInvalidStringInput {} + + diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/set-trait-errors.errors b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/set-trait-errors.errors new file mode 100644 index 00000000000..aeeedc7256b --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/set-trait-errors.errors @@ -0,0 +1,2 @@ +[ERROR] test.smithy.traitcodegen.uniqueitems#repeatedNumberValues: Error validating trait `test.smithy.traitcodegen.uniqueitems#NumberSetTrait`: Value provided for `test.smithy.traitcodegen.uniqueitems#NumberSetTrait` must have unique items, but the following items had multiple entries: [`1`] | TraitValue +[ERROR] test.smithy.traitcodegen.uniqueitems#repeatedStringValues: Error validating trait `test.smithy.traitcodegen.uniqueitems#StringSetTrait`: Value provided for `test.smithy.traitcodegen.uniqueitems#StringSetTrait` must have unique items, but the following items had multiple entries: [`a`] | TraitValue diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/set-trait-errors.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/set-trait-errors.smithy new file mode 100644 index 00000000000..33b33db404f --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/set-trait-errors.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.uniqueitems + +// Doesnt have unique items. Expect failure +@NumberSetTrait([1, 1, 3, 4]) +structure repeatedNumberValues { +} + +@StringSetTrait(["a", "a", "b"]) +structure repeatedStringValues { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/strlist-trait-errors.errors b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/strlist-trait-errors.errors new file mode 100644 index 00000000000..c8ed935f621 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/strlist-trait-errors.errors @@ -0,0 +1,3 @@ +[ERROR] test.smithy.traitcodegen.lists#badInputTypes: Error creating trait `test.smithy.traitcodegen.lists#StringListTrait`: Expected array element 0 to be a string but found number. | Model +[ERROR] test.smithy.traitcodegen.lists#badInputType: Error creating trait `test.smithy.traitcodegen.lists#StringListTrait`: Expected `test.smithy.traitcodegen.lists#badInputType` to be an array of strings. Found number. | Model +[ERROR] test.smithy.traitcodegen.lists#inconsistentInputTypes: Error creating trait `test.smithy.traitcodegen.lists#StringListTrait`: Expected array element 1 to be a string but found number. | Model diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/strlist-trait-errors.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/strlist-trait-errors.smithy new file mode 100644 index 00000000000..4c3ce1a1c7e --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/strlist-trait-errors.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.lists + +@StringListTrait([1, 2, 3]) +structure badInputTypes {} + +@StringListTrait(1) +structure badInputType {} + +@StringListTrait(["a", 2, "b"]) +structure inconsistentInputTypes {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/structure-trait-warns.errors b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/structure-trait-warns.errors new file mode 100644 index 00000000000..49420ae8721 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/structure-trait-warns.errors @@ -0,0 +1,2 @@ +[WARNING] test.smithy.traitcodegen.structures#myStruct: Error validating trait `test.smithy.traitcodegen.structures#structureTrait`: Invalid structure member `extraA` found for `test.smithy.traitcodegen.structures#structureTrait` | TraitValue.test.smithy.traitcodegen.structures#structureTrait.extraA +[WARNING] test.smithy.traitcodegen.structures#myStruct: Error validating trait `test.smithy.traitcodegen.structures#structureTrait`: Invalid structure member `extraB` found for `test.smithy.traitcodegen.structures#structureTrait` | TraitValue.test.smithy.traitcodegen.structures#structureTrait.extraB diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/structure-trait-warns.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/structure-trait-warns.smithy new file mode 100644 index 00000000000..8af8dcedf71 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/errorfiles/structure-trait-warns.smithy @@ -0,0 +1,25 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.structures + + +@structureTrait( + fieldA: "first" + fieldB: false + fieldC: { + fieldN: "nested" + fieldQ: true + fieldZ: "A" + } + fieldD: ["a", "b", "c"] + fieldE: { + a: "one" + b: "two" + } + fieldF: 100.01, + fieldG: 100, + extraA: 100, + extraB: 200 +) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-list.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-list.smithy new file mode 100644 index 00000000000..2459bc7eacc --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-list.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.idref#IdRefList + +@IdRefList([IdRefTarget1, IdRefTarget2]) +structure myStruct {} + +string IdRefTarget1 + +string IdRefTarget2 diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-map.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-map.smithy new file mode 100644 index 00000000000..f73eb02e873 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-map.smithy @@ -0,0 +1,15 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.idref#IdRefMap + +@IdRefMap( + a: IdRefTarget1 + b: IdRefTarget2 +) +structure myStruct {} + +string IdRefTarget1 + +string IdRefTarget2 diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-string.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-string.smithy new file mode 100644 index 00000000000..73573c293a8 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-string.smithy @@ -0,0 +1,10 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.idref#IdRefString + +@IdRefString(IdRefTarget1) +structure myStruct {} + +string IdRefTarget1 diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-struct-with-nested-refs.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-struct-with-nested-refs.smithy new file mode 100644 index 00000000000..cc3dc064d81 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-struct-with-nested-refs.smithy @@ -0,0 +1,21 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.idref#IdRefStructWithNestedIds + +@IdRefStructWithNestedIds( + idRefHolder: { + id: IdRefTarget1 + } + idList: [IdRefTarget1, IdRefTarget2] + idMap: { + a: IdRefTarget1 + b: IdRefTarget2 + } +) +structure myStruct {} + +string IdRefTarget1 + +string IdRefTarget2 diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-struct.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-struct.smithy new file mode 100644 index 00000000000..deaeea99a3b --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/idref/idref-struct.smithy @@ -0,0 +1,10 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.idref#IdRefStruct + +@IdRefStruct(fieldA: IdRefTarget1) +structure myStruct {} + +string IdRefTarget1 diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/lists/number-list-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/lists/number-list-trait.smithy new file mode 100644 index 00000000000..cab37d2271d --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/lists/number-list-trait.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.lists#NumberListTrait + +@NumberListTrait([1, 2, 3, 4, 5]) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/lists/string-list-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/lists/string-list-trait.smithy new file mode 100644 index 00000000000..8821df4dfd9 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/lists/string-list-trait.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.lists#StringListTrait + +@StringListTrait(["a", "b", "c", "d"]) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/lists/struct-list-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/lists/struct-list-trait.smithy new file mode 100644 index 00000000000..528317b1fb3 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/lists/struct-list-trait.smithy @@ -0,0 +1,19 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.lists#StructureListTrait + +@StructureListTrait([ + { + a: "first" + b: 1 + c: "other" + } { + a: "second" + b: 2 + c: "more" + } +]) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/maps/string-string-map-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/maps/string-string-map-trait.smithy new file mode 100644 index 00000000000..bf824192e2d --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/maps/string-string-map-trait.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.maps#StringStringMap + +@StringStringMap( + a: "stuff" + b: "other" + c: "more!" +) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/maps/string-to-struct-map-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/maps/string-to-struct-map-trait.smithy new file mode 100644 index 00000000000..55d20f23f03 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/maps/string-to-struct-map-trait.smithy @@ -0,0 +1,18 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.maps#StringToStructMap + +@StringToStructMap( + one: { + a: "foo" + b: 2 + } + two: { + a: "bar" + b: 4 + } +) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/mixins/struct-with-mixin-member.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/mixins/struct-with-mixin-member.smithy new file mode 100644 index 00000000000..6c908fccccc --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/mixins/struct-with-mixin-member.smithy @@ -0,0 +1,20 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.mixins#structureListWithMixinMember + +@structureListWithMixinMember([ + { + a: "first" + b: 1 + c: "other" + d: "mixed-in" + } { + a: "second" + b: 2 + c: "more" + d: "mixins are cool" + } +]) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/mixins/struct-with-only-mixin-member.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/mixins/struct-with-only-mixin-member.smithy new file mode 100644 index 00000000000..dc738524fc2 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/mixins/struct-with-only-mixin-member.smithy @@ -0,0 +1,11 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.mixins#structWithMixin + +@structWithMixin( + d: "mixed-in" +) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/names/snake-case-struct.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/names/snake-case-struct.smithy new file mode 100644 index 00000000000..6f97ca27f17 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/names/snake-case-struct.smithy @@ -0,0 +1,11 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.names#snake_case_structure + +@snake_case_structure( + snake_case_member: "stuff" +) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/big-decimal-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/big-decimal-trait.smithy new file mode 100644 index 00000000000..bfdf4249f84 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/big-decimal-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.numbers#BigDecimalTrait + +@BigDecimalTrait(100.01) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/big-integer-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/big-integer-trait.smithy new file mode 100644 index 00000000000..b0f35a56bb1 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/big-integer-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.numbers#BigIntegerTrait + +@BigIntegerTrait(100) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/byte-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/byte-trait.smithy new file mode 100644 index 00000000000..677cc9a667f --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/byte-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.numbers#ByteTrait + +@ByteTrait(1) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/double-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/double-trait.smithy new file mode 100644 index 00000000000..8e58e7b3f60 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/double-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.numbers#DoubleTrait + +@DoubleTrait(100.01) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/float-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/float-trait.smithy new file mode 100644 index 00000000000..4d4a9103d14 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/float-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.numbers#FloatTrait + +@FloatTrait(1.1) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/integer-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/integer-trait.smithy new file mode 100644 index 00000000000..3c6871963ef --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/integer-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.numbers#IntegerTrait + +@IntegerTrait(1) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/long-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/long-trait.smithy new file mode 100644 index 00000000000..7d0b855b72e --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/long-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.numbers#LongTrait + +@LongTrait(1) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/short-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/short-trait.smithy new file mode 100644 index 00000000000..b6b2a481ba0 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/numbers/short-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.numbers#ShortTrait + +@ShortTrait(1) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/string-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/string-trait.smithy new file mode 100644 index 00000000000..71f3edb3200 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/string-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +@stringTrait("Testing String Trait") +structure myStruct { + fieldA: String +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/structures/annotation-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/structures/annotation-trait.smithy new file mode 100644 index 00000000000..9f3925c7349 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/structures/annotation-trait.smithy @@ -0,0 +1,10 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.structures#basicAnnotationTrait + +@basicAnnotationTrait +structure myStruct { + fieldA: String +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/structures/struct-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/structures/struct-trait.smithy new file mode 100644 index 00000000000..df16a7253ef --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/structures/struct-trait.smithy @@ -0,0 +1,24 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.structures#structureTrait + +@structureTrait( + fieldA: "first" + fieldB: false + fieldC: { + fieldN: "nested" + fieldQ: true + fieldZ: "A" + } + fieldD: ["a", "b", "c"] + fieldE: { + a: "one" + b: "two" + } + fieldF: 100.01, + fieldG: 100 +) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/structures/struct-with-non-existent-collections.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/structures/struct-with-non-existent-collections.smithy new file mode 100644 index 00000000000..ab195d5d6b5 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/structures/struct-with-non-existent-collections.smithy @@ -0,0 +1,17 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.structures#structureTrait + +@structureTrait( + fieldA: "first" + fieldB: false + fieldC: { + fieldN: "nested" + fieldQ: true + fieldZ: "A" + } +) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/date-time-format-timestamp-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/date-time-format-timestamp-trait.smithy new file mode 100644 index 00000000000..5f6327a4965 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/date-time-format-timestamp-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.timestamps#dateTimeTimestampTrait + +@dateTimeTimestampTrait("1985-04-12T23:20:50.52Z") +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/epoch-seconds-format-timestamp-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/epoch-seconds-format-timestamp-trait.smithy new file mode 100644 index 00000000000..5aeee64c28c --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/epoch-seconds-format-timestamp-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.timestamps#epochSecondsTimestampTrait + +@epochSecondsTimestampTrait(1515531081.123) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/http-date-format-timestamp-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/http-date-format-timestamp-trait.smithy new file mode 100644 index 00000000000..6ea2eaf1f1d --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/http-date-format-timestamp-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.timestamps#httpDateTimestampTrait + +@httpDateTimestampTrait("Tue, 29 Apr 2014 18:30:38 GMT") +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/struct-with-nested-timestamps.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/struct-with-nested-timestamps.smithy new file mode 100644 index 00000000000..a015ce52e24 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/struct-with-nested-timestamps.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.timestamps#structWithNestedTimestamps + +@structWithNestedTimestamps( + baseTime: "1985-04-12T23:20:50.52Z" + dateTime: "1985-04-12T23:20:50.52Z" + httpDate: "Tue, 29 Apr 2014 18:30:38 GMT" + epochSeconds: 1515531081.123 +) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/timestamp-trait-date-time.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/timestamp-trait-date-time.smithy new file mode 100644 index 00000000000..a71ee122513 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/timestamp-trait-date-time.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.timestamps#TimestampTrait + +@TimestampTrait("1985-04-12T23:20:50.52Z") +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/timestamp-trait-epoch-sec.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/timestamp-trait-epoch-sec.smithy new file mode 100644 index 00000000000..6fabbbd9e73 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/timestamps/timestamp-trait-epoch-sec.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.timestamps#TimestampTrait + +@TimestampTrait(1515531081.123) +structure myStruct {} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/uniqueitems/number-set-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/uniqueitems/number-set-trait.smithy new file mode 100644 index 00000000000..3dd069c0736 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/uniqueitems/number-set-trait.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.uniqueitems#NumberSetTrait + +@NumberSetTrait([1, 2, 3, 4]) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/uniqueitems/string-set-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/uniqueitems/string-set-trait.smithy new file mode 100644 index 00000000000..8145ca6d370 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/uniqueitems/string-set-trait.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.uniqueitems#StringSetTrait + +@StringSetTrait(["a", "b", "c", "d"]) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/uniqueitems/struct-set-trait.smithy b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/uniqueitems/struct-set-trait.smithy new file mode 100644 index 00000000000..0e0c14b2ef8 --- /dev/null +++ b/smithy-trait-codegen/src/it/resources/software/amazon/smithy/traitcodegen/test/uniqueitems/struct-set-trait.smithy @@ -0,0 +1,19 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +use test.smithy.traitcodegen.uniqueitems#StructureSetTrait + +@StructureSetTrait([ + { + a: "first" + b: 1 + c: "other" + } { + a: "second" + b: 2 + c: "more" + } +]) +structure myStruct { +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/GenerateTraitDirective.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/GenerateTraitDirective.java new file mode 100644 index 00000000000..4c76d70d0b9 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/GenerateTraitDirective.java @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + +import java.util.Objects; +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.shapes.Shape; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Custom directive that contains contextual information needed + * to generate a trait class. + */ +@SmithyInternalApi +public final class GenerateTraitDirective { + private final Shape shape; + private final Symbol symbol; + private final SymbolProvider symbolProvider; + private final TraitCodegenContext context; + private final Model model; + + GenerateTraitDirective(TraitCodegenContext context, Shape shape) { + this.context = Objects.requireNonNull(context); + this.shape = Objects.requireNonNull(shape); + this.symbol = Objects.requireNonNull(context.symbolProvider().toSymbol(shape)); + this.symbolProvider = Objects.requireNonNull(context.symbolProvider()); + this.model = Objects.requireNonNull(context.model()); + } + + public Shape shape() { + return shape; + } + + public Symbol symbol() { + return symbol; + } + + public SymbolProvider symbolProvider() { + return symbolProvider; + } + + public TraitCodegenContext context() { + return context; + } + + public Model model() { + return model; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/SymbolProperties.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/SymbolProperties.java new file mode 100644 index 00000000000..4ab1f865cd4 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/SymbolProperties.java @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class SymbolProperties { + /** + * Provides an initializer for the builder ref. + * + *

This should always be included on collection shapes such as Maps and Lists. + */ + public static final String BUILDER_REF_INITIALIZER = "builder-ref-initializer"; + + /** + * The "base" symbol for a trait. + * + *

This is the symbol that the shape would resolve + * to if it were not marked with `@trait`. This property is expected on all + * trait symbols. + */ + public static final String BASE_SYMBOL = "base-symbol"; + + /** + * The unboxed or primitive version of a Symbol. + * + *

This property should be included on symbols such as `Integer` that + * have the boxed version {@code Integer} and an unboxed (primitive) version + * {@code integer}. + */ + public static final String UNBOXED_SYMBOL = "unboxed-symbol"; + + /** + * Indicates that the given symbol is a primitive type. + * + *

This property is checked for existence only and should have no meaningful value. + */ + public static final String IS_PRIMITIVE = "primitive"; + + private SymbolProperties() { + // No constructor for constants class + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegen.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegen.java new file mode 100644 index 00000000000..0e4a5b24484 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegen.java @@ -0,0 +1,204 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.codegen.core.SmithyIntegration; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.loader.Prelude; +import software.amazon.smithy.model.neighbor.Walker; +import software.amazon.smithy.model.selector.Selector; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.TraitDefinition; +import software.amazon.smithy.model.traits.TraitService; +import software.amazon.smithy.model.transform.ModelTransformer; +import software.amazon.smithy.traitcodegen.generators.ShapeGenerator; +import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.CodeSection; + +/** + * Orchestration class for Trait code generation. + * + *

Trait codegen executes the following steps: + *

    + *
  • Orchestrator creation - Plugin creates an instance of {@link TraitCodegen}.
  • + *
  • Initialization - {@link #initialize()} is called to discover integration, filter out + * any shapes with excluded tags, and set up the codegen context.
  • + *
  • Execution - {@link #run()} is called to build a list of shapes to generate by pulling + * all shapes with the {@link TraitDefinition} trait applied and walking the nested shapes inside + * of those trait shapes. Then the {@link ShapeGenerator} is applied to each of the shapes to + * generate. Finally, all of the writers created during the shapes generation process are flushed.
  • + *
+ * + */ +final class TraitCodegen { + private static final Logger LOGGER = Logger.getLogger(TraitCodegen.class.getName()); + // Get all trait definitions within a namespace + private static final String SELECTOR_TEMPLATE = "[trait|trait][id|namespace ^= '%s']"; + + private Model model; + private final TraitCodegenSettings settings; + private final FileManifest fileManifest; + private final Selector traitSelector; + private final PluginContext pluginContext; + + private List integrations; + private TraitCodegenContext codegenContext; + + private TraitCodegen(Model model, + TraitCodegenSettings settings, + FileManifest fileManifest, + PluginContext pluginContext + ) { + this.model = Objects.requireNonNull(model); + this.settings = Objects.requireNonNull(settings); + this.fileManifest = Objects.requireNonNull(fileManifest); + this.traitSelector = Selector.parse(String.format(SELECTOR_TEMPLATE, settings.smithyNamespace())); + // Only allow this plugin to be run on the source projection. + if (!pluginContext.getProjectionName().equals("source")) { + throw new IllegalArgumentException("Trait code generation can ONLY be run on the `source` projection."); + } + this.pluginContext = pluginContext; + } + + public static TraitCodegen fromPluginContext(PluginContext context) { + return new TraitCodegen( + context.getModel(), + TraitCodegenSettings.fromNode(context.getSettings()), + context.getFileManifest(), + context + ); + } + + public void initialize() { + LOGGER.info("Initializing trait codegen plugin."); + integrations = getIntegrations(); + model = applyBaseTransforms(model); + SymbolProvider symbolProvider = createSymbolProvider(); + codegenContext = new TraitCodegenContext(model, settings, symbolProvider, fileManifest, integrations); + registerInterceptors(codegenContext); + LOGGER.info("Trait codegen plugin Initialized."); + } + + public void run() { + // Check that all required fields have been correctly initialized. + Objects.requireNonNull(integrations, "`integrations` not initialized."); + Objects.requireNonNull(codegenContext, "`codegenContext` not initialized."); + + // Find all trait definition shapes excluding traits in the prelude. + LOGGER.info("Generating trait classes."); + Set traitClosure = getTraitClosure(codegenContext.model()); + for (Shape trait : traitClosure) { + new ShapeGenerator().accept(new GenerateTraitDirective(codegenContext, trait)); + } + + LOGGER.info("Flushing writers"); + // Flush all writers + if (!codegenContext.writerDelegator().getWriters().isEmpty()) { + codegenContext.writerDelegator().flushWriters(); + } + } + + /** + * Applies standard transforms to the model. + *
+ *
changeStringEnumsToEnumShapes
+ *
Changes string enums to enum shapes for compatibility
+ *
flattenAndRemoveMixins
+ *
Ensures mixins are flattened into any generated traits or nested structures
+ *
+ */ + private static Model applyBaseTransforms(Model model) { + ModelTransformer transformer = ModelTransformer.create(); + model = transformer.changeStringEnumsToEnumShapes(model); + model = transformer.flattenAndRemoveMixins(model); + return model; + } + + private List getIntegrations() { + LOGGER.fine(() -> String.format("Finding integrations using the %s class loader", getClass().getSimpleName())); + return SmithyIntegration.sort(ServiceLoader.load(TraitCodegenIntegration.class, getClass().getClassLoader())); + } + + private SymbolProvider createSymbolProvider() { + SymbolProvider provider = new TraitCodegenSymbolProvider(settings, model); + for (TraitCodegenIntegration integration : integrations) { + provider = integration.decorateSymbolProvider(model, settings, provider); + } + return SymbolProvider.cache(provider); + } + + private void registerInterceptors(TraitCodegenContext context) { + List> interceptors = new ArrayList<>(); + for (TraitCodegenIntegration integration : integrations) { + interceptors.addAll(integration.interceptors(context)); + } + context.writerDelegator().setInterceptors(interceptors); + } + + private Set getTraitClosure(Model model) { + // Get a map of existing providers, so we do not generate any trait definitions + // for traits we have already manually defined a provider for. + Set existingProviders = new HashSet<>(); + ServiceLoader.load(TraitService.class, TraitCodegen.class.getClassLoader()) + .forEach(service -> existingProviders.add(service.getShapeId())); + + // Get all trait shapes within the specified namespace, but filter out + // any trait shapes for which a provider is already defined or which have + // excluded tags + Set traitClosure = traitSelector.select(model).stream() + .filter(pluginContext::isSourceShape) + .filter(shape -> !existingProviders.contains(shape.getId())) + .filter(shape -> !this.hasExcludeTag(shape)) + .collect(Collectors.toSet()); + + if (traitClosure.isEmpty()) { + LOGGER.warning("Could not find any trait definitions to generate."); + return traitClosure; + } + + // Find all shapes connected to trait shapes and therefore within generation closure. + // These shapes must all be within the same namespace. Note: we do not need to add members + // to the closure + Set nested = new HashSet<>(); + Walker walker = new Walker(model); + for (Shape traitShape : traitClosure) { + nested.addAll(walker.walkShapes(traitShape).stream() + .filter(shape -> !shape.isMemberShape()) + .filter(shape -> !Prelude.isPreludeShape(shape)) + .collect(Collectors.toSet())); + } + + // If any nested shapes are not in the specified namespace, throw an error. + Set invalidNested = nested.stream() + .filter(shape -> !shape.getId().getNamespace().startsWith(settings.smithyNamespace())) + .collect(Collectors.toSet()); + if (!invalidNested.isEmpty()) { + throw new RuntimeException("Shapes: " + invalidNested + " are within the trait closure but are not within " + + "the specified namespace `" + settings.smithyNamespace() + "`."); + } + traitClosure.addAll(nested); + + return traitClosure; + } + + private boolean hasExcludeTag(Shape shape) { + return shape.getTags().stream().anyMatch(t -> settings.excludeTags().contains(t)); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenContext.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenContext.java new file mode 100644 index 00000000000..33e73ee970d --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenContext.java @@ -0,0 +1,75 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + +import java.util.List; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.codegen.core.CodegenContext; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.WriterDelegator; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contextual information that is made available during most parts of trait code generation. + */ +@SmithyUnstableApi +public final class TraitCodegenContext implements CodegenContext { + private final Model model; + private final TraitCodegenSettings settings; + private final SymbolProvider symbolProvider; + private final FileManifest fileManifest; + private final List integrations; + private final WriterDelegator writerDelegator; + + TraitCodegenContext(Model model, + TraitCodegenSettings settings, + SymbolProvider symbolProvider, + FileManifest fileManifest, + List integrations + ) { + this.model = model; + this.settings = settings; + this.symbolProvider = symbolProvider; + this.fileManifest = fileManifest; + this.integrations = integrations; + this.writerDelegator = new WriterDelegator<>(fileManifest, symbolProvider, + (filename, namespace) -> new TraitCodegenWriter(filename, namespace, settings)); + } + + @Override + public Model model() { + return model; + } + + @Override + public TraitCodegenSettings settings() { + return settings; + } + + @Override + public SymbolProvider symbolProvider() { + return symbolProvider; + } + + @Override + public FileManifest fileManifest() { + return fileManifest; + } + + @Override + public WriterDelegator writerDelegator() { + return writerDelegator; + } + + @Override + public List integrations() { + return integrations; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenPlugin.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenPlugin.java new file mode 100644 index 00000000000..e617f278bbc --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenPlugin.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + +import java.util.logging.Logger; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates Java code implementations of traits from a Smithy model. + */ +@SmithyUnstableApi +public final class TraitCodegenPlugin implements SmithyBuildPlugin { + private static final Logger LOGGER = Logger.getLogger(TraitCodegenPlugin.class.getName()); + + @Override + public String getName() { + return "trait-codegen"; + } + + @Override + public void execute(PluginContext context) { + LOGGER.info("Running trait codegen plugin..."); + TraitCodegen traitCodegen = TraitCodegen.fromPluginContext(context); + traitCodegen.initialize(); + traitCodegen.run(); + LOGGER.info("Trait codegen plugin execution completed."); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenSettings.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenSettings.java new file mode 100644 index 00000000000..1fdc15a3137 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenSettings.java @@ -0,0 +1,122 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Settings for trait code generation. These can be set in the + * {@code smithy-build.json} configuration for this plugin. + * + *

The following options are provided: + *

+ *
"packageName" ({@code String})
+ *
Sets the package namespace to use for generated Java classes.
+ *
"headerLines" ({@code List})
+ *
Defines the header comment to include in all output files. Use + * this setting to add license and/or author information to all generated files. Each entry in the list + * is generated as a new line in the generated comment.
+ *
"excludeTags" ({@code List})
+ *
List of Smithy tags to use for filtering out trait shapes + * from the trait code generation process.
+ *
+ */ +@SmithyUnstableApi +public final class TraitCodegenSettings { + private static final String SMITHY_MODEL_NAMESPACE = "software.amazon.smithy"; + private static final String SMITHY_API_NAMESPACE = "smithy"; + + private final String packageName; + private final String smithyNamespace; + private final List headerLines; + private final List excludeTags; + + /** + * Settings for trait code generation. These can be set in the + * {@code smithy-build.json} configuration for this plugin. + * + * @param packageName java package name to use for generated code. For example {@code com.example.traits}. + * @param smithyNamespace smithy namespace to search for traits in. + * @param headerLines lines of text to included as a header in all generated code files. This might be a + * license header or copyright header that should be included in all generated files. + * @param excludeTags smithy tags to exclude from trait code generation. Traits with these tags will be + * ignored when generating java classes. + */ + TraitCodegenSettings(String packageName, + String smithyNamespace, + List headerLines, + List excludeTags + ) { + this.packageName = Objects.requireNonNull(packageName); + if (packageName.startsWith(SMITHY_MODEL_NAMESPACE)) { + throw new IllegalArgumentException("The `software.amazon.smithy` package namespace is reserved."); + } + this.smithyNamespace = Objects.requireNonNull(smithyNamespace); + if (smithyNamespace.startsWith(SMITHY_API_NAMESPACE)) { + throw new IllegalArgumentException("The `smithy` namespace is reserved."); + } + this.headerLines = Objects.requireNonNull(headerLines); + this.excludeTags = Objects.requireNonNull(excludeTags); + } + + /** + * Loads settings from an {@link ObjectNode}. + * + * @param node object node to load settings from + * @return settings loaded from given node + */ + public static TraitCodegenSettings fromNode(ObjectNode node) { + return new TraitCodegenSettings( + node.expectStringMember("package").getValue(), + node.expectStringMember("namespace").getValue(), + node.expectArrayMember("header") + .getElementsAs(el -> el.expectStringNode().getValue()), + node.getArrayMember("excludeTags") + .map(n -> n.getElementsAs(el -> el.expectStringNode().getValue())) + .orElse(Collections.emptyList()) + ); + } + + /** + * Java package name to generate traits into. + * + * @return package name + */ + public String packageName() { + return packageName; + } + + /** + * Smithy namespace to search for traits. + * + * @return namespace + */ + public String smithyNamespace() { + return smithyNamespace; + } + + /** + * List of lines added to the top of every generated file as a header. + * + * @return header lines as a list + */ + public List headerLines() { + return headerLines; + } + + /** + * List of tags to exclude from shape code generation. + * + * @return tag list + */ + public List excludeTags() { + return excludeTags; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenSymbolProvider.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenSymbolProvider.java new file mode 100644 index 00000000000..4361bb1b96d --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenSymbolProvider.java @@ -0,0 +1,212 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Instant; +import java.util.List; +import java.util.Locale; +import java.util.Map; +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.ObjectNode; +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.EnumShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +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.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.utils.CaseUtils; + +/** + * Responsible for mapping Smithy {@link Shape}'s to Java types. + */ +final class TraitCodegenSymbolProvider extends ShapeVisitor.DataShapeVisitor implements SymbolProvider { + + private final String packageNamespace; + private final String smithyNamespace; + private final Model model; + + TraitCodegenSymbolProvider(TraitCodegenSettings settings, Model model) { + this.packageNamespace = settings.packageName(); + this.smithyNamespace = settings.smithyNamespace(); + this.model = model; + } + + @Override + public Symbol blobShape(BlobShape shape) { + throw new UnsupportedOperationException("Blob shapes are not supported at this time."); + } + + @Override + public Symbol booleanShape(BooleanShape shape) { + return TraitCodegenUtils.fromClass(Boolean.class); + } + + @Override + public Symbol byteShape(ByteShape shape) { + return TraitCodegenUtils.fromClass(Byte.class).toBuilder() + .putProperty(SymbolProperties.UNBOXED_SYMBOL, TraitCodegenUtils.fromClass(byte.class)) + .build(); + } + + @Override + public Symbol shortShape(ShortShape shape) { + return TraitCodegenUtils.fromClass(Short.class).toBuilder() + .putProperty(SymbolProperties.UNBOXED_SYMBOL, TraitCodegenUtils.fromClass(short.class)) + .build(); + } + + @Override + public Symbol integerShape(IntegerShape shape) { + return TraitCodegenUtils.fromClass(Integer.class) + .toBuilder() + .putProperty(SymbolProperties.UNBOXED_SYMBOL, TraitCodegenUtils.fromClass(int.class)) + .build(); + } + + @Override + public Symbol intEnumShape(IntEnumShape shape) { + return getJavaClassSymbol(shape); + } + + @Override + public Symbol longShape(LongShape shape) { + return TraitCodegenUtils.fromClass(Long.class).toBuilder() + .putProperty(SymbolProperties.UNBOXED_SYMBOL, TraitCodegenUtils.fromClass(long.class)) + .build(); + } + + @Override + public Symbol floatShape(FloatShape shape) { + return TraitCodegenUtils.fromClass(Float.class) + .toBuilder() + .putProperty(SymbolProperties.UNBOXED_SYMBOL, TraitCodegenUtils.fromClass(float.class)) + .build(); + } + + @Override + public Symbol doubleShape(DoubleShape shape) { + return TraitCodegenUtils.fromClass(Double.class) + .toBuilder() + .putProperty(SymbolProperties.UNBOXED_SYMBOL, TraitCodegenUtils.fromClass(double.class)) + .build(); + } + + @Override + public Symbol bigIntegerShape(BigIntegerShape shape) { + return TraitCodegenUtils.fromClass(BigInteger.class); + } + + @Override + public Symbol bigDecimalShape(BigDecimalShape shape) { + return TraitCodegenUtils.fromClass(BigDecimal.class); + } + + @Override + public Symbol listShape(ListShape shape) { + Symbol.Builder builder = TraitCodegenUtils.fromClass(List.class).toBuilder() + .addReference(toSymbol(shape.getMember())) + .putProperty(SymbolProperties.BUILDER_REF_INITIALIZER, "forList()"); + return builder.build(); + } + + @Override + public Symbol mapShape(MapShape shape) { + return TraitCodegenUtils.fromClass(Map.class).toBuilder() + .addReference(shape.getKey().accept(this)) + .addReference(shape.getValue().accept(this)) + .putProperty(SymbolProperties.BUILDER_REF_INITIALIZER, "forOrderedMap()") + .build(); + } + + @Override + public Symbol stringShape(StringShape shape) { + return TraitCodegenUtils.fromClass(String.class); + } + + @Override + public Symbol enumShape(EnumShape shape) { + return getJavaClassSymbol(shape); + } + + @Override + public Symbol structureShape(StructureShape shape) { + return getJavaClassSymbol(shape); + } + + @Override + public Symbol documentShape(DocumentShape shape) { + return TraitCodegenUtils.fromClass(ObjectNode.class); + } + + @Override + public Symbol memberShape(MemberShape shape) { + return toSymbol(model.expectShape(shape.getTarget())); + } + + @Override + public Symbol timestampShape(TimestampShape shape) { + return TraitCodegenUtils.fromClass(Instant.class); + } + + @Override + public Symbol unionShape(UnionShape shape) { + throw new UnsupportedOperationException("Union shapes are not supported at this time."); + } + + @Override + public Symbol toSymbol(Shape shape) { + return shape.accept(this); + } + + @Override + public String toMemberName(MemberShape member) { + Shape containerShape = model.expectShape(member.getContainer()); + // If the container is a Map list then we assign a simple "values" holder for the collection + if (containerShape.isMapShape() || containerShape.isListShape()) { + return "values"; + // Enum shapes should have upper snake case members + } else if (containerShape.isEnumShape() || containerShape.isIntEnumShape()) { + return CaseUtils.toSnakeCase(TraitCodegenUtils.MEMBER_ESCAPER.escape(member.getMemberName())) + .toUpperCase(Locale.ROOT); + } + + if (member.getMemberName().contains("_")) { + return TraitCodegenUtils.MEMBER_ESCAPER.escape(CaseUtils.toCamelCase(member.getMemberName())); + } else { + return TraitCodegenUtils.MEMBER_ESCAPER.escape(member.getMemberName()); + } + } + + private Symbol getJavaClassSymbol(Shape shape) { + String name = TraitCodegenUtils.getDefaultName(shape); + String namespace = TraitCodegenUtils.mapNamespace(smithyNamespace, + shape.getId().getNamespace(), packageNamespace); + return Symbol.builder().name(name) + .namespace(namespace, ".") + .declarationFile("./" + namespace.replace(".", "/") + "/" + name + ".java") + .build(); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenUtils.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenUtils.java new file mode 100644 index 00000000000..aa4999e1e4b --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/TraitCodegenUtils.java @@ -0,0 +1,138 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + +import java.net.URL; +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.Shape; +import software.amazon.smithy.model.traits.UniqueItemsTrait; +import software.amazon.smithy.utils.CaseUtils; +import software.amazon.smithy.utils.SmithyInternalApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * Provides utility methods for trait code generation. + */ +@SmithyInternalApi +public final class TraitCodegenUtils { + public static final Symbol JAVA_STRING_SYMBOL = TraitCodegenUtils.fromClass(String.class); + public static final URL RESERVED_WORDS_FILE = TraitCodegenUtils.class.getResource("reserved-words.txt"); + public static final ReservedWords SHAPE_ESCAPER = new ReservedWordsBuilder() + .loadCaseInsensitiveWords(RESERVED_WORDS_FILE, word -> word + "Shape") + .build(); + public static final ReservedWords MEMBER_ESCAPER = new ReservedWordsBuilder() + .loadCaseInsensitiveWords(RESERVED_WORDS_FILE, word -> word + "Member") + .build(); + + private TraitCodegenUtils() {} + + /** + * Gets a Smithy codegen {@link Symbol} for a Java class. + * + * @param clazz class to get symbol for. + * @return Symbol representing the provided class. + */ + public static Symbol fromClass(Class clazz) { + Symbol.Builder builder = Symbol.builder() + .name(clazz.getSimpleName()) + .namespace(clazz.getCanonicalName().replace("." + clazz.getSimpleName(), ""), "."); + + if (clazz.isPrimitive()) { + builder.putProperty(SymbolProperties.IS_PRIMITIVE, true); + } + + return builder.build(); + } + + /** + * Gets the default class name to use for a given Smithy {@link Shape}. + * + * @param shape Shape to get name for. + * @return Default name. + */ + public static String getDefaultName(Shape shape) { + String baseName = shape.getId().getName(); + + // If the name contains any problematic delimiters, use PascalCase converter, + // otherwise, just capitalize first letter to avoid messing with user-defined + // capitalization. + String unescaped; + if (baseName.contains("_")) { + unescaped = CaseUtils.toPascalCase(shape.getId().getName()); + } else { + unescaped = StringUtils.capitalize(baseName); + } + + return SHAPE_ESCAPER.escape(unescaped); + } + + /** + * Gets the default class name to use for a given Smithy {@link Shape} that + * defines a trait. + * + * @param shape Shape to get name for. + * @return Default name. + */ + public static String getDefaultTraitName(Shape shape) { + String name = getDefaultName(shape); + + // If the trait class name doesn't already end with `Trait`, + // use that. Otherwise, append the `Trait` suffix. + if (name.endsWith("Trait")) { + return name; + } + + return name + "Trait"; + } + + /** + * Checks if a symbol maps to a Java {@link String}. + * + * @param symbol Symbol to check. + * @return Returns true if the symbol maps to a Java String. + */ + public static boolean isJavaString(Symbol symbol) { + Symbol baseSymbol = symbol.getProperty(SymbolProperties.BASE_SYMBOL, Symbol.class) + .orElse(symbol); + return JAVA_STRING_SYMBOL.getName().equals(baseSymbol.getName()) + && JAVA_STRING_SYMBOL.getNamespace().equals(baseSymbol.getNamespace()); + } + + /** + * Checks if a symbol maps to a Java {@code List}. + * + * @param shape shape to check if it resolves to a list of java strings + * @param symbolProvider symbol provider to use for checking member type + * @return Returns true if the symbol maps to a Java String List. + */ + public static boolean isJavaStringList(Shape shape, SymbolProvider symbolProvider) { + return shape.isListShape() + && !shape.hasTrait(UniqueItemsTrait.class) + && TraitCodegenUtils.isJavaString(symbolProvider.toSymbol( + shape.asListShape().get().getMember())); + } + + /** + * Maps a smithy namespace to a java package namespace. + * + * @param rootSmithyNamespace base smithy namespace in use for trait codegen trait discovery + * @param shapeNamespace namespace of shape to map into package namespace. + * @param packageNamespace Java package namespace for trait codegen. + */ + public static String mapNamespace(String rootSmithyNamespace, + String shapeNamespace, + String packageNamespace + ) { + if (!shapeNamespace.startsWith(rootSmithyNamespace)) { + throw new IllegalArgumentException("Cannot relativize non-nested namespaces " + + "Root: " + rootSmithyNamespace + " Nested: " + shapeNamespace + "."); + } + return shapeNamespace.replace(rootSmithyNamespace, packageNamespace); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java new file mode 100644 index 00000000000..ea484c16ff0 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/BuilderGenerator.java @@ -0,0 +1,317 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +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.shapes.ListShape; +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.StructureShape; +import software.amazon.smithy.model.traits.AbstractTraitBuilder; +import software.amazon.smithy.model.traits.StringListTrait; +import software.amazon.smithy.model.traits.TraitDefinition; +import software.amazon.smithy.traitcodegen.SymbolProperties; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.sections.BuilderClassSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.StringUtils; + +/** + * Generates a static builder for a Java class. + * + *

In addition to the static builder class, this generator will create + * {@code builder()} and {@code toBuilder()} methods for the target class. + */ +final class BuilderGenerator implements Runnable { + + private final TraitCodegenWriter writer; + private final Symbol symbol; + private final SymbolProvider symbolProvider; + private final Shape baseShape; + private final Model model; + + BuilderGenerator(TraitCodegenWriter writer, Symbol symbol, SymbolProvider symbolProvider, Shape baseShape, + Model model) { + this.writer = writer; + this.symbol = symbol; + this.symbolProvider = symbolProvider; + this.baseShape = baseShape; + this.model = model; + } + + @Override + public void run() { + // Only create builder methods for aggregate types. + if (baseShape.getType().getCategory().equals(ShapeType.Category.AGGREGATE)) { + writeToBuilderMethod(); + writeBuilderMethod(); + writeBuilderClass(); + } + } + + private void writeBuilderClass() { + writer.pushState(new BuilderClassSection(symbol)); + writer.writeInline("public static final class Builder $C", (Runnable) this::writeBuilderInterface); + writer.indent(); + baseShape.accept(new BuilderPropertyGenerator()); + writer.newLine(); + writer.writeWithNoFormatting("private Builder() {}").newLine(); + baseShape.accept(new BuilderSetterGenerator()); + writer.override(); + writer.openBlock("public $T build() {", "}", symbol, + () -> writer.write("return new $C;", (Runnable) this::writeBuilderReturn)); + writer.dedent().write("}"); + writer.popState(); + writer.newLine(); + } + + private void writeBuilderInterface() { + if (baseShape.hasTrait(TraitDefinition.class)) { + if (TraitCodegenUtils.isJavaStringList(baseShape, symbolProvider)) { + writer.write("extends $T.Builder<$T, Builder> {", StringListTrait.class, symbol); + } else { + writer.write("extends $T<$T, Builder> {", AbstractTraitBuilder.class, symbol); + } + } else { + writer.write("implements $T<$T> {", SmithyBuilder.class, symbol); + } + } + + private void writeBuilderReturn() { + // String list traits need a custom builder return + if (TraitCodegenUtils.isJavaStringList(baseShape, symbolProvider)) { + writer.write("$T(getValues(), getSourceLocation())", symbol); + } else { + writer.write("$T(this)", symbol); + } + } + + + private void writeToBuilderMethod() { + writer.openDocstring(); + writer.writeDocStringContents("Creates a builder used to build a {@link $T}.", symbol); + writer.closeDocstring(); + writer.override(); + writer.openBlock("public $T<$T> toBuilder() {", "}", + SmithyBuilder.class, symbol, () -> { + writer.writeInlineWithNoFormatting("return builder()"); + writer.indent(); + if (baseShape.hasTrait(TraitDefinition.class)) { + writer.writeInlineWithNoFormatting(".sourceLocation(getSourceLocation())"); + } + if (baseShape.members().isEmpty()) { + writer.writeInlineWithNoFormatting(";"); + } + writer.newLine(); + + // Set all builder properties for any members in the shape + if (baseShape.isListShape()) { + writer.writeWithNoFormatting(".values(getValues());"); + } else { + Iterator memberIterator = baseShape.members().iterator(); + while (memberIterator.hasNext()) { + MemberShape member = memberIterator.next(); + writer.writeInline(".$1L($1L)", symbolProvider.toMemberName(member)); + if (memberIterator.hasNext()) { + writer.writeInlineWithNoFormatting("\n"); + } else { + writer.writeInlineWithNoFormatting(";\n"); + } + } + } + writer.dedent(); + }); + writer.newLine(); + } + + private void writeBuilderMethod() { + writer.openBlock("public static Builder builder() {", "}", + () -> writer.write("return new Builder();")).newLine(); + } + + private final class BuilderPropertyGenerator extends ShapeVisitor.Default { + + @Override + protected Void getDefault(Shape shape) { + throw new UnsupportedOperationException("Does not support shape of type: " + shape.getType()); + } + + @Override + public Void listShape(ListShape shape) { + // String list shapes do not need value properties + if (TraitCodegenUtils.isJavaStringList(shape, symbolProvider)) { + return null; + } + writeValuesProperty(shape); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + writeValuesProperty(shape); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + shape.members().forEach(this::writeProperty); + return null; + } + + private void writeProperty(MemberShape shape) { + Optional builderRefOptional = + symbolProvider.toSymbol(shape).getProperty(SymbolProperties.BUILDER_REF_INITIALIZER, String.class); + if (builderRefOptional.isPresent()) { + writer.write("private final $1T<$2B> $3L = $1T.$4L;", BuilderRef.class, + symbolProvider.toSymbol(shape), + symbolProvider.toMemberName(shape), + builderRefOptional.orElseThrow(RuntimeException::new)); + } else { + writer.write("private $B $L;", symbolProvider.toSymbol(shape), + symbolProvider.toMemberName(shape)); + } + } + + private void writeValuesProperty(Shape shape) { + Symbol collectionSymbol = symbolProvider.toSymbol(shape); + writer.write("private final $1T<$2B> $3L = $1T.$4L;", BuilderRef.class, + collectionSymbol, "values", + collectionSymbol.expectProperty(SymbolProperties.BUILDER_REF_INITIALIZER)); + } + } + + private final class BuilderSetterGenerator extends ShapeVisitor.Default { + @Override + protected Void getDefault(Shape shape) { + throw new UnsupportedOperationException("Does not support shape of type: " + shape.getType()); + } + + @Override + public Void listShape(ListShape shape) { + // String list shapes do not need setters + if (TraitCodegenUtils.isJavaStringList(shape, symbolProvider)) { + return null; + } + shape.accept(new SetterVisitor("values")); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + shape.accept(new SetterVisitor("values")); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + shape.members().forEach( + memberShape -> memberShape.accept(new SetterVisitor(symbolProvider.toMemberName(memberShape)))); + return null; + } + } + + private final class SetterVisitor extends ShapeVisitor.Default { + private final String memberName; + + private SetterVisitor(String memberName) { + this.memberName = memberName; + } + + @Override + protected Void getDefault(Shape shape) { + writer.openBlock("public Builder $1L($2B $1L) {", "}", + memberName, symbolProvider.toSymbol(shape), () -> { + writer.write("this.$1L = $1L;", memberName); + writer.writeWithNoFormatting("return this;"); + }).newLine(); + return null; + } + + @Override + public Void listShape(ListShape shape) { + writer.openBlock("public Builder $1L($2B $1L) {", "}", + memberName, symbolProvider.toSymbol(shape), () -> { + writer.write("clear$L();", StringUtils.capitalize(memberName)); + writer.write("this.$1L.get().addAll($1L);", memberName); + writer.writeWithNoFormatting("return this;"); + }).newLine(); + + // Clear all + writer.openBlock("public Builder clear$L() {", "}", + StringUtils.capitalize(memberName), () -> { + writer.write("$L.get().clear();", memberName); + writer.writeWithNoFormatting("return this;"); + }).newLine(); + + // Set one + writer.openBlock("public Builder add$L($T value) {", "}", + StringUtils.capitalize(memberName), symbolProvider.toSymbol(shape.getMember()), + () -> { + writer.write("$L.get().add(value);", memberName); + writer.write("return this;"); + }).newLine(); + + // Remove one + writer.openBlock("public Builder remove$L($T value) {", "}", + StringUtils.capitalize(memberName), symbolProvider.toSymbol(shape.getMember()), + () -> { + writer.write("$L.get().remove(value);", memberName); + writer.write("return this;"); + }).newLine(); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + // Set all + writer.openBlock("public Builder $1L($2B $1L) {", "}", + memberName, symbolProvider.toSymbol(shape), () -> { + writer.write("clear$L();", StringUtils.capitalize(memberName)); + writer.write("this.$1L.get().putAll($1L);", memberName); + writer.write("return this;"); + }); + writer.newLine(); + + // Clear all + writer.openBlock("public Builder clear$L() {", "}", StringUtils.capitalize(memberName), () -> { + writer.write("this.$L.get().clear();", memberName); + writer.write("return this;"); + }).newLine(); + + // Set one + MemberShape keyShape = shape.getKey(); + MemberShape valueShape = shape.getValue(); + writer.openBlock("public Builder put$L($T key, $T value) {", "}", + StringUtils.capitalize(memberName), symbolProvider.toSymbol(keyShape), + symbolProvider.toSymbol(valueShape), () -> { + writer.write("this.$L.get().put(key, value);", memberName); + writer.write("return this;"); + }).newLine(); + + // Remove one + writer.openBlock("public Builder remove$L($T $L) {", "}", + StringUtils.capitalize(memberName), symbolProvider.toSymbol(keyShape), memberName, () -> { + writer.write("this.$1L.get().remove($1L);", memberName); + writer.write("return this;"); + }).newLine(); + return null; + } + + @Override + public Void memberShape(MemberShape shape) { + return model.expectShape(shape.getTarget()).accept(this); + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ConstructorGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ConstructorGenerator.java new file mode 100644 index 00000000000..91fa81f0669 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ConstructorGenerator.java @@ -0,0 +1,316 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.FromSourceLocation; +import software.amazon.smithy.model.SourceLocation; +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.EnumShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +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.NumberShape; +import software.amazon.smithy.model.shapes.Shape; +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.TraitDefinition; +import software.amazon.smithy.model.traits.UniqueItemsTrait; +import software.amazon.smithy.traitcodegen.SymbolProperties; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.SmithyBuilder; + +/** + * Generates a constructor for a type. + */ +final class ConstructorGenerator extends TraitVisitor implements Runnable { + private final TraitCodegenWriter writer; + private final Symbol symbol; + private final Shape shape; + private final SymbolProvider symbolProvider; + + ConstructorGenerator(TraitCodegenWriter writer, + Symbol symbol, + Shape shape, + SymbolProvider symbolProvider + ) { + this.writer = writer; + this.symbol = symbol; + this.shape = shape; + this.symbolProvider = symbolProvider; + } + + @Override + public void run() { + shape.accept(this); + } + + @Override + public Void listShape(ListShape shape) { + if (!shape.hasTrait(UniqueItemsTrait.class) + && TraitCodegenUtils.isJavaString(symbolProvider.toSymbol(shape.getMember())) + ) { + writer.openBlock("public $1T($1B values, $2T sourceLocation) {", "}", + symbol, FromSourceLocation.class, () -> writer.write("super(ID, values, sourceLocation);")); + writer.newLine(); + + writer.openBlock("public $1T($1B values) {", "}", symbol, + () -> writer.write("super(ID, values, $T.NONE);", SourceLocation.class)); + writer.newLine(); + } else { + writeConstructorWithBuilder(); + } + + return null; + } + + @Override + public Void mapShape(MapShape shape) { + writeConstructorWithBuilder(); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + Symbol integerSymbol = TraitCodegenUtils.fromClass(Integer.class); + // Constructor with no source location + writer.openBlock("public $T($T value) {", "}", + symbol, integerSymbol, () -> { + writer.write("super(ID, $T.NONE);", SourceLocation.class); + writer.writeWithNoFormatting("this.value = value;"); + }); + writer.newLine(); + + // Constructor with source location + writer.openBlock("public $T($T value, $T sourceLocation) {", "}", + symbol, integerSymbol, FromSourceLocation.class, () -> { + writer.writeWithNoFormatting("super(ID, sourceLocation);"); + writer.writeWithNoFormatting("this.value = value;"); + }); + writer.newLine(); + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + writer.openBlock("public $T($T value) {", "}", + symbol, Node.class, () -> writer.writeWithNoFormatting("super(ID, value);")); + writer.newLine(); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + if (TraitCodegenUtils.isJavaString(symbol)) { + writeStringTraitConstructors(); + } else { + writeValueShapeConstructors(); + } + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + writeStringTraitConstructors(); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + writeConstructorWithBuilder(); + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + writeValueShapeConstructors(); + return null; + } + + @Override + protected Void numberShape(NumberShape shape) { + writeValueShapeConstructors(); + return null; + } + + private void writeConstructorWithBuilder() { + writer.openBlock("private $T(Builder builder) {", "}", symbol, () -> { + // If the shape is a trait include the source location. Nested shapes don't have a separate source location. + if (shape.hasTrait(TraitDefinition.class)) { + writer.writeWithNoFormatting("super(ID, builder.getSourceLocation());"); + } + shape.accept(new InitializerVisitor()); + }); + writer.newLine(); + } + + private void writeValueShapeConstructors() { + // Constructor with no source location + writer.openBlock("public $1T($1B value) {", "}", symbol, () -> { + writer.write("super(ID, $T.NONE);", SourceLocation.class); + writer.writeWithNoFormatting("this.value = value;"); + }); + writer.newLine(); + + // Constructor with source location + writer.openBlock("public $1T($1B value, $2T sourceLocation) {", "}", + symbol, FromSourceLocation.class, () -> { + writer.writeWithNoFormatting("super(ID, sourceLocation);"); + writer.writeWithNoFormatting("this.value = value;"); + }); + writer.newLine(); + } + + private void writeStringTraitConstructors() { + // Without source location + writer.openBlock("public $T(String value) {", "}", symbol, + () -> writer.write("super(ID, value, $T.NONE);", SourceLocation.class)); + writer.newLine(); + + // With source location + writer.openBlock("public $T($T value, $T sourceLocation) {", "}", + symbol, String.class, FromSourceLocation.class, + () -> writer.writeWithNoFormatting("super(ID, value, sourceLocation);")); + writer.newLine(); + } + + /** + * Generates the actual field initialization statements for each member of a shape. + */ + private final class InitializerVisitor extends ShapeVisitor.DataShapeVisitor { + + @Override + public Void booleanShape(BooleanShape shape) { + return null; + } + + @Override + public Void listShape(ListShape shape) { + writeValuesInitializer(); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + writeValuesInitializer(); + return null; + } + + @Override + public Void byteShape(ByteShape shape) { + return null; + } + + @Override + public Void shortShape(ShortShape shape) { + return null; + } + + @Override + public Void integerShape(IntegerShape shape) { + return null; + } + + @Override + public Void longShape(LongShape shape) { + return null; + } + + @Override + public Void floatShape(FloatShape shape) { + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + return null; + } + + @Override + public Void doubleShape(DoubleShape shape) { + return null; + } + + @Override + public Void bigIntegerShape(BigIntegerShape shape) { + return null; + } + + @Override + public Void bigDecimalShape(BigDecimalShape shape) { + return null; + } + + @Override + public Void stringShape(StringShape shape) { + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + for (MemberShape member : shape.members()) { + if (member.isRequired()) { + 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; + } + + @Override + public Void unionShape(UnionShape shape) { + throw new UnsupportedOperationException("Does not support shape of type " + shape.getType()); + } + + @Override + public Void blobShape(BlobShape shape) { + throw new UnsupportedOperationException("Does not support shape of type " + shape.getType()); + } + + @Override + public Void memberShape(MemberShape shape) { + throw new UnsupportedOperationException("Does not support shape of type " + shape.getType()); + } + + @Override + public Void timestampShape(TimestampShape shape) { + return null; + } + + private String getBuilderValue(MemberShape 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)); + } + } + + private void writeValuesInitializer() { + writer.writeWithNoFormatting("this.values = builder.values.copy();"); + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java new file mode 100644 index 00000000000..58a44e26565 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/EnumShapeGenerator.java @@ -0,0 +1,191 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +import java.util.Objects; +import java.util.function.Consumer; +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.node.ToNode; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.traitcodegen.GenerateTraitDirective; +import software.amazon.smithy.traitcodegen.sections.ClassSection; +import software.amazon.smithy.traitcodegen.sections.EnumVariantSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; + +/** + * Effectively sealed base class for generating a Java Enum class from a Smithy model. + * + *

The two public implementations provided by this base class are: + *

+ *
{@link StringEnumShapeGenerator}
+ *
Generates a java enum from a Smithy {@link software.amazon.smithy.model.shapes.EnumShape}.
+ *
{@link IntEnumShapeGenerator}
+ *
Generates a java enum from a Smithy {@link software.amazon.smithy.model.shapes.IntEnumShape}.
+ *
+ */ +abstract class EnumShapeGenerator implements Consumer { + + // Private constructor to make abstract class effectively sealed. + private EnumShapeGenerator() {} + + @Override + public void accept(GenerateTraitDirective directive) { + directive.context().writerDelegator().useShapeWriter(directive.shape(), + writer -> writeEnum(directive.shape(), directive.symbolProvider(), writer, directive.model())); + } + + public void writeEnum(Shape enumShape, + SymbolProvider provider, + TraitCodegenWriter writer, + Model model) { + writeEnum(enumShape, provider, writer, model, true); + } + + /** + * Writes an Enum class from an enum shape. + * + * @param enumShape enum shape to generate enum class for. + * @param provider symbol provider. + * @param writer writer to write generated code to. + * @param model smithy model used for code generation. + * @param isStandaloneClass flag indicating if enum is a standalone class (i.e. defined in its own class file). + */ + public void writeEnum(Shape enumShape, + SymbolProvider provider, + TraitCodegenWriter writer, + Model model, + boolean isStandaloneClass + ) { + Symbol enumSymbol = provider.toSymbol(enumShape); + writer.pushState(new ClassSection(enumShape)) + .putContext("standalone", isStandaloneClass) + .openBlock("public enum $B ${?standalone}implements $T ${/standalone}{", "}", + enumSymbol, ToNode.class, () -> { + writeVariants(enumShape, provider, writer); + writer.newLine(); + + writeValueField(writer); + writer.newLine(); + + writeConstructor(enumSymbol, writer); + + writeValueGetter(writer); + writer.newLine(); + + writeFromMethod(enumSymbol, writer); + + // Only generate From and To Node when we are in a standalone class. + if (isStandaloneClass) { + writeToNode(writer); + new FromNodeGenerator(writer, enumSymbol, enumShape, provider, model).run(); + } + }) + .popState(); + } + + abstract String getVariantTemplate(); + + abstract Class getValueType(); + + abstract Object getEnumValue(MemberShape member); + + private void writeVariants(Shape enumShape, SymbolProvider provider, TraitCodegenWriter writer) { + for (MemberShape member : enumShape.members()) { + writer.pushState(new EnumVariantSection(member)); + writer.write(getVariantTemplate() + ",", provider.toMemberName(member), getEnumValue(member)); + writer.popState(); + } + writer.write("UNKNOWN(null);"); + } + + private void writeValueField(TraitCodegenWriter writer) { + writer.write("private final $T value;", getValueType()); + } + + private void writeValueGetter(TraitCodegenWriter writer) { + writer.openBlock("public $T getValue() {", "}", getValueType(), + () -> writer.writeWithNoFormatting("return value;")); + } + + private void writeToNode(TraitCodegenWriter writer) { + writer.override(); + writer.openBlock("public $T toNode() {", "}", Node.class, + () -> writer.write("return $T.from(value);", Node.class)); + writer.newLine(); + } + + private void writeConstructor(Symbol enumSymbol, TraitCodegenWriter writer) { + writer.openBlock("$B($T value) {", "}", + enumSymbol, getValueType(), () -> writer.write("this.value = value;")); + writer.newLine(); + } + + private void writeFromMethod(Symbol enumSymbol, TraitCodegenWriter writer) { + writer.openDocstring(); + writer.writeDocStringContents("Create a {@code $B} from a value in a model.", enumSymbol); + writer.writeDocStringContents(""); + writer.writeDocStringContents("

Any unknown value is returned as {@code UNKNOWN}."); + writer.writeDocStringContents(""); + writer.writeDocStringContents("@param value Value to create enum from."); + writer.writeDocStringContents("@return Returns the {@link $B} enum value.", enumSymbol); + writer.closeDocstring(); + writer.openBlock("public static $B from($T value) {", "}", + enumSymbol, getValueType(), () -> { + writer.write("$T.requireNonNull(value, \"Enum value should not be null.\");", Objects.class); + writer.openBlock("for ($B val: values()) {", "}", + enumSymbol, + () -> writer.openBlock("if ($T.equals(val.getValue(), value)) {", "}", + Objects.class, () -> writer.writeWithNoFormatting("return val;"))); + writer.writeWithNoFormatting("return UNKNOWN;"); + }); + writer.newLine(); + } + + /** + * Generates a Java Enum class from a smithy {@link software.amazon.smithy.model.shapes.EnumShape}. + */ + public static final class StringEnumShapeGenerator extends EnumShapeGenerator { + @Override + String getVariantTemplate() { + return "$L($S)"; + } + + @Override + Class getValueType() { + return String.class; + } + + @Override + Object getEnumValue(MemberShape member) { + return member.expectTrait(EnumValueTrait.class).expectStringValue(); + } + } + + /** + * Generates a Java Enum class from a smithy {@link software.amazon.smithy.model.shapes.IntEnumShape}. + */ + public static final class IntEnumShapeGenerator extends EnumShapeGenerator { + @Override + String getVariantTemplate() { + return "$L($L)"; + } + + @Override + Class getValueType() { + return Integer.class; + } + + @Override + Object getEnumValue(MemberShape member) { + return member.expectTrait(EnumValueTrait.class).expectIntValue(); + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java new file mode 100644 index 00000000000..8d71f05fd49 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeGenerator.java @@ -0,0 +1,353 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +import java.time.Instant; +import java.util.Iterator; +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.ExpectationNotMetException; +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.EnumShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +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.NumberShape; +import software.amazon.smithy.model.shapes.Shape; +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.TimestampFormatTrait; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.StringUtils; + +/** + * Generates the static {@code fromNode} method to deserialize a smithy node into an instance of a Java class. + */ +final class FromNodeGenerator extends TraitVisitor implements Runnable { + private final TraitCodegenWriter writer; + private final Symbol symbol; + private final Shape shape; + private final SymbolProvider symbolProvider; + private final Model model; + + FromNodeGenerator(TraitCodegenWriter writer, Symbol symbol, Shape shape, SymbolProvider symbolProvider, + Model model) { + this.writer = writer; + this.symbol = symbol; + this.shape = shape; + this.symbolProvider = symbolProvider; + this.model = model; + } + + @Override + public void run() { + shape.accept(this); + } + + @Override + public Void listShape(ListShape shape) { + if (TraitCodegenUtils.isJavaStringList(shape, symbolProvider)) { + return null; + } + + writeFromNodeJavaDoc(); + writer.openBlock("public static $T fromNode($T node) {", "}", + symbol, Node.class, () -> { + writer.writeWithNoFormatting("Builder builder = builder();"); + shape.accept(new FromNodeMapperVisitor(writer, model, "node")); + writer.writeWithNoFormatting("return builder.build();"); + }); + writer.newLine(); + + return null; + } + + @Override + public Void mapShape(MapShape shape) { + writeFromNodeJavaDoc(); + writer.openBlock("public static $T fromNode($T node) {", "}", + symbol, Node.class, () -> { + writer.writeWithNoFormatting("Builder builder = builder();"); + shape.accept(new FromNodeMapperVisitor(writer, model, "node")); + writer.writeWithNoFormatting("return builder.build();"); + }); + writer.newLine(); + + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + return null; + } + + @Override + public Void stringShape(StringShape shape) { + return null; + } + + @Override + protected Void numberShape(NumberShape shape) { + // Number shapes do not create a from node method + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + writeFromNodeJavaDoc(); + writer.write("public static $T fromNode($T node) {", symbol, Node.class); + writer.indent(); + writer.write("Builder builder = builder();"); + // If the shape has no members (i.e. is an annotation trait) then there will be no member setters, and we + // need to terminate the line. + writer.putContext("isEmpty", shape.members().isEmpty()); + writer.write("node.expectObjectNode()${?isEmpty};${/isEmpty}"); + writer.indent(); + Iterator memberIterator = shape.members().iterator(); + while (memberIterator.hasNext()) { + MemberShape member = memberIterator.next(); + member.accept(new MemberGenerator(member)); + if (memberIterator.hasNext()) { + writer.writeInlineWithNoFormatting("\n"); + } else { + writer.writeWithNoFormatting(";\n"); + } + } + writer.dedent(); + writer.write("return builder.build();"); + writer.dedent().write("}"); + writer.newLine(); + + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + writeFromNodeJavaDoc(); + writer.openBlock("public static $T fromNode($T node) {", "}", + symbol, Node.class, this::writeTimestampDeser); + writer.newLine(); + + return null; + } + + private void writeTimestampDeser() { + // If the trait has the timestamp format trait then we only need a single + // way to deserialize the input node. Without the timestamp format trait + // timestamp should be able to handle both epoch seconds and date time formats. + if (shape.hasTrait(TimestampFormatTrait.class)) { + writer.write("return new $T($C, node.getSourceLocation());", + symbol, + (Runnable) () -> shape.accept(new FromNodeMapperVisitor(writer, model, "node"))); + } else { + writer.openBlock("if (node.isNumberNode()) {", "}", () -> { + writer.write("return new $T($T.ofEpochSecond(node.expectNumberNode().getValue().longValue()),", + symbol, Instant.class).indent(); + writer.writeWithNoFormatting("node.getSourceLocation());").dedent(); + }); + writer.write("return new $T($T.parse(node.expectStringNode().getValue()), node.getSourceLocation());", + symbol, Instant.class); + } + writer.newLine(); + } + + private void writeFromNodeJavaDoc() { + // Add docstring for method + writer.openDocstring(); + writer.writeDocStringContents("Creates a {@link $T} from a {@link Node}.", symbol); + writer.writeDocStringContents(""); + writer.writeDocStringContents("@param node Node to create the $T from.", symbol); + writer.writeDocStringContents("@return Returns the created $T.", symbol); + writer.writeDocStringContents("@throws $T if the given Node is invalid.", + ExpectationNotMetException.class); + writer.closeDocstring(); + } + + /** + * Generates the mapping from a node member to a builder field. + */ + private final class MemberGenerator extends ShapeVisitor.DataShapeVisitor { + private final String memberName; + private final String fieldName; + private final String memberPrefix; + + private MemberGenerator(MemberShape member) { + this.fieldName = member.getMemberName(); + this.memberName = symbolProvider.toMemberName(member); + this.memberPrefix = member.isRequired() ? ".expect" : ".get"; + } + + @Override + public Void memberShape(MemberShape shape) { + return model.expectShape(shape.getTarget()).accept(this); + } + + @Override + public Void booleanShape(BooleanShape shape) { + writer.writeInline(memberPrefix + "BooleanMember($S, builder::$L)", fieldName, memberName); + return null; + } + + @Override + public Void listShape(ListShape shape) { + writer.writeInline(memberPrefix + "ArrayMember($1S, n -> $3C, builder::$2L)", + fieldName, memberName, + (Runnable) () -> shape.getMember().accept(new FromNodeMapperVisitor(writer, model, "n"))); + return null; + } + + @Override + public Void byteShape(ByteShape shape) { + writer.writeInline(memberPrefix + "NumberMember($S, n -> builder.$L(n.byteValue()))", + fieldName, memberName); + return null; + } + + @Override + public Void shortShape(ShortShape shape) { + writer.writeInline(memberPrefix + "NumberMember($S, n -> builder.$L(n.shortValue()))", + fieldName, memberName); + return null; + } + + @Override + public Void integerShape(IntegerShape shape) { + writer.writeInline(memberPrefix + "NumberMember($S, n -> builder.$L(n.intValue()))", + fieldName, memberName); + return null; + } + + @Override + public Void longShape(LongShape shape) { + writer.writeInline(memberPrefix + "NumberMember($S, n -> builder.$L(n.longValue()))", + fieldName, memberName); + return null; + } + + @Override + public Void floatShape(FloatShape shape) { + writer.writeInline(memberPrefix + "NumberMember($S, n -> builder.$L(n.floatValue()))", + fieldName, memberName); + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + writer.writeInline(memberPrefix + "Member($1S, $3T::expectObjectNode, builder::$2L)", + fieldName, memberName, Node.class); + return null; + } + + @Override + public Void doubleShape(DoubleShape shape) { + writer.writeInline(memberPrefix + "NumberMember($S, n -> builder.$L(n.doubleValue()))", + fieldName, memberName); + return null; + } + + @Override + public Void bigIntegerShape(BigIntegerShape shape) { + writer.writeInline(memberPrefix + + "Member($S, n -> n.expectNumberNode().asBigDecimal().get().toBigInteger(), builder::$L)", + fieldName, memberName); + return null; + } + + @Override + public Void bigDecimalShape(BigDecimalShape shape) { + writer.writeInline(memberPrefix + + "Member($S, n -> n.expectNumberNode().asBigDecimal().get(), builder::$L)", + fieldName, memberName); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + writer.disableNewlines(); + writer.openBlock(memberPrefix + + "ObjectMember($S, o -> o.getMembers().forEach((k, v) -> {\n", "}))", + fieldName, + () -> writer.write("builder.put$L($C, $C);\n", + StringUtils.capitalize(memberName), + (Runnable) () -> shape.getKey().accept(new FromNodeMapperVisitor(writer, model, "k")), + (Runnable) () -> shape.getValue().accept(new FromNodeMapperVisitor(writer, model, "v"))) + ); + writer.enableNewlines(); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + writer.writeInline(memberPrefix + "NumberMember($S, n -> builder.$L($T.from(n.intValue())))", + fieldName, memberName, symbolProvider.toSymbol(shape)); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + if (TraitCodegenUtils.isJavaString(symbolProvider.toSymbol(shape))) { + writer.writeInline(memberPrefix + "StringMember($S, builder::$L)", fieldName, memberName); + } else { + writer.writeInline(memberPrefix + "Member($1S, n -> $3C, builder::$2L)", + fieldName, memberName, + (Runnable) () -> shape.accept(new FromNodeMapperVisitor(writer, model, "n")) + ); + } + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + writer.writeInline(memberPrefix + "StringMember($S, n -> builder.$L($T.from(n)))", + fieldName, memberName, symbolProvider.toSymbol(shape)); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + writer.writeInline(memberPrefix + "Member($1S, n -> $3C, builder::$2L)", + fieldName, memberName, + (Runnable) () -> shape.accept(new FromNodeMapperVisitor(writer, model, "n")) + ); + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + writer.writeInline(memberPrefix + "Member($1S, n -> $3C, builder::$2L)", + fieldName, memberName, + (Runnable) () -> shape.accept(new FromNodeMapperVisitor(writer, model, "n"))); + return null; + } + + @Override + public Void unionShape(UnionShape shape) { + throw new UnsupportedOperationException("Shape not supported " + shape); + } + + @Override + public Void blobShape(BlobShape shape) { + throw new UnsupportedOperationException("Shape not supported " + shape); + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeMapperVisitor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeMapperVisitor.java new file mode 100644 index 00000000000..3b0f2bd145b --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/FromNodeMapperVisitor.java @@ -0,0 +1,204 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import software.amazon.smithy.model.Model; +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.EnumShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +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.ShapeId; +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.IdRefTrait; +import software.amazon.smithy.model.traits.TimestampFormatTrait; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; + +/** + * Determines how to map a node to a shape. + */ +final class FromNodeMapperVisitor extends ShapeVisitor.DataShapeVisitor { + + private final TraitCodegenWriter writer; + private final Model model; + private final String varName; + + FromNodeMapperVisitor(TraitCodegenWriter writer, Model model, String varName) { + this.writer = writer; + this.model = model; + this.varName = varName; + } + + @Override + public Void booleanShape(BooleanShape shape) { + writer.write("BooleanMember($1S, builder::$1L)", varName); + return null; + } + + @Override + public Void listShape(ListShape shape) { + writer.write("$L.expectArrayNode()", varName); + writer.indent(); + writer.writeWithNoFormatting(".getElements().stream()"); + writer.write(".map(n -> $C)", + (Runnable) () -> shape.getMember().accept(new FromNodeMapperVisitor(writer, model, "n")) + ); + writer.writeWithNoFormatting(".forEach(builder::addValues);"); + writer.dedent(); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + writer.openBlock("$L.expectObjectNode().getMembers().forEach((k, v) -> {", "});", + varName, + () -> writer.write("builder.putValues($C, $C);", + (Runnable) () -> shape.getKey().accept(new FromNodeMapperVisitor(writer, model, "k")), + (Runnable) () -> shape.getValue().accept(new FromNodeMapperVisitor(writer, model, "v"))) + ); + return null; + } + + @Override + public Void byteShape(ByteShape shape) { + writer.write("$L.expectNumberNode().getValue().byteValue()", varName); + return null; + } + + @Override + public Void shortShape(ShortShape shape) { + writer.write("$L.expectNumberNode().getValue().shortValue()", varName); + return null; + } + + @Override + public Void integerShape(IntegerShape shape) { + writer.write("$L.expectNumberNode().getValue().intValue()", varName); + return null; + } + + @Override + public Void longShape(LongShape shape) { + writer.write("$L.expectNumberNode().getValue().longValue()", varName); + return null; + } + + @Override + public Void floatShape(FloatShape shape) { + writer.write("$L.expectNumberNode().getValue().floatValue()", varName); + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + return null; + } + + @Override + public Void doubleShape(DoubleShape shape) { + writer.write("$L.expectNumberNode().getValue().doubleValue()", varName); + return null; + } + + @Override + public Void bigIntegerShape(BigIntegerShape shape) { + writer.write("$L.expectNumberNode().asBigDecimal().get().toBigInteger()", varName); + return null; + } + + @Override + public Void bigDecimalShape(BigDecimalShape shape) { + writer.write("$L.expectNumberNode().asBigDecimal().get()", varName); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + if (shape.hasTrait(IdRefTrait.class)) { + writer.write("$T.fromNode($L)", ShapeId.class, varName); + } else { + writer.write("$L.expectStringNode().getValue()", varName); + } + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + writer.write("$L.fromNode($L)", TraitCodegenUtils.getDefaultName(shape), varName); + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + if (shape.hasTrait(TimestampFormatTrait.class)) { + switch (shape.expectTrait(TimestampFormatTrait.class).getFormat()) { + case EPOCH_SECONDS: + writer.writeInline("$2T.ofEpochSecond($1L.expectNumberNode().getValue().longValue())", + varName, Instant.class); + return null; + case HTTP_DATE: + writer.writeInline("$2T.from($3T.RFC_1123_DATE_TIME.parse($1L.expectStringNode().getValue()))", + varName, Instant.class, DateTimeFormatter.class); + return null; + default: + // Fall through on default + break; + } + } + writer.writeInline("$2T.parse($1L.expectStringNode().getValue())", varName, Instant.class); + return null; + } + + @Override + public Void unionShape(UnionShape shape) { + throw new UnsupportedOperationException("Union shapes not supported at this time."); + } + + @Override + public Void blobShape(BlobShape shape) { + throw new UnsupportedOperationException("Blob shapes not supported at this time."); + } + + @Override + public Void memberShape(MemberShape shape) { + if (shape.hasTrait(IdRefTrait.class)) { + writer.write("$T.fromNode($L)", ShapeId.class, varName); + } else { + model.expectShape(shape.getTarget()).accept(this); + } + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + writer.write("$L.fromNode($L)", TraitCodegenUtils.getDefaultName(shape), varName); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + writer.write("$L.expectNumberNode().getValue().intValue()", varName); + return null; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java new file mode 100644 index 00000000000..7940b4a5acd --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/GetterGenerator.java @@ -0,0 +1,187 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +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.DocumentShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.NumberShape; +import software.amazon.smithy.model.shapes.Shape; +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.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.sections.GetterSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.StringUtils; + +/** + * Generates getter methods for each shape member or the value type held by the trait. + * + *

Optional member getters will return the member type wrapped in an {@code Optional}. + */ +final class GetterGenerator implements Runnable { + private final TraitCodegenWriter writer; + private final SymbolProvider symbolProvider; + private final Model model; + private final Shape shape; + + GetterGenerator(TraitCodegenWriter writer, SymbolProvider symbolProvider, Model model, Shape shape) { + this.writer = writer; + this.symbolProvider = symbolProvider; + this.model = model; + this.shape = shape; + } + + @Override + public void run() { + shape.accept(new GetterVisitor()); + } + + public final class GetterVisitor extends TraitVisitor { + + @Override + public Void documentShape(DocumentShape shape) { + writer.openBlock("public $T getValue() {", "}", Node.class, + () -> writer.writeWithNoFormatting("return toNode();")); + writer.newLine(); + return null; + } + + @Override + public Void listShape(ListShape shape) { + if (TraitCodegenUtils.isJavaStringList(shape, symbolProvider)) { + return null; + } + generateValuesGetter(shape); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + generateValuesGetter(shape); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + if (TraitCodegenUtils.isJavaString(symbolProvider.toSymbol(shape))) { + return null; + } + generateValueGetter(shape); + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + Symbol shapeSymbol = symbolProvider.toSymbol(shape); + generateEnumValueGetterDocstring(shapeSymbol); + writer.openBlock("public $B getEnumValue() {", "}", + shapeSymbol, + () -> writer.write("return $B.from(getValue());", shapeSymbol)); + writer.newLine(); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + writer.pushState(new GetterSection(shape)); + writer.openBlock("public $T getValue() {", "}", + Integer.class, () -> writer.write("return value;")); + writer.popState(); + writer.newLine(); + + Symbol shapeSymbol = symbolProvider.toSymbol(shape); + generateEnumValueGetterDocstring(shapeSymbol); + writer.openBlock("public $B getEnumValue() {", "}", + shapeSymbol, + () -> writer.write("return $B.from(value);", shapeSymbol)); + writer.newLine(); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + for (MemberShape member : shape.members()) { + // 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$L() {", "}", + symbolProvider.toSymbol(member), + StringUtils.capitalize(symbolProvider.toMemberName(member)), + () -> writer.write("return $L;", symbolProvider.toMemberName(member))); + writer.popState(); + writer.newLine(); + } else { + writer.openBlock("public $T<$T> get$L() {", "}", + Optional.class, symbolProvider.toSymbol(member), + StringUtils.capitalize(symbolProvider.toMemberName(member)), + () -> writer.write("return $T.ofNullable($L);", + Optional.class, symbolProvider.toMemberName(member))); + writer.popState(); + writer.newLine(); + + // If the member targets a collection shape and is optional then generate an unwrapped + // getter as a convenience method as well. + Shape target = model.expectShape(member.getTarget()); + if (target.isListShape() || target.isMapShape()) { + writer.openBlock("public $T get$LOrEmpty() {", "}", + symbolProvider.toSymbol(member), + StringUtils.capitalize(symbolProvider.toMemberName(member)), + () -> writer.write("return $L;", symbolProvider.toMemberName(member))); + } + } + writer.newLine(); + } + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + generateValueGetter(shape); + return null; + } + + @Override + protected Void numberShape(NumberShape shape) { + generateValueGetter(shape); + return null; + } + + private void generateEnumValueGetterDocstring(Symbol symbol) { + writer.openDocstring(); + writer.writeDocStringContents("Gets the {@code $1T} value as a {@code $1B} enum.", symbol); + writer.writeDocStringContents(""); + writer.writeDocStringContents("@return Returns the {@code $B} enum.", symbol); + writer.closeDocstring(); + } + + private void generateValuesGetter(Shape shape) { + writer.pushState(new GetterSection(shape)); + writer.openBlock("public $B getValues() {", "}", + symbolProvider.toSymbol(shape), () -> writer.write("return values;")); + writer.popState(); + writer.newLine(); + } + + private void generateValueGetter(Shape shape) { + writer.pushState(new GetterSection(shape)); + writer.openBlock("public $B getValue() {", "}", + symbolProvider.toSymbol(shape), () -> writer.write("return value;")); + writer.popState(); + writer.newLine(); + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/PropertiesGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/PropertiesGenerator.java new file mode 100644 index 00000000000..aa3f2e69498 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/PropertiesGenerator.java @@ -0,0 +1,129 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.NumberShape; +import software.amazon.smithy.model.shapes.Shape; +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.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; + +/** + * Generates properties for a Java class from Smithy shape members. + * + *

The generated properties hold the value types of member shapes or a value property representing + * the data the trait holds. In the following two cases the generated property has a static name: + *

+ *
Value Shapes (numbers, enum, strings)
+ *
property {@code "value"} represents the single data type held by the trait such as a {@code int} value.
+ *
List and Map shapes
+ *
property {@code "values"} represents the collection held by the trait such as a list of strings.
+ *
+ * + */ +final class PropertiesGenerator implements Runnable { + private final TraitCodegenWriter writer; + private final Shape shape; + private final SymbolProvider symbolProvider; + + PropertiesGenerator(TraitCodegenWriter writer, Shape shape, SymbolProvider symbolProvider) { + this.writer = writer; + this.shape = shape; + this.symbolProvider = symbolProvider; + } + + @Override + public void run() { + shape.accept(new PropertyGenerator()); + writer.newLine(); + } + + private final class PropertyGenerator extends TraitVisitor { + + @Override + public Void listShape(ListShape shape) { + // Do not create a property if the shape can inherit from the StringListTrait base class. + if (TraitCodegenUtils.isJavaStringList(shape, symbolProvider)) { + return null; + } + createValuesProperty(shape); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + writer.write("private final $T value;", Integer.class); + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + // Document traits have no properties + return null; + } + + @Override + public Void mapShape(MapShape shape) { + createValuesProperty(shape); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + // Only create a value property if the shape is not a java string. + // If it is a string it will use the value from the StringTrait base class. + if (!TraitCodegenUtils.isJavaString(symbolProvider.toSymbol(shape))) { + createValueProperty(shape); + } + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + // Enum shapes have no properties + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + for (MemberShape member : shape.members()) { + writer.write("private final $T $L;", + symbolProvider.toSymbol(member), + symbolProvider.toMemberName(member)); + } + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + createValueProperty(shape); + return null; + } + + @Override + protected Void numberShape(NumberShape shape) { + createValueProperty(shape); + return null; + } + + private void createValueProperty(Shape shape) { + writer.write("private final $B value;", symbolProvider.toSymbol(shape)); + } + + private void createValuesProperty(Shape shape) { + writer.write("private final $B values;", symbolProvider.toSymbol(shape)); + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ProviderGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ProviderGenerator.java new file mode 100644 index 00000000000..2d640e17708 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ProviderGenerator.java @@ -0,0 +1,176 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +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.DocumentShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.NumberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +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.traits.AbstractTrait; +import software.amazon.smithy.model.traits.StringListTrait; +import software.amazon.smithy.model.traits.StringTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; + + +/** + * Adds provider class to use as the {@link software.amazon.smithy.model.traits.TraitService} implementation for a + * trait. + * + *

This provider class is only required for Trait classes, and is not needed in nested shapes. The {@code fromNode} + * method is used where possible to create the smithy node from the provided node. Provider methods MUST + * be added to the {@code META-INF/services/software.amazon.smithy.model.traits.TraitService} service provider file + * for the Trait class to be discovered during model assembly. + */ +final class ProviderGenerator implements Runnable { + + private final TraitCodegenWriter writer; + private final Model model; + private final Shape shape; + private final SymbolProvider provider; + private final Symbol traitSymbol; + + + ProviderGenerator(TraitCodegenWriter writer, + Model model, + Shape shape, + SymbolProvider provider, + Symbol traitSymbol + ) { + this.writer = writer; + this.model = model; + this.shape = shape; + this.provider = provider; + this.traitSymbol = traitSymbol; + } + + @Override + public void run() { + shape.accept(new ProviderMethodVisitor()); + } + + private final class ProviderMethodVisitor extends TraitVisitor { + + @Override + public Void documentShape(DocumentShape shape) { + writer.openBlock("public static final class Provider extends $T.Provider {", "}", + AbstractTrait.class, () -> { + generateProviderConstructor(); + writer.newLine(); + writer.override(); + writer.openBlock("public $T createTrait($T target, $T value) {", "}", + Trait.class, ShapeId.class, Node.class, + () -> writer.write("return new $T(value);", traitSymbol)); + }); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + // If the symbol resolves to a simple java string use the simplified string + // provider. Otherwise, use a generic value shape provider. + if (TraitCodegenUtils.isJavaString(traitSymbol)) { + generateStringShapeProvider(); + } else { + generateValueShapeProvider(); + } + return null; + } + + @Override + public Void mapShape(MapShape shape) { + generateAbstractTraitProvider(); + return null; + } + + @Override + public Void listShape(ListShape shape) { + // If the trait is a string-only list we can use a simpler provider from the StringListTrait base class + if (TraitCodegenUtils.isJavaStringList(shape, provider)) { + writer.openBlock("public static final class Provider extends $T.Provider<$T> {", "}", + StringListTrait.class, traitSymbol, + () -> writer.openBlock("public Provider() {", "}", + () -> writer.write("super(ID, $T::new);", traitSymbol))); + } else { + generateAbstractTraitProvider(); + } + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + generateStringShapeProvider(); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + generateAbstractTraitProvider(); + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + generateAbstractTraitProvider(); + return null; + } + + @Override + protected Void numberShape(NumberShape shape) { + generateValueShapeProvider(); + return null; + } + + private void generateAbstractTraitProvider() { + writer.openBlock("public static final class Provider extends $T.Provider {", "}", + AbstractTrait.class, () -> { + generateProviderConstructor(); + writer.override(); + writer.openBlock("public $T createTrait($T target, $T value) {", "}", + Trait.class, ShapeId.class, Node.class, () -> { + writer.write("$1T result = $1T.fromNode(value);", traitSymbol); + writer.writeWithNoFormatting("result.setNodeCache(value);"); + writer.writeWithNoFormatting("return result;"); + }); + }); + } + + private void generateProviderConstructor() { + writer.openBlock("public Provider() {", "}", () -> writer.write("super(ID);")).newLine(); + } + + private void generateValueShapeProvider() { + writer.openBlock("public static final class Provider extends $T.Provider {", "}", + AbstractTrait.class, () -> { + generateProviderConstructor(); + writer.override(); + writer.openBlock("public $T createTrait($T target, $T value) {", "}", + Trait.class, ShapeId.class, Node.class, + () -> writer.write("return new $1T($2C, value.getSourceLocation());", + traitSymbol, + (Runnable) () -> shape.accept(new FromNodeMapperVisitor(writer, model, "value"))) + ); + }); + } + + private void generateStringShapeProvider() { + writer.openBlock("public static final class Provider extends $T.Provider<$T> {", "}", + StringTrait.class, traitSymbol, () -> writer.openBlock("public Provider() {", "}", + () -> writer.write("super(ID, $T::new);", traitSymbol))); + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ShapeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ShapeGenerator.java new file mode 100644 index 00000000000..e879e878fcf --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ShapeGenerator.java @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +import java.util.function.Consumer; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.TraitDefinition; +import software.amazon.smithy.traitcodegen.GenerateTraitDirective; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Base class used for the generation of traits and nested shapes from a {@link GenerateTraitDirective}. + * + *

This class will determine if a shape is a trait (i.e. has the {@link TraitDefinition} trait) or if the + * shape provided should be treated as a nested shape (i.e. defines a simple pojo). + */ +@SmithyInternalApi +public final class ShapeGenerator implements Consumer { + @Override + public void accept(GenerateTraitDirective directive) { + if (directive.shape().hasTrait(TraitDefinition.class)) { + new TraitGenerator().accept(directive); + } else { + directive.shape().accept(new NestedShapeGenerator(directive)); + } + } + + private static final class NestedShapeGenerator extends ShapeVisitor.Default { + + private final GenerateTraitDirective directive; + + private NestedShapeGenerator(GenerateTraitDirective directive) { + this.directive = directive; + } + + @Override + protected Void getDefault(Shape shape) { + // Most nested shapes do not generate new classes. + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + new StructureShapeGenerator().accept(directive); + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + new EnumShapeGenerator.StringEnumShapeGenerator().accept(directive); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + new EnumShapeGenerator.IntEnumShapeGenerator().accept(directive); + return null; + } + + @Override + public Void unionShape(UnionShape shape) { + throw new UnsupportedOperationException("Generation of nested types for Union shapes " + + " is not supported at this time."); + } + + @Override + public Void memberShape(MemberShape shape) { + throw new IllegalArgumentException("NestedShapeGenerator should not visit member shapes. " + + " Attempted to visit " + shape); + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/StructureShapeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/StructureShapeGenerator.java new file mode 100644 index 00000000000..e1ea7e1a1dd --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/StructureShapeGenerator.java @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.traitcodegen.GenerateTraitDirective; +import software.amazon.smithy.traitcodegen.sections.ClassSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Generates a Java class from a Smithy {@code StructureShape}. + */ +final class StructureShapeGenerator implements Consumer { + @Override + public void accept(GenerateTraitDirective directive) { + directive.context().writerDelegator().useShapeWriter(directive.shape(), writer -> { + writer.pushState(new ClassSection(directive.shape())) + .openBlock("public final class $1T implements $2T, $3T<$1T> {", "}", + directive.symbol(), ToNode.class, ToSmithyBuilder.class, () -> { + new PropertiesGenerator(writer, directive.shape(), directive.symbolProvider()).run(); + new ConstructorGenerator(writer, directive.symbol(), directive.shape(), + directive.symbolProvider()).run(); + new ToNodeGenerator(writer, directive.shape(), directive.symbolProvider(), + directive.model()).run(); + new FromNodeGenerator(writer, directive.symbol(), directive.shape(), + directive.symbolProvider(), directive.model()).run(); + new GetterGenerator(writer, directive.symbolProvider(), directive.model(), + directive.shape()).run(); + new BuilderGenerator(writer, directive.symbol(), directive.symbolProvider(), + directive.shape(), directive.model()).run(); + writeEquals(writer, directive.symbol()); + writeHashCode(writer); + }) + .popState(); + writer.newLine(); + }); + } + + private void writeEquals(TraitCodegenWriter writer, Symbol symbol) { + writer.override(); + writer.openBlock("public boolean equals(Object other) {", "}", () -> { + writer.disableNewlines(); + writer.openBlock("if (other == this) {\n", "}", + () -> writer.writeWithNoFormatting("return true;").newLine()); + writer.openBlock(" else if (!(other instanceof $T)) {\n", "}", symbol, + () -> writer.writeWithNoFormatting("return false;").newLine()); + writer.openBlock(" else {\n", "}", () -> { + writer.write("$1T b = ($1T) other;", symbol).newLine(); + writer.writeWithNoFormatting("return toNode().equals(b.toNode());\n"); + }).newLine(); + writer.enableNewlines(); + }); + writer.newLine(); + } + + private void writeHashCode(TraitCodegenWriter writer) { + writer.override(); + writer.openBlock("public int hashCode() {", "}", + () -> writer.writeWithNoFormatting("return toNode().hashCode();")); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ToNodeGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ToNodeGenerator.java new file mode 100644 index 00000000000..03c00999eb1 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/ToNodeGenerator.java @@ -0,0 +1,317 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.AbstractMap; +import java.util.Map; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NumberNode; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.NumberShape; +import software.amazon.smithy.model.shapes.Shape; +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.traits.IdRefTrait; +import software.amazon.smithy.model.traits.TimestampFormatTrait; +import software.amazon.smithy.model.traits.TraitDefinition; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.StringUtils; + +/** + * Generates methods to serialize a Java class to a smithy {@code Node}. + * + *

If the shape this generator is targeting is a trait then the serialization method is + * called {@code createNode()}, otherwise the method generated is called {@code toNode()}. + * This is because Trait classes inherit from {@link software.amazon.smithy.model.traits.AbstractTrait} + * which requires that they override {@code createNode()} for serialization. + */ +final class ToNodeGenerator implements Runnable { + + private final TraitCodegenWriter writer; + private final Shape shape; + private final SymbolProvider symbolProvider; + private final Model model; + + ToNodeGenerator(TraitCodegenWriter writer, Shape shape, SymbolProvider symbolProvider, Model model) { + this.writer = writer; + this.shape = shape; + this.symbolProvider = symbolProvider; + this.model = model; + } + + @Override + public void run() { + writer.override(); + writer.openBlock(shape.hasTrait(TraitDefinition.class) ? "protected $T createNode() {" : "public $T toNode() {", + "}", Node.class, () -> shape.accept(new CreateNodeBodyGenerator())); + writer.newLine(); + } + + private final class CreateNodeBodyGenerator extends TraitVisitor { + + @Override + public Void listShape(ListShape shape) { + writer.write("return values.stream()") + .indent() + .write(".map(s -> $C)", + (Runnable) () -> shape.getMember().accept(new ToNodeMapperVisitor("s"))) + .write(".collect($T.collect(getSourceLocation()));", ArrayNode.class) + .dedent(); + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + writer.writeWithNoFormatting("throw new UnsupportedOperationException(\"NodeCache is always set\");"); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + // If it is a Map use a simpler syntax + if (TraitCodegenUtils.isJavaString(symbolProvider.toSymbol(shape.getKey())) + && TraitCodegenUtils.isJavaString(symbolProvider.toSymbol(shape.getValue())) + ) { + writer.write("return $T.fromStringMap(values).toBuilder()", ObjectNode.class) + .writeWithNoFormatting(".sourceLocation(getSourceLocation()).build();"); + return null; + } + writer.writeWithNoFormatting("return values.entrySet().stream()") + .indent() + .write(".map(entry -> new $T<>(", AbstractMap.SimpleImmutableEntry.class) + .indent() + .write("$C, $C))", + (Runnable) () -> shape.getKey().accept( + new ToNodeMapperVisitor("entry.getKey()")), + (Runnable) () -> shape.getValue().accept( + new ToNodeMapperVisitor("entry.getValue()"))) + .dedent() + .write(".collect($1T.collect($2T::getKey, $2T::getValue))", + ObjectNode.class, Map.Entry.class) + .writeWithNoFormatting(".toBuilder().sourceLocation(getSourceLocation()).build();") + .dedent(); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + if (TraitCodegenUtils.isJavaString(symbolProvider.toSymbol(shape))) { + return null; + } + toStringCreator(); + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + if (shape.hasTrait(TraitDefinition.class)) { + writer.write("return $T.from(value);", Node.class); + } else { + toStringCreator(); + } + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + writer.write("return $T.objectNodeBuilder()", Node.class).indent(); + if (shape.hasTrait(TraitDefinition.class)) { + // If the shape is a trait we need to add the source location of trait to the + // generated node. + 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 { + writer.write(".withOptionalMember($S, get$L().map(m -> $C))", + mem.getMemberName(), StringUtils.capitalize(symbolProvider.toMemberName(mem)), + (Runnable) () -> mem.accept(new ToNodeMapperVisitor("m"))); + } + } + writer.writeWithNoFormatting(".build();"); + writer.dedent(); + return null; + } + + @Override + protected Void numberShape(NumberShape shape) { + writer.write("return new $T(value, getSourceLocation());", NumberNode.class); + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + if (shape.hasTrait(TimestampFormatTrait.class)) { + switch (shape.expectTrait(TimestampFormatTrait.class).getFormat()) { + case EPOCH_SECONDS: + writer.write("return new $T(value.getEpochSecond(), getSourceLocation());", + NumberNode.class); + break; + case HTTP_DATE: + writer.write("return new $T($T.RFC_1123_DATE_TIME.format(", + StringNode.class, DateTimeFormatter.class); + writer.indent(); + writer.write("$T.ofInstant(value, $T.UTC)), getSourceLocation());", + ZonedDateTime.class, ZoneOffset.class); + writer.dedent(); + break; + default: + toStringCreator(); + break; + } + } else { + toStringCreator(); + } + + return null; + } + + private void toStringCreator() { + writer.write("return new $T(value.toString(), getSourceLocation());", StringNode.class); + } + } + + /** + * Determines how to map a shape to a node. + */ + private final class ToNodeMapperVisitor extends TraitVisitor { + private final String varName; + + ToNodeMapperVisitor(String varName) { + this.varName = varName; + } + + @Override + public Void stringShape(StringShape shape) { + if (shape.hasTrait(IdRefTrait.class)) { + toStringMapper(); + } else { + fromNodeMapper(); + } + return null; + } + + @Override + public Void booleanShape(BooleanShape shape) { + fromNodeMapper(); + return null; + } + + @Override + public Void listShape(ListShape shape) { + writer.write("$L.stream().map(s -> $C).collect($T.collect())", + varName, + (Runnable) () -> shape.getMember().accept(new ToNodeMapperVisitor("s")), + ArrayNode.class + ); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + writer.openBlock("$L.entrySet().stream()", "", + varName, + () -> writer.write(".map(entry -> new $T<>(", AbstractMap.SimpleImmutableEntry.class) + .indent() + .write("$C, $C))", + (Runnable) () -> shape.getKey().accept( + new ToNodeMapperVisitor("entry.getKey()")), + (Runnable) () -> shape.getValue().accept( + new ToNodeMapperVisitor("entry.getValue()"))) + .dedent() + .write(".collect($1T.collect($2T::getKey, $2T::getValue))", + ObjectNode.class, Map.Entry.class)); + return null; + } + + @Override + public Void memberShape(MemberShape shape) { + if (shape.hasTrait(IdRefTrait.class)) { + toStringMapper(); + } else { + model.expectShape(shape.getTarget()).accept(this); + } + return null; + } + + @Override + protected Void numberShape(NumberShape shape) { + fromNodeMapper(); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + writer.write("$L.toNode()", varName); + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + fromNodeMapper(); + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + writer.write("$T.from($L.getValue())", Node.class, varName); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + writer.write("$L.toNode()", varName); + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + if (shape.hasTrait(TimestampFormatTrait.class)) { + switch (shape.expectTrait(TimestampFormatTrait.class).getFormat()) { + case EPOCH_SECONDS: + writer.write("$T.from($L.getEpochSecond())", Node.class, varName); + return null; + case HTTP_DATE: + writer.write("$T.from($T.RFC_1123_DATE_TIME.format($L))", + Node.class, DateTimeFormatter.class, varName); + return null; + default: + // Fall through on default + break; + } + } + toStringMapper(); + return null; + } + + private void fromNodeMapper() { + writer.write("$T.from($L)", Node.class, varName); + } + + private void toStringMapper() { + writer.write("$T.from($L.toString())", Node.class, varName); + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/TraitGenerator.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/TraitGenerator.java new file mode 100644 index 00000000000..fc5779a7f23 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/TraitGenerator.java @@ -0,0 +1,185 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +import java.util.function.Consumer; +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.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.NumberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeType; +import software.amazon.smithy.model.shapes.ShapeVisitor; +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.traits.AbstractTrait; +import software.amazon.smithy.model.traits.StringListTrait; +import software.amazon.smithy.model.traits.StringTrait; +import software.amazon.smithy.traitcodegen.GenerateTraitDirective; +import software.amazon.smithy.traitcodegen.TraitCodegenContext; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.sections.ClassSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Consumer that generates a trait class definition from a {@link GenerateTraitDirective}. + * + *

This base class will automatically generate a provider method and add that provider to the + * {@code META-INF/services/software.amazon.smithy.model.traits.TraitService} service provider + * file so the generated trait implementation will be discoverable by a {@code ServiceLoader}. + */ +class TraitGenerator implements Consumer { + private static final String PROVIDER_FILE = "META-INF/services/software.amazon.smithy.model.traits.TraitService"; + + @Override + public void accept(GenerateTraitDirective directive) { + directive.context().writerDelegator().useShapeWriter(directive.shape(), writer -> { + writer.pushState(new ClassSection(directive.shape())); + // Add class definition context + writer.putContext("baseClass", directive.shape().accept(new BaseClassVisitor(directive.symbolProvider()))); + // Only collection types implement ToSmithyBuilder + boolean isAggregateType = directive.shape().getType().getCategory().equals(ShapeType.Category.AGGREGATE); + writer.putContext("isAggregateType", isAggregateType); + writer.openBlock("public final class $2T extends $baseClass:T" + + "${?isAggregateType} implements $1T<$2T>${/isAggregateType} {", "}", + ToSmithyBuilder.class, directive.symbol(), () -> { + // All traits include a static ID property + writer.write("public static final $1T ID = $1T.from($2S);", + ShapeId.class, directive.shape().getId()); + writer.newLine(); + new PropertiesGenerator(writer, directive.shape(), directive.symbolProvider()).run(); + new ConstructorGenerator(writer, directive.symbol(), directive.shape(), + directive.symbolProvider()).run(); + // Abstract Traits need to define serde methods + if (AbstractTrait.class.equals( + directive.shape().accept(new BaseClassVisitor(directive.symbolProvider()))) + ) { + new ToNodeGenerator(writer, directive.shape(), directive.symbolProvider(), directive.model()).run(); + } + new FromNodeGenerator(writer, directive.symbol(), directive.shape(), + directive.symbolProvider(), directive.model()).run(); + new GetterGenerator(writer, directive.symbolProvider(), directive.model(), directive.shape()).run(); + directive.shape().accept(new NestedClassVisitor(writer, directive.symbolProvider(), directive.model())); + new BuilderGenerator(writer, directive.symbol(), directive.symbolProvider(), directive.shape(), + directive.model()).run(); + new ProviderGenerator(writer, directive.model(), directive.shape(), + directive.symbolProvider(), directive.symbol()).run(); + }); + writer.popState(); + }); + // Add the trait provider to the META-INF/services/TraitService file + addSpiTraitProvider(directive.context(), directive.symbol()); + } + + /** + * Write provider method to Java SPI to service file for {@link software.amazon.smithy.model.traits.TraitService}. + * + * @param context Codegen context + * @param symbol Symbol for trait class + */ + private static void addSpiTraitProvider(TraitCodegenContext context, Symbol symbol) { + context.writerDelegator().useFileWriter(PROVIDER_FILE, + writer -> writer.writeInline("$L$$Provider", symbol.getFullName())); + } + + /** + * Returns the base class to use for a trait. + */ + private static final class BaseClassVisitor extends TraitVisitor> { + private final SymbolProvider symbolProvider; + + private BaseClassVisitor(SymbolProvider symbolProvider) { + this.symbolProvider = symbolProvider; + } + + @Override + public Class listShape(ListShape shape) { + // Do not create a property if the shape can inherit from the StringListTrait base class. + if (TraitCodegenUtils.isJavaStringList(shape, symbolProvider)) { + return StringListTrait.class; + } + return AbstractTrait.class; + } + + @Override + public Class mapShape(MapShape shape) { + return AbstractTrait.class; + } + + @Override + public Class documentShape(DocumentShape shape) { + return AbstractTrait.class; + } + + @Override + public Class stringShape(StringShape shape) { + if (TraitCodegenUtils.isJavaString(symbolProvider.toSymbol(shape))) { + return StringTrait.class; + } + return AbstractTrait.class; + } + + @Override + public Class enumShape(EnumShape shape) { + return StringTrait.class; + } + + @Override + public Class structureShape(StructureShape shape) { + return AbstractTrait.class; + } + + @Override + public Class timestampShape(TimestampShape shape) { + return AbstractTrait.class; + } + + @Override + protected Class numberShape(NumberShape shape) { + return AbstractTrait.class; + } + } + + private static final class NestedClassVisitor extends ShapeVisitor.Default { + private final TraitCodegenWriter writer; + private final SymbolProvider symbolProvider; + private final Model model; + + private NestedClassVisitor(TraitCodegenWriter writer, SymbolProvider symbolProvider, Model model) { + this.writer = writer; + this.symbolProvider = symbolProvider; + this.model = model; + } + + @Override + protected Void getDefault(Shape shape) { + // Most classes have no nested classes + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + new EnumShapeGenerator.IntEnumShapeGenerator().writeEnum(shape, symbolProvider, writer, model, false); + writer.newLine(); + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + new EnumShapeGenerator.StringEnumShapeGenerator().writeEnum(shape, symbolProvider, writer, model, false); + writer.newLine(); + return null; + } + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/TraitVisitor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/TraitVisitor.java new file mode 100644 index 00000000000..d49d5a60437 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/generators/TraitVisitor.java @@ -0,0 +1,104 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.generators; + +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.DoubleShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.LongShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.NumberShape; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.ShortShape; +import software.amazon.smithy.model.shapes.UnionShape; + +/** + * This class provides a simplified visitor interface for visiting trait shapes. + * + *

When handling trait shapes, all number shapes are treat the same, so we can handle + * all numberShape branches with a single common method to reduce duplication. + * The following shapes are not supported when visiting traits: + *

    + *
  • MemberShapes
  • + *
  • UnionShapes
  • + *
  • BlobShapes
  • + *
+ * + * @param Return type + */ +abstract class TraitVisitor extends ShapeVisitor.DataShapeVisitor { + + @Override + public R booleanShape(BooleanShape shape) { + throw new UnsupportedOperationException("Boolean traits not supported. Consider using an " + + " Annotation Trait."); + } + + @Override + public R byteShape(ByteShape shape) { + return numberShape(shape); + } + + @Override + public R shortShape(ShortShape shape) { + return numberShape(shape); + } + + @Override + public R integerShape(IntegerShape shape) { + return numberShape(shape); + } + + @Override + public R longShape(LongShape shape) { + return numberShape(shape); + } + + @Override + public R floatShape(FloatShape shape) { + return numberShape(shape); + } + + @Override + public R doubleShape(DoubleShape shape) { + return numberShape(shape); + } + + @Override + public R bigIntegerShape(BigIntegerShape shape) { + return numberShape(shape); + } + + @Override + public R bigDecimalShape(BigDecimalShape shape) { + return numberShape(shape); + } + + @Override + public R unionShape(UnionShape shape) { + throw new UnsupportedOperationException("Property generator does not support shape " + + shape + " of type " + shape.getType()); + } + + @Override + public R blobShape(BlobShape shape) { + throw new UnsupportedOperationException("Property generator does not support shape " + + shape + " of type " + shape.getType()); + } + + @Override + public R memberShape(MemberShape shape) { + throw new IllegalArgumentException("Property generator cannot visit member shapes. Attempted " + + "to visit " + shape); + } + + protected abstract R numberShape(NumberShape shape); +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/TraitCodegenIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/TraitCodegenIntegration.java new file mode 100644 index 00000000000..c68232711ed --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/TraitCodegenIntegration.java @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations; + +import software.amazon.smithy.codegen.core.SmithyIntegration; +import software.amazon.smithy.traitcodegen.TraitCodegenContext; +import software.amazon.smithy.traitcodegen.TraitCodegenSettings; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; + +/** + * Allows additional functionality to be added into the trait codegen generator. + * + *

{@code TraitCodegenIntegration}'s are loaded as a Java SPI. To make your integration + * discoverable, add a file to {@code META-INF/services} named + * {@code software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration} where each line is + * the fully-qualified class name of your integrations. Several tools, such as + * {@code AutoService}, can do this for you. + */ +public interface TraitCodegenIntegration extends SmithyIntegration { +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java new file mode 100644 index 00000000000..f537ffcbf59 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/core/CoreIntegration.java @@ -0,0 +1,91 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.core; + +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.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.TraitDefinition; +import software.amazon.smithy.traitcodegen.SymbolProperties; +import software.amazon.smithy.traitcodegen.TraitCodegenSettings; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Core integration for Trait code generation. + * + *

This integration applies no built-in's, but decorates the Symbol provider to replace a + * shape symbol with a trait symbol definition if the trait has the + * {@link software.amazon.smithy.model.traits.TraitDefinition} trait applied. + * Trait symbols are named {@code Trait} and always have a definition file. + * This integration runs after all other integrations have finished to ensure that + * any other type decorators and integrations have already been applied before creating any Trait + * definitions from the resulting type. + */ +@SmithyInternalApi +public final class CoreIntegration implements TraitCodegenIntegration { + + @Override + public String name() { + return "core"; + } + + @Override + public byte priority() { + // This integration should be run last, so it picks up the correct + // base symbol with all other decorators applied. + return -1; + } + + @Override + public SymbolProvider decorateSymbolProvider(Model model, + TraitCodegenSettings settings, + SymbolProvider symbolProvider + ) { + return new SymbolProvider() { + @Override + public Symbol toSymbol(Shape shape) { + if (shape.hasTrait(TraitDefinition.class)) { + return getTraitSymbol(settings, shape, symbolProvider.toSymbol(shape)); + } + return symbolProvider.toSymbol(shape); + } + + // Necessary to ensure initial toMemberName is not squashed by decorating + @Override + public String toMemberName(MemberShape shape) { + return symbolProvider.toMemberName(shape); + } + }; + } + + private Symbol getTraitSymbol(TraitCodegenSettings settings, Shape shape, Symbol baseSymbol) { + String relativeNamespace = TraitCodegenUtils.mapNamespace(settings.smithyNamespace(), + shape.getId().getNamespace(), settings.packageName()); + String name = TraitCodegenUtils.getDefaultTraitName(shape); + + // If the base symbol has an unboxed version, use that as the base symbol + // instead of the Boxed version. + if (baseSymbol.getProperty(SymbolProperties.UNBOXED_SYMBOL).isPresent()) { + baseSymbol = baseSymbol.expectProperty(SymbolProperties.UNBOXED_SYMBOL, Symbol.class); + } + + // Maintain all existing properties, but change the namespace and name of the shape + // and add the base symbol as a property. The references need to be set to empty list + // to prevent writing as parameterized classes. + return baseSymbol.toBuilder() + .name(name) + .references(ListUtils.of()) + .namespace(relativeNamespace, ".") + .putProperty(SymbolProperties.BASE_SYMBOL, baseSymbol) + .definitionFile("./" + relativeNamespace.replace(".", "/") + "/" + name + ".java") + .build(); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/idref/IdRefDecoratorIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/idref/IdRefDecoratorIntegration.java new file mode 100644 index 00000000000..e1992f67400 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/idref/IdRefDecoratorIntegration.java @@ -0,0 +1,88 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.idref; + +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.model.Model; +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.ShapeId; +import software.amazon.smithy.model.traits.IdRefTrait; +import software.amazon.smithy.traitcodegen.TraitCodegenSettings; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Handles the conversion of String members and String types with the {@link IdRefTrait} trait map to + * the {@link ShapeId} type. + * + *

This integration is run with a high priority to ensure downstream integrations see a + * {@code ShapeId} type instead of a {@code string}. + */ +@SmithyInternalApi +public class IdRefDecoratorIntegration implements TraitCodegenIntegration { + private static final Symbol SHAPE_ID_SYMBOL = TraitCodegenUtils.fromClass(ShapeId.class); + + @Override + public String name() { + return "id-ref-integration"; + } + + @Override + public byte priority() { + // Make sure this runs before all other integration + return 127; + } + + @Override + public SymbolProvider decorateSymbolProvider(Model model, TraitCodegenSettings settings, + SymbolProvider symbolProvider) { + return new SymbolProvider() { + @Override + public Symbol toSymbol(Shape shape) { + return provideSymbol(shape, symbolProvider, model); + } + + // Necessary to ensure initial toMemberName is not squashed by decorating + @Override + public String toMemberName(MemberShape shape) { + return symbolProvider.toMemberName(shape); + } + }; + } + + private Symbol provideSymbol(Shape shape, SymbolProvider symbolProvider, Model model) { + if (shape.hasTrait(IdRefTrait.class)) { + return SHAPE_ID_SYMBOL; + } else if (shape.isMemberShape()) { + Shape target = model.expectShape(shape.asMemberShape().orElseThrow(RuntimeException::new).getTarget()); + return provideSymbol(target, symbolProvider, model); + } else if (shape.isListShape()) { + // Replace any members reference by a list shape as the decorator does wrap the internal call from the + // toSymbol(member) + MemberShape member = shape.asListShape().orElseThrow(RuntimeException::new).getMember(); + return symbolProvider.toSymbol(shape).toBuilder() + .references(ListUtils.of(new SymbolReference(provideSymbol(member, symbolProvider, model)))) + .build(); + } else if (shape.isMapShape()) { + // Same as list replacement but for map shapes + MapShape mapShape = shape.asMapShape().orElseThrow(RuntimeException::new); + return symbolProvider.toSymbol(shape) + .toBuilder() + .references(ListUtils.of( + new SymbolReference(provideSymbol(mapShape.getKey(), symbolProvider, model)), + new SymbolReference(provideSymbol(mapShape.getValue(), symbolProvider, model)) + )) + .build(); + } + return symbolProvider.toSymbol(shape); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/BuilderClassSectionDocsInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/BuilderClassSectionDocsInterceptor.java new file mode 100644 index 00000000000..a18bc95cac2 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/BuilderClassSectionDocsInterceptor.java @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.javadoc; + + +import software.amazon.smithy.traitcodegen.sections.BuilderClassSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; + +/** + * Adds JavaDocs to static builder classes and is triggered by the {@link BuilderClassSection}. + */ +final class BuilderClassSectionDocsInterceptor implements CodeInterceptor.Prepender { + @Override + public void prepend(TraitCodegenWriter writer, BuilderClassSection section) { + writer.openDocstring(); + writer.writeDocStringContents("Builder for {@link $T}.", section.symbol()); + writer.closeDocstring(); + } + + @Override + public Class sectionType() { + return BuilderClassSection.class; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ClassJavaDocInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ClassJavaDocInterceptor.java new file mode 100644 index 00000000000..6586ea309f4 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ClassJavaDocInterceptor.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.javadoc; + +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.traitcodegen.sections.ClassSection; +import software.amazon.smithy.traitcodegen.sections.JavaDocSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyGenerated; + +/** + * Adds basic JavaDocs for generated java classes. + */ +final class ClassJavaDocInterceptor implements CodeInterceptor.Prepender { + @Override + public void prepend(TraitCodegenWriter writer, ClassSection section) { + writer.openDocstring(); + writer.pushState(new JavaDocSection(section.shape())); + writer.writeDocStringContents(section.shape().expectTrait(DocumentationTrait.class).getValue()); + writer.popState(); + writer.closeDocstring(); + // Adds smithy generated annotation to indicate class was generated + // by a code generator + writer.write("@$T", SmithyGenerated.class); + } + + @Override + public Class sectionType() { + return ClassSection.class; + } + + @Override + public boolean isIntercepted(ClassSection section) { + return section.shape().hasTrait(DocumentationTrait.class); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedAnnotationClassInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedAnnotationClassInterceptor.java new file mode 100644 index 00000000000..4990a0f8ef4 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedAnnotationClassInterceptor.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.javadoc; + +import software.amazon.smithy.model.traits.DeprecatedTrait; +import software.amazon.smithy.traitcodegen.sections.ClassSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; + +/** + * Adds the {@code @Deprecated} annotation to generated classes if the smithy shape the class corresponds to + * has the {@link DeprecatedTrait} trait applied. + */ +final class DeprecatedAnnotationClassInterceptor implements CodeInterceptor.Prepender { + @Override + public void prepend(TraitCodegenWriter writer, ClassSection section) { + writer.writeWithNoFormatting("@Deprecated"); + } + + @Override + public Class sectionType() { + return ClassSection.class; + } + + @Override + public boolean isIntercepted(ClassSection section) { + return section.shape().hasTrait(DeprecatedTrait.class); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedNoteInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedNoteInterceptor.java new file mode 100644 index 00000000000..733b293c144 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/DeprecatedNoteInterceptor.java @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.javadoc; + +import software.amazon.smithy.model.traits.DeprecatedTrait; +import software.amazon.smithy.traitcodegen.sections.JavaDocSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; + +/** + * Adds the {@code @deprecated} javadoc tag to generated Javadoc documentation for a class + * if the smithy shape the class corresponds to has the {@link DeprecatedTrait} trait applied. + * + *

If the {@code DeprecatedTrait} contains a {@code since} field, then "As of " note will be added + * to the generated tag. + */ +final class DeprecatedNoteInterceptor implements CodeInterceptor.Appender { + @Override + public void append(TraitCodegenWriter writer, JavaDocSection section) { + DeprecatedTrait trait = section.shape().expectTrait(DeprecatedTrait.class); + writer.putContext("since", trait.getSince()); + // Add spacing + writer.writeDocStringContents(""); + writer.writeDocStringContents("@deprecated ${?since}As of ${since:L}. ${/since}$L", trait.getMessage()); + } + + @Override + public Class sectionType() { + return JavaDocSection.class; + } + + @Override + public boolean isIntercepted(JavaDocSection section) { + return section.shape().hasTrait(DeprecatedTrait.class); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/EnumVariantJavaDocInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/EnumVariantJavaDocInterceptor.java new file mode 100644 index 00000000000..b3e75dfb323 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/EnumVariantJavaDocInterceptor.java @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.javadoc; + +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.traitcodegen.sections.EnumVariantSection; +import software.amazon.smithy.traitcodegen.sections.JavaDocSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; + +/** + * Adds a docstring to each java enum variant if the corresponding enum member has a doc comment. + */ +final class EnumVariantJavaDocInterceptor implements CodeInterceptor.Prepender { + @Override + public void prepend(TraitCodegenWriter writer, EnumVariantSection section) { + DocumentationTrait trait = section.memberShape().expectTrait(DocumentationTrait.class); + writer.newLine(); + writer.openDocstring(); + writer.pushState(new JavaDocSection(section.memberShape())); + writer.writeDocStringContents(trait.getValue()); + writer.popState(); + writer.closeDocstring(); + } + + @Override + public Class sectionType() { + return EnumVariantSection.class; + } + + @Override + public boolean isIntercepted(EnumVariantSection section) { + return section.memberShape().hasTrait(DocumentationTrait.class); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocsInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocsInterceptor.java new file mode 100644 index 00000000000..6fb08ddacc9 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/ExternalDocsInterceptor.java @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.javadoc; + +import java.util.Map; +import software.amazon.smithy.model.traits.ExternalDocumentationTrait; +import software.amazon.smithy.traitcodegen.sections.JavaDocSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; + +/** + * Adds the javadoc {@code @see} tag to the generated javadocs if the corresponding smithy shape + * has the {@link ExternalDocumentationTrait} trait applied. + */ +final class ExternalDocsInterceptor implements CodeInterceptor.Appender { + + @Override + public void append(TraitCodegenWriter writer, JavaDocSection section) { + ExternalDocumentationTrait trait = section.shape().expectTrait(ExternalDocumentationTrait.class); + // Add a space to make it easier to read + writer.writeDocStringContents(""); + for (Map.Entry entry : trait.getUrls().entrySet()) { + writer.writeDocStringContents("@see $L", entry.getKey(), entry.getValue()); + } + } + + @Override + public Class sectionType() { + return JavaDocSection.class; + } + + @Override + public boolean isIntercepted(JavaDocSection section) { + return section.shape().hasTrait(ExternalDocumentationTrait.class); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/GetterJavaDocInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/GetterJavaDocInterceptor.java new file mode 100644 index 00000000000..c06cb2caacc --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/GetterJavaDocInterceptor.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.javadoc; + +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.traitcodegen.sections.GetterSection; +import software.amazon.smithy.traitcodegen.sections.JavaDocSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; + +/** + * Adds Javadocs to the getter for a member if the corresponding member has a {@link DocumentationTrait} + * trait applied. The Javadoc contents will just be the contents of the {@code DocumentationTrait}. + */ +final class GetterJavaDocInterceptor implements CodeInterceptor.Prepender { + @Override + public void prepend(TraitCodegenWriter writer, GetterSection section) { + DocumentationTrait trait = section.shape().expectTrait(DocumentationTrait.class); + writer.newLine(); + writer.openDocstring(); + writer.pushState(new JavaDocSection(section.shape())); + writer.writeDocStringContents(trait.getValue()); + writer.popState(); + writer.closeDocstring(); + } + + @Override + public Class sectionType() { + return GetterSection.class; + } + + @Override + public boolean isIntercepted(GetterSection section) { + return section.shape().hasTrait(DocumentationTrait.class) + && section.shape().isMemberShape(); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java new file mode 100644 index 00000000000..9d3724088f4 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/JavaDocIntegration.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.javadoc; + +import java.util.List; +import software.amazon.smithy.traitcodegen.TraitCodegenContext; +import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds all built-in Javadoc-generating interceptors. + * + *

This integration adds all the required documentation interceptors that ensure + * that methods, classes, and properties all have JavaDocs added. This integration also + * adds Annotations such as {@code @Deprecated} that serve as documentation. + */ +@SmithyInternalApi +public final class JavaDocIntegration implements TraitCodegenIntegration { + + @Override + public String name() { + return "javadoc"; + } + + @Override + public List> interceptors( + TraitCodegenContext codegenContext) { + return ListUtils.of( + new DeprecatedAnnotationClassInterceptor(), + new DeprecatedNoteInterceptor(), + new UnstableAnnotationClassInterceptor(), + new ClassJavaDocInterceptor(), + new ExternalDocsInterceptor(), + new BuilderClassSectionDocsInterceptor(), + new GetterJavaDocInterceptor(), + new EnumVariantJavaDocInterceptor() + ); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/UnstableAnnotationClassInterceptor.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/UnstableAnnotationClassInterceptor.java new file mode 100644 index 00000000000..bdaaef5450f --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/javadoc/UnstableAnnotationClassInterceptor.java @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.javadoc; + +import software.amazon.smithy.model.traits.UnstableTrait; +import software.amazon.smithy.traitcodegen.sections.ClassSection; +import software.amazon.smithy.traitcodegen.writer.TraitCodegenWriter; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Adds the {@code @SmithyUnstableApi} annotation to generated classes if the smithy shape the class corresponds to + * has the {@link UnstableTrait} trait applied. + */ +final class UnstableAnnotationClassInterceptor implements CodeInterceptor.Prepender { + + @Override + public void prepend(TraitCodegenWriter writer, ClassSection section) { + writer.write("@$T", SmithyUnstableApi.class); + } + + @Override + public Class sectionType() { + return ClassSection.class; + } + + @Override + public boolean isIntercepted(ClassSection section) { + return section.shape().hasTrait(UnstableTrait.class); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/uniqueitems/UniqueItemDecoratorIntegration.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/uniqueitems/UniqueItemDecoratorIntegration.java new file mode 100644 index 00000000000..bc012d33865 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/integrations/uniqueitems/UniqueItemDecoratorIntegration.java @@ -0,0 +1,76 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.integrations.uniqueitems; + +import java.util.List; +import java.util.Set; +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.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.UniqueItemsTrait; +import software.amazon.smithy.traitcodegen.SymbolProperties; +import software.amazon.smithy.traitcodegen.TraitCodegenSettings; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration; +import software.amazon.smithy.utils.ListUtils; + +/** + * Handles type conversions associated with the use of the {@code UniqueItems} trait. + * + *

Lists shapes and list members with the UniqueItems trait should be represented by + * a {@link Set} rather than a list. + */ +public class UniqueItemDecoratorIntegration implements TraitCodegenIntegration { + + @Override + public String name() { + return "unique-items-integration"; + } + + @Override + public List runAfter() { + return ListUtils.of("id-ref-integration"); + } + + @Override + public byte priority() { + // Run before other integrations to make sure symbol changes are picked up + return 127; + } + + @Override + public SymbolProvider decorateSymbolProvider(Model model, TraitCodegenSettings settings, + SymbolProvider symbolProvider) { + return new SymbolProvider() { + @Override + public Symbol toSymbol(Shape shape) { + return provideSymbol(shape, symbolProvider, model); + } + + // Necessary to ensure initial toMemberName is not squashed by decorating + @Override + public String toMemberName(MemberShape shape) { + return symbolProvider.toMemberName(shape); + } + }; + } + + private Symbol provideSymbol(Shape shape, SymbolProvider symbolProvider, Model model) { + if (shape.isListShape() && shape.hasTrait(UniqueItemsTrait.class)) { + return TraitCodegenUtils.fromClass(Set.class).toBuilder() + .addReference(symbolProvider.toSymbol(shape.asListShape() + .orElseThrow(RuntimeException::new).getMember())) + .putProperty(SymbolProperties.BUILDER_REF_INITIALIZER, "forOrderedSet()") + .build(); + } else if (shape.isMemberShape()) { + Shape target = model.expectShape(shape.asMemberShape().orElseThrow(RuntimeException::new).getTarget()); + return provideSymbol(target, symbolProvider, model); + } + return symbolProvider.toSymbol(shape); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/BuilderClassSection.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/BuilderClassSection.java new file mode 100644 index 00000000000..de3fc05b975 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/BuilderClassSection.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.sections; + +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.utils.CodeSection; + +/** + * Contains the static builder class for a shape. + */ +public final class BuilderClassSection implements CodeSection { + private final Symbol symbol; + + public BuilderClassSection(Symbol symbol) { + this.symbol = symbol; + } + + /** + * {@link Symbol} representing the enclosing class for this builder. + */ + public Symbol symbol() { + return symbol; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/ClassSection.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/ClassSection.java new file mode 100644 index 00000000000..0e2d8c66cab --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/ClassSection.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.sections; + +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.CodeSection; + +/** + * Contains a Java class defining a trait or nested shape. + */ +public final class ClassSection implements CodeSection { + private final Shape shape; + + public ClassSection(Shape shape) { + this.shape = shape; + } + + /** + * {@link Shape} that this Java class represents. + */ + public Shape shape() { + return shape; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/EnumVariantSection.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/EnumVariantSection.java new file mode 100644 index 00000000000..78176d38f56 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/EnumVariantSection.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.sections; + +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.utils.CodeSection; + +/** + * Contains an enum variant. + */ +public final class EnumVariantSection implements CodeSection { + private final MemberShape memberShape; + + public EnumVariantSection(MemberShape memberShape) { + this.memberShape = memberShape; + } + + /** + * {@link MemberShape} that this enum variant represents. + */ + public MemberShape memberShape() { + return memberShape; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/GetterSection.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/GetterSection.java new file mode 100644 index 00000000000..ceb9b1664e0 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/GetterSection.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.sections; + +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.CodeSection; + +/** + * Contains a getter method. + */ +public final class GetterSection implements CodeSection { + private final Shape shape; + + public GetterSection(Shape shape) { + this.shape = shape; + } + + /** + * {@link Shape} that this getter returns a Java object for. + */ + public Shape shape() { + return shape; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/JavaDocSection.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/JavaDocSection.java new file mode 100644 index 00000000000..f328927a924 --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/sections/JavaDocSection.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.sections; + +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.CodeSection; + +/** + * Contains a Java doc section attached to a class or method. + */ +public final class JavaDocSection implements CodeSection { + private final Shape shape; + + public JavaDocSection(Shape shape) { + this.shape = shape; + } + + /** + * {@link Shape} that the class the Javadoc is added to represents. + */ + public Shape shape() { + return shape; + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenImportContainer.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenImportContainer.java new file mode 100644 index 00000000000..203899ac91d --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenImportContainer.java @@ -0,0 +1,64 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.writer; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import software.amazon.smithy.codegen.core.ImportContainer; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.traitcodegen.SymbolProperties; + +/** + * Import container for Java imports. + */ +final class TraitCodegenImportContainer implements ImportContainer { + private final Map> imports = new HashMap<>(); + private final String namespace; + + TraitCodegenImportContainer(String namespace) { + this.namespace = namespace; + } + + @Override + public void importSymbol(Symbol symbol, String alias) { + // Do not import primitive types + if (symbol.getProperty(SymbolProperties.IS_PRIMITIVE).isPresent()) { + return; + } + Set duplicates = imports.computeIfAbsent(symbol.getName(), sn -> new HashSet<>()); + duplicates.add(symbol); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (String importName : getSortedAndFilteredImports()) { + builder.append("import ").append(importName).append(";"); + builder.append(System.lineSeparator()); + } + return builder.toString(); + } + + /** + * Sort imports then filter out any instances of duplicates. Then filter out and instances of base java classes + * that do not need to be imported. Finally, filter out cases where the symbol has the same namespace as the file. + * + * @return sorted list of imports + */ + private Set getSortedAndFilteredImports() { + return imports.values().stream() + .filter(s -> s.size() == 1) + .map(s -> s.iterator().next()) + .filter(s -> !s.getNamespace().startsWith("java.lang")) + .filter(s -> !s.getNamespace().equals(namespace)) + .map(Symbol::getFullName) + .collect(Collectors.toCollection(TreeSet::new)); + } +} diff --git a/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java new file mode 100644 index 00000000000..9198167f88a --- /dev/null +++ b/smithy-trait-codegen/src/main/java/software/amazon/smithy/traitcodegen/writer/TraitCodegenWriter.java @@ -0,0 +1,242 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen.writer; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.codegen.core.SymbolWriter; +import software.amazon.smithy.traitcodegen.SymbolProperties; +import software.amazon.smithy.traitcodegen.TraitCodegenSettings; +import software.amazon.smithy.traitcodegen.TraitCodegenUtils; +import software.amazon.smithy.utils.StringUtils; + +/** + * Writes Java code for trait definitions. + * + *

This writer supports two custom formatters, a Java type formatter '$T' and + * a Base type formatter '$B'. + *

    + *
  • {@link JavaTypeFormatter}|{@code 'T'}: This formatter handles the formatting of + * Java types and also ensures that parameterized types (such as {@code List} are + * written correctly. + * + *
  • {@link BaseTypeFormatter}|{@code 'B'}: This formatter allows you to use the base type + * for a trait. For example a String Trait may have a base type of {@code ShapeId}. To write + * this base type, use the {@code $B} formatter and provide the trait symbol. Note that + * if no base type is found (i.e. type is not a trait) then this formatter behaves exactly the + * same as the {@link JavaTypeFormatter}. + *
+ */ +public class TraitCodegenWriter extends SymbolWriter { + private static final int MAX_LINE_LENGTH = 120; + private static final Pattern PATTERN = Pattern.compile("<([a-z]+)*>.*?", Pattern.DOTALL); + private final String namespace; + private final String fileName; + private final TraitCodegenSettings settings; + private final Map> symbolNames = new HashMap<>(); + + public TraitCodegenWriter(String fileName, + String namespace, + TraitCodegenSettings settings + ) { + super(new TraitCodegenImportContainer(namespace)); + this.namespace = namespace; + this.fileName = fileName; + this.settings = settings; + + // Ensure extraneous white space is trimmed + trimBlankLines(); + trimTrailingSpaces(); + + putFormatter('T', new JavaTypeFormatter()); + putFormatter('B', new BaseTypeFormatter()); + } + + + private void addImport(Symbol symbol) { + addImport(symbol, symbol.getName()); + } + + public void openDocstring() { + pushState().writeWithNoFormatting("/**"); + } + + public void writeDocStringContents(String contents) { + // Split out any HTML-tag wrapped sections as we do not want to wrap + // any customer documentation with tags + Matcher matcher = PATTERN.matcher(contents); + int lastMatchPos = 0; + writeInlineWithNoFormatting(" * "); + while (matcher.find()) { + // write all contents up to the match. + writeInlineWithNoFormatting(StringUtils.wrap(contents.substring(lastMatchPos, matcher.start()) + .replace("\n", "\n * "), MAX_LINE_LENGTH - 8, + getNewline() + " * ", false)); + // write match contents + writeInlineWithNoFormatting(contents.substring(matcher.start(), matcher.end()).replace("\n", "\n * ")); + lastMatchPos = matcher.end(); + } + // Write out all remaining contents + writeWithNoFormatting(StringUtils.wrap(contents.substring(lastMatchPos).replace("\n", "\n * "), + MAX_LINE_LENGTH - 8, + getNewline() + " * ", + false)); + } + + public void writeDocStringContents(String contents, Object... args) { + writeInlineWithNoFormatting(" * "); + write(StringUtils.wrap(contents.replace("\n", "\n * "), MAX_LINE_LENGTH - 8, + getNewline() + " * ", false), args); + } + + public void closeDocstring() { + writeWithNoFormatting(" */").popState(); + } + + @Override + public String toString() { + // Do not add code headers to META-INF files + if (fileName.startsWith("META-INF")) { + return super.toString(); + } + + StringBuilder builder = new StringBuilder(); + builder.append(getHeader()).append(getNewline()); + builder.append(getPackageHeader()).append(getNewline()); + builder.append(getImportContainer().toString()).append(getNewline()); + + // Handle duplicates that may need to use full name + putContext(resolveNameContext()); + builder.append(format(super.toString())); + + return builder.toString(); + } + + private Map resolveNameContext() { + Map contextMap = new HashMap<>(); + for (Map.Entry> entry : symbolNames.entrySet()) { + Set duplicates = entry.getValue(); + // If the duplicates list has more than one entry + // then duplicates are present, and we need to de-dupe + if (duplicates.size() > 1) { + duplicates.forEach(dupe -> { + // If we are in the namespace of a Symbol, use its + // short name, otherwise use the full name + if (dupe.getNamespace().equals(namespace)) { + contextMap.put(dupe.getFullName(), dupe.getName()); + } else { + contextMap.put(dupe.getFullName(), dupe.getFullName()); + } + }); + } else { + Symbol symbol = duplicates.iterator().next(); + contextMap.put(symbol.getFullName(), symbol.getName()); + } + } + + return contextMap; + } + + public String getPackageHeader() { + return String.format("package %s;%n", namespace); + } + + public String getHeader() { + StringBuilder builder = new StringBuilder().append("/**").append(getNewline()); + for (String line : settings.headerLines()) { + builder.append(" * ").append(line).append(getNewline()); + } + builder.append(" */").append(getNewline()); + return builder.toString(); + } + + public void newLine() { + writeInlineWithNoFormatting(getNewline()); + } + + public void override() { + writeWithNoFormatting("@Override"); + } + + /** + * Implements a formatter for {@code $T} that formats Java types. + */ + private final class JavaTypeFormatter implements BiFunction { + @Override + public String apply(Object type, String indent) { + Symbol typeSymbol; + if (type instanceof Symbol) { + typeSymbol = (Symbol) type; + } else if (type instanceof Class) { + typeSymbol = TraitCodegenUtils.fromClass((Class) type); + } else { + throw new IllegalArgumentException("Invalid type provided for $T. Expected a Symbol or Class " + + "but found: `" + type + "`."); + } + + if (typeSymbol.getReferences().isEmpty()) { + return getPlaceholder(typeSymbol); + } + + // Add type references as type references (ex. `List`) + StringBuilder builder = new StringBuilder(); + builder.append(getPlaceholder(typeSymbol)); + builder.append("<"); + Iterator iterator = typeSymbol.getReferences().iterator(); + while (iterator.hasNext()) { + String placeholder = getPlaceholder(iterator.next().getSymbol()); + builder.append(placeholder); + if (iterator.hasNext()) { + builder.append(", "); + } + } + builder.append(">"); + return builder.toString(); + } + + private String getPlaceholder(Symbol symbol) { + // Add symbol to import container + addImport(symbol); + + // Add symbol to symbol map, so we can handle potential type name conflicts + Set nameSet = symbolNames.computeIfAbsent(symbol.getName(), n -> new HashSet<>()); + nameSet.add(symbol); + + // Return a placeholder value that will be filled when toString is called + return format("$${$L:L}", symbol.getFullName()); + } + } + + /** + * Implements a formatter for {@code $B} that formats the base Java type for a Trait Symbol. + */ + private final class BaseTypeFormatter implements BiFunction { + private final JavaTypeFormatter javaTypeFormatter = new JavaTypeFormatter(); + + @Override + public String apply(Object type, String indent) { + if (!(type instanceof Symbol)) { + throw new IllegalArgumentException("Invalid type provided for $T. Expected a Symbol but found: `" + + type + "`."); + } + Symbol symbol = (Symbol) type; + Optional baseSymbolOptional = symbol.getProperty(SymbolProperties.BASE_SYMBOL, Symbol.class); + if (baseSymbolOptional.isPresent()) { + return javaTypeFormatter.apply(baseSymbolOptional.get(), indent); + } + return javaTypeFormatter.apply(symbol, indent); + } + } +} diff --git a/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin b/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin new file mode 100644 index 00000000000..f4a381e37bb --- /dev/null +++ b/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin @@ -0,0 +1 @@ +software.amazon.smithy.traitcodegen.TraitCodegenPlugin diff --git a/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration b/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration new file mode 100644 index 00000000000..d438a4a377e --- /dev/null +++ b/smithy-trait-codegen/src/main/resources/META-INF/services/software.amazon.smithy.traitcodegen.integrations.TraitCodegenIntegration @@ -0,0 +1,4 @@ +software.amazon.smithy.traitcodegen.integrations.core.CoreIntegration +software.amazon.smithy.traitcodegen.integrations.javadoc.JavaDocIntegration +software.amazon.smithy.traitcodegen.integrations.idref.IdRefDecoratorIntegration +software.amazon.smithy.traitcodegen.integrations.uniqueitems.UniqueItemDecoratorIntegration diff --git a/smithy-trait-codegen/src/main/resources/software/amazon/smithy/traitcodegen/reserved-words.txt b/smithy-trait-codegen/src/main/resources/software/amazon/smithy/traitcodegen/reserved-words.txt new file mode 100644 index 00000000000..af805170004 --- /dev/null +++ b/smithy-trait-codegen/src/main/resources/software/amazon/smithy/traitcodegen/reserved-words.txt @@ -0,0 +1,51 @@ +abstract +assert +boolean +break +builder +byte +case +catch +char +class +const +continue +default +do +double +else +enum +extends +final +finally +float +for +goto +if +implements +import +instanceof +int +interface +long +native +new +package +private +protected +public +return +short +static +strictfp +super +switch +synchronized +this +throw +throws +transient +try +void +volatile +while diff --git a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/PluginExecutor.java b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/PluginExecutor.java new file mode 100644 index 00000000000..d053f6cbb05 --- /dev/null +++ b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/PluginExecutor.java @@ -0,0 +1,43 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + + +import java.nio.file.Paths; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.ObjectNode; + +/** + * Simple wrapper class used to execute the Trait codegen plugin for integration tests. + */ +public final class PluginExecutor { + private PluginExecutor() { + // Utility class does not have constructor + } + + public static void main(String[] args) { + TraitCodegenPlugin plugin = new TraitCodegenPlugin(); + Model model = Model.assembler(PluginExecutor.class.getClassLoader()) + .discoverModels(PluginExecutor.class.getClassLoader()) + .assemble() + .unwrap(); + PluginContext context = PluginContext.builder() + .fileManifest(FileManifest.create(Paths.get("build/integ"))) + .settings(ObjectNode.builder() + .withMember("package", "com.example.traits") + .withMember("namespace", "test.smithy.traitcodegen") + .withMember("header", ArrayNode.fromStrings("Header line One")) + .build() + ) + .model(model) + .build(); + plugin.execute(context); + } +} + diff --git a/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java new file mode 100644 index 00000000000..75820a20ee6 --- /dev/null +++ b/smithy-trait-codegen/src/test/java/software/amazon/smithy/traitcodegen/TraitCodegenPluginTest.java @@ -0,0 +1,166 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.traitcodegen; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.ObjectNode; + + +public class TraitCodegenPluginTest { + private static final int EXPECTED_NUMBER_OF_FILES = 55; + + @Test + public void generatesExpectedTraitFiles() { + MockManifest manifest = new MockManifest(); + Model model = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .assemble() + .unwrap(); + PluginContext context = PluginContext.builder() + .fileManifest(manifest) + .settings(ObjectNode.builder() + .withMember("package", "com.example.traits") + .withMember("namespace", "test.smithy.traitcodegen") + .withMember("header", ArrayNode.fromStrings("Header line One")) + .build() + ) + .model(model) + .build(); + + SmithyBuildPlugin plugin = new TraitCodegenPlugin(); + plugin.execute(context); + + assertFalse(manifest.getFiles().isEmpty()); + assertEquals(EXPECTED_NUMBER_OF_FILES, manifest.getFiles().size()); + List fileList = manifest.getFiles().stream().map(Path::toString).collect(Collectors.toList()); + assertThat(fileList, hasItem( + Paths.get("/META-INF/services/software.amazon.smithy.model.traits.TraitService").toString())); + assertThat(fileList, hasItem( + Paths.get("/com/example/traits/nested/NestedNamespaceTrait.java").toString())); + assertThat(fileList, hasItem( + Paths.get("/com/example/traits/nested/NestedNamespaceStruct.java").toString())); + } + + @Test + public void filtersTags() { + MockManifest manifest = new MockManifest(); + Model model = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .assemble() + .unwrap(); + PluginContext context = PluginContext.builder() + .fileManifest(manifest) + .settings(ObjectNode.builder() + .withMember("package", "com.example.traits") + .withMember("namespace", "test.smithy.traitcodegen") + .withMember("header", ArrayNode.fromStrings("Header line One")) + .withMember("excludeTags", ArrayNode.fromStrings("filterOut")) + .build() + ) + .model(model) + .build(); + + SmithyBuildPlugin plugin = new TraitCodegenPlugin(); + plugin.execute(context); + + assertFalse(manifest.getFiles().isEmpty()); + assertEquals(EXPECTED_NUMBER_OF_FILES - 1, manifest.getFiles().size()); + } + + @Test + public void addsHeaderLines() { + MockManifest manifest = new MockManifest(); + Model model = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .assemble() + .unwrap(); + PluginContext context = PluginContext.builder() + .fileManifest(manifest) + .settings(ObjectNode.builder() + .withMember("package", "com.example.traits") + .withMember("namespace", "test.smithy.traitcodegen") + .withMember("header", ArrayNode.fromStrings("Header line one", "Header line two")) + .build() + ) + .model(model) + .build(); + + SmithyBuildPlugin plugin = new TraitCodegenPlugin(); + plugin.execute(context); + + assertFalse(manifest.getFiles().isEmpty()); + assertEquals(EXPECTED_NUMBER_OF_FILES, manifest.getFiles().size()); + Optional fileStringOptional = manifest.getFileString( + Paths.get("com/example/traits/idref/IdRefStructTrait.java").toString()); + assertTrue(fileStringOptional.isPresent()); + assertThat(fileStringOptional.get(), startsWith("/**\n" + + " * Header line one\n" + + " * Header line two\n" + + " */")); + } + + @Test + public void doesNotFormatContentInsideHtmlTags() { + MockManifest manifest = new MockManifest(); + Model model = Model.assembler() + .discoverModels(getClass().getClassLoader()) + .assemble() + .unwrap(); + PluginContext context = PluginContext.builder() + .fileManifest(manifest) + .settings(ObjectNode.builder() + .withMember("package", "com.example.traits") + .withMember("namespace", "test.smithy.traitcodegen") + .withMember("header", ArrayNode.fromStrings("Header line one", "Header line two")) + .build() + ) + .model(model) + .build(); + + SmithyBuildPlugin plugin = new TraitCodegenPlugin(); + plugin.execute(context); + + assertFalse(manifest.getFiles().isEmpty()); + assertEquals(EXPECTED_NUMBER_OF_FILES, manifest.getFiles().size()); + Optional fileStringOptional = manifest.getFileString( + Paths.get("com/example/traits/structures/StructureTrait.java").toString()); + assertTrue(fileStringOptional.isPresent()); + String expected = " /**\n" + + " * Documentation includes preformatted text that should not be messed with. This sentence should still be partially\n" + + " * wrapped.\n" + + " * For example:\n" + + " *
\n"
+                          + "     * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n"
+                          + "     * 
\n" + + " *\n" + + " *
    \n" + + " *
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit
  • \n" + + " *
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit
  • \n" + + " *
\n" + + " */\n" + + " public Optional> getFieldD() {\n" + + " return Optional.ofNullable(fieldD);\n" + + " }\n"; + assertTrue(fileStringOptional.get().contains(expected)); + } +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/deprecated.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/deprecated.smithy new file mode 100644 index 00000000000..2e06f524461 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/deprecated.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +/// Checks that a deprecated annotation is added to deprecated traits along with +/// java deprecated tag +@deprecated(since: "a long long time ago", message: "because you should stop using it") +@trait +string DeprecatedStringTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/documents/document-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/documents/document-trait.smithy new file mode 100644 index 00000000000..7732bb0d039 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/documents/document-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.documents + +@trait +document DocumentTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/documents/struct-with-nested-document.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/documents/struct-with-nested-document.smithy new file mode 100644 index 00000000000..a192cae8c56 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/documents/struct-with-nested-document.smithy @@ -0,0 +1,11 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.documents + +@trait +structure structWithNestedDocument { + doc: nestedDoc +} + +@private +document nestedDoc diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/enum-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/enum-trait.smithy new file mode 100644 index 00000000000..249a50d5cea --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/enum-trait.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.enums + +@trait +enum StringEnum { + /// Positive response + YES = "yes" + + /// Negative response + NO = "no" +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/int-enum-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/int-enum-trait.smithy new file mode 100644 index 00000000000..63793dc3b31 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/int-enum-trait.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.enums + +@trait +intEnum IntEnum { + /// Positive response + YES = 1 + + /// Negative response + NO = 2 +} + diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/string-enum-compatibility.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/string-enum-compatibility.smithy new file mode 100644 index 00000000000..46ed2ec2e75 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/enums/string-enum-compatibility.smithy @@ -0,0 +1,30 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.enums + +// ======================== +// Legacy String Enum test +// ======================== +// The following trait check that the plugin can generate traits from a +// legacy string enum (i.e. a string with the @enum trait applied). + +@enum([ + { + name: "DIAMOND", + value: "diamond" + }, + { + name: "CLUB", + value: "club" + }, + { + name: "HEART", + value: "heart" + }, + { + name: "SPADE", + value: "spade" + } +]) +@trait +string Suit diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/filtered-by-tag.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/filtered-by-tag.smithy new file mode 100644 index 00000000000..8419c8896d3 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/filtered-by-tag.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +@trait +@tags(["filterOut"]) +string ShouldBeFilteredOut diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-list.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-list.smithy new file mode 100644 index 00000000000..9a8cdf699f0 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-list.smithy @@ -0,0 +1,15 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.idref + +// The following trait check to make sure that Strings are converted to ShapeIds +// when an @IdRef trait is added to a string + +@trait +list IdRefList { + member: IdRefListmember +} + +@private +@idRef +string IdRefListmember diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-map.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-map.smithy new file mode 100644 index 00000000000..b488a726e95 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-map.smithy @@ -0,0 +1,13 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.idref + +@trait +map IdRefMap { + key: String + value: IdRefMapMember +} + +@private +@idRef +string IdRefMapMember diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-string.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-string.smithy new file mode 100644 index 00000000000..f660635ca29 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-string.smithy @@ -0,0 +1,10 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.idref + +// The following trait check to make sure that Strings are converted to ShapeIds +// when an @IdRef trait is added to a string + +@trait +@idRef +string IdRefString diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-struct-with-nested-refs.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-struct-with-nested-refs.smithy new file mode 100644 index 00000000000..73dd97286fb --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-struct-with-nested-refs.smithy @@ -0,0 +1,34 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.idref + +@trait +structure IdRefStructWithNestedIds { + @required + idRefHolder: NestedIdRefHolder + + idList: NestedIdList + + idMap: NestedIdMap +} + +@private +structure NestedIdRefHolder { + @required + id: IdRefNestedMember +} + +@private +list NestedIdList { + member: IdRefNestedMember +} + +@private +map NestedIdMap { + key: String + value: IdRefNestedMember +} + +@private +@idRef +string IdRefNestedMember diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-struct.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-struct.smithy new file mode 100644 index 00000000000..c8160f491a6 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/idref/idref-struct.smithy @@ -0,0 +1,12 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.idref + +@trait +structure IdRefStruct { + fieldA: IdRefStructember +} + +@private +@idRef +string IdRefStructember diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/ignored.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/ignored.smithy new file mode 100644 index 00000000000..6222abd3e3d --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/ignored.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +namespace test.smithy.ignored + +/// A trait that should be ignored +@trait +structure shouldNotGenerate {} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/lists/number-list-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/lists/number-list-trait.smithy new file mode 100644 index 00000000000..66c16752dea --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/lists/number-list-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.lists + +@trait +list NumberListTrait { + member: Integer +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/lists/string-list-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/lists/string-list-trait.smithy new file mode 100644 index 00000000000..7e66cf1c778 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/lists/string-list-trait.smithy @@ -0,0 +1,8 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.lists + +@trait +list StringListTrait { + member: String +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/lists/struct-list-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/lists/struct-list-trait.smithy new file mode 100644 index 00000000000..901c0bfb5a1 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/lists/struct-list-trait.smithy @@ -0,0 +1,15 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.lists + +@trait +list StructureListTrait { + member: listMember +} + +@private +structure listMember { + a: String + b: Integer + c: String +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest b/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest new file mode 100644 index 00000000000..2dcecb80089 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/manifest @@ -0,0 +1,44 @@ +deprecated.smithy +documents/document-trait.smithy +documents/struct-with-nested-document.smithy +enums/enum-trait.smithy +enums/int-enum-trait.smithy +enums/string-enum-compatibility.smithy +idref/idref-list.smithy +idref/idref-map.smithy +idref/idref-string.smithy +idref/idref-struct-with-nested-refs.smithy +idref/idref-struct.smithy +ignored.smithy +lists/number-list-trait.smithy +lists/string-list-trait.smithy +lists/struct-list-trait.smithy +maps/string-string-map-trait.smithy +maps/string-to-struct-map-trait.smithy +mixins/struct-with-mixin-member.smithy +mixins/struct-with-only-mixin-member.smithy +names/snake-case-structure.smithy +names/struct-member-name-conflicts.smithy +names/trait-with-name-conflict.smithy +nested/nested-namespace.smithy +numbers/big-decimal-trait.smithy +numbers/big-integer-trait.smithy +numbers/byte-trait.smithy +numbers/double-trait.smithy +numbers/float-trait.smithy +numbers/integer-trait.smithy +numbers/long-trait.smithy +numbers/short-trait.smithy +string-trait.smithy +structures/annotation-trait.smithy +structures/structure-trait.smithy +timestamps/date-time-format-timestamp-trait.smithy +timestamps/epoch-seconds-format-timestamp-trait.smithy +timestamps/http-date-format-timestamp-trait.smithy +timestamps/struct-with-nested-timestamps.smithy +timestamps/timestamp-trait.smithy +uniqueitems/number-set-trait.smithy +uniqueitems/string-set-trait.smithy +uniqueitems/struct-set-trait.smithy +filtered-by-tag.smithy + diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/maps/string-string-map-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/maps/string-string-map-trait.smithy new file mode 100644 index 00000000000..5a2d2f3f68f --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/maps/string-string-map-trait.smithy @@ -0,0 +1,11 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.maps + +/// Map of only simple strings. These are handled slightly differently than +/// other maps +@trait +map StringStringMap { + key: String + value: String +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/maps/string-to-struct-map-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/maps/string-to-struct-map-trait.smithy new file mode 100644 index 00000000000..5d2e51a38f4 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/maps/string-to-struct-map-trait.smithy @@ -0,0 +1,15 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.maps + +@trait +map StringToStructMap { + key: String + value: MapValue +} + +@private +structure MapValue { + a: String + b: Integer +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/mixins/struct-with-mixin-member.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/mixins/struct-with-mixin-member.smithy new file mode 100644 index 00000000000..7ddf89b46bc --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/mixins/struct-with-mixin-member.smithy @@ -0,0 +1,18 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.mixins + +// The following trait checks that mixins are correctly flattened by +// the trait codegen plugin + +@trait +list structureListWithMixinMember { + member: listMemberWithMixin +} + +@private +structure listMemberWithMixin with [extras] { + a: String + b: Integer + c: String +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/mixins/struct-with-only-mixin-member.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/mixins/struct-with-only-mixin-member.smithy new file mode 100644 index 00000000000..bb41257e208 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/mixins/struct-with-only-mixin-member.smithy @@ -0,0 +1,16 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.mixins + +// The following trait checks that mixins are correctly flattened by +// the trait codegen plugin + +@trait +structure structWithMixin with [extras] {} + +@private +@mixin +structure extras { + @required + d: String +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/names/snake-case-structure.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/names/snake-case-structure.smithy new file mode 100644 index 00000000000..ad2c853fa87 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/names/snake-case-structure.smithy @@ -0,0 +1,15 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.names + +// =================== +// Non-java-name test +// =================== +// The following traits check that non-java-style names are +// correctly changed into a useable Java-compatible name + +/// Snake cased +@trait +structure snake_case_structure { + snake_case_member: String +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/names/struct-member-name-conflicts.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/names/struct-member-name-conflicts.smithy new file mode 100644 index 00000000000..f38a420fbe5 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/names/struct-member-name-conflicts.smithy @@ -0,0 +1,26 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.names + +// The following traits check to make sure that name conflicts between shapes and +// java classes used in the generated codegen code are correctly handled +// The names of the members conflict with +// the imported classes required for traits +@trait +structure hasMembersWithConflictingNames { + toSmithyBuilder: toSmithyBuilder + builder: Builder + static: static +} + +/// Conflicts with ToSmithyBuilder interface +@private +structure toSmithyBuilder {} + +/// Conflicts with static `builder` name that is a reserved word +@private +structure Builder {} + +/// Conflicts with java `static` keyword +@private +structure static {} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/names/trait-with-name-conflict.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/names/trait-with-name-conflict.smithy new file mode 100644 index 00000000000..0ed2f02dfad --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/names/trait-with-name-conflict.smithy @@ -0,0 +1,10 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.names + +// The following traits check to make sure that name conflicts between shapes and +// java classes used in the generated codegen code are correctly handled + +/// Conflicts with AbstractTrait base class +@trait +structure Abstract {} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/nested/nested-namespace.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/nested/nested-namespace.smithy new file mode 100644 index 00000000000..0dacd2eeae2 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/nested/nested-namespace.smithy @@ -0,0 +1,20 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.nested + +// ======================= +// Nested namespace tests +// ======================= +// The following traits check to make sure that traits within a nested smithy +// namespace are mapped to a nested java namespace + +/// A trait that should be generated in a nested namespace +@trait +structure nestedNamespaceTrait { + nested: NestedNamespaceStruct +} + +@private +structure NestedNamespaceStruct { + field: String +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/big-decimal-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/big-decimal-trait.smithy new file mode 100644 index 00000000000..3afeda294e5 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/big-decimal-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.numbers + +@trait +bigDecimal BigDecimalTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/big-integer-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/big-integer-trait.smithy new file mode 100644 index 00000000000..c9d245bec52 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/big-integer-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.numbers + +@trait +bigInteger BigIntegerTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/byte-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/byte-trait.smithy new file mode 100644 index 00000000000..2eb7984df3e --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/byte-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.numbers + +@trait +byte ByteTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/double-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/double-trait.smithy new file mode 100644 index 00000000000..cd9239e7ad1 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/double-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.numbers + +@trait +double DoubleTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/float-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/float-trait.smithy new file mode 100644 index 00000000000..daf84fd26b6 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/float-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.numbers + +@trait +float FloatTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/integer-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/integer-trait.smithy new file mode 100644 index 00000000000..bf4f2b74d8a --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/integer-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.numbers + +@trait +integer IntegerTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/long-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/long-trait.smithy new file mode 100644 index 00000000000..b680cbc9e2f --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/long-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.numbers + +@trait +long LongTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/short-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/short-trait.smithy new file mode 100644 index 00000000000..e59069a77ca --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/numbers/short-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.numbers + +@trait +short ShortTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/string-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/string-trait.smithy new file mode 100644 index 00000000000..dd275eb6893 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/string-trait.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen + +/// Simple String trait +@trait +string stringTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/structures/annotation-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/structures/annotation-trait.smithy new file mode 100644 index 00000000000..f5722db7590 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/structures/annotation-trait.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.structures + +/// A basic annotation (empty structure) trait +@trait +structure basicAnnotationTrait {} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/structures/structure-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/structures/structure-trait.smithy new file mode 100644 index 00000000000..9ff4944b609 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/structures/structure-trait.smithy @@ -0,0 +1,73 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.structures + +@trait +structure structureTrait { + @required + @pattern("^[^#+]+$") + fieldA: String + + /// Some member documentation + fieldB: Boolean + + @documentation("More documentation") + fieldC: NestedA + + /// Documentation includes preformatted text that should not be messed with. This sentence should still be partially wrapped. + /// For example: + ///
+    /// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+    /// 
+ /// + ///
    + ///
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit
  • + ///
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit Lorem ipsum dolor sit amet, consectetur adipiscing elit
  • + ///
+ fieldD: ListD + + fieldE: MyMap + + fieldF: BigDecimal + + fieldG: BigInteger +} + +@private +list ListD { + member: String +} + +@private +map MyMap { + key: String + value: String +} + +@private +structure NestedA { + @required + fieldN: String + + fieldQ: Boolean + + fieldZ: NestedB + + fieldAA: NestedC +} + +@private +enum NestedB { + /// An A! + A + /// A B! + B +} + +@private +intEnum NestedC { + /// An A! + A = 1 + /// A B! + B = 2 +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/date-time-format-timestamp-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/date-time-format-timestamp-trait.smithy new file mode 100644 index 00000000000..4b24e8347ec --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/date-time-format-timestamp-trait.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.timestamps + +@trait +@timestampFormat("date-time") +timestamp dateTimeTimestampTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/epoch-seconds-format-timestamp-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/epoch-seconds-format-timestamp-trait.smithy new file mode 100644 index 00000000000..7f8f855f1c2 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/epoch-seconds-format-timestamp-trait.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.timestamps + +@trait +@timestampFormat("epoch-seconds") +timestamp epochSecondsTimestampTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/http-date-format-timestamp-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/http-date-format-timestamp-trait.smithy new file mode 100644 index 00000000000..8a1d8fecd1d --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/http-date-format-timestamp-trait.smithy @@ -0,0 +1,7 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.timestamps + +@trait +@timestampFormat("http-date") +timestamp httpDateTimestampTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/struct-with-nested-timestamps.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/struct-with-nested-timestamps.smithy new file mode 100644 index 00000000000..394427bf6b3 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/struct-with-nested-timestamps.smithy @@ -0,0 +1,30 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.timestamps + +@trait +structure structWithNestedTimestamps { + @required + baseTime: basicTimestamp + @required + dateTime: dateTimeTimestamp + @required + httpDate: httpDateTimestamp + @required + epochSeconds: epochSecondsTimestamp +} + +@private +timestamp basicTimestamp + +@private +@timestampFormat("date-time") +timestamp dateTimeTimestamp + +@private +@timestampFormat("http-date") +timestamp httpDateTimestamp + +@private +@timestampFormat("epoch-seconds") +timestamp epochSecondsTimestamp diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/timestamp-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/timestamp-trait.smithy new file mode 100644 index 00000000000..536c6ccc013 --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/timestamps/timestamp-trait.smithy @@ -0,0 +1,6 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.timestamps + +@trait +timestamp TimestampTrait diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/uniqueitems/number-set-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/uniqueitems/number-set-trait.smithy new file mode 100644 index 00000000000..fe17a2999dc --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/uniqueitems/number-set-trait.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.uniqueitems + +@trait +@uniqueItems +list NumberSetTrait { + member: Integer +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/uniqueitems/string-set-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/uniqueitems/string-set-trait.smithy new file mode 100644 index 00000000000..8ee9a34dd0c --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/uniqueitems/string-set-trait.smithy @@ -0,0 +1,9 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.uniqueitems + +@trait +@uniqueItems +list StringSetTrait { + member: String +} diff --git a/smithy-trait-codegen/src/test/resources/META-INF/smithy/uniqueitems/struct-set-trait.smithy b/smithy-trait-codegen/src/test/resources/META-INF/smithy/uniqueitems/struct-set-trait.smithy new file mode 100644 index 00000000000..005e1966fca --- /dev/null +++ b/smithy-trait-codegen/src/test/resources/META-INF/smithy/uniqueitems/struct-set-trait.smithy @@ -0,0 +1,16 @@ +$version: "2.0" + +namespace test.smithy.traitcodegen.uniqueitems + +@trait +@uniqueItems +list StructureSetTrait { + member: setMember +} + +@private +structure setMember { + a: String + b: Integer + c: String +}