Skip to content

Commit

Permalink
Add templates listing to smithy init command
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewFossAWS committed Jun 16, 2023
1 parent 8c8accb commit 72aef0d
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public void unexpectedTemplate() {
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])"));
containsString("Invalid template `blabla`. `Smithy-Examples` provides the following templates"));
assertThat(result.getExitCode(), is(1));
});
});
Expand All @@ -82,6 +82,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 +111,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 @@ -21,7 +21,10 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import software.amazon.smithy.cli.ArgumentReceiver;
import software.amazon.smithy.cli.Arguments;
Expand All @@ -30,14 +33,21 @@
import software.amazon.smithy.cli.ColorTheme;
import software.amazon.smithy.cli.Command;
import software.amazon.smithy.cli.HelpPrinter;
import software.amazon.smithy.cli.Style;
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 @@ -64,45 +74,93 @@ public int execute(Arguments arguments, Env 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), Style.BRIGHT_WHITE);
}
}

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

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;
StringBuilder builder = new StringBuilder()
.append(pad(NAME.toUpperCase(Locale.US), maxTemplateLength))
.append(space)
.append(DOCUMENTATION.toUpperCase(Locale.US))
.append(System.lineSeparator())
.append(pad("", maxTemplateLength).replace(' ', '-'))
.append(space)
.append(pad("", maxDocumentationLength).replace(' ', '-'))
.append(System.lineSeparator());

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

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

// 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 +171,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) {
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 +220,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 +272,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,
"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> {
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;
}
}
}

0 comments on commit 72aef0d

Please sign in to comment.