Skip to content

Commit

Permalink
Add trait code generation plugin package (#2074)
Browse files Browse the repository at this point in the history
Adds a new trait code generation plugin, `trait-codegen`, in a new package, `smithy-trait-codegen`.

Adds a new plugin that generates Java trait classes from smithy models. The generation of java trait classes directly from smithy models removes the need to hand-write most trait implementations. Because traits definitions should be isolated in separate java packages from other smithy models and should never be projected, this plugin will only run on the `source` projection. Nested shapes within traits are also generated by this plugin.

This code generation plugin defines its own codegen orchestration class `TraitCodegen` that handles the trait code generation process. The set of shapes to generate as shape classes is determined as follows:

1. Get a list of all classes with the @trait trait applied in the specified namespace
2. Filter out any traits that are not sources  (see: isSourceShape)
3. Filtering out any traits with existing TraitService providers available on the classpath
4. Filtering out any traits with an excluded tag 
5. Walking the closure of the applicable trait classes to pick up any nested shapes that need to be generated
6. Filtering out any member shapes from the discovered closure
7. Filtering out any prelude shapes from the discovered closure

These shapes are then iterated through to generate all required trait classes and nested classes.

In addition to generating trait implementation classes, this plugin also automatically adds trait Implementation classes to a generated trait service provider file (META-INF/services/software.amazon.smithy.model.traits.TraitService). 

Note: Blob and Union traits are not currently handled by this plugin at this time.
  • Loading branch information
hpmellema authored Mar 27, 2024
1 parent 7a5efbd commit d2be471
Show file tree
Hide file tree
Showing 145 changed files with 6,484 additions and 0 deletions.
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
74 changes: 74 additions & 0 deletions smithy-trait-codegen/build.gradle
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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<Arguments> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit d2be471

Please sign in to comment.