From 8924f6ab47cb7718b4a15db097ef532b15521010 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 11 May 2020 10:38:29 -0700 Subject: [PATCH 1/3] Generate clients --- .../smithy/go/codegen/CodegenVisitor.java | 38 +++- .../smithy/go/codegen/OperationGenerator.java | 107 +++++++++ .../smithy/go/codegen/ServiceGenerator.java | 139 ++++++++++++ .../smithy/go/codegen/StructureGenerator.java | 23 +- .../smithy/go/codegen/SymbolVisitor.java | 33 ++- .../go/codegen/integration/GoIntegration.java | 57 +++++ .../integration/RuntimeClientPlugin.java | 208 ++++++++++++++++++ 7 files changed, 592 insertions(+), 13 deletions(-) create mode 100644 codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java create mode 100644 codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java create mode 100644 codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/RuntimeClientPlugin.java diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java index 872abbb01..3535a122b 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java @@ -21,19 +21,24 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.ServiceLoader; import java.util.Set; import java.util.TreeSet; import java.util.logging.Logger; import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.codegen.core.SymbolDependency; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.go.codegen.integration.GoIntegration; import software.amazon.smithy.go.codegen.integration.ProtocolGenerator; +import software.amazon.smithy.go.codegen.integration.RuntimeClientPlugin; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.ServiceIndex; +import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.neighbor.Walker; +import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.model.shapes.ShapeId; @@ -61,6 +66,7 @@ final class CodegenVisitor extends ShapeVisitor.Default { private final List integrations = new ArrayList<>(); private final ProtocolGenerator protocolGenerator; private final ApplicationProtocol applicationProtocol; + private final List runtimePlugins = new ArrayList<>(); CodegenVisitor(PluginContext context) { // Load all integrations. @@ -70,6 +76,10 @@ final class CodegenVisitor extends ShapeVisitor.Default { .forEach(integration -> { LOGGER.info(() -> "Adding GoIntegration: " + integration.getClass().getName()); integrations.add(integration); + integration.getClientPlugins().forEach(runtimePlugin -> { + LOGGER.info(() -> "Adding Go runtime plugin: " + runtimePlugin); + runtimePlugins.add(runtimePlugin); + }); }); integrations.sort(Comparator.comparingInt(GoIntegration::getOrder)); @@ -184,7 +194,12 @@ protected Void getDefault(Shape shape) { @Override public Void structureShape(StructureShape shape) { - writers.useShapeWriter(shape, writer -> new StructureGenerator(model, symbolProvider, writer, shape).run()); + if (shape.hasTrait(SyntheticClone.ID)) { + return null; + } + Symbol symbol = symbolProvider.toSymbol(shape); + writers.useShapeWriter(shape, writer -> new StructureGenerator( + model, symbolProvider, writer, shape, symbol).run()); return null; } @@ -204,8 +219,25 @@ public Void unionShape(UnionShape shape) { @Override public Void serviceShape(ServiceShape shape) { - // TODO: implement client generation - writers.useShapeWriter(shape, writer -> { + if (!Objects.equals(service, shape)) { + LOGGER.fine(() -> "Skipping `" + shape.getId() + "` because it is not `" + service.getId() + "`"); + return null; + } + + writers.useShapeWriter(shape, serviceWriter -> { + new ServiceGenerator(settings, model, symbolProvider, serviceWriter, shape, integrations, + runtimePlugins).run(); + + // Generate each operation for the service. We do this here instead of via the operation visitor method to + // limit it to the operations bound to the service. + TopDownIndex topDownIndex = model.getKnowledge(TopDownIndex.class); + Set containedOperations = new TreeSet<>(topDownIndex.getContainedOperations(service)); + for (OperationShape operation : containedOperations) { + Symbol operationSymbol = symbolProvider.toSymbol(operation); + writers.useShapeWriter(operation, operationWriter -> new OperationGenerator( + settings, model, symbolProvider, operationWriter, service, operation, + operationSymbol).run()); + } }); return null; } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java new file mode 100644 index 000000000..0145ea2f2 --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.go.codegen; + +import software.amazon.smithy.codegen.core.CodegenException; +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.knowledge.OperationIndex; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.StructureShape; + +/** + * Generates a client operation and associated custom shapes. + */ +final class OperationGenerator implements Runnable { + + private final GoSettings settings; + private final Model model; + private final SymbolProvider symbolProvider; + private final GoWriter writer; + private final ServiceShape service; + private final OperationShape operation; + private final Symbol operationSymbol; + + OperationGenerator( + GoSettings settings, + Model model, + SymbolProvider symbolProvider, + GoWriter writer, + ServiceShape service, + OperationShape operation, + Symbol operationSymbol + ) { + this.settings = settings; + this.model = model; + this.symbolProvider = symbolProvider; + this.writer = writer; + this.service = service; + this.operation = operation; + this.operationSymbol = operationSymbol; + } + + @Override + public void run() { + OperationIndex operationIndex = model.getKnowledge(OperationIndex.class); + Symbol serviceSymbol = symbolProvider.toSymbol(service); + + if (!operationIndex.getInput(operation).isPresent()) { + // Theoretically this shouldn't ever get hit since we automatically insert synthetic inputs / outputs. + throw new CodegenException( + "Operations are required to have input shapes in order to allow for future evolution."); + } + StructureShape inputShape = operationIndex.getInput(operation).get(); + Symbol inputSymbol = symbolProvider.toSymbol(inputShape); + + if (!operationIndex.getOutput(operation).isPresent()) { + throw new CodegenException( + "Operations are required to have output shapes in order to allow for future evolution."); + } + StructureShape outputShape = operationIndex.getOutput(operation).get(); + Symbol outputSymbol = symbolProvider.toSymbol(outputShape); + + writer.writeShapeDocs(operation); + Symbol contextSymbol = SymbolUtils.createValueSymbolBuilder("Context", GoDependency.CONTEXT).build(); + writer.openBlock("func (c $P) $T(ctx $T, params $P, opts ...func(*Options)) ($P, error) {", "}", + serviceSymbol, operationSymbol, contextSymbol, inputSymbol, outputSymbol, () -> { + // TODO: create middleware stack + writer.write("options := c.options.Copy()"); + writer.openBlock("for _, fn := range optFns {", "}", () -> { + writer.write("fn(&options)"); + }); + writer.openBlock("for _, fn := range options.APIOptions {", "}", () -> { + writer.write("if err := fn(stack); err != nil { return nil, err }"); + }); + // TODO: resolve middleware stack + writer.write("return nil, nil"); + }).write(""); + + // Write out the input and output structures. These are written out here to prevent naming conflicts with other + // shapes in the model. + new StructureGenerator(model, symbolProvider, writer, inputShape, inputSymbol).run(); + + // The output structure gets a metadata member added. + Symbol metadataSymbol = SymbolUtils.createValueSymbolBuilder( + "Metadata", GoDependency.SMITHY_MIDDLEWARE).build(); + new StructureGenerator(model, symbolProvider, writer, outputShape, outputSymbol).renderStructure(() -> { + writer.write(""); + writer.writeDocs("Metadata pertaining to the operation's result."); + writer.write("ResultMetadata $T", metadataSymbol); + }); + } +} diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java new file mode 100644 index 000000000..df5ce5ad9 --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.go.codegen; + +import java.util.List; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.go.codegen.integration.GoIntegration; +import software.amazon.smithy.go.codegen.integration.RuntimeClientPlugin; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.traits.StringTrait; +import software.amazon.smithy.model.traits.TitleTrait; + +/** + * Generates a service client and configuration. + */ +final class ServiceGenerator implements Runnable { + + public static final String CONFIG_NAME = "Options"; + public static final String API_OPTIONS_FUNC_NAME = "APIOptionFunc"; + + private final GoSettings settings; + private final Model model; + private final SymbolProvider symbolProvider; + private final GoWriter writer; + private final ServiceShape service; + private final List integrations; + private final List runtimePlugins; + + ServiceGenerator( + GoSettings settings, + Model model, + SymbolProvider symbolProvider, + GoWriter writer, + ServiceShape service, + List integrations, + List runtimePlugins + ) { + this.settings = settings; + this.model = model; + this.symbolProvider = symbolProvider; + this.writer = writer; + this.service = service; + this.integrations = integrations; + this.runtimePlugins = runtimePlugins; + } + + @Override + public void run() { + Symbol serviceSymbol = symbolProvider.toSymbol(service); + writer.writeShapeDocs(service); + writer.openBlock("type $T struct {", "}", serviceSymbol, () -> { + writer.write("options $L", CONFIG_NAME); + }); + + generateConstructor(serviceSymbol); + + String clientId = CodegenUtils.getDefaultPackageImportName(settings.getModuleName()); + String clientTitle = service.getTrait(TitleTrait.class).map(StringTrait::getValue).orElse(clientId); + + writer.writeDocs("ServiceID returns the name of the identifier for the service API."); + writer.write("func (c $P) ServiceID() string { return $S }", serviceSymbol, clientId); + + writer.writeDocs("ServiceName returns the full service title."); + writer.write("func (c $P) ServiceName() string { return $S }", serviceSymbol, clientTitle); + + generateConfig(); + + Symbol stackSymbol = SymbolUtils.createPointableSymbolBuilder("Stack", GoDependency.SMITHY_MIDDLEWARE).build(); + writer.write("type $L func($P) error", API_OPTIONS_FUNC_NAME, stackSymbol); + } + + private void generateConstructor(Symbol serviceSymbol) { + writer.writeDocs(String.format("New returns an initialized %s based on the functional options. " + + "Provide additional functional options to further configure the behavior " + + "of the client, such as changing the client's endpoint or adding custom " + + "middleware behavior.", serviceSymbol.getName())); + writer.openBlock("func New(options $L) ($P, error) {", "}", + CONFIG_NAME, serviceSymbol, () -> { + + writer.write("options = options.Copy()").write(""); + + // Run any config initialization functions registered by runtime plugins. + for (RuntimeClientPlugin runtimeClientPlugin : runtimePlugins) { + if (!runtimeClientPlugin.matchesService(model, service) + || !runtimeClientPlugin.getResolveFunction().isPresent()) { + continue; + } + writer.write("if err := $T(&options); err != nil { return nil, err }", + runtimeClientPlugin.getResolveFunction().get()); + writer.write(""); + } + + writer.openBlock("client := &$T {", "}", serviceSymbol, () -> { + writer.write("options: options,"); + }).write(""); + + writer.write("return client, nil"); + }); + } + + private void generateConfig() { + writer.openBlock("type $L struct {", "}", CONFIG_NAME, () -> { + writer.writeDocs("Set of options to modify how an operation is invoked. These apply to all operations " + + "invoked for this client. Use functional options on operation call to modify this " + + "list for per operation behavior." + ); + writer.write("APIOptions []$L", API_OPTIONS_FUNC_NAME).write(""); + + for (GoIntegration integration : integrations) { + integration.addConfigFields(settings, model, symbolProvider, writer); + } + + // TODO: add application protocol defaults + }).write(""); + + writer.writeDocs("Copy creates a clone where the APIOptions list is deep copied."); + writer.openBlock("func (o $L) Copy() $L {", "}", CONFIG_NAME, CONFIG_NAME, () -> { + writer.write("to := o"); + writer.write("to.APIOptions = make([]$L, len(o.APIOptions))", API_OPTIONS_FUNC_NAME); + writer.write("copy(to.APIOptions, o.APIOptions)"); + writer.write("return to"); + }); + } +} diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/StructureGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/StructureGenerator.java index 115071da3..26f025852 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/StructureGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/StructureGenerator.java @@ -41,28 +41,38 @@ final class StructureGenerator implements Runnable { private final SymbolProvider symbolProvider; private final GoWriter writer; private final StructureShape shape; - - StructureGenerator(Model model, SymbolProvider symbolProvider, GoWriter writer, StructureShape shape) { + private final Symbol symbol; + + StructureGenerator( + Model model, + SymbolProvider symbolProvider, + GoWriter writer, + StructureShape shape, + Symbol symbol + ) { this.model = model; this.symbolProvider = symbolProvider; this.writer = writer; this.shape = shape; + this.symbol = symbol; } @Override public void run() { if (!shape.hasTrait(ErrorTrait.class)) { - renderStructure(); + renderStructure(() -> { }); } else { renderErrorStructure(); } } /** - * Renders a normal, non-error structure. + * Renders a non-error structure. + * + * @param runnable A runnable that runs before the structure definition is closed. This can be used to write + * additional members. */ - private void renderStructure() { - Symbol symbol = symbolProvider.toSymbol(shape); + public void renderStructure(Runnable runnable) { writer.writeShapeDocs(shape); writer.openBlock("type $L struct {", symbol.getName()); for (MemberShape member : shape.getAllMembers().values()) { @@ -70,6 +80,7 @@ private void renderStructure() { writer.writeMemberDocs(model, member); writer.write("$L $P", memberName, symbolProvider.toSymbol(member)); } + runnable.run(); writer.closeBlock("}").write(""); } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SymbolVisitor.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SymbolVisitor.java index df6a46cd6..9e2bf1703 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SymbolVisitor.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/SymbolVisitor.java @@ -17,6 +17,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.logging.Logger; import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider; @@ -25,6 +26,10 @@ 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.knowledge.NeighborProviderIndex; +import software.amazon.smithy.model.neighbor.NeighborProvider; +import software.amazon.smithy.model.neighbor.Relationship; +import software.amazon.smithy.model.neighbor.RelationshipType; import software.amazon.smithy.model.shapes.BigDecimalShape; import software.amazon.smithy.model.shapes.BigIntegerShape; import software.amazon.smithy.model.shapes.BlobShape; @@ -316,8 +321,9 @@ private Symbol createBigSymbol(Shape shape, String symbolName) { @Override public Symbol operationShape(OperationShape shape) { - // TODO: implement operations - return SymbolUtils.createPointableSymbolBuilder(shape, "nil") + String name = getDefaultShapeName(shape); + return SymbolUtils.createPointableSymbolBuilder(shape, name, rootModuleName) + .definitionFile(String.format("./api_op_%s.go", name)) .build(); } @@ -329,8 +335,7 @@ public Symbol resourceShape(ResourceShape shape) { @Override public Symbol serviceShape(ServiceShape shape) { - // TODO: implement client generation - return SymbolUtils.createValueSymbolBuilder(shape, "Client", rootModuleName) + return SymbolUtils.createPointableSymbolBuilder(shape, "Client", rootModuleName) .definitionFile("./api_client.go") .build(); } @@ -352,6 +357,15 @@ public Symbol stringShape(StringShape shape) { @Override public Symbol structureShape(StructureShape shape) { String name = getDefaultShapeName(shape); + if (shape.hasTrait(SyntheticClone.ID)) { + Optional boundOperationName = getNameOfBoundOperation(shape); + if (boundOperationName.isPresent()) { + return SymbolUtils.createPointableSymbolBuilder(shape, name, rootModuleName) + .definitionFile("./api_op_" + boundOperationName.get() + ".go") + .build(); + } + } + Symbol.Builder builder = SymbolUtils.createPointableSymbolBuilder(shape, name, typesPackageName); if (shape.hasTrait(ErrorTrait.ID)) { builder.definitionFile("./types/errors.go"); @@ -361,6 +375,17 @@ public Symbol structureShape(StructureShape shape) { return builder.build(); } + private Optional getNameOfBoundOperation(StructureShape shape) { + NeighborProvider provider = model.getKnowledge(NeighborProviderIndex.class).getReverseProvider(); + for (Relationship relationship : provider.getNeighbors(shape)) { + RelationshipType relationshipType = relationship.getRelationshipType(); + if (relationshipType == RelationshipType.INPUT || relationshipType == RelationshipType.OUTPUT) { + return Optional.of(getDefaultShapeName(relationship.getNeighborShape().get())); + } + } + return Optional.empty(); + } + @Override public Symbol unionShape(UnionShape shape) { String name = getDefaultShapeName(shape); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/GoIntegration.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/GoIntegration.java index 33a06d3c9..d7c5b9b57 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/GoIntegration.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/GoIntegration.java @@ -19,7 +19,10 @@ import java.util.List; import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolDependency; import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.SymbolReference; import software.amazon.smithy.go.codegen.GoSettings; import software.amazon.smithy.go.codegen.GoWriter; import software.amazon.smithy.go.codegen.TriConsumer; @@ -152,4 +155,58 @@ default void addConfigInterfaceFields( ) { // pass } + + /** + * Gets a list of plugins to apply to the generated client. + * + * @return Returns the list of RuntimePlugins to apply to the client. + */ + default List getClientPlugins() { + return Collections.emptyList(); + } + + /** + * Adds additional client config fields. + * + *

Implementations of this method are expected to add fields to the + * "ClientOptions" struct of a generated client. Implementations are + * expected to write field names and their type signatures. Any number + * of fields can be added, and any {@link Symbol} or {@link SymbolReference} + * objects that are written to the writer are automatically imported, and + * any of their contained {@link SymbolDependency} values are automatically + * added to the generated {@code go.mod} file. + * + *

For example, the following code adds two fields to a client: + * + *

+     * {@code
+     * public final class MyIntegration implements GoIntegration {
+     *     public void addConfigFields(
+     *             GoSettings settings,
+     *             Model model,
+     *             SymbolProvider symbolProvider,
+     *             GoWriter writer
+     *     ) {
+     *         writer.writeDocs("The docs for foo...");
+     *         writer.write("Foo *string");
+     *
+     *         writer.writeDocs("The docs for bar...");
+     *         writer.write("Bar string;");
+     *     }
+     * }
+     * }
+ * + * @param settings Settings used to generate. + * @param model Model to generate from. + * @param symbolProvider Symbol provider used for codegen. + * @param writer Go writer to write to. + */ + default void addConfigFields( + GoSettings settings, + Model model, + SymbolProvider symbolProvider, + GoWriter writer + ) { + // pass + } } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/RuntimeClientPlugin.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/RuntimeClientPlugin.java new file mode 100644 index 000000000..0224e9382 --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/RuntimeClientPlugin.java @@ -0,0 +1,208 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.go.codegen.integration; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiPredicate; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Represents a runtime plugin for a client that hooks into various aspects + * of Go code generation, including adding configuration settings + * to clients and middleware plugins to both clients and commands. + * + *

These runtime client plugins are registered through the + * {@link GoIntegration} SPI and applied to the code generator at + * build-time. + */ +public final class RuntimeClientPlugin implements ToSmithyBuilder { + + private final Symbol resolveFunction; + private final BiPredicate servicePredicate; + private final OperationPredicate operationPredicate; + + private RuntimeClientPlugin(Builder builder) { + resolveFunction = builder.resolveFunction; + operationPredicate = builder.operationPredicate; + servicePredicate = builder.servicePredicate; + } + + @FunctionalInterface + public interface OperationPredicate { + /** + * Tests if middleware is applied to an individual operation. + * + * @param model Model the operation belongs to. + * @param service Service the operation belongs to. + * @param operation Operation to test. + * @return Returns true if middleware should be applied to the operation. + */ + boolean test(Model model, ServiceShape service, OperationShape operation); + } + + /** + * Gets the optionally present symbol that points to a function that operates + * on the client options at creation time. + * + *

Any configuration that a plugin requires in order to function should be + * checked in this function, either setting a default value if possible or + * returning an error if not. + * + *

This function must take a client options struct as input and return a + * client options struct and an error as output. + * + * @return Returns the optionally present resolve function. + */ + public Optional getResolveFunction() { + return Optional.ofNullable(resolveFunction); + } + + /** + * Returns true if this plugin applies to the given service. + * + *

By default, a plugin applies to all services but not to specific + * commands. You an configure a plugin to apply only to a subset of + * services (for example, only apply to a known service or a service + * with specific traits) or to no services at all (for example, if + * the plugin is meant to by command-specific and not on every + * command executed by the service). + * + * @param model The model the service belongs to. + * @param service Service shape to test against. + * @return Returns true if the plugin is applied to the given service. + * @see #matchesOperation(Model, ServiceShape, OperationShape) + */ + public boolean matchesService(Model model, ServiceShape service) { + return servicePredicate.test(model, service); + } + + /** + * Returns true if this plugin applies to the given operation. + * + * @param model Model the operation belongs to. + * @param service Service the operation belongs to. + * @param operation Operation to test against. + * @return Returns true if the plugin is applied to the given operation. + * @see #matchesService(Model, ServiceShape) + */ + public boolean matchesOperation(Model model, ServiceShape service, OperationShape operation) { + return operationPredicate.test(model, service, operation); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public SmithyBuilder toBuilder() { + return builder() + .resolveFunction(resolveFunction) + .servicePredicate(servicePredicate) + .operationPredicate(operationPredicate); + } + + /** + * Builds a {@code RuntimeClientPlugin}. + */ + public static final class Builder implements SmithyBuilder { + private Symbol resolveFunction; + private BiPredicate servicePredicate = (model, service) -> true; + private OperationPredicate operationPredicate = (model, service, operation) -> false; + + @Override + public RuntimeClientPlugin build() { + return new RuntimeClientPlugin(this); + } + + /** + * Sets the symbol used to configure client options. + * + * @param resolveFunction Resolved configuration symbol to set. + * @return Returns the builder. + */ + public Builder resolveFunction(Symbol resolveFunction) { + this.resolveFunction = resolveFunction; + return this; + } + + /** + * Sets a predicate that determines if the plugin applies to a + * specific operation. + * + *

When this method is called, the {@code servicePredicate} is + * automatically configured to return false for every service. + * + *

By default, a plugin applies globally to a service, which thereby + * applies to every operation when the middleware stack is copied. + * + * @param operationPredicate Operation matching predicate. + * @return Returns the builder. + * @see #servicePredicate(BiPredicate) + */ + public Builder operationPredicate(OperationPredicate operationPredicate) { + this.operationPredicate = Objects.requireNonNull(operationPredicate); + servicePredicate = (model, service) -> false; + return this; + } + + /** + * Configures a predicate that makes a plugin only apply to a set of + * operations that match one or more of the set of given shape names, + * and ensures that the plugin is not applied globally to services. + * + *

By default, a plugin applies globally to a service, which thereby + * applies to every operation when the middleware stack is copied. + * + * @param operationNames Set of operation names. + * @return Returns the builder. + */ + public Builder appliesOnlyToOperations(Set operationNames) { + operationPredicate((model, service, operation) -> operationNames.contains(operation.getId().getName())); + return servicePredicate((model, service) -> false); + } + + /** + * Configures a predicate that applies the plugin to a service if the + * predicate matches a given model and service. + * + *

When this method is called, the {@code operationPredicate} is + * automatically configured to return false for every operation, + * causing the plugin to only apply to services and not to individual + * operations. + * + *

By default, a plugin applies globally to a service, which + * thereby applies to every operation when the middleware stack is + * copied. Setting a custom service predicate is useful for plugins + * that should only be applied to specific services or only applied + * at the operation level. + * + * @param servicePredicate Service predicate. + * @return Returns the builder. + */ + public Builder servicePredicate(BiPredicate servicePredicate) { + this.servicePredicate = Objects.requireNonNull(servicePredicate); + operationPredicate = (model, service, operation) -> false; + return this; + } + } +} From 4f2698444475a06e7cdebf72eeffbc2547816b66 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 12 May 2020 17:19:59 -0700 Subject: [PATCH 2/3] Add application protocol client defaults --- .../smithy/go/codegen/CodegenVisitor.java | 4 +-- .../smithy/go/codegen/OperationGenerator.java | 24 +++++++++++--- .../smithy/go/codegen/ServiceGenerator.java | 31 +++++++++++++++++-- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java index 3535a122b..da55156fb 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java @@ -226,7 +226,7 @@ public Void serviceShape(ServiceShape shape) { writers.useShapeWriter(shape, serviceWriter -> { new ServiceGenerator(settings, model, symbolProvider, serviceWriter, shape, integrations, - runtimePlugins).run(); + runtimePlugins, applicationProtocol).run(); // Generate each operation for the service. We do this here instead of via the operation visitor method to // limit it to the operations bound to the service. @@ -236,7 +236,7 @@ public Void serviceShape(ServiceShape shape) { Symbol operationSymbol = symbolProvider.toSymbol(operation); writers.useShapeWriter(operation, operationWriter -> new OperationGenerator( settings, model, symbolProvider, operationWriter, service, operation, - operationSymbol).run()); + operationSymbol, applicationProtocol).run()); } }); return null; diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java index 0145ea2f2..0cca503eb 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java @@ -36,6 +36,7 @@ final class OperationGenerator implements Runnable { private final ServiceShape service; private final OperationShape operation; private final Symbol operationSymbol; + private final ApplicationProtocol applicationProtocol; OperationGenerator( GoSettings settings, @@ -44,7 +45,8 @@ final class OperationGenerator implements Runnable { GoWriter writer, ServiceShape service, OperationShape operation, - Symbol operationSymbol + Symbol operationSymbol, + ApplicationProtocol applicationProtocol ) { this.settings = settings; this.model = model; @@ -53,6 +55,7 @@ final class OperationGenerator implements Runnable { this.service = service; this.operation = operation; this.operationSymbol = operationSymbol; + this.applicationProtocol = applicationProtocol; } @Override @@ -79,7 +82,8 @@ public void run() { Symbol contextSymbol = SymbolUtils.createValueSymbolBuilder("Context", GoDependency.CONTEXT).build(); writer.openBlock("func (c $P) $T(ctx $T, params $P, opts ...func(*Options)) ($P, error) {", "}", serviceSymbol, operationSymbol, contextSymbol, inputSymbol, outputSymbol, () -> { - // TODO: create middleware stack + constructStack(); + writer.write("options := c.options.Copy()"); writer.openBlock("for _, fn := range optFns {", "}", () -> { writer.write("fn(&options)"); @@ -87,8 +91,10 @@ public void run() { writer.openBlock("for _, fn := range options.APIOptions {", "}", () -> { writer.write("if err := fn(stack); err != nil { return nil, err }"); }); - // TODO: resolve middleware stack - writer.write("return nil, nil"); + + writer.write("result, err := handler.Handle(ctx, params)"); + writer.write("if err != nil { return nil, err }"); + writer.write("return result.($P), nil", outputSymbol); }).write(""); // Write out the input and output structures. These are written out here to prevent naming conflicts with other @@ -104,4 +110,14 @@ public void run() { writer.write("ResultMetadata $T", metadataSymbol); }); } + + private void constructStack() { + if (!applicationProtocol.isHttpProtocol()) { + throw new UnsupportedOperationException( + "Protocols other than HTTP are not yet implemented: " + applicationProtocol); + } + writer.addUseImports(GoDependency.SMITHY_MIDDLEWARE); + writer.addUseImports(GoDependency.SMITHY_HTTP_TRANSPORT); + writer.write("stack := middleware.NewStack($S, smithyhttp.NewStackRequest)", operationSymbol.getName()); + } } diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java index df5ce5ad9..c478dcba2 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java @@ -40,6 +40,7 @@ final class ServiceGenerator implements Runnable { private final ServiceShape service; private final List integrations; private final List runtimePlugins; + private final ApplicationProtocol applicationProtocol; ServiceGenerator( GoSettings settings, @@ -48,7 +49,8 @@ final class ServiceGenerator implements Runnable { GoWriter writer, ServiceShape service, List integrations, - List runtimePlugins + List runtimePlugins, + ApplicationProtocol applicationProtocol ) { this.settings = settings; this.model = model; @@ -57,6 +59,7 @@ final class ServiceGenerator implements Runnable { this.service = service; this.integrations = integrations; this.runtimePlugins = runtimePlugins; + this.applicationProtocol = applicationProtocol; } @Override @@ -125,9 +128,11 @@ private void generateConfig() { integration.addConfigFields(settings, model, symbolProvider, writer); } - // TODO: add application protocol defaults + generateApplicationProtocolConfig(); }).write(""); + generateApplicationProtocolTypes(); + writer.writeDocs("Copy creates a clone where the APIOptions list is deep copied."); writer.openBlock("func (o $L) Copy() $L {", "}", CONFIG_NAME, CONFIG_NAME, () -> { writer.write("to := o"); @@ -136,4 +141,26 @@ private void generateConfig() { writer.write("return to"); }); } + + private void generateApplicationProtocolConfig() { + ensureSupportedProtocol(); + writer.writeDocs( + "The HTTP client to invoke API calls with. Defaults to client's default HTTP implementation if nil."); + writer.write("HTTPClient HTTPClient").write(""); + } + + private void generateApplicationProtocolTypes() { + ensureSupportedProtocol(); + writer.openBlock("type HTTPClient interface {", "}", () -> { + writer.write("Do($P) ($P, error)", applicationProtocol.getRequestType(), + applicationProtocol.getResponseType()); + }).write(""); + } + + private void ensureSupportedProtocol() { + if (!applicationProtocol.isHttpProtocol()) { + throw new UnsupportedOperationException( + "Protocols other than HTTP are not yet implemented: " + applicationProtocol); + } + } } From b3369b22d580f95b2634837efbeb22088e1036de Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 19 May 2020 13:30:29 -0700 Subject: [PATCH 3/3] Generate Handler --- .../smithy/go/codegen/OperationGenerator.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java index 0cca503eb..cd321163f 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/OperationGenerator.java @@ -92,9 +92,13 @@ public void run() { writer.write("if err := fn(stack); err != nil { return nil, err }"); }); - writer.write("result, err := handler.Handle(ctx, params)"); + constructHandler(); + + writer.write("result, metadata, err := handler.Handle(ctx, params)"); writer.write("if err != nil { return nil, err }"); - writer.write("return result.($P), nil", outputSymbol); + writer.write("out := result.($P)", outputSymbol); + writer.write("out.ResultMetadata = metadata"); + writer.write("return out, nil"); }).write(""); // Write out the input and output structures. These are written out here to prevent naming conflicts with other @@ -120,4 +124,16 @@ private void constructStack() { writer.addUseImports(GoDependency.SMITHY_HTTP_TRANSPORT); writer.write("stack := middleware.NewStack($S, smithyhttp.NewStackRequest)", operationSymbol.getName()); } + + private void constructHandler() { + if (!applicationProtocol.isHttpProtocol()) { + throw new UnsupportedOperationException( + "Protocols other than HTTP are not yet implemented: " + applicationProtocol); + } + Symbol decorateHandler = SymbolUtils.createValueSymbolBuilder( + "DecorateHandler", GoDependency.SMITHY_MIDDLEWARE).build(); + Symbol clientHandler = SymbolUtils.createValueSymbolBuilder( + "ClientHandler", GoDependency.SMITHY_HTTP_TRANSPORT).build(); + writer.write("handler := $T($T{Client: options.HTTPClient}, stack)", decorateHandler, clientHandler); + } }