Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trait code generation #2074

Merged
merged 88 commits into from
Mar 27, 2024
Merged

Conversation

hpmellema
Copy link
Contributor

@hpmellema hpmellema commented Dec 14, 2023

resolves #117

Description of changes:

This pull request 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.

Background

There are a number of common trait definitions that we see across smithy projects. Each of these trait definitions requires a developer to:

  1. Write a smithy model containing the trait definition and backwards compatibility constraints for the trait
  2. Write a Java class implementation
    1. A provider class implementation for the trait
    2. A builder implementation (some traits don’t need this, such as StringTraits)
    3. Getters
    4. ToNode implementations
    5. CreateNode or FromNode implementations
    6. Define java classes for any nested structures or enums in the trait (and for any nested within those)
      1. Some of these will also need custom hashcode and equals implementations
  3. Make sure to add the trait provider to the correct Java SPI service definition file

This boilerplate can be removed for the majority of custom trait use cases using smithy code generation (handling steps 2 and 3). Issue #117 first introduced this idea.

Caveats

  1. We are not trying to handle every possible use case of traits, just eliminate the need to write trait definitions for the majority of use cases. Complex trait definitions with knowledge indices, additional validations, parsing, etc. hand-written trait definitions may still be necessary.
  2. We do not plan to back-port existing traits to use this generator. New trait definitions will use this generator going forwards.
  3. The plugin does not currently handle ALL possible trait types. In particular, Blob and Union traits are not currently handled by this plugin. We can add support for these trait types in later PRs as the need arises.

Configuration

The plugin defines the following configuration options:

  • package (string) - Java namespace to use as root for all generated code
  • namespace (string) - Smithy namespace to search for traits and to use as root namespace when mapping to java namespace
  • header (list) - Header (such as license spec) to add to the top of all generated code files. Each entry of this list is added as a separate line in the header.
  • excludeTags (list) - Smithy tags to ignore. Any traits with these tags will be skipped for generation

How is the list of shapes to generate determined?

This code generation plugin creates its own Codegen Orchestration class TraitCodegen that handles the trait code generation process. The set of shapes to generate is determined as follows:

  1. Getting a list of all classes with the @trait trait applied in the specified namespace
  2. Filtering out any traits with existing TraitService providers available on the classpath
  3. Filtering out any traits that are prelude shapes
  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 prelude shapes from the discovered closure

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

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

Example

Lets say we have the following smithy model:

$version: "2.0"

namespace test.smithy.traitcodegen

@trait
long HttpCodeLong

We can add the trait codegen plugin to our smithy-build.json as follows:

{
    "plugins": {
        "trait-codegen": {
             "package": "com.example.traits"
             "header": ["Header line one", "Header line two"]
        }
    }
}

Running smithy build will then generate the following class definition for our trait:

/**
 * Header line one
 * Header line two
 */
package com.example.traits;

import software.amazon.smithy.model.FromSourceLocation;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.NumberNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.AbstractTrait;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.utils.SmithyGenerated;

@SmithyGenerated
public final class HttpCodeLongTrait extends AbstractTrait {
    public static final ShapeId ID = ShapeId.from("test.smithy.traitcodegen#HttpCodeLong");
    
    private final Long value;
    
    public HttpCodeLongTrait(Long value) {
        super(ID, SourceLocation.NONE);
        this.value = value;
    }
    
    public HttpCodeLongTrait(Long value, FromSourceLocation sourceLocation) {
        super(ID, sourceLocation);
        this.value = value;
    }
    
    @Override
    protected Node createNode() {
        return new NumberNode(value, getSourceLocation());
    }
    
    public Long getValue() {
        return value;
    }
    
    public static final class Provider extends AbstractTrait.Provider {
        public Provider() {
            super(ID);
        }
        
        @Override
        public Trait createTrait(ShapeId target, Node value) {
            return new HttpCodeLongTrait(value.expectNumberNode().getValue().longValue(), value.getSourceLocation());
        }
    }
}

Additional examples can be viewed by pulling down this branch, building it and inspecting the generated code in smithy-trait-codegen/build/integ.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Copy link
Contributor

@JordonPhillips JordonPhillips left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union shapes don't appear to be supported anywhere. We need like a DataShapeVisitor or something that forces people to implement all data shapes but no service shapes.

review is still in progress, this covers everything before the interceptors.

@hpmellema hpmellema force-pushed the trait-code-generation branch 2 times, most recently from 83eb527 to 2765ba7 Compare January 25, 2024 19:08
@hpmellema hpmellema marked this pull request as ready for review February 7, 2024 20:41
@hpmellema hpmellema requested a review from a team as a code owner February 7, 2024 20:41
@hpmellema hpmellema force-pushed the trait-code-generation branch 5 times, most recently from a2c5b37 to d7e51c4 Compare February 22, 2024 20:49
@hpmellema hpmellema merged commit d2be471 into smithy-lang:main Mar 27, 2024
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add trait code generation
3 participants