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

Add templates listing to smithy init command #1825

Merged
merged 1 commit into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,20 @@ public void unexpectedTemplate() {
IntegUtils.withTempDir("unexpectedTemplate", dir -> {
RunResult result = IntegUtils.run(
dir, ListUtils.of("init", "-t", "blabla", "-u", templatesDir.toString()));
assertThat(result.getOutput(),
containsString("Missing expected member `blabla` from `templates` object ([3, 18])"));

String expectedOutput = new StringBuilder()
.append("Invalid template `blabla`. `Smithy-Examples` provides the following templates:")
.append(System.lineSeparator())
.append(System.lineSeparator())
.append("NAME DOCUMENTATION")
.append(System.lineSeparator())
.append("-------------- ---------------------------------------------------------------")
.append(System.lineSeparator())
.append("quickstart-cli Smithy Quickstart example weather service using the Smithy CLI.")
.append(System.lineSeparator())
.toString();

assertThat(result.getOutput(), containsString(expectedOutput));
assertThat(result.getExitCode(), is(1));
});
});
Expand All @@ -82,6 +94,15 @@ public void withDirectoryArg() {
assertThat(result.getExitCode(), is(0));
assertThat(Files.exists(Paths.get(dir.toString(), "hello-world")), is(true));
});

IntegUtils.withTempDir("withNestedDirectoryArg", dir -> {
RunResult result = IntegUtils.run(dir, ListUtils.of(
"init", "-t", "quickstart-cli", "-o", "./hello/world", "-u", templatesDir.toString()));
assertThat(result.getOutput(),
containsString("Smithy project created in directory: ./hello/world"));
assertThat(result.getExitCode(), is(0));
assertThat(Files.exists(Paths.get(dir.toString(), "./hello/world")), is(true));
});
});
}

Expand All @@ -102,6 +123,30 @@ public void withLongHandArgs() {
});
}

@Test
public void withListArg() {
IntegUtils.withProject(PROJECT_NAME, templatesDir -> {
setupTemplatesDirectory(templatesDir);

IntegUtils.withTempDir("withListArg", dir -> {
RunResult result = IntegUtils.run(dir, ListUtils.of(
"init", "--list", "--url", templatesDir.toString()));

String expectedOutput = new StringBuilder()
.append("NAME DOCUMENTATION")
.append(System.lineSeparator())
.append("-------------- ---------------------------------------------------------------")
.append(System.lineSeparator())
.append("quickstart-cli Smithy Quickstart example weather service using the Smithy CLI.")
.append(System.lineSeparator())
.toString();

assertThat(result.getOutput(), containsString(expectedOutput));
assertThat(result.getExitCode(), is(0));
});
});
}

private static void run(List<String> args, Path root) {
StringBuilder output = new StringBuilder();
int result = IoUtils.runCommand(args, root, output, Collections.emptyMap());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Consumer;
import software.amazon.smithy.cli.ArgumentReceiver;
import software.amazon.smithy.cli.Arguments;
Expand All @@ -31,13 +34,19 @@
import software.amazon.smithy.cli.Command;
import software.amazon.smithy.cli.HelpPrinter;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.utils.IoUtils;
import software.amazon.smithy.utils.ListUtils;

final class InitCommand implements Command {
private static final String SMITHY_TEMPLATE_JSON = "smithy-templates.json";
private static final String DEFAULT_REPOSITORY_URL = "https://github.com/smithy-lang/smithy-examples.git";

private static final String DOCUMENTATION = "documentation";

private static final String NAME = "name";

private final String parentCommandName;

InitCommand(String parentCommandName) {
Expand All @@ -57,52 +66,106 @@ public String getSummary() {
@Override
public int execute(Arguments arguments, Env env) {
arguments.addReceiver(new Options());
CommandAction action = HelpActionWrapper.fromCommand(this, parentCommandName, this::run);
CommandAction action = HelpActionWrapper.fromCommand(this, parentCommandName, c -> {
ColorBuffer buffer = ColorBuffer.of(c, new StringBuilder());
buffer.println("Examples:");
buffer.println(" smithy init --list", ColorTheme.LITERAL);
buffer.println(" smithy init -o /tmp/quickstart-gradle -t quickstart-gradle", ColorTheme.LITERAL);
return buffer.toString();
}, this::run);
return action.apply(arguments, env);
}

private int run(Arguments arguments, Env env) {
Options options = arguments.getReceiver(Options.class);
try {
this.cloneTemplate(options.repositoryUrl, options.template, options.directory, env);
final Path root = Paths.get(".");
final Path temp = Files.createTempDirectory("temp");

loadSmithyTemplateJsonFile(options.repositoryUrl, root, temp);

final ObjectNode smithyTemplatesNode = getSmithyTemplatesNode(temp);

if (options.listTemplates) {
this.listTemplates(smithyTemplatesNode, env);
} else {
this.cloneTemplate(temp, smithyTemplatesNode, options.template, options.directory, env);
}
} catch (IOException | InterruptedException | URISyntaxException e) {
throw new RuntimeException(e);
}

return 0;
}

private void cloneTemplate(String repositoryUrl, String template, String directory, Env env)
throws IOException, InterruptedException, URISyntaxException {
private void listTemplates(ObjectNode smithyTemplatesNode, Env env) throws IOException {
try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stderr())) {
buffer.println(getTemplateList(smithyTemplatesNode, env));
}
}

if (template == null || template.isEmpty()) {
throw new IllegalArgumentException("Please specify a template using `--template` or `-t`");
private String getTemplateList(ObjectNode smithyTemplatesNode, Env env) {
int maxTemplateLength = 0;
int maxDocumentationLength = 0;
Map<String, String> templates = new TreeMap<>();

for (Map.Entry<StringNode, Node> entry : getTemplatesNode(smithyTemplatesNode).getMembers().entrySet()) {
String template = entry.getKey().getValue();
String documentation = entry.getValue()
.expectObjectNode()
.expectMember(DOCUMENTATION, String.format(
"Missing expected member `%s` from `%s` object", DOCUMENTATION, template))
.expectStringNode()
.getValue();

templates.put(template, documentation);

maxTemplateLength = Math.max(maxTemplateLength, template.length());
maxDocumentationLength = Math.max(maxDocumentationLength, documentation.length());
}

final Path root = Paths.get(".");
final Path temp = Files.createTempDirectory("temp");
final String space = " ";

// Use templateName if directory is not specified
if (directory == null) {
directory = template;
ColorBuffer builder = ColorBuffer.of(env.colors(), new StringBuilder())
.print(pad(NAME.toUpperCase(Locale.US), maxTemplateLength), ColorTheme.LITERAL)
.print(space)
.print(DOCUMENTATION.toUpperCase(Locale.US), ColorTheme.LITERAL)
.println()
.print(pad("", maxTemplateLength).replace(' ', '-'), ColorTheme.MUTED)
.print(space)
.print(pad("", maxDocumentationLength).replace(' ', '-'), ColorTheme.MUTED)
.println();

for (Map.Entry<String, String> entry : templates.entrySet()) {
String template = entry.getKey();
String doc = entry.getValue();
builder.print(pad(template, maxTemplateLength))
.print(space)
.print(pad(doc, maxDocumentationLength))
.println();
}

// Check Git is installed
exec(ListUtils.of("git", "clone", "--filter=blob:none", "--no-checkout", "--depth", "1", "--sparse",
repositoryUrl, temp.toString()), root);
return builder.toString();
}

// Download template json file
exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", SMITHY_TEMPLATE_JSON), temp);
private void cloneTemplate(Path temp, ObjectNode smithyTemplatesNode, String template, String directory, Env env)
throws IOException, InterruptedException, URISyntaxException {

exec(ListUtils.of("git", "checkout"), temp);
if (template == null || template.isEmpty()) {
throw new IllegalArgumentException("Please specify a template using `--template` or `-t`");
}

ObjectNode templatesNode = getTemplatesNode(smithyTemplatesNode);

if (!templatesNode.containsMember(template)) {
throw new IllegalArgumentException(String.format(
"Invalid template `%s`. `%s` provides the following templates:%n%n%s",
template, getTemplatesName(smithyTemplatesNode), getTemplateList(smithyTemplatesNode, env)));
}

// Retrieve template path from smithy-templates.json
String templatePath = readJsonFileAsNode(Paths.get(temp.toString(), SMITHY_TEMPLATE_JSON))
.expectObjectNode()
.expectMember("templates", String.format(
"Missing expected member `templates` from %s", SMITHY_TEMPLATE_JSON))
.expectObjectNode()
.expectMember(template, String.format("Missing expected member `%s` from `templates` object", template))
final String templatePath = templatesNode
.expectObjectMember(template)
.expectObjectNode()
.expectMember("path", String.format("Missing expected member `path` from `%s` object", template))
.expectStringNode()
Expand All @@ -113,13 +176,45 @@ private void cloneTemplate(String repositoryUrl, String template, String directo

exec(ListUtils.of("git", "checkout"), temp);

// Use templateName if directory is not specified
if (directory == null) {
directory = template;
}

IoUtils.copyDir(Paths.get(temp.toString(), templatePath), Paths.get(directory));

try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stderr())) {
buffer.println(String.format("Smithy project created in directory: %s", directory), ColorTheme.SUCCESS);
}
}

private static void loadSmithyTemplateJsonFile(String repositoryUrl, Path root, Path temp) {
AndrewFossAWS marked this conversation as resolved.
Show resolved Hide resolved
exec(ListUtils.of("git", "clone", "--filter=blob:none", "--no-checkout", "--depth", "1", "--sparse",
repositoryUrl, temp.toString()), root);

exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", SMITHY_TEMPLATE_JSON), temp);

exec(ListUtils.of("git", "checkout"), temp);
}

private static ObjectNode getTemplatesNode(ObjectNode smithyTemplatesNode) {
return smithyTemplatesNode
.expectMember("templates", String.format(
"Missing expected member `templates` from %s", SMITHY_TEMPLATE_JSON))
.expectObjectNode();
}

private static String getTemplatesName(ObjectNode smithyTemplatesNode) {
return smithyTemplatesNode
.expectMember(NAME, String.format("Missing expected member `%s` from %s", NAME, SMITHY_TEMPLATE_JSON))
.expectStringNode()
.getValue();
}

private static ObjectNode getSmithyTemplatesNode(Path jsonFilePath) {
return readJsonFileAsNode(Paths.get(jsonFilePath.toString(), SMITHY_TEMPLATE_JSON)).expectObjectNode();
}

private static String exec(List<String> args, Path directory) {
StringBuilder output = new StringBuilder();
int code = IoUtils.runCommand(args, directory, output, Collections.emptyMap());
Expand All @@ -130,15 +225,32 @@ private static String exec(List<String> args, Path directory) {
return output.toString();
}

private Node readJsonFileAsNode(Path jsonFilePath) {
private static Node readJsonFileAsNode(Path jsonFilePath) {
return Node.parse(IoUtils.readUtf8File(jsonFilePath));
}

private static String pad(String s, int n) {
return String.format("%-" + n + "s", s);
}

private static final class Options implements ArgumentReceiver {
private String template;
private String directory;
private Boolean listTemplates = false;
private String repositoryUrl = DEFAULT_REPOSITORY_URL;

@Override
public boolean testOption(String name) {
switch (name) {
case "--list":
case "-l":
listTemplates = true;
return true;
default:
return false;
}
}

@Override
public Consumer<String> testParameter(String name) {
switch (name) {
Expand All @@ -165,6 +277,8 @@ public void registerHelp(HelpPrinter printer) {
"Smithy templates repository url");
printer.param("--output", "-o", "new-smithy-project",
"Smithy project directory");
printer.param("--list", "-l", null,
AndrewFossAWS marked this conversation as resolved.
Show resolved Hide resolved
"List available templates");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@

package software.amazon.smithy.utils;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
Expand All @@ -31,6 +29,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -40,7 +39,6 @@
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;

/**
* Utilities for IO operations.
Expand Down Expand Up @@ -365,19 +363,43 @@ public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOExce
}

public static void copyDir(Path src, Path dest) {
try (Stream<Path> stream = Files.walk(src)) {
stream.forEach(source -> copyFile(source, dest.resolve(src.relativize(source))));
try {
Files.walkFileTree(src, new CopyFileVisitor(src, dest));
} catch (IOException e) {
throw new RuntimeException(String.format(
"Error copying directory from \"%s\" to \"%s\": %s", src, dest, e.getMessage()), e);
}
}

public static void copyFile(Path source, Path dest) {
try {
Files.copy(source, dest, REPLACE_EXISTING);
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
private static final class CopyFileVisitor extends SimpleFileVisitor<Path> {
AndrewFossAWS marked this conversation as resolved.
Show resolved Hide resolved
private final Path source;
private final Path target;

CopyFileVisitor(Path source, Path target) {
this.source = source;
this.target = target;
}

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path resolve = target.resolve(source.relativize(dir));
if (Files.notExists(resolve)) {
Files.createDirectories(resolve);
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path resolve = target.resolve(source.relativize(file));
Files.copy(file, resolve, StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;

}

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.TERMINATE;
}
}
}