Implementing the Generator#
This document describes how to implement a code generator using the high-level DirectedCodegen interface.
DirectedCodegen#
Smithy code generators typically all follow the same patterns. In fact,
the layout of existing code generators is so similar that a kind of
"golden-path" codegen architecture was designed called
directed codegen. The DirectedCodegen
interface and CodegenDirector
class provide a kind of guided template for building a code generator.
DirectedCodegen
brings together all the opinionated abstractions for
implementing a Smithy code generator.
Symbol
andSymbolProvider
classes used to map shapes to code and decouple this logic from templates. (see Decoupling Codegen with Symbols)- A language-specific
SymbolWriter
subclass used to generate code using a simple template engine. (see Generating Code) - A
SmithyIntegration
subtype used to provide extension points to the generator. (see Making Codegen Pluggable) - Easy to find helper methods for getting information from the model. (see Using the Semantic Model)
- Pre-defined "directives" that tell you the kinds of shape and trait combinations that need to be code generated.
Implementing DirectedCodegen
#
The methods of DirectedCodegen
break down the process of building up
and running a generator into specific methods.
public interface DirectedCodegen<C extends CodegenContext<S, ?, I>, S, I extends SmithyIntegration<S, ?, C>> {
SymbolProvider createSymbolProvider(CreateSymbolProviderDirective<S> directive);
C createContext(CreateContextDirective<S, I> directive);
void generateService(GenerateServiceDirective<C, S> directive);
default void generateResource(GenerateResourceDirective<C, S> directive) {}
void generateStructure(GenerateStructureDirective<C, S> directive);
void generateError(GenerateErrorDirective<C, S> directive);
void generateUnion(GenerateUnionDirective<C, S> directive);
void generateEnumShape(GenerateEnumDirective<C, S> directive);
void generateIntEnumShape(GenerateIntEnumDirective<C, S> directive);
default void customizeBeforeShapeGeneration(CustomizeDirective<C, S> directive) {}
default void customizeBeforeIntegrations(CustomizeDirective<C, S> directive) {}
default void customizeAfterIntegrations(CustomizeDirective<C, S> directive) {}
}
The source code for DirectedCodegen can be found on GitHub.
DirectedCodegen prerequisites#
DirectedCodegen
has a few prerequisites before it can be implemented.
- A
SymbolProvider
implementation used to map Smithy shapes to Symbols (typeS
). Mapping Smithy Shapes to Your Language provides guidance on how shapes should map to a programming language, and Decoupling Codegen with Symbols describes how Symbols are used to perform the actual mapping. - A specific implementation of
CodegenContext
(typeC
). This object provides access to codegen settings, abstractions for writing files, and abstractions for creating code writers. This context object has its own prerequisites:- A settings object for the code generator. This context object
contains the codegen settings passed to your generator through
smithy-build.json
plugins (see Configuring the Generator). - A subclass of
SymbolWriter
used to generate code for the target language.
- A settings object for the code generator. This context object
contains the codegen settings passed to your generator through
- A
SmithyIntegration
implementation used to make the generator extensible (typeI
).
Running DirectedCodegen
using a CodegenDirector
#
A CodegenDirector
is used in concert with a DirectedCodegen
implementation to build up the context needed to run the generator and
call methods in the right order. CodegenDirector
is typically called
in a Smithy-Build plugin using the data provided by
software.amazon.smithy.build.PluginContext
.
@Override
public void execute(PluginContext context) {
CodegenDirector<MylangWriter,
MylangIntegration,
MylangContext,
MylangSettings> runner = new CodegenDirector<>();
// Assuming MylangGenerator is an implementation of DirectedCodegen.
runner.directedCodegen(new MylangGenerator());
// Set the SmithyIntegration class to look for and apply using SPI.
runner.integrationClass(TestIntegration.class);
// Set the FileManifest and Model from the plugin.
runner.fileManifest(context.getFileManifest());
runner.model(context.getModel());
// Create a MylangSettings object from the plugin settings.
MylangSettings settings = runner.settings(MylangSettings.class,
context.getSettings());
// Assuming service() returns the configured service shape ID.
runner.service(settings.service());
// Configure the director to perform some common model transforms.
runner.performDefaultCodegenTransforms();
runner.createDedicatedInputsAndOutputs();
runner.run();
}
After performing the above steps, CodegenDirector
will:
- Perform any requested model transformations
- Automatically find implementations of your
SmithyIntegration
class using Java SPI. These implementations are then used throughout the rest of code generation. - Register the
CodeInterceptors
from eachSmithyIntegration
with yourWriterDelegator
- Call each
generate
* method in a topologically sorted order (that is, things with no references to other shapes come before shapes that reference them) - Call
DirectedCodegen#customizeBeforeIntegrations
- Run the
customize
method of eachSmithyIntegration
- Call
DirectedCodegen#customizeAfterIntegrations
- Flush any open
SymbolWriter
s in yourWriterDelegator
.
Creating a settings class#
A code generator uses a settings object to configure the generator in
Smithy-Build and during directed code generation. At a minimum, this
class should have a ShapeId
for the service to generate.
public final class MylangSettings {
private ShapeId service;
public void service(ShapeId service) {
this.service = service;
}
public ShapeId service() {
return service;
}
}
See also
Configuring the Generator defines recommended settings
Creating a CodegenContext
class#
This object provides access to codegen settings, abstractions for
writing files, and abstractions for creating code writers. You should
create a specific implementation of CodegenContext
for each
generator. This can be done using a Java record, POJO, builder, etc.
public record MylangContext (
Model model,
MylangSettings settings,
SymbolProvider symbolProvider,
FileManifest fileManifest,
WriterDelegator<MylangWriter> writerDelegator,
List<MylangIntegration> integrations,
ServiceShape service
) implements CodegenContext<MylangSettings, MylangWriter, MylangIntegration> {}
DirectedCodegen#createContext
is responsible for creating a
CodegenContext
. Ensure that the data provided by your CodegenContext
are available using the data available to CreateContextDirective
.
Tips for using DirectedCodegen
#
- Each directive object provided to the methods of a DirectedCodegen implementation provide all the context needed to perform that action.
- In addition to context, directives often provide helper methods to get information out of the model or shape being generated.
- If additional data is needed in a given directive, you can:
- Add new getters to your
CodegenContext
class. - Add state to your
DirectedCodegen
class to set the context data you need.
- Add new getters to your