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 8c6281d06..1fbe2120e 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 @@ -18,17 +18,22 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +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.RuntimeClientPlugin; import software.amazon.smithy.model.Model; +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.ShapeVisitor; @@ -53,6 +58,7 @@ final class CodegenVisitor extends ShapeVisitor.Default { private final SymbolProvider symbolProvider; private final GoDelegator writers; private final List integrations = new ArrayList<>(); + private final List runtimePlugins = new ArrayList<>(); CodegenVisitor(PluginContext context) { // Load all integrations. @@ -62,6 +68,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)); @@ -123,7 +133,9 @@ protected Void getDefault(Shape shape) { @Override public Void structureShape(StructureShape shape) { - writers.useShapeWriter(shape, writer -> new StructureGenerator(model, symbolProvider, writer, shape).run()); + Symbol symbol = symbolProvider.toSymbol(shape); + writers.useShapeWriter(shape, writer -> new StructureGenerator( + model, symbolProvider, writer, shape, symbol).run()); return null; } @@ -143,8 +155,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 `" + service.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..a8fef6002 --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ServiceGenerator.java @@ -0,0 +1,136 @@ +/* + * 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) ServiceName() string { return $S }", serviceSymbol, clientId); + + writer.writeDocs("ServiceName returns the full service title."); + writer.write("func (c $P) ServiceID() 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(() -> writer.write( + "New returns an initialized $T based on the functional options.\n" + + "Provide additional functional options to further configure the behavior\n" + + "of the client, such as changing the client's endpoint or adding custom\n" + + "middleware behavior.", serviceSymbol + )); + writer.openBlock("func New(options $L) ($P, error) {", "}", + CONFIG_NAME, serviceSymbol, () -> { + + writer.write("resolvedOptions = options.Copy()").write(""); + + // Run any config initialization functions registered by runtime plugins. + for (RuntimeClientPlugin runtimeClientPlugin : runtimePlugins) { + writer.write("resolvedOptions, err = $T(resolvedOptions)", runtimeClientPlugin.getResolveFunction()); + writer.write("if err != nil { return nil, err }").write(""); + } + + writer.openBlock("client := &$T {", "}", serviceSymbol, () -> { + writer.write("options: resolvedOptions,"); + }).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 8fcb8dfb7..07a1c0071 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; @@ -298,8 +303,10 @@ private Symbol createBigSymbol(Shape shape, String symbolName) { @Override public Symbol operationShape(OperationShape shape) { - // TODO: implement operations - return SymbolUtils.createPointableSymbolBuilder(shape, "nil").build(); + String name = getDefaultShapeName(shape); + return SymbolUtils.createPointableSymbolBuilder(shape, name, rootModuleName) + .definitionFile(String.format("./api_Op_%s.go", name)) + .build(); } @Override @@ -311,7 +318,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(); } @@ -331,13 +338,32 @@ 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()) + .build(); + } + } + Symbol.Builder builder = SymbolUtils.createPointableSymbolBuilder(shape, name, typesPackageName); if (shape.hasTrait(ErrorTrait.ID)) { - builder.definitionFile("./types/errors.go"); + return builder.definitionFile("./types/errors.go").build(); } else { - builder.definitionFile("./types/types.go"); + return builder.definitionFile("./types/types.go").build(); } - 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 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 f8eae1917..c94eee2ad 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 @@ -15,9 +15,14 @@ package software.amazon.smithy.go.codegen.integration; +import java.util.Collections; +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; @@ -97,4 +102,58 @@ default void writeAdditionalFiles( ) { // 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; + } + } +}