Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate basic Go module with structures #2

Merged
merged 10 commits into from
Mar 18, 2020
8 changes: 2 additions & 6 deletions codegen/smithy-go-codegen-test/smithy-build.json
Expand Up @@ -3,12 +3,8 @@
"plugins": {
"go-codegen": {
"service": "example.weather#Weather",
"targetNamespace": "Weather",
"package": "weather",
"packageVersion": "0.0.1",
"packageJson": {
"license": "Apache-2.0"
}
"module": "weather",
"moduleVersion": "0.0.1"
}
}
}
@@ -0,0 +1,97 @@
/*
* 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.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.utils.StringUtils;

/**
* Utility methods likely to be needed across packages.
*/
public final class CodegenUtils {

private static final Logger LOGGER = Logger.getLogger(CodegenUtils.class.getName());

private CodegenUtils() {}

/**
* Executes a given shell command in a given directory.
*
* @param command The string command to execute, e.g. "go fmt".
* @param directory The directory to run the command in.
* @return Returns the console output of the command.
*/
public static String runCommand(String command, Path directory) {
String[] finalizedCommand;
if (System.getProperty("os.name").toLowerCase().startsWith("windows")) {
finalizedCommand = new String[]{"cmd.exe", "/c", command};
} else {
finalizedCommand = new String[]{"sh", "-c", command};
}

ProcessBuilder processBuilder = new ProcessBuilder(finalizedCommand)
.redirectErrorStream(true)
.directory(directory.toFile());

try {
Process process = processBuilder.start();
List<String> output = new ArrayList<>();

// Capture output for reporting.
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
process.getInputStream(), Charset.defaultCharset()))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
LOGGER.finest(line);
output.add(line);
}
}

process.waitFor();
process.destroy();

String joinedOutput = String.join(System.lineSeparator(), output);
if (process.exitValue() != 0) {
throw new CodegenException(String.format(
"Command `%s` failed with output:%n%n%s", command, joinedOutput));
}
return joinedOutput;
} catch (InterruptedException | IOException e) {
throw new CodegenException(e);
}
}

/**
* Gets the name under which the given package will be exported by default.
*
* @param packageName The full package name of the exported package.
* @return The name a the package will be imported under by default.
*/
public static String getDefaultPackageImportName(String packageName) {
if (StringUtils.isBlank(packageName) || !packageName.contains("/")) {
return packageName;
}
return packageName.substring(packageName.lastIndexOf('/') + 1);
}
}
@@ -0,0 +1,87 @@
/*
* 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.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.SymbolProvider;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.neighbor.Walker;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeVisitor;
import software.amazon.smithy.model.shapes.StructureShape;

/**
* Orchestrates Go client generation.
*/
final class CodegenVisitor extends ShapeVisitor.Default<Void> {

private static final Logger LOGGER = Logger.getLogger(CodegenVisitor.class.getName());

private final GoSettings settings;
private final Model model;
private final Model modelWithoutTraitShapes;
private final ServiceShape service;
private final FileManifest fileManifest;
private final SymbolProvider symbolProvider;
private final GoDelegator writers;

CodegenVisitor(PluginContext context) {
settings = GoSettings.from(context.getSettings());
model = context.getModel();
modelWithoutTraitShapes = context.getModelWithoutTraitShapes();
service = settings.getService(model);
fileManifest = context.getFileManifest();
LOGGER.info(() -> "Generating Go client for service " + service.getId());

symbolProvider = GoCodegenPlugin.createSymbolProvider(model);
writers = new GoDelegator(settings, model, fileManifest, symbolProvider);
}

void execute() {
// Generate models that are connected to the service being generated.
LOGGER.fine("Walking shapes from " + service.getId() + " to find shapes to generate");
Set<Shape> serviceShapes = new TreeSet<>(new Walker(modelWithoutTraitShapes).walkShapes(service));

for (Shape shape : serviceShapes) {
shape.accept(this);
}

LOGGER.fine("Flushing go writers");
writers.flushWriters();

LOGGER.fine("Generating go.mod file");
GoModGenerator.writeGoMod(settings, fileManifest);

LOGGER.fine("Running go fmt");
CodegenUtils.runCommand("go fmt", fileManifest.getBaseDir());
}

@Override
protected Void getDefault(Shape shape) {
return null;
}

@Override
public Void structureShape(StructureShape shape) {
writers.useShapeWriter(shape, writer -> new StructureGenerator(model, symbolProvider, writer, shape).run());
return null;
}
}
@@ -0,0 +1,45 @@
/*
* 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.build.PluginContext;
import software.amazon.smithy.build.SmithyBuildPlugin;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.model.Model;

/**
* Plugin to trigger Go code generation.
*/
public final class GoCodegenPlugin implements SmithyBuildPlugin {
@Override
public String getName() {
return "go-codegen";
}

@Override
public void execute(PluginContext context) {
new CodegenVisitor(context).execute();
}

/**
* Creates a Go symbol provider.
* @param model The model to generate symbols for.
* @return Returns the created provider.
*/
public static SymbolProvider createSymbolProvider(Model model) {
return new SymbolVisitor(model);
}
}
@@ -0,0 +1,85 @@
/*
* 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.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import software.amazon.smithy.build.FileManifest;
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.shapes.Shape;

/**
* Manages writers for Go files.
*/
final class GoDelegator {

private final GoSettings settings;
private final Model model;
private final FileManifest fileManifest;
private final SymbolProvider symbolProvider;
private final Map<String, GoWriter> writers = new HashMap<>();

GoDelegator(GoSettings settings, Model model, FileManifest fileManifest, SymbolProvider symbolProvider) {
this.settings = settings;
this.model = model;
this.fileManifest = fileManifest;
this.symbolProvider = symbolProvider;
}

/**
* Writes all pending writers to disk and then clears them out.
*/
void flushWriters() {
writers.forEach((filename, writer) -> fileManifest.writeFile(filename, writer.toString()));
writers.clear();
}

/**
* Gets a previously created writer or creates a new one if needed.
*
* @param shape Shape to create the writer for.
* @param writerConsumer Consumer that accepts and works with the file.
*/
void useShapeWriter(Shape shape, Consumer<GoWriter> writerConsumer) {
Symbol symbol = symbolProvider.toSymbol(shape);
String namespace = symbol.getNamespace();
if (namespace.equals(".")) {
namespace = CodegenUtils.getDefaultPackageImportName(settings.getModuleName());
}
GoWriter writer = checkoutWriter(symbol.getDefinitionFile(), namespace);

writer.pushState();
writerConsumer.accept(writer);
writer.popState();
}

private GoWriter checkoutWriter(String filename, String namespace) {
String formattedFilename = Paths.get(filename).normalize().toString();
boolean needsNewline = writers.containsKey(formattedFilename);

GoWriter writer = writers.computeIfAbsent(formattedFilename, f -> new GoWriter(namespace));

if (needsNewline) {
writer.write("\n");
}

return writer;
}
}
@@ -0,0 +1,50 @@
/*
* 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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import software.amazon.smithy.build.FileManifest;
import software.amazon.smithy.codegen.core.CodegenException;

/**
* Generates a go.mod file for the project.
*
* <p>See here for more information on the format: https://github.com/golang/go/wiki/Modules#gomod
*
* TODO: pull in dependencies
*/
final class GoModGenerator {

private GoModGenerator() {}

static void writeGoMod(GoSettings settings, FileManifest manifest) {
Path goModFile = manifest.getBaseDir().resolve("go.mod");

// `go mod init` will fail if the `go.mod` already exists, so this deletes
// it if it's present in the output. While it's technically possible
// to simply edit the file, it's easier to just start fresh.
if (Files.exists(goModFile)) {
try {
Files.delete(goModFile);
} catch (IOException e) {
throw new CodegenException("Failed to delete existing go.mod file", e);
}
}
CodegenUtils.runCommand("go mod init " + settings.getModuleName(), manifest.getBaseDir());
}
}