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

Add automatic integration configuration #2014

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.List;
import software.amazon.smithy.build.FileManifest;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.utils.AbstractCodeWriter;
import software.amazon.smithy.utils.CodeInterceptor;
import software.amazon.smithy.utils.CodeSection;
Expand Down Expand Up @@ -70,6 +71,49 @@ default byte priority() {
return 0;
}

/**
* Configures the integration.
*
* <p>This provides access to both the parsed settings for the generator and
* an unparsed {@link ObjectNode} containing settings for this particular
* integration.
*
* <p>The following {@code smithy-build.json} file contains an example of how
* this configuration will be set.
*
* <pre>{@code
* {
* "version": "1.0",
* "projections": {
* "codegen-projection": {
* "plugins": {
* "code-generator": {
* "service": "com.example#DocumentedService",
* "integrations": {
* "my-integration": {
* "example-setting": "foo"
* }
* }
* }
* }
* }
* }
* }
* }</pre>
*
* <p>In this example, an integration whose {@link #name} is {@code my-integration}
* Would receive the extra settings from the key of the same name within the
* {@code integrations} node.
*
* <p>Integrations SHOULD use modeled traits as much as possible to drive
* configuration. This is intended for configuration that doesn't make sense
* as a trait, such as configuring a documentation theme.
*
* @param settings Settings used to generate code.
* @param integrationSettings Settings used to configure integrations.
*/
default void configure(S settings, ObjectNode integrationSettings) {}

/**
* Gets the names of integrations that this integration must come before.
*
Expand Down Expand Up @@ -103,7 +147,7 @@ default List<String> runAfter() {
* <p>By default, this method will return the given {@code model} as-is.
*
* @param model Model being generated.
* @param settings Setting used to generate code.
* @param settings Settings used to generate code.
* @return Returns the updated model.
*/
default Model preprocessModel(Model model, S settings) {
Expand All @@ -122,7 +166,7 @@ default Model preprocessModel(Model model, S settings) {
* <p>This integration method should be called only after {@link #preprocessModel}.
*
* @param model Model being generated.
* @param settings Setting used to generate.
* @param settings Settings used to generate.
* @param symbolProvider The original {@code SymbolProvider}.
* @return The decorated {@code SymbolProvider}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import software.amazon.smithy.model.neighbor.Walker;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.NodeMapper;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.EnumShape;
import software.amazon.smithy.model.shapes.IntEnumShape;
import software.amazon.smithy.model.shapes.OperationShape;
Expand Down Expand Up @@ -74,6 +75,7 @@ public final class CodegenDirector<
private ShapeId service;
private Model model;
private S settings;
private ObjectNode integrationSettings = Node.objectNode();
private FileManifest fileManifest;
private Supplier<Iterable<I>> integrationFinder;
private DirectedCodegen<C, S, I> directedCodegen;
Expand Down Expand Up @@ -143,6 +145,8 @@ public void model(Model model) {
/**
* Sets the required settings object used for code generation.
*
* <p>{@link #integrationSettings} MUST also be set.
*
* @param settings Settings object.
*/
public void settings(S settings) {
Expand All @@ -159,6 +163,9 @@ public void settings(S settings) {
* You will need to manually deserialize your settings if using types that
* are not supported by Smithy's {@link NodeMapper}.
*
* <p>This will also set {@link #integrationSettings} if the {@code integrations}
* key is present.
*
* @param settingsType Settings type to deserialize into.
* @param settingsNode Settings node value to deserialize.
* @return Returns the deserialized settings as this is needed to provide a service shape ID.
Expand All @@ -167,9 +174,48 @@ public S settings(Class<S> settingsType, Node settingsNode) {
LOGGER.fine(() -> "Loading codegen settings from node value: " + settingsNode.getSourceLocation());
S deserialized = new NodeMapper().deserialize(settingsNode, settingsType);
settings(deserialized);
settingsNode.asObjectNode()
.flatMap(node -> node.getObjectMember("integrations"))
.ifPresent(this::integrationSettings);
return deserialized;
}

/**
* Sets the settings node to be passed to integrations.
*
* <p>Generators MUST set this with the {@code integrations} key from their
* plugin settings.
*
* <pre>{@code
* {
* "version": "1.0",
* "projections": {
* "codegen-projection": {
* "plugins": {
* "code-generator": {
* "service": "com.example#DocumentedService",
* "integrations": {
* "my-integration": {
* "example-setting": "foo"
* }
* }
* }
* }
* }
* }
* }
* }</pre>
*
* <p>In this example, the value of the {@code integrations} key is what must
* be passed to this method. The value of the {@code my-integration} key will
* then be provided to an integration with the name {@code my-integration}.
*
* @param integrationSettings Settings used to configure integrations.
*/
public void integrationSettings(ObjectNode integrationSettings) {
this.integrationSettings = Objects.requireNonNull(integrationSettings);
}

/**
* Sets the required file manifest used to write files to disk.
*
Expand Down Expand Up @@ -366,7 +412,10 @@ private void performModelTransforms() {
private List<I> findIntegrations() {
LOGGER.fine(() -> "Finding integration implementations of " + integrationClass.getName());
List<I> integrations = SmithyIntegration.sort(integrationFinder.get());
integrations.forEach(i -> LOGGER.finest(() -> "Found integration " + i.getClass().getCanonicalName()));
integrations.forEach(i -> {
LOGGER.finest(() -> "Found integration " + i.getClass().getCanonicalName());
i.configure(settings, integrationSettings.getObjectMember(i.name()).orElse(Node.objectNode()));
});
return integrations;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.codegen.core.directed;

import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;

public class CapturingIntegration implements TestIntegration {
public ObjectNode integrationSettings = Node.objectNode();

@Override
public String name() {
return "capturing-integration";
}

@Override
public void configure(TestSettings settings, ObjectNode integrationSettings) {
this.integrationSettings = integrationSettings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -33,6 +36,7 @@
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.node.ObjectNode;
import software.amazon.smithy.model.shapes.ShapeId;

public class CodegenDirectorTest {
Expand All @@ -42,6 +46,8 @@ private static final class TestDirected implements DirectedCodegen<TestContext,
public final List<ShapeId> generatedEnumTypeEnums = new ArrayList<>();
public final List<ShapeId> generatedStringTypeEnums = new ArrayList<>();

public final List<TestIntegration> integrations = new ArrayList<>();

@Override
public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective<TestSettings> directive) {
return shape -> Symbol.builder()
Expand All @@ -52,6 +58,8 @@ public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective<TestSet

@Override
public TestContext createContext(CreateContextDirective<TestSettings, TestIntegration> directive) {
integrations.clear();
integrations.addAll(directive.integrations());
WriterDelegator<TestWriter> delegator = new WriterDelegator<>(
directive.fileManifest(),
directive.symbolProvider(),
Expand Down Expand Up @@ -339,4 +347,42 @@ public void testShapesGenerationWithoutOrder() {
ShapeId.from("smithy.example#Foo")
));
}

@Test
public void testConfiguresIntegrations() {
TestDirected testDirected = new TestDirected();
CodegenDirector<TestWriter, TestIntegration, TestContext, TestSettings> runner
= new CodegenDirector<>();
FileManifest manifest = new MockManifest();
Model model = Model.assembler()
.addImport(getClass().getResource("needs-sorting.smithy"))
.assemble()
.unwrap();

ObjectNode integrationSettings = Node.objectNode().withMember("spam", "eggs");
ObjectNode allIntegrationSettings = Node.objectNode()
.withMember("capturing-integration", integrationSettings);
ObjectNode settings = Node.objectNode()
.withMember("foo", "hi")
.withMember("integrations", allIntegrationSettings);
runner.settings(TestSettings.class, settings);
runner.directedCodegen(testDirected);
runner.fileManifest(manifest);
runner.service(ShapeId.from("smithy.example#Foo"));
runner.model(model);
runner.integrationClass(TestIntegration.class);
runner.performDefaultCodegenTransforms();
runner.shapeGenerationOrder(ShapeGenerationOrder.NONE);
runner.run();

assertThat(testDirected.integrations, not(empty()));
CapturingIntegration capturingIntegration = null;
for (TestIntegration integration : testDirected.integrations) {
if (integration instanceof CapturingIntegration) {
capturingIntegration = (CapturingIntegration) integration;
}
}
assertThat(capturingIntegration, notNullValue());
assertThat(capturingIntegration.integrationSettings, equalTo(integrationSettings));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
software.amazon.smithy.codegen.core.directed.CapturingIntegration