Skip to content

Commit

Permalink
Generate clients
Browse files Browse the repository at this point in the history
  • Loading branch information
JordonPhillips committed May 11, 2020
1 parent fc5a89c commit fc88148
Show file tree
Hide file tree
Showing 7 changed files with 591 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -53,6 +58,7 @@ final class CodegenVisitor extends ShapeVisitor.Default<Void> {
private final SymbolProvider symbolProvider;
private final GoDelegator writers;
private final List<GoIntegration> integrations = new ArrayList<>();
private final List<RuntimeClientPlugin> runtimePlugins = new ArrayList<>();

CodegenVisitor(PluginContext context) {
// Load all integrations.
Expand All @@ -62,6 +68,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 @@ -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;
}

Expand All @@ -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<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).run());
}
});
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
Original file line number Diff line number Diff line change
@@ -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<GoIntegration> integrations;
private final List<RuntimeClientPlugin> runtimePlugins;

ServiceGenerator(
GoSettings settings,
Model model,
SymbolProvider symbolProvider,
GoWriter writer,
ServiceShape service,
List<GoIntegration> integrations,
List<RuntimeClientPlugin> 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");
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,35 +41,46 @@ 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()) {
String memberName = symbolProvider.toMemberName(member);
writer.writeMemberDocs(model, member);
writer.write("$L $P", memberName, symbolProvider.toSymbol(member));
}
runnable.run();
writer.closeBlock("}").write("");
}

Expand Down
Loading

0 comments on commit fc88148

Please sign in to comment.