Skip to content

Commit 7d4bbcc

Browse files
authored
[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
1 parent a017f3a commit 7d4bbcc

File tree

5 files changed

+360
-3
lines changed

5 files changed

+360
-3
lines changed

docs/templating.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,41 @@ java -cp /path/totemplate-classpath-example-1.0-SNAPSHOT.jar:modules/openapi-gen
5050

5151
Note that our template directory is relative to the resource directory of the JAR defined on the classpath.
5252

53+
### Retrieving Templates
54+
55+
You will need to find and retrieve the templates for your desired generator in order to redefine structures, documentation, or API logic. We cover template customization in the following sections.
56+
57+
In OpenAPI Generator 5.0 and later, you can use the CLI command `author template` to extract embedded templates for your target generator. For example:
58+
59+
```
60+
openapi-generator author template -g java --library webclient
61+
```
62+
63+
For OpenAPI Generator versions prior to 5.0, you will want to find the [resources directory](https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources) for the generator you want to extend. This is generally easy to find as directories commonly follow the convention of `resources/<generator name>`. In cases where you're unsure, you will need to find the `embeddedTemplateDir` assignment in your desired generator. This is almost always assigned in the constructor of the generator class. The C# .Net Core generator assigns this as:
64+
65+
```
66+
embeddedTemplateDir = templateDir = "csharp-netcore";
67+
```
68+
69+
These templates are in our source repository at [modules/openapi-generator/src/main/resources/csharp-netcore](https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/csharp-netcore). Be sure to select the tag or branch for the version of OpenAPI Generator you're using before grabbing the templates.
70+
71+
**NOTE** If you have specific logic you'd like to modify such as modifying the generated README, you _only_ need to pull and modify this individual template. OpenAPI Generator will lookup templates in this order:
72+
73+
* User customized library path (e.g. `custom_template/libraries/feign/model.mustache`)
74+
* User customized generator top-level path (e.g. `custom_template/model.mustache`)
75+
* Embedded library path (e.g. `resources/Java/libraries/feign/model.mustache`)
76+
* Embedded top-level path (e.g. `resources/Java/model.mustache`)
77+
* Common embedded path (e.g. `resources/_common/model.mustache`)
78+
5379
### Custom Logic
5480

5581
For this example, let's modify a Java client to use AOP via [jcabi/jcabi-aspects](https://github.com/jcabi/jcabi-aspects). We'll log API method execution at the `INFO` level. The jcabi-aspects project could also be used to implement method retries on failures; this would be a great exercise to further play around with templating.
5682

5783
The Java generator supports a `library` option. This option works by defining base templates, then applying library-specific template overrides. This allows for template reuse for libraries sharing the same programming language. Templates defined as a library need only modify or extend the templates concerning the library, and generation falls back to the root templates (the "defaults") when not extended by the library. Generators which support the `library` option will only support the libraries known by the generator at compile time, and will throw a runtime error if you try to provide a custom library name.
5884

59-
To get started, we will need to copy our target generator's directory in full. The directory will be located under `modules/opeanpi-generator/src/main/resources/{generator}`. In general, the generator directory matches the generator name (what you would pass to the `generator` option), but this is not a requirement-- if you are having a hard time finding the template directory, look at the `embeddedTemplateDir` option in your target generator's implementation.
85+
To get started, we will need to copy our target generator's directory in full.
86+
87+
The directory will be located under `modules/opeanpi-generator/src/main/resources/{generator}`. In general, the generator directory matches the generator name (what you would pass to the `generator` option), but this is not a requirement-- if you are having a hard time finding the template directory, look at the `embeddedTemplateDir` option in your target generator's implementation.
6088

6189
If you've already cloned openapi-generator, find and copy the `modules/opeanpi-generator/src/main/resources/Java` directory. If you have the [Refined GitHub](https://github.com/sindresorhus/refined-github) Chrome or Firefox Extension, you can navigate to this directory on GitHub and click the "Download" button. Or, to pull the directory from latest master:
6290

docs/usage.md

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ openapi-generator help
1515
usage: openapi-generator-cli <command> [<args>]
1616

1717
The most commonly used openapi-generator-cli commands are:
18+
author Utilities for authoring generators or customizing templates.
1819
config-help Config help for chosen lang
1920
generate Generate code with the specified generator.
20-
help Display help information
21+
help Display help information about openapi-generator
2122
list Lists the available generators
2223
meta MetaGenerator. Generator for creating a new template set and configuration for Codegen. The output will be based on the language you specify, and includes default templates to include.
2324
validate Validate specification
24-
version Show version information
25+
version Show version information used in tooling
2526

2627
See 'openapi-generator-cli help <command>' for more information on a specific
2728
command.
@@ -670,3 +671,87 @@ EOF
670671
openapi-generator batch *.yaml
671672
```
672673
674+
## author
675+
676+
This command group contains utilities for authoring generators or customizing templates.
677+
678+
```
679+
openapi-generator help author
680+
NAME
681+
openapi-generator-cli author - Utilities for authoring generators or
682+
customizing templates.
683+
684+
SYNOPSIS
685+
openapi-generator-cli author
686+
openapi-generator-cli author template [(-v | --verbose)]
687+
[(-o <output directory> | --output <output directory>)]
688+
[--library <library>]
689+
(-g <generator name> | --generator-name <generator name>)
690+
691+
OPTIONS
692+
--help
693+
Display help about the tool
694+
695+
--version
696+
Display full version output
697+
698+
COMMANDS
699+
With no arguments, Display help information about openapi-generator
700+
701+
template
702+
Retrieve templates for local modification
703+
704+
With --verbose option, verbose mode
705+
706+
With --output option, where to write the template files (defaults to
707+
'out')
708+
709+
With --library option, library template (sub-template)
710+
711+
With --generator-name option, generator to use (see list command for
712+
list)
713+
```
714+
715+
### template
716+
717+
This command allows user to extract templates from the CLI jar which simplifies customization efforts.
718+
719+
```
720+
NAME
721+
openapi-generator-cli author template - Retrieve templates for local
722+
modification
723+
724+
SYNOPSIS
725+
openapi-generator-cli author template
726+
(-g <generator name> | --generator-name <generator name>)
727+
[--library <library>]
728+
[(-o <output directory> | --output <output directory>)]
729+
[(-v | --verbose)]
730+
731+
OPTIONS
732+
-g <generator name>, --generator-name <generator name>
733+
generator to use (see list command for list)
734+
735+
--library <library>
736+
library template (sub-template)
737+
738+
-o <output directory>, --output <output directory>
739+
where to write the template files (defaults to 'out')
740+
741+
-v, --verbose
742+
verbose mode
743+
```
744+
745+
Example:
746+
747+
Extract Java templates, limiting to the `webclient` library.
748+
749+
```
750+
openapi-generator author template -g java --library webclient
751+
```
752+
753+
Extract all Java templates:
754+
755+
```
756+
openapi-generator author template -g java
757+
```

modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/OpenAPIGenerator.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ public static void main(String[] args) {
5757
GenerateBatch.class
5858
);
5959

60+
builder.withGroup("author")
61+
.withDescription("Utilities for authoring generators or customizing templates.")
62+
.withDefaultCommand(HelpCommand.class)
63+
.withCommands(AuthorTemplate.class);
64+
6065
try {
6166
builder.build().parse(args).run();
6267

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package org.openapitools.codegen.cmd;
2+
3+
import io.airlift.airline.Command;
4+
import io.airlift.airline.Option;
5+
import org.apache.commons.lang3.StringUtils;
6+
import org.openapitools.codegen.CodegenConfig;
7+
import org.openapitools.codegen.CodegenConfigLoader;
8+
import org.openapitools.codegen.CodegenConstants;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.net.URI;
15+
import java.net.URISyntaxException;
16+
import java.nio.file.*;
17+
import java.nio.file.spi.FileSystemProvider;
18+
import java.util.*;
19+
import java.util.regex.Pattern;
20+
import java.util.stream.Stream;
21+
22+
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal", "unused"})
23+
@Command(name = "template", description = "Retrieve templates for local modification")
24+
public class AuthorTemplate extends OpenApiGeneratorCommand {
25+
26+
private static final Logger LOGGER = LoggerFactory.getLogger(AuthorTemplate.class);
27+
28+
@Option(name = {"-g", "--generator-name"}, title = "generator name",
29+
description = "generator to use (see list command for list)",
30+
required = true)
31+
private String generatorName;
32+
33+
@Option(name = {"--library"}, title = "library", description = CodegenConstants.LIBRARY_DESC)
34+
private String library;
35+
36+
@Option(name = {"-o", "--output"}, title = "output directory",
37+
description = "where to write the template files (defaults to 'out')")
38+
private String output = "";
39+
40+
@Option(name = {"-v", "--verbose"}, description = "verbose mode")
41+
private boolean verbose;
42+
43+
private Pattern pattern = null;
44+
45+
@Override
46+
void execute() {
47+
CodegenConfig config = CodegenConfigLoader.forName(generatorName);
48+
String templateDirectory = config.templateDir();
49+
50+
log("Requesting '{}' from embedded resource directory '{}'", generatorName, templateDirectory);
51+
52+
Path embeddedTemplatePath;
53+
try {
54+
URI uri = Objects.requireNonNull(this.getClass().getClassLoader().getResource(templateDirectory)).toURI();
55+
56+
if ("jar".equals(uri.getScheme())) {
57+
Optional<FileSystemProvider> provider = FileSystemProvider.installedProviders()
58+
.stream()
59+
.filter(p -> p.getScheme().equalsIgnoreCase("jar"))
60+
.findFirst();
61+
62+
if (!provider.isPresent()) {
63+
throw new ProviderNotFoundException("Unable to load jar file system provider");
64+
}
65+
66+
try {
67+
provider.get().getFileSystem(uri);
68+
} catch (FileSystemNotFoundException ex) {
69+
// File system wasn't loaded, so create it.
70+
provider.get().newFileSystem(uri, Collections.emptyMap());
71+
}
72+
}
73+
74+
embeddedTemplatePath = Paths.get(uri);
75+
76+
log("Copying from jar location {}", embeddedTemplatePath.toAbsolutePath().toString());
77+
78+
File outputDir;
79+
if (StringUtils.isNotEmpty(output)) {
80+
outputDir = new File(output);
81+
} else {
82+
outputDir = new File("out");
83+
}
84+
85+
Path outputDirPath = outputDir.toPath();
86+
if (!Files.exists(outputDirPath)) {
87+
Files.createDirectories(outputDirPath);
88+
}
89+
List<Path> generatedFiles = new ArrayList<>();
90+
try (final Stream<Path> templates = Files.walk(embeddedTemplatePath)) {
91+
templates.forEach(template -> {
92+
log("Found template: {}", template.toAbsolutePath());
93+
Path relativePath = embeddedTemplatePath.relativize(template);
94+
if (shouldCopy(relativePath)) {
95+
Path target = outputDirPath.resolve(relativePath.toString());
96+
generatedFiles.add(target);
97+
try {
98+
if (Files.isDirectory(template)) {
99+
if (Files.notExists(target)) {
100+
log("Creating directory: {}", target.toAbsolutePath());
101+
Files.createDirectories(target);
102+
}
103+
} else {
104+
if (target.getParent() != null && Files.notExists(target.getParent())) {
105+
log("Creating directory: {}", target.getParent());
106+
Files.createDirectories(target.getParent());
107+
}
108+
log("Copying to: {}", target.toAbsolutePath());
109+
Files.copy(template, target, StandardCopyOption.REPLACE_EXISTING);
110+
}
111+
} catch (IOException e) {
112+
LOGGER.error("Unable to create target location '{}'.", target);
113+
}
114+
} else {
115+
log("Directory is excluded by library option: {}", relativePath);
116+
}
117+
});
118+
}
119+
120+
if (StringUtils.isNotEmpty(library) && !generatedFiles.isEmpty()) {
121+
Path librariesPath = outputDirPath.resolve("libraries");
122+
Path targetLibrary = librariesPath.resolve(library);
123+
String librariesPrefix = librariesPath.toString();
124+
if (!Files.isDirectory(targetLibrary)) {
125+
LOGGER.warn("The library '{}' was not extracted. Please verify the spelling and retry.", targetLibrary);
126+
}
127+
generatedFiles.stream()
128+
.filter(p -> p.startsWith(librariesPrefix))
129+
.forEach(p -> {
130+
if (p.startsWith(targetLibrary)) {
131+
// We don't care about empty directories, and not need to check directory for files.
132+
if (!Files.isDirectory(p)) {
133+
// warn if the file was not written
134+
if (Files.notExists(p)) {
135+
LOGGER.warn("An expected library file was not extracted: {}", p.toAbsolutePath());
136+
}
137+
}
138+
} else {
139+
LOGGER.warn("The library filter '{}' extracted an unexpected library path: {}", library, p.toAbsolutePath());
140+
}
141+
});
142+
}
143+
144+
LOGGER.info("Extracted templates to '{}' directory. Refer to https://openapi-generator.tech/docs/templating for customization details.", outputDirPath);
145+
} catch (URISyntaxException | IOException e) {
146+
LOGGER.error("Unable to load embedded template directory.", e);
147+
}
148+
}
149+
150+
private void log(String format, Object... arguments) {
151+
if (verbose) {
152+
LOGGER.info(format, arguments);
153+
}
154+
}
155+
156+
private boolean shouldCopy(Path relativePath) {
157+
String path = relativePath.toString();
158+
if (StringUtils.isNotEmpty(library) && path.contains("libraries")) {
159+
if (pattern == null) {
160+
pattern = Pattern.compile(String.format(Locale.ROOT, "libraries[/\\\\]{1}%s[/\\\\]{1}.*", Pattern.quote(library)));
161+
}
162+
163+
return pattern.matcher(path).matches();
164+
}
165+
166+
return true;
167+
}
168+
}

0 commit comments

Comments
 (0)