[cli] new 'author template' command (#6441)

* [cli] new 'author template' command

This new command allows users to extract templates for authoring
(customization) without the complexity of finding and downloading a
specific directory for their versioned artifact.

Example usage:

```
openapi-generator author template -g java --library webclient
```

This will write all templates with library-specific templates to the
'./out' directory relative to the current directory.

CLI will refer the user to
https://openapi-generator.tech/docs/templating after generation

* [docs] Usage of author template command

* Log warning if author template fails to output requested library
This commit is contained in:
Jim Schubert
2020-05-28 00:12:59 -04:00
committed by GitHub
parent a017f3a892
commit 7d4bbcc29b
5 changed files with 360 additions and 3 deletions

View File

@@ -57,6 +57,11 @@ public class OpenAPIGenerator {
GenerateBatch.class
);
builder.withGroup("author")
.withDescription("Utilities for authoring generators or customizing templates.")
.withDefaultCommand(HelpCommand.class)
.withCommands(AuthorTemplate.class);
try {
builder.build().parse(args).run();

View File

@@ -0,0 +1,168 @@
package org.openapitools.codegen.cmd;
import io.airlift.airline.Command;
import io.airlift.airline.Option;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenConfigLoader;
import org.openapitools.codegen.CodegenConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.nio.file.spi.FileSystemProvider;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal", "unused"})
@Command(name = "template", description = "Retrieve templates for local modification")
public class AuthorTemplate extends OpenApiGeneratorCommand {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthorTemplate.class);
@Option(name = {"-g", "--generator-name"}, title = "generator name",
description = "generator to use (see list command for list)",
required = true)
private String generatorName;
@Option(name = {"--library"}, title = "library", description = CodegenConstants.LIBRARY_DESC)
private String library;
@Option(name = {"-o", "--output"}, title = "output directory",
description = "where to write the template files (defaults to 'out')")
private String output = "";
@Option(name = {"-v", "--verbose"}, description = "verbose mode")
private boolean verbose;
private Pattern pattern = null;
@Override
void execute() {
CodegenConfig config = CodegenConfigLoader.forName(generatorName);
String templateDirectory = config.templateDir();
log("Requesting '{}' from embedded resource directory '{}'", generatorName, templateDirectory);
Path embeddedTemplatePath;
try {
URI uri = Objects.requireNonNull(this.getClass().getClassLoader().getResource(templateDirectory)).toURI();
if ("jar".equals(uri.getScheme())) {
Optional<FileSystemProvider> provider = FileSystemProvider.installedProviders()
.stream()
.filter(p -> p.getScheme().equalsIgnoreCase("jar"))
.findFirst();
if (!provider.isPresent()) {
throw new ProviderNotFoundException("Unable to load jar file system provider");
}
try {
provider.get().getFileSystem(uri);
} catch (FileSystemNotFoundException ex) {
// File system wasn't loaded, so create it.
provider.get().newFileSystem(uri, Collections.emptyMap());
}
}
embeddedTemplatePath = Paths.get(uri);
log("Copying from jar location {}", embeddedTemplatePath.toAbsolutePath().toString());
File outputDir;
if (StringUtils.isNotEmpty(output)) {
outputDir = new File(output);
} else {
outputDir = new File("out");
}
Path outputDirPath = outputDir.toPath();
if (!Files.exists(outputDirPath)) {
Files.createDirectories(outputDirPath);
}
List<Path> generatedFiles = new ArrayList<>();
try (final Stream<Path> templates = Files.walk(embeddedTemplatePath)) {
templates.forEach(template -> {
log("Found template: {}", template.toAbsolutePath());
Path relativePath = embeddedTemplatePath.relativize(template);
if (shouldCopy(relativePath)) {
Path target = outputDirPath.resolve(relativePath.toString());
generatedFiles.add(target);
try {
if (Files.isDirectory(template)) {
if (Files.notExists(target)) {
log("Creating directory: {}", target.toAbsolutePath());
Files.createDirectories(target);
}
} else {
if (target.getParent() != null && Files.notExists(target.getParent())) {
log("Creating directory: {}", target.getParent());
Files.createDirectories(target.getParent());
}
log("Copying to: {}", target.toAbsolutePath());
Files.copy(template, target, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
LOGGER.error("Unable to create target location '{}'.", target);
}
} else {
log("Directory is excluded by library option: {}", relativePath);
}
});
}
if (StringUtils.isNotEmpty(library) && !generatedFiles.isEmpty()) {
Path librariesPath = outputDirPath.resolve("libraries");
Path targetLibrary = librariesPath.resolve(library);
String librariesPrefix = librariesPath.toString();
if (!Files.isDirectory(targetLibrary)) {
LOGGER.warn("The library '{}' was not extracted. Please verify the spelling and retry.", targetLibrary);
}
generatedFiles.stream()
.filter(p -> p.startsWith(librariesPrefix))
.forEach(p -> {
if (p.startsWith(targetLibrary)) {
// We don't care about empty directories, and not need to check directory for files.
if (!Files.isDirectory(p)) {
// warn if the file was not written
if (Files.notExists(p)) {
LOGGER.warn("An expected library file was not extracted: {}", p.toAbsolutePath());
}
}
} else {
LOGGER.warn("The library filter '{}' extracted an unexpected library path: {}", library, p.toAbsolutePath());
}
});
}
LOGGER.info("Extracted templates to '{}' directory. Refer to https://openapi-generator.tech/docs/templating for customization details.", outputDirPath);
} catch (URISyntaxException | IOException e) {
LOGGER.error("Unable to load embedded template directory.", e);
}
}
private void log(String format, Object... arguments) {
if (verbose) {
LOGGER.info(format, arguments);
}
}
private boolean shouldCopy(Path relativePath) {
String path = relativePath.toString();
if (StringUtils.isNotEmpty(library) && path.contains("libraries")) {
if (pattern == null) {
pattern = Pattern.compile(String.format(Locale.ROOT, "libraries[/\\\\]{1}%s[/\\\\]{1}.*", Pattern.quote(library)));
}
return pattern.matcher(path).matches();
}
return true;
}
}

View File

@@ -0,0 +1,71 @@
package org.openapitools.codegen.cmd;
import io.airlift.airline.Cli;
import org.testng.Assert;
import org.testng.ITestContext;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
public class AuthorTemplateTest {
Path outputDirectory;
@BeforeTest
public void setUp(ITestContext ctx) throws IOException {
outputDirectory = Files.createTempDirectory("AuthorTemplateTest");
outputDirectory.toFile().deleteOnExit();
}
@Test
public void smokeTestAuthorTemplateCommand(){
Cli.CliBuilder<Runnable> builder = createBuilder();
String[] arguments = new String[]{
"author",
"template",
"-g",
"java",
"--library",
"webclient",
"--output",
outputDirectory.toAbsolutePath().toString()
};
builder.build().parse(arguments).run();
// spot check root files
Assert.assertTrue(Files.exists(outputDirectory.resolve("ApiClient.mustache")));
Assert.assertTrue(Files.exists(outputDirectory.resolve("api_doc.mustache")));
Assert.assertTrue(Files.exists(outputDirectory.resolve("pom.mustache")));
Assert.assertTrue(Files.exists(outputDirectory.resolve("auth/OAuth.mustache")));
// check libraries files and subdirectories
Assert.assertTrue(Files.exists(outputDirectory.resolve("libraries/webclient/ApiClient.mustache")));
Assert.assertTrue(Files.exists(outputDirectory.resolve("libraries/webclient/pom.mustache")));
Assert.assertTrue(Files.exists(outputDirectory.resolve("libraries/webclient/auth/OAuth.mustache")));
// check non-existence of unselected libraries
Assert.assertFalse(Files.exists(outputDirectory.resolve("libraries/feign/build.gradle.mustache")));
Assert.assertFalse(Files.exists(outputDirectory.resolve("libraries/feign/auth/OAuth.mustache")));
Assert.assertFalse(Files.exists(outputDirectory.resolve("libraries/jersey2/api_doc.mustache")));
Assert.assertFalse(Files.exists(outputDirectory.resolve("libraries/jersey2/auth/HttpBasicAuth.mustache")));
Assert.assertFalse(Files.exists(outputDirectory.resolve("libraries/okhttp-gson/api.mustache")));
Assert.assertFalse(Files.exists(outputDirectory.resolve("libraries/okhttp-gson/auth/RetryingOAuth.mustache")));
}
private Cli.CliBuilder<Runnable> createBuilder(){
Cli.CliBuilder<Runnable> builder = new Cli.CliBuilder<>("openapi-generator-cli");
builder.withGroup("author")
.withDescription("Utilities for authoring generators or customizing templates.")
.withDefaultCommand(HelpCommand.class)
.withCommands(AuthorTemplate.class);
return builder;
}
}