Skip to content

Commit

Permalink
Merge pull request #30 from JordonPhillips/generate-clients
Browse files Browse the repository at this point in the history
Generate clients
  • Loading branch information
JordonPhillips authored May 20, 2020
2 parents 8852105 + b3369b2 commit 6d3b538
Show file tree
Hide file tree
Showing 7 changed files with 651 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,6 +66,7 @@ final class CodegenVisitor extends ShapeVisitor.Default<Void> {
private final List<GoIntegration> integrations = new ArrayList<>();
private final ProtocolGenerator protocolGenerator;
private final ApplicationProtocol applicationProtocol;
private final List<RuntimeClientPlugin> runtimePlugins = new ArrayList<>();

CodegenVisitor(PluginContext context) {
// Load all integrations.
Expand All @@ -70,6 +76,10 @@ final class CodegenVisitor extends ShapeVisitor.Default<Void> {
.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));

Expand Down Expand Up @@ -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;
}

Expand All @@ -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, 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.
TopDownIndex topDownIndex = model.getKnowledge(TopDownIndex.class);
Set<OperationShape> 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, applicationProtocol).run());
}
});
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 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;
private final ApplicationProtocol applicationProtocol;

OperationGenerator(
GoSettings settings,
Model model,
SymbolProvider symbolProvider,
GoWriter writer,
ServiceShape service,
OperationShape operation,
Symbol operationSymbol,
ApplicationProtocol applicationProtocol
) {
this.settings = settings;
this.model = model;
this.symbolProvider = symbolProvider;
this.writer = writer;
this.service = service;
this.operation = operation;
this.operationSymbol = operationSymbol;
this.applicationProtocol = applicationProtocol;
}

@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, () -> {
constructStack();

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 }");
});

constructHandler();

writer.write("result, metadata, err := handler.Handle(ctx, params)");
writer.write("if err != nil { return nil, err }");
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
// 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);
});
}

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());
}

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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* 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<GoIntegration> integrations;
private final List<RuntimeClientPlugin> runtimePlugins;
private final ApplicationProtocol applicationProtocol;

ServiceGenerator(
GoSettings settings,
Model model,
SymbolProvider symbolProvider,
GoWriter writer,
ServiceShape service,
List<GoIntegration> integrations,
List<RuntimeClientPlugin> runtimePlugins,
ApplicationProtocol applicationProtocol
) {
this.settings = settings;
this.model = model;
this.symbolProvider = symbolProvider;
this.writer = writer;
this.service = service;
this.integrations = integrations;
this.runtimePlugins = runtimePlugins;
this.applicationProtocol = applicationProtocol;
}

@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);
}

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");
writer.write("to.APIOptions = make([]$L, len(o.APIOptions))", API_OPTIONS_FUNC_NAME);
writer.write("copy(to.APIOptions, o.APIOptions)");
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);
}
}
}
Loading

0 comments on commit 6d3b538

Please sign in to comment.