[cli][core] Add support for dry-run and display (#5332)

* 👕🎨 Minor refactor DefaultGenerator

This cleans up some lint warnings and improve general code cleanliness
of DefaultGenerator.

Specifically:

* logger strings are now using the built-in log formatter rather than
  constructing new strings regardless of log level.
* Diamond operators are used where possible
* Some long-unused commented code has been removed
* Lambdas are used where possible
* Redundant operations are merged (HashMap constructor used rather than
  subsequent putAll on a collection, for example)

* [cli][core] Add support for dry-run and display

CLI now supports `--dry-run`, which will output a file change status
similar to git status --porcelain.

The user may also specify `--verbose` for a one-liner below each file
explaining why the change operation might take place.
This commit is contained in:
Jim Schubert 2020-02-18 20:36:17 -05:00 committed by GitHub
parent 1ec2c26053
commit db47b95fc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 597 additions and 193 deletions

View File

@ -3,6 +3,76 @@ id: debugging
title: Debugging title: Debugging
--- ---
## Generation
As a user there may be times when generated outputs don't match your expectations it's unclear why. The CLI supports a `--dry-run` option which may be used to inspect the anticipated file operations without making changes to the file system.
Suppose you generate using the `--minimal-update` option, and you notice on subsequent generations of a client that no files have changed. This is by design.
For example, if you generate the aspnetcore generator passing `--minimal-update --dry-run` to the sample generation script in the code repository:
```bash
export JAVA_OPTS="-Dlog.level=off"
./bin/aspnetcore-petstore-server.sh --minimal-update --dry-run
```
You'll see the output similar to the following:
```
# START SCRIPT: ./bin/aspnetcore-petstore-server.sh
Dry Run Results:
s /path/to/aspnetcore/.openapi-generator-ignore
n /path/to/aspnetcore/.openapi-generator/VERSION
n /path/to/aspnetcore/Org.OpenAPITools.sln
n /path/to/aspnetcore/README.md
n /path/to/aspnetcore/build.bat
n /path/to/aspnetcore/build.sh
w /path/to/aspnetcore/src/Org.OpenAPITools/.gitignore
n /path/to/aspnetcore/src/Org.OpenAPITools/Attributes/ValidateModelStateAttribute.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Authentication/ApiAuthentication.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Controllers/PetApi.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Controllers/StoreApi.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Controllers/UserApi.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Converters/CustomEnumConverter.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Dockerfile
n /path/to/aspnetcore/src/Org.OpenAPITools/Filters/BasePathFilter.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Filters/GeneratePathParamsValidationFilter.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Models/ApiResponse.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Models/Category.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Models/Order.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Models/Pet.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Models/Tag.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Models/User.cs
n /path/to/aspnetcore/src/Org.OpenAPITools/Org.OpenAPITools.csproj
n /path/to/aspnetcore/src/Org.OpenAPITools/Program.cs
w /path/to/aspnetcore/src/Org.OpenAPITools/Properties/launchSettings.json
n /path/to/aspnetcore/src/Org.OpenAPITools/Startup.cs
w /path/to/aspnetcore/src/Org.OpenAPITools/appsettings.json
w /path/to/aspnetcore/src/Org.OpenAPITools/wwwroot/README.md
w /path/to/aspnetcore/src/Org.OpenAPITools/wwwroot/index.html
n /path/to/aspnetcore/src/Org.OpenAPITools/wwwroot/openapi-original.json
w /path/to/aspnetcore/src/Org.OpenAPITools/wwwroot/web.config
States:
- w Write
- n Write if New/Updated
- i Ignored
- s Skipped Overwrite
- k Skipped by user option(s)
- e Error evaluating file write state
```
The output lists the files which would be written in a normal run of the tool. Notice that we skip `.openapi-generator-ignore` because the file exists and we don't want to blow away the user's generation rules. Most of these files will overwrite output files only if the contents slated for write are different from those on the filesystem; this is denoted by an `n` preceding the filename. Some of the above lines begin with a `w`, meaning these files will _always_ result in a write operation.
If you find an operation that you feel should result in a different state, please [open an issue](https://github.com/OpenAPITools/openapi-generator/issues/new/choose) or [submit a pull request](https://github.com/OpenAPITools/openapi-generator/compare) to change the behavior (we welcome all contributions).
## Templates ## Templates
Sometimes, you may have issues with variables in your templates. As discussed in the [templating](./templating.md) docs, we offer a variety of system properties for inspecting the models bound to templates. Sometimes, you may have issues with variables in your templates. As discussed in the [templating](./templating.md) docs, we offer a variety of system properties for inspecting the models bound to templates.

View File

@ -242,16 +242,16 @@ NAME
SYNOPSIS SYNOPSIS
openapi-generator-cli generate openapi-generator-cli generate
[(-a <authorization> | --auth <authorization>)] [(-a <authorization> | --auth <authorization>)]
[--api-package <api package>] [--artifact-id <artifact id>] [--api-name-suffix <api name suffix>] [--api-package <api package>]
[--artifact-version <artifact version>] [--artifact-id <artifact id>] [--artifact-version <artifact version>]
[(-c <configuration file> | --config <configuration file>)] [(-c <configuration file> | --config <configuration file>)]
[-D <system properties>...] [-D <system properties>...] [--dry-run]
[(-e <templating engine> | --engine <templating engine>)] [(-e <templating engine> | --engine <templating engine>)]
[--enable-post-process-file] [--enable-post-process-file]
[(-g <generator name> | --generator-name <generator name>)] [(-g <generator name> | --generator-name <generator name>)]
[--generate-alias-as-model] [--git-repo-id <git repo id>] [--generate-alias-as-model] [--git-host <git host>]
[--git-user-id <git user id>] [--group-id <group id>] [--git-repo-id <git repo id>] [--git-user-id <git user id>]
[--http-user-agent <http user agent>] [--group-id <group id>] [--http-user-agent <http user agent>]
(-i <spec file> | --input-spec <spec file>) (-i <spec file> | --input-spec <spec file>)
[--ignore-file-override <ignore file override location>] [--ignore-file-override <ignore file override location>]
[--import-mappings <import mappings>...] [--import-mappings <import mappings>...]
@ -283,6 +283,11 @@ OPTIONS
remotely. Pass in a URL-encoded string of name:header with a comma remotely. Pass in a URL-encoded string of name:header with a comma
separating multiple values separating multiple values
--api-name-suffix <api name suffix>
Suffix that will be appended to all API names ('tags'). Default:
Api. e.g. Pet => PetApi. Note: Only ruby, python, jaxrs generators
suppport this feature at the moment.
--api-package <api package> --api-package <api package>
package for generated api classes package for generated api classes
@ -295,23 +300,26 @@ OPTIONS
generated library's filename generated library's filename
-c <configuration file>, --config <configuration file> -c <configuration file>, --config <configuration file>
Path to configuration file configuration file. It can be json or Path to configuration file. It can be JSON or YAML. If file is JSON,
yaml.If file is json, the content should have the format the content should have the format {"optionKey":"optionValue",
{"optionKey":"optionValue", "optionKey1":"optionValue1"...}.If file "optionKey1":"optionValue1"...}. If file is YAML, the content should
is yaml, the content should have the format optionKey: have the format optionKey: optionValue. Supported options can be
optionValueSupported options can be different for each language. Run different for each language. Run config-help -g {generator name}
config-help -g {generator name} command for language specific config command for language-specific config options.
options.
-D <system properties> -D <system properties>
sets specified system properties in the format of sets specified system properties in the format of
name=value,name=value (or multiple options, each with name=value) name=value,name=value (or multiple options, each with name=value)
--dry-run
Try things out and report on potential changes (without actually
making changes).
-e <templating engine>, --engine <templating engine> -e <templating engine>, --engine <templating engine>
templating engine: "mustache" (default) or "handlebars" (beta) templating engine: "mustache" (default) or "handlebars" (beta)
--enable-post-process-file --enable-post-process-file
enablePostProcessFile Enable post-processing file using environment variables.
-g <generator name>, --generator-name <generator name> -g <generator name>, --generator-name <generator name>
generator to use (see list command for list) generator to use (see list command for list)
@ -324,6 +332,9 @@ OPTIONS
i.e. the 'additionalproperties' attribute is set on that object. i.e. the 'additionalproperties' attribute is set on that object.
An 'array' schema is a list of sub schemas in a OAS document. An 'array' schema is a list of sub schemas in a OAS document.
--git-host <git host>
Git host, e.g. gitlab.com.
--git-repo-id <git repo id> --git-repo-id <git repo id>
Git repo ID, e.g. openapi-generator. Git repo ID, e.g. openapi-generator.
@ -377,12 +388,10 @@ OPTIONS
Only write output files that have changed. Only write output files that have changed.
--model-name-prefix <model name prefix> --model-name-prefix <model name prefix>
Prefix that will be prepended to all model names. Default is the Prefix that will be prepended to all model names.
empty string.
--model-name-suffix <model name suffix> --model-name-suffix <model name suffix>
Suffix that will be appended to all model names. Default is the Suffix that will be appended to all model names.
empty string.
--model-package <model package> --model-package <model package>
package for generated models package for generated models
@ -415,8 +424,8 @@ OPTIONS
generation. generation.
--server-variables <server variables> --server-variables <server variables>
sets server variables for spec documents which support variable sets server variables overrides for spec documents which support
templating of servers. variable templating of servers.
--skip-validate-spec --skip-validate-spec
Skips the default behavior of validating an input specification. Skips the default behavior of validating an input specification.

View File

@ -91,6 +91,9 @@ public class Generate implements Runnable {
+ "overwritten during the generation.") + "overwritten during the generation.")
private Boolean skipOverwrite; private Boolean skipOverwrite;
@Option(name = { "--dry-run" }, title = "Dry run",
description = "Try things out and report on potential changes (without actually making changes).")
private Boolean isDryRun;
@Option(name = {"--package-name"}, title = "package name", @Option(name = {"--package-name"}, title = "package name",
description = CodegenConstants.PACKAGE_NAME_DESC) description = CodegenConstants.PACKAGE_NAME_DESC)
@ -416,7 +419,7 @@ public class Generate implements Runnable {
// this null check allows us to inject for unit testing. // this null check allows us to inject for unit testing.
if (generator == null) { if (generator == null) {
generator = new DefaultGenerator(); generator = new DefaultGenerator(isDryRun);
} }
generator.opts(clientOptInput); generator.opts(clientOptInput);

View File

@ -17,7 +17,7 @@
package org.openapitools.codegen; package org.openapitools.codegen;
import java.nio.charset.Charset; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.Arrays; import java.util.Arrays;
@ -28,11 +28,15 @@ import org.slf4j.LoggerFactory;
import java.io.*; import java.io.*;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner; import java.util.Scanner;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public abstract class AbstractGenerator implements TemplatingGenerator { public abstract class AbstractGenerator implements TemplatingGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractGenerator.class); private static final Logger LOGGER = LoggerFactory.getLogger(AbstractGenerator.class);
protected boolean dryRun = false;
protected Map<String, DryRunStatus> dryRunStatusMap = new HashMap<>();
/** /**
* Is the minimal-file-update option enabled? * Is the minimal-file-update option enabled?
@ -50,19 +54,19 @@ public abstract class AbstractGenerator implements TemplatingGenerator {
* @throws IOException If file cannot be written. * @throws IOException If file cannot be written.
*/ */
public File writeToFile(String filename, String contents) throws IOException { public File writeToFile(String filename, String contents) throws IOException {
return writeToFile(filename, contents.getBytes(Charset.forName("UTF-8"))); return writeToFile(filename, contents.getBytes(StandardCharsets.UTF_8));
} }
/** /**
* Write bytes to a file * Write bytes to a file
* *
* @param filename The name of file to write * @param filename The name of file to write
* @param contents The contents bytes. Typically this is a UTF-8 formatted string. * @param contents The contents bytes. Typically, this is a UTF-8 formatted string.
* @return File representing the written file. * @return File representing the written file.
* @throws IOException If file cannot be written. * @throws IOException If file cannot be written.
*/ */
@SuppressWarnings("static-method") @SuppressWarnings("static-method")
public File writeToFile(String filename, byte contents[]) throws IOException { public File writeToFile(String filename, byte[] contents) throws IOException {
if (getEnableMinimalUpdate()) { if (getEnableMinimalUpdate()) {
String tempFilename = filename + ".tmp"; String tempFilename = filename + ".tmp";
// Use Paths.get here to normalize path (for Windows file separator, space escaping on Linux/Mac, etc) // Use Paths.get here to normalize path (for Windows file separator, space escaping on Linux/Mac, etc)

View File

@ -53,6 +53,9 @@ import java.nio.file.Path;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.*; import java.util.*;
import static org.openapitools.codegen.utils.OnceLogger.once;
@SuppressWarnings("rawtypes")
public class DefaultGenerator extends AbstractGenerator implements Generator { public class DefaultGenerator extends AbstractGenerator implements Generator {
protected final Logger LOGGER = LoggerFactory.getLogger(DefaultGenerator.class); protected final Logger LOGGER = LoggerFactory.getLogger(DefaultGenerator.class);
protected CodegenConfig config; protected CodegenConfig config;
@ -73,11 +76,21 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
private String contextPath; private String contextPath;
private Map<String, String> generatorPropertyDefaults = new HashMap<>(); private Map<String, String> generatorPropertyDefaults = new HashMap<>();
public DefaultGenerator() {
}
public DefaultGenerator(Boolean dryRun) {
this.dryRun = Boolean.TRUE.equals(dryRun);
LOGGER.info("Generating with dryRun={}", this.dryRun);
}
@Override @Override
public boolean getEnableMinimalUpdate() { public boolean getEnableMinimalUpdate() {
return config.isEnableMinimalUpdate(); return config.isEnableMinimalUpdate();
} }
@SuppressWarnings("deprecation")
@Override @Override
public Generator opts(ClientOptInput opts) { public Generator opts(ClientOptInput opts) {
this.opts = opts; this.opts = opts;
@ -285,18 +298,36 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
for (String templateName : config.modelTestTemplateFiles().keySet()) { for (String templateName : config.modelTestTemplateFiles().keySet()) {
String suffix = config.modelTestTemplateFiles().get(templateName); String suffix = config.modelTestTemplateFiles().get(templateName);
String filename = config.modelTestFileFolder() + File.separator + config.toModelTestFilename(modelName) + suffix; String filename = config.modelTestFileFolder() + File.separator + config.toModelTestFilename(modelName) + suffix;
// do not overwrite test file that already exists
if (generateModelTests) {
// do not overwrite test file that already exists (regardless of config's skipOverwrite setting)
if (new File(filename).exists()) { if (new File(filename).exists()) {
LOGGER.info("File exists. Skipped overwriting " + filename); LOGGER.info("File exists. Skipped overwriting {}", filename);
if (dryRun) {
dryRunStatusMap.put(filename,
new DryRunStatus(
java.nio.file.Paths.get(filename),
DryRunStatus.State.SkippedOverwrite,
"Test files never overwrite an existing file of the same name."
));
}
continue; continue;
} }
File written = processTemplateToFile(models, templateName, filename); File written = processTemplateToFile(models, templateName, filename);
if (written != null) { if (written != null) {
files.add(written); files.add(written);
if (config.isEnablePostProcessFile()) { if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(written, "model-test"); config.postProcessFile(written, "model-test");
} }
} }
} else if (dryRun) {
dryRunStatusMap.put(filename,
new DryRunStatus(
java.nio.file.Paths.get(filename),
DryRunStatus.State.Skipped,
"Skipped by modelTests option supplied by user."
));
}
} }
} }
@ -305,57 +336,86 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
String docExtension = config.getDocExtension(); String docExtension = config.getDocExtension();
String suffix = docExtension != null ? docExtension : config.modelDocTemplateFiles().get(templateName); String suffix = docExtension != null ? docExtension : config.modelDocTemplateFiles().get(templateName);
String filename = config.modelDocFileFolder() + File.separator + config.toModelDocFilename(modelName) + suffix; String filename = config.modelDocFileFolder() + File.separator + config.toModelDocFilename(modelName) + suffix;
if (generateModelDocumentation) {
if (!config.shouldOverwrite(filename)) { if (!config.shouldOverwrite(filename)) {
LOGGER.info("Skipped overwriting " + filename); LOGGER.info("Skipped overwriting {}", filename);
if (dryRun) {
dryRunStatusMap.put(filename, new DryRunStatus(java.nio.file.Paths.get(filename), DryRunStatus.State.SkippedOverwrite));
}
continue; continue;
} }
File written = processTemplateToFile(models, templateName, filename); File written = processTemplateToFile(models, templateName, filename);
if (written != null) { if (written != null) {
files.add(written); files.add(written);
if (config.isEnablePostProcessFile()) { if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(written, "model-doc"); config.postProcessFile(written, "model-doc");
} }
} }
} else if (dryRun) {
dryRunStatusMap.put(filename,
new DryRunStatus(
java.nio.file.Paths.get(filename),
DryRunStatus.State.Skipped,
"Skipped by modelDocs option supplied by user."
));
} }
} }
}
private String getModelFilenameByTemplate(String modelName, String templateName){
String suffix = config.modelTemplateFiles().get(templateName);
return config.modelFileFolder() + File.separator + config.toModelFilename(modelName) + suffix;
}
private void generateModel(List<File> files, Map<String, Object> models, String modelName) throws IOException { private void generateModel(List<File> files, Map<String, Object> models, String modelName) throws IOException {
for (String templateName : config.modelTemplateFiles().keySet()) { for (String templateName : config.modelTemplateFiles().keySet()) {
String suffix = config.modelTemplateFiles().get(templateName); String filename = getModelFilenameByTemplate(modelName, templateName);
String filename = config.modelFileFolder() + File.separator + config.toModelFilename(modelName) + suffix;
if (!config.shouldOverwrite(filename)) { if (!config.shouldOverwrite(filename)) {
LOGGER.info("Skipped overwriting " + filename); LOGGER.info("Skipped overwriting {}", filename);
if (dryRun) {
dryRunStatusMap.put(filename, new DryRunStatus(
java.nio.file.Paths.get(filename),
DryRunStatus.State.SkippedOverwrite
));
}
continue; continue;
} }
File written = processTemplateToFile(models, templateName, filename); File written = processTemplateToFile(models, templateName, filename);
if (written != null) { if (written != null) {
files.add(written); files.add(written);
if (config.isEnablePostProcessFile()) { if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(written, "model"); config.postProcessFile(written, "model");
} }
} else {
LOGGER.warn("Unknown issue writing {}", filename);
} }
} }
} }
@SuppressWarnings("unchecked")
private void generateModels(List<File> files, List<Object> allModels, List<String> unusedModels) { private void generateModels(List<File> files, List<Object> allModels, List<String> unusedModels) {
if (!generateModels) { if (!generateModels) {
// TODO: Process these anyway and add to dryRun info
LOGGER.info("Skipping generation of models.");
return; return;
} }
final Map<String, Schema> schemas = ModelUtils.getSchemas(this.openAPI); final Map<String, Schema> schemas = ModelUtils.getSchemas(this.openAPI);
if (schemas == null) { if (schemas == null) {
LOGGER.warn("Skipping generation of models because specification document has no schemas.");
return; return;
} }
String modelNames = GlobalSettings.getProperty("models"); String modelNames = GlobalSettings.getProperty("models");
Set<String> modelsToGenerate = null; Set<String> modelsToGenerate = null;
if (modelNames != null && !modelNames.isEmpty()) { if (modelNames != null && !modelNames.isEmpty()) {
modelsToGenerate = new HashSet<String>(Arrays.asList(modelNames.split(","))); modelsToGenerate = new HashSet<>(Arrays.asList(modelNames.split(",")));
} }
Set<String> modelKeys = schemas.keySet(); Set<String> modelKeys = schemas.keySet();
if (modelsToGenerate != null && !modelsToGenerate.isEmpty()) { if (modelsToGenerate != null && !modelsToGenerate.isEmpty()) {
Set<String> updatedKeys = new HashSet<String>(); Set<String> updatedKeys = new HashSet<>();
for (String m : modelKeys) { for (String m : modelKeys) {
if (modelsToGenerate.contains(m)) { if (modelsToGenerate.contains(m)) {
updatedKeys.add(m); updatedKeys.add(m);
@ -366,59 +426,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
} }
// store all processed models // store all processed models
Map<String, Object> allProcessedModels = new TreeMap<String, Object>(new Comparator<String>() { Map<String, Object> allProcessedModels = new TreeMap<>((o1, o2) -> ObjectUtils.compare(config.toModelName(o1), config.toModelName(o2)));
@Override
public int compare(String o1, String o2) {
return ObjectUtils.compare(config.toModelName(o1), config.toModelName(o2));
}
/* TODO need to revise the logic below
Model model1 = definitions.get(o1);
Model model2 = definitions.get(o2);
int model1InheritanceDepth = getInheritanceDepth(model1);
int model2InheritanceDepth = getInheritanceDepth(model2);
if (model1InheritanceDepth == model2InheritanceDepth) {
return ObjectUtils.compare(config.toModelName(o1), config.toModelName(o2));
} else if (model1InheritanceDepth > model2InheritanceDepth) {
return 1;
} else {
return -1;
}
}
private int getInheritanceDepth(Model model) {
int inheritanceDepth = 0;
Model parent = getParent(model);
while (parent != null) {
inheritanceDepth++;
parent = getParent(parent);
}
return inheritanceDepth;
}
private Model getParent(Model model) {
if (model instanceof ComposedModel) {
Model parent = ((ComposedModel) model).getParent();
if (parent == null) {
// check for interfaces
List<RefModel> interfaces = ((ComposedModel) model).getInterfaces();
if (interfaces.size() > 0) {
RefModel interf = interfaces.get(0);
return definitions.get(interf.getSimpleRef());
}
}
if (parent != null) {
return definitions.get(parent.getReference());
}
}
return null;
} */
});
Boolean skipFormModel = GlobalSettings.getProperty(CodegenConstants.SKIP_FORM_MODEL) != null ? Boolean skipFormModel = GlobalSettings.getProperty(CodegenConstants.SKIP_FORM_MODEL) != null ?
Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.SKIP_FORM_MODEL)) : Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.SKIP_FORM_MODEL)) :
@ -429,7 +437,18 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
try { try {
//don't generate models that have an import mapping //don't generate models that have an import mapping
if (config.importMapping().containsKey(name)) { if (config.importMapping().containsKey(name)) {
LOGGER.debug("Model " + name + " not imported due to import mapping"); LOGGER.debug("Model {} not imported due to import mapping", name);
if (dryRun) {
// HACK: Because this returns early, could lead to some invalid model reporting.
for (String templateName : config.modelTemplateFiles().keySet()) {
String filename = getModelFilenameByTemplate(name, templateName);
dryRunStatusMap.put(filename, new DryRunStatus(
java.nio.file.Paths.get(filename),
DryRunStatus.State.Skipped,
"Skipped prior to model processing due to import mapping conflict (either by user or by generator)."
));
}
}
continue; continue;
} }
@ -437,9 +456,10 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
if (unusedModels.contains(name)) { if (unusedModels.contains(name)) {
if (Boolean.FALSE.equals(skipFormModel)) { if (Boolean.FALSE.equals(skipFormModel)) {
// if skipFormModel sets to true, still generate the model and log the result // if skipFormModel sets to true, still generate the model and log the result
LOGGER.info("Model " + name + " (marked as unused due to form parameters) is generated due to the system property skipFormModel=false (default)"); LOGGER.info("Model {} (marked as unused due to form parameters) is generated due to the system property skipFormModel=false (default)", name);
} else { } else {
LOGGER.info("Model " + name + " not generated since it's marked as unused (due to form parameters) and skipFormModel (system property) set to true"); LOGGER.info("Model {} not generated since it's marked as unused (due to form parameters) and skipFormModel (system property) set to true", name);
// TODO: Should this be added to dryRun? If not, this seems like a weird place to return early from processing.
continue; continue;
} }
} }
@ -447,7 +467,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
Schema schema = schemas.get(name); Schema schema = schemas.get(name);
if (ModelUtils.isFreeFormObject(schema)) { // check to see if it'a a free-form object if (ModelUtils.isFreeFormObject(schema)) { // check to see if it'a a free-form object
LOGGER.info("Model " + name + " not generated since it's a free-form object"); LOGGER.info("Model {} not generated since it's a free-form object", name);
continue; continue;
} else if (ModelUtils.isMapSchema(schema)) { // check to see if it's a "map" model } else if (ModelUtils.isMapSchema(schema)) { // check to see if it's a "map" model
// A composed schema (allOf, oneOf, anyOf) is considered a Map schema if the additionalproperties attribute is set // A composed schema (allOf, oneOf, anyOf) is considered a Map schema if the additionalproperties attribute is set
@ -455,13 +475,13 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
// in the inner schemas, and the outer schema does not have properties. // in the inner schemas, and the outer schema does not have properties.
if (!ModelUtils.isGenerateAliasAsModel() && !ModelUtils.isComposedSchema(schema) && (schema.getProperties() == null || schema.getProperties().isEmpty())) { if (!ModelUtils.isGenerateAliasAsModel() && !ModelUtils.isComposedSchema(schema) && (schema.getProperties() == null || schema.getProperties().isEmpty())) {
// schema without property, i.e. alias to map // schema without property, i.e. alias to map
LOGGER.info("Model " + name + " not generated since it's an alias to map (without property) and `generateAliasAsModel` is set to false (default)"); LOGGER.info("Model {} not generated since it's an alias to map (without property) and `generateAliasAsModel` is set to false (default)", name);
continue; continue;
} }
} else if (ModelUtils.isArraySchema(schema)) { // check to see if it's an "array" model } else if (ModelUtils.isArraySchema(schema)) { // check to see if it's an "array" model
if (!ModelUtils.isGenerateAliasAsModel() && (schema.getProperties() == null || schema.getProperties().isEmpty())) { if (!ModelUtils.isGenerateAliasAsModel() && (schema.getProperties() == null || schema.getProperties().isEmpty())) {
// schema without property, i.e. alias to array // schema without property, i.e. alias to array
LOGGER.info("Model " + name + " not generated since it's an alias to array (without property) and `generateAliasAsModel` is set to false (default)"); LOGGER.info("Model {} not generated since it's an alias to array (without property) and `generateAliasAsModel` is set to false (default)", name);
continue; continue;
} }
} }
@ -509,14 +529,12 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
// to generate model files // to generate model files
generateModel(files, models, modelName); generateModel(files, models, modelName);
if (generateModelTests) {
// to generate model test files // to generate model test files
generateModelTests(files, models, modelName); generateModelTests(files, models, modelName);
}
if (generateModelDocumentation) {
// to generate model documentation files // to generate model documentation files
generateModelDocumentation(files, models, modelName); generateModelDocumentation(files, models, modelName);
}
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Could not generate model '" + modelName + "'", e); throw new RuntimeException("Could not generate model '" + modelName + "'", e);
} }
@ -528,18 +546,21 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
} }
@SuppressWarnings("unchecked")
private void generateApis(List<File> files, List<Object> allOperations, List<Object> allModels) { private void generateApis(List<File> files, List<Object> allOperations, List<Object> allModels) {
if (!generateApis) { if (!generateApis) {
// TODO: Process these anyway and present info via dryRun?
LOGGER.info("Skipping generation of APIs.");
return; return;
} }
Map<String, List<CodegenOperation>> paths = processPaths(this.openAPI.getPaths()); Map<String, List<CodegenOperation>> paths = processPaths(this.openAPI.getPaths());
Set<String> apisToGenerate = null; Set<String> apisToGenerate = null;
String apiNames = GlobalSettings.getProperty("apis"); String apiNames = GlobalSettings.getProperty("apis");
if (apiNames != null && !apiNames.isEmpty()) { if (apiNames != null && !apiNames.isEmpty()) {
apisToGenerate = new HashSet<String>(Arrays.asList(apiNames.split(","))); apisToGenerate = new HashSet<>(Arrays.asList(apiNames.split(",")));
} }
if (apisToGenerate != null && !apisToGenerate.isEmpty()) { if (apisToGenerate != null && !apisToGenerate.isEmpty()) {
Map<String, List<CodegenOperation>> updatedPaths = new TreeMap<String, List<CodegenOperation>>(); Map<String, List<CodegenOperation>> updatedPaths = new TreeMap<>();
for (String m : paths.keySet()) { for (String m : paths.keySet()) {
if (apisToGenerate.contains(m)) { if (apisToGenerate.contains(m)) {
updatedPaths.put(m, paths.get(m)); updatedPaths.put(m, paths.get(m));
@ -550,12 +571,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
for (String tag : paths.keySet()) { for (String tag : paths.keySet()) {
try { try {
List<CodegenOperation> ops = paths.get(tag); List<CodegenOperation> ops = paths.get(tag);
Collections.sort(ops, new Comparator<CodegenOperation>() { ops.sort((one, another) -> ObjectUtils.compare(one.operationId, another.operationId));
@Override
public int compare(CodegenOperation one, CodegenOperation another) {
return ObjectUtils.compare(one.operationId, another.operationId);
}
});
Map<String, Object> operation = processOperations(config, tag, ops, allModels); Map<String, Object> operation = processOperations(config, tag, ops, allModels);
URL url = URLPathUtils.getServerURL(openAPI, config.serverVariableOverrides()); URL url = URLPathUtils.getServerURL(openAPI, config.serverVariableOverrides());
operation.put("basePath", basePath); operation.put("basePath", basePath);
@ -583,7 +599,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
// process top-level x-group-parameters // process top-level x-group-parameters
if (config.vendorExtensions().containsKey("x-group-parameters")) { if (config.vendorExtensions().containsKey("x-group-parameters")) {
Boolean isGroupParameters = Boolean.valueOf(config.vendorExtensions().get("x-group-parameters").toString()); boolean isGroupParameters = Boolean.parseBoolean(config.vendorExtensions().get("x-group-parameters").toString());
Map<String, Object> objectMap = (Map<String, Object>) operation.get("operations"); Map<String, Object> objectMap = (Map<String, Object>) operation.get("operations");
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -598,7 +614,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
// Pass sortParamsByRequiredFlag through to the Mustache template... // Pass sortParamsByRequiredFlag through to the Mustache template...
boolean sortParamsByRequiredFlag = true; boolean sortParamsByRequiredFlag = true;
if (this.config.additionalProperties().containsKey(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG)) { if (this.config.additionalProperties().containsKey(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG)) {
sortParamsByRequiredFlag = Boolean.valueOf(this.config.additionalProperties().get(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG).toString()); sortParamsByRequiredFlag = Boolean.parseBoolean(this.config.additionalProperties().get(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG).toString());
} }
operation.put("sortParamsByRequiredFlag", sortParamsByRequiredFlag); operation.put("sortParamsByRequiredFlag", sortParamsByRequiredFlag);
@ -607,7 +623,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
processMimeTypes(swagger.getProduces(), operation, "produces"); processMimeTypes(swagger.getProduces(), operation, "produces");
*/ */
allOperations.add(new HashMap<String, Object>(operation)); allOperations.add(new HashMap<>(operation));
for (int i = 0; i < allOperations.size(); i++) { for (int i = 0; i < allOperations.size(); i++) {
Map<String, Object> oo = (Map<String, Object>) allOperations.get(i); Map<String, Object> oo = (Map<String, Object>) allOperations.get(i);
if (i < (allOperations.size() - 1)) { if (i < (allOperations.size() - 1)) {
@ -617,56 +633,81 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
for (String templateName : config.apiTemplateFiles().keySet()) { for (String templateName : config.apiTemplateFiles().keySet()) {
String filename = config.apiFilename(templateName, tag); String filename = config.apiFilename(templateName, tag);
if (!config.shouldOverwrite(filename) && new File(filename).exists()) { File apiFile = new File(filename);
LOGGER.info("Skipped overwriting " + filename); if (!config.shouldOverwrite(filename) && apiFile.exists()) {
LOGGER.info("Skipped overwriting {}", filename);
if (dryRun) {
DryRunStatus status = new DryRunStatus(apiFile.toPath(), DryRunStatus.State.SkippedOverwrite);
dryRunStatusMap.put(filename, status);
}
continue; continue;
} }
File written = processTemplateToFile(operation, templateName, filename); File written = processTemplateToFile(operation, templateName, filename);
if (written != null) { if (written != null) {
files.add(written); files.add(written);
if (config.isEnablePostProcessFile()) { if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(written, "api"); config.postProcessFile(written, "api");
} }
} }
} }
if (generateApiTests) {
// to generate api test files // to generate api test files
for (String templateName : config.apiTestTemplateFiles().keySet()) { for (String templateName : config.apiTestTemplateFiles().keySet()) {
String filename = config.apiTestFilename(templateName, tag); String filename = config.apiTestFilename(templateName, tag);
File apiTestFile = new File(filename);
if (generateApiTests) {
// do not overwrite test file that already exists // do not overwrite test file that already exists
if (new File(filename).exists()) { if (apiTestFile.exists()) {
LOGGER.info("File exists. Skipped overwriting " + filename); LOGGER.info("File exists. Skipped overwriting {}", filename);
if (dryRun) {
dryRunStatusMap.put(filename, new DryRunStatus(apiTestFile.toPath(), DryRunStatus.State.SkippedOverwrite));
}
continue; continue;
} }
File written = processTemplateToFile(operation, templateName, filename); File written = processTemplateToFile(operation, templateName, filename);
if (written != null) { if (written != null) {
files.add(written); files.add(written);
if (config.isEnablePostProcessFile()) { if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(written, "api-test"); config.postProcessFile(written, "api-test");
} }
} }
} else if (dryRun) {
dryRunStatusMap.put(filename, new DryRunStatus(
apiTestFile.toPath(),
DryRunStatus.State.Skipped,
"Skipped by apiTests option supplied by user."
));
} }
} }
if (generateApiDocumentation) {
// to generate api documentation files // to generate api documentation files
for (String templateName : config.apiDocTemplateFiles().keySet()) { for (String templateName : config.apiDocTemplateFiles().keySet()) {
String filename = config.apiDocFilename(templateName, tag); String filename = config.apiDocFilename(templateName, tag);
if (!config.shouldOverwrite(filename) && new File(filename).exists()) { File apiDocFile = new File(filename);
LOGGER.info("Skipped overwriting " + filename); if (generateApiDocumentation) {
if (!config.shouldOverwrite(filename) && apiDocFile.exists()) {
LOGGER.info("Skipped overwriting {}", filename);
if (dryRun) {
dryRunStatusMap.put(filename, new DryRunStatus(apiDocFile.toPath(), DryRunStatus.State.SkippedOverwrite));
}
continue; continue;
} }
File written = processTemplateToFile(operation, templateName, filename); File written = processTemplateToFile(operation, templateName, filename);
if (written != null) { if (written != null) {
files.add(written); files.add(written);
if (config.isEnablePostProcessFile()) { if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(written, "api-doc"); config.postProcessFile(written, "api-doc");
} }
} }
} else if (dryRun) {
dryRunStatusMap.put(filename, new DryRunStatus(
apiDocFile.toPath(),
DryRunStatus.State.Skipped,
"Skipped by apiDocs option supplied by user."
));
} }
} }
@ -683,12 +724,14 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
private void generateSupportingFiles(List<File> files, Map<String, Object> bundle) { private void generateSupportingFiles(List<File> files, Map<String, Object> bundle) {
if (!generateSupportingFiles) { if (!generateSupportingFiles) {
// TODO: process these anyway and report via dryRun?
LOGGER.info("Skipping generation of supporting files.");
return; return;
} }
Set<String> supportingFilesToGenerate = null; Set<String> supportingFilesToGenerate = null;
String supportingFiles = GlobalSettings.getProperty(CodegenConstants.SUPPORTING_FILES); String supportingFiles = GlobalSettings.getProperty(CodegenConstants.SUPPORTING_FILES);
if (supportingFiles != null && !supportingFiles.isEmpty()) { if (supportingFiles != null && !supportingFiles.isEmpty()) {
supportingFilesToGenerate = new HashSet<String>(Arrays.asList(supportingFiles.split(","))); supportingFilesToGenerate = new HashSet<>(Arrays.asList(supportingFiles.split(",")));
} }
for (SupportingFile support : config.supportingFiles()) { for (SupportingFile support : config.supportingFiles()) {
@ -699,13 +742,22 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
} }
File of = new File(outputFolder); File of = new File(outputFolder);
if (!of.isDirectory()) { if (!of.isDirectory()) {
of.mkdirs(); if(!dryRun && !of.mkdirs()) {
once(LOGGER).debug("Output directory {} not created. It {}.", outputFolder, of.exists() ? "already exists." : "may not have appropriate permissions.");
}
} }
String outputFilename = new File(support.destinationFilename).isAbsolute() // split String outputFilename = new File(support.destinationFilename).isAbsolute() // split
? support.destinationFilename ? support.destinationFilename
: outputFolder + File.separator + support.destinationFilename.replace('/', File.separatorChar); : outputFolder + File.separator + support.destinationFilename.replace('/', File.separatorChar);
if (!config.shouldOverwrite(outputFilename)) { if (!config.shouldOverwrite(outputFilename)) {
LOGGER.info("Skipped overwriting " + outputFilename); LOGGER.info("Skipped overwriting {}", outputFilename);
if (dryRun) {
Path skippedSupportingFile = java.nio.file.Paths.get(outputFilename);
DryRunStatus status = new DryRunStatus(
skippedSupportingFile,
DryRunStatus.State.SkippedOverwrite
);
}
continue; continue;
} }
String templateFile; String templateFile;
@ -719,6 +771,15 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
shouldGenerate = supportingFilesToGenerate.contains(support.destinationFilename); shouldGenerate = supportingFilesToGenerate.contains(support.destinationFilename);
} }
if (!shouldGenerate) { if (!shouldGenerate) {
if (dryRun) {
Path skippedSupportingFile = java.nio.file.Paths.get(outputFilename);
DryRunStatus status = new DryRunStatus(
skippedSupportingFile,
DryRunStatus.State.Skipped,
"Skipped by supportingFiles option supplied by user."
);
dryRunStatusMap.put(outputFilename, status);
}
continue; continue;
} }
@ -744,12 +805,16 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
} }
File outputFile = writeInputStreamToFile(outputFilename, in, templateFile); File outputFile = writeInputStreamToFile(outputFilename, in, templateFile);
files.add(outputFile); files.add(outputFile);
if (config.isEnablePostProcessFile()) { if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(outputFile, "supporting-common"); config.postProcessFile(outputFile, "supporting-common");
} }
} }
} else { } else {
LOGGER.info("Skipped generation of " + outputFilename + " due to rule in .openapi-generator-ignore"); if (dryRun) {
dryRunStatusMap.put(outputFilename, new DryRunStatus(java.nio.file.Paths.get(outputFilename), DryRunStatus.State.Ignored));
}
LOGGER.info("Skipped generation of {} due to rule in .openapi-generator-ignore", outputFilename);
} }
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Could not generate supporting file '" + support + "'", e); throw new RuntimeException("Could not generate supporting file '" + support + "'", e);
@ -770,23 +835,35 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
throw new RuntimeException("Could not generate supporting file '" + openapiGeneratorIgnore + "'", e); throw new RuntimeException("Could not generate supporting file '" + openapiGeneratorIgnore + "'", e);
} }
files.add(ignoreFile); files.add(ignoreFile);
if (config.isEnablePostProcessFile()) { if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(ignoreFile, "openapi-generator-ignore"); config.postProcessFile(ignoreFile, "openapi-generator-ignore");
} }
} else if (generateMetadata && dryRun && ignoreFile.exists()) {
dryRunStatusMap.put(ignoreFileNameTarget, new DryRunStatus(ignoreFile.toPath(), DryRunStatus.State.SkippedOverwrite));
} else if (!generateMetadata && dryRun) {
dryRunStatusMap.put(ignoreFileNameTarget, new DryRunStatus(
ignoreFile.toPath(),
DryRunStatus.State.Skipped,
"Skipped by generateMetadata option supplied by user"
));
} }
String versionMetadata = config.outputFolder() + File.separator + ".openapi-generator" + File.separator + "VERSION";
if (generateMetadata) { if (generateMetadata) {
final String versionMetadata = config.outputFolder() + File.separator + ".openapi-generator" + File.separator + "VERSION";
File versionMetadataFile = new File(versionMetadata); File versionMetadataFile = new File(versionMetadata);
try { try {
writeToFile(versionMetadata, ImplementationVersion.read()); writeToFile(versionMetadata, ImplementationVersion.read());
files.add(versionMetadataFile); files.add(versionMetadataFile);
if (config.isEnablePostProcessFile()) { if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(ignoreFile, "openapi-generator-version"); config.postProcessFile(ignoreFile, "openapi-generator-version");
} }
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Could not generate supporting file '" + versionMetadata + "'", e); throw new RuntimeException("Could not generate supporting file '" + versionMetadata + "'", e);
} }
} else if(!generateMetadata && dryRun) {
Path metadata = java.nio.file.Paths.get(versionMetadata);
DryRunStatus status = new DryRunStatus(metadata, DryRunStatus.State.Skipped, "Skipped by generateMetadata option supplied by user.");
dryRunStatusMap.put(versionMetadata, status);
} }
/* /*
@ -809,23 +886,13 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
} }
protected File writeInputStreamToFile(String filename, InputStream in, String templateFile) throws FileNotFoundException, IOException { @SuppressWarnings("unchecked")
if (in != null) {
byte bytes[] = IOUtils.toByteArray(in);
return writeToFile(filename, bytes);
} else {
LOGGER.error("can't open '" + templateFile + "' for input; cannot write '" + filename + "'");
return null;
}
}
private Map<String, Object> buildSupportFileBundle(List<Object> allOperations, List<Object> allModels) { private Map<String, Object> buildSupportFileBundle(List<Object> allOperations, List<Object> allModels) {
Map<String, Object> bundle = new HashMap<String, Object>(); Map<String, Object> bundle = new HashMap<>(config.additionalProperties());
bundle.putAll(config.additionalProperties());
bundle.put("apiPackage", config.apiPackage()); bundle.put("apiPackage", config.apiPackage());
Map<String, Object> apis = new HashMap<String, Object>(); Map<String, Object> apis = new HashMap<>();
apis.put("apis", allOperations); apis.put("apis", allOperations);
URL url = URLPathUtils.getServerURL(openAPI, config.serverVariableOverrides()); URL url = URLPathUtils.getServerURL(openAPI, config.serverVariableOverrides());
@ -903,7 +970,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
} }
if (config.getGeneratorMetadata() == null) { if (config.getGeneratorMetadata() == null) {
LOGGER.warn(String.format(Locale.ROOT, "Generator '%s' is missing generator metadata!", config.getName())); LOGGER.warn("Generator '{}' is missing generator metadata!", config.getName());
} else { } else {
GeneratorMetadata generatorMetadata = config.getGeneratorMetadata(); GeneratorMetadata generatorMetadata = config.getGeneratorMetadata();
if (StringUtils.isNotEmpty(generatorMetadata.getGenerationMessage())) { if (StringUtils.isNotEmpty(generatorMetadata.getGenerationMessage())) {
@ -929,13 +996,13 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
// If the template adapter is mustache, we'll set the config-modified Compiler. // If the template adapter is mustache, we'll set the config-modified Compiler.
configPostProcessMustacheCompiler(); configPostProcessMustacheCompiler();
List<File> files = new ArrayList<File>(); List<File> files = new ArrayList<>();
// models // models
List<String> filteredSchemas = ModelUtils.getSchemasUsedOnlyInFormParam(openAPI); List<String> filteredSchemas = ModelUtils.getSchemasUsedOnlyInFormParam(openAPI);
List<Object> allModels = new ArrayList<Object>(); List<Object> allModels = new ArrayList<>();
generateModels(files, allModels, filteredSchemas); generateModels(files, allModels, filteredSchemas);
// apis // apis
List<Object> allOperations = new ArrayList<Object>(); List<Object> allOperations = new ArrayList<>();
generateApis(files, allOperations, allModels); generateApis(files, allOperations, allModels);
// supporting files // supporting files
@ -943,6 +1010,43 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
generateSupportingFiles(files, bundle); generateSupportingFiles(files, bundle);
config.processOpenAPI(openAPI); config.processOpenAPI(openAPI);
if(dryRun) {
boolean verbose = Boolean.parseBoolean(GlobalSettings.getProperty("verbose"));
StringBuilder sb = new StringBuilder();
sb.append(System.lineSeparator()).append(System.lineSeparator());
sb.append("Dry Run Results:");
sb.append(System.lineSeparator()).append(System.lineSeparator());
dryRunStatusMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> {
DryRunStatus status = entry.getValue();
try {
status.appendTo(sb);
sb.append(System.lineSeparator());
if (verbose) {
sb.append(" ")
.append(StringUtils.rightPad(status.getState().getDescription(), 20, "."))
.append(" ").append(status.getReason())
.append(System.lineSeparator());
}
} catch (IOException e) {
LOGGER.debug("Unable to document dry run status for {}.", entry.getKey());
}
});
sb.append(System.lineSeparator()).append(System.lineSeparator());
sb.append("States:");
sb.append(System.lineSeparator()).append(System.lineSeparator());
for (DryRunStatus.State state : DryRunStatus.State.values()) {
sb.append(" - ").append(state.getShortDisplay()).append(" ").append(state.getDescription()).append(System.lineSeparator());
}
sb.append(System.lineSeparator());
System.err.println(sb.toString());
}
// reset GlobalSettings, so that the running thread can be reused for another generator-run // reset GlobalSettings, so that the running thread can be reused for another generator-run
GlobalSettings.reset(); GlobalSettings.reset();
@ -968,18 +1072,22 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
protected File processTemplateToFile(Map<String, Object> templateData, String templateName, String outputFilename) throws IOException { protected File processTemplateToFile(Map<String, Object> templateData, String templateName, String outputFilename) throws IOException {
String adjustedOutputFilename = outputFilename.replaceAll("//", "/").replace('/', File.separatorChar); String adjustedOutputFilename = outputFilename.replaceAll("//", "/").replace('/', File.separatorChar);
if (ignoreProcessor.allowsFile(new File(adjustedOutputFilename))) { File target = new File(adjustedOutputFilename);
if (ignoreProcessor.allowsFile(target)) {
String templateContent = templatingEngine.compileTemplate(this, templateData, templateName); String templateContent = templatingEngine.compileTemplate(this, templateData, templateName);
writeToFile(adjustedOutputFilename, templateContent); writeToFile(adjustedOutputFilename, templateContent);
return new File(adjustedOutputFilename); return target;
} else if (this.dryRun) {
dryRunStatusMap.put(adjustedOutputFilename, new DryRunStatus(target.toPath(), DryRunStatus.State.Ignored));
return target;
} }
LOGGER.info("Skipped generation of " + adjustedOutputFilename + " due to rule in .openapi-generator-ignore"); LOGGER.info("Skipped generation of {} due to rule in .openapi-generator-ignore", adjustedOutputFilename);
return null; return null;
} }
public Map<String, List<CodegenOperation>> processPaths(Paths paths) { public Map<String, List<CodegenOperation>> processPaths(Paths paths) {
Map<String, List<CodegenOperation>> ops = new TreeMap<String, List<CodegenOperation>>(); Map<String, List<CodegenOperation>> ops = new TreeMap<>();
for (String resourcePath : paths.keySet()) { for (String resourcePath : paths.keySet()) {
PathItem path = paths.get(resourcePath); PathItem path = paths.get(resourcePath);
processOperation(resourcePath, "get", path.getGet(), ops, path); processOperation(resourcePath, "get", path.getGet(), ops, path);
@ -1000,10 +1108,10 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
} }
if (GlobalSettings.getProperty("debugOperations") != null) { if (GlobalSettings.getProperty("debugOperations") != null) {
LOGGER.info("processOperation: resourcePath= " + resourcePath + "\t;" + httpMethod + " " + operation + "\n"); LOGGER.info("processOperation: resourcePath= {}\t;{} {}\n", resourcePath, httpMethod, operation);
} }
List<Tag> tags = new ArrayList<Tag>(); List<Tag> tags = new ArrayList<>();
List<String> tagNames = operation.getTags(); List<String> tagNames = operation.getTags();
List<Tag> swaggerTags = openAPI.getTags(); List<Tag> swaggerTags = openAPI.getTags();
if (tagNames != null) { if (tagNames != null) {
@ -1038,7 +1146,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
per the swagger 2.0 spec "A unique parameter is defined by a combination of a name and location" per the swagger 2.0 spec "A unique parameter is defined by a combination of a name and location"
i'm assuming "location" == "in" i'm assuming "location" == "in"
*/ */
Set<String> operationParameters = new HashSet<String>(); Set<String> operationParameters = new HashSet<>();
if (operation.getParameters() != null) { if (operation.getParameters() != null) {
for (Parameter parameter : operation.getParameters()) { for (Parameter parameter : operation.getParameters()) {
operationParameters.add(generateParameterId(parameter)); operationParameters.add(generateParameterId(parameter));
@ -1055,7 +1163,6 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
} }
} }
final Map<String, Schema> schemas = openAPI.getComponents() != null ? openAPI.getComponents().getSchemas() : null;
final Map<String, SecurityScheme> securitySchemes = openAPI.getComponents() != null ? openAPI.getComponents().getSecuritySchemes() : null; final Map<String, SecurityScheme> securitySchemes = openAPI.getComponents() != null ? openAPI.getComponents().getSecuritySchemes() : null;
final List<SecurityRequirement> globalSecurities = openAPI.getSecurity(); final List<SecurityRequirement> globalSecurities = openAPI.getSecurity();
for (Tag tag : tags) { for (Tag tag : tags) {
@ -1102,14 +1209,15 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
return parameter.getName() + ":" + parameter.getIn(); return parameter.getName() + ":" + parameter.getIn();
} }
@SuppressWarnings("unchecked")
private Map<String, Object> processOperations(CodegenConfig config, String tag, List<CodegenOperation> ops, List<Object> allModels) { private Map<String, Object> processOperations(CodegenConfig config, String tag, List<CodegenOperation> ops, List<Object> allModels) {
Map<String, Object> operations = new HashMap<String, Object>(); Map<String, Object> operations = new HashMap<>();
Map<String, Object> objs = new HashMap<String, Object>(); Map<String, Object> objs = new HashMap<>();
objs.put("classname", config.toApiName(tag)); objs.put("classname", config.toApiName(tag));
objs.put("pathPrefix", config.toApiVarName(tag)); objs.put("pathPrefix", config.toApiVarName(tag));
// check for operationId uniqueness // check for operationId uniqueness
Set<String> opIds = new HashSet<String>(); Set<String> opIds = new HashSet<>();
int counter = 0; int counter = 0;
for (CodegenOperation op : ops) { for (CodegenOperation op : ops) {
String opId = op.nickname; String opId = op.nickname;
@ -1124,15 +1232,15 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
operations.put("operations", objs); operations.put("operations", objs);
operations.put("package", config.apiPackage()); operations.put("package", config.apiPackage());
Set<String> allImports = new TreeSet<String>(); Set<String> allImports = new TreeSet<>();
for (CodegenOperation op : ops) { for (CodegenOperation op : ops) {
allImports.addAll(op.imports); allImports.addAll(op.imports);
} }
List<Map<String, String>> imports = new ArrayList<Map<String, String>>(); List<Map<String, String>> imports = new ArrayList<>();
Set<String> mappingSet = new TreeSet<>(); Set<String> mappingSet = new TreeSet<>();
for (String nextImport : allImports) { for (String nextImport : allImports) {
Map<String, String> im = new LinkedHashMap<String, String>(); Map<String, String> im = new LinkedHashMap<>();
String mapping = config.importMapping().get(nextImport); String mapping = config.importMapping().get(nextImport);
if (mapping == null) { if (mapping == null) {
mapping = config.toModelImport(nextImport); mapping = config.toModelImport(nextImport);
@ -1168,16 +1276,16 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
} }
private Map<String, Object> processModels(CodegenConfig config, Map<String, Schema> definitions) { private Map<String, Object> processModels(CodegenConfig config, Map<String, Schema> definitions) {
Map<String, Object> objs = new HashMap<String, Object>(); Map<String, Object> objs = new HashMap<>();
objs.put("package", config.modelPackage()); objs.put("package", config.modelPackage());
List<Object> models = new ArrayList<Object>(); List<Object> models = new ArrayList<>();
Set<String> allImports = new LinkedHashSet<String>(); Set<String> allImports = new LinkedHashSet<>();
for (String key : definitions.keySet()) { for (String key : definitions.keySet()) {
Schema schema = definitions.get(key); Schema schema = definitions.get(key);
if (schema == null) if (schema == null)
throw new RuntimeException("schema cannot be null in processModels"); throw new RuntimeException("schema cannot be null in processModels");
CodegenModel cm = config.fromModel(key, schema); CodegenModel cm = config.fromModel(key, schema);
Map<String, Object> mo = new HashMap<String, Object>(); Map<String, Object> mo = new HashMap<>();
mo.put("model", cm); mo.put("model", cm);
mo.put("importPath", config.toModelImport(cm.classname)); mo.put("importPath", config.toModelImport(cm.classname));
models.add(mo); models.add(mo);
@ -1187,7 +1295,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
allImports.addAll(cm.imports); allImports.addAll(cm.imports);
} }
objs.put("models", models); objs.put("models", models);
Set<String> importSet = new TreeSet<String>(); Set<String> importSet = new TreeSet<>();
for (String nextImport : allImports) { for (String nextImport : allImports) {
String mapping = config.importMapping().get(nextImport); String mapping = config.importMapping().get(nextImport);
if (mapping == null) { if (mapping == null) {
@ -1202,9 +1310,9 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
importSet.add(mapping); importSet.add(mapping);
} }
} }
List<Map<String, String>> imports = new ArrayList<Map<String, String>>(); List<Map<String, String>> imports = new ArrayList<>();
for (String s : importSet) { for (String s : importSet) {
Map<String, String> item = new HashMap<String, String>(); Map<String, String> item = new HashMap<>();
item.put("import", s); item.put("import", s);
imports.add(item); imports.add(item);
} }
@ -1295,7 +1403,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
return authMethods; return authMethods;
} }
List<CodegenSecurity> result = new ArrayList<CodegenSecurity>(); List<CodegenSecurity> result = new ArrayList<>();
for (CodegenSecurity security : authMethods) { for (CodegenSecurity security : authMethods) {
boolean filtered = false; boolean filtered = false;
@ -1356,4 +1464,51 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
return oauthMethods; return oauthMethods;
} }
protected File writeInputStreamToFile(String filename, InputStream in, String templateFile) throws IOException {
if (in != null) {
byte[] bytes = IOUtils.toByteArray(in);
if (dryRun) {
Path path = java.nio.file.Paths.get(filename);
dryRunStatusMap.put(filename, new DryRunStatus(path));
return path.toFile();
}
return writeToFile(filename, bytes);
} else {
LOGGER.error("can't open '{}' for input; cannot write '{}'", templateFile, filename);
if (dryRun) {
Path path = java.nio.file.Paths.get(filename);
dryRunStatusMap.put(filename, new DryRunStatus(path, DryRunStatus.State.Error));
}
return null;
}
}
/**
* Write bytes to a file
*
* @param filename The name of file to write
* @param contents The contents bytes. Typically this is a UTF-8 formatted string.
* @return File representing the written file.
* @throws IOException If file cannot be written.
*/
@Override
public File writeToFile(String filename, byte[] contents) throws IOException {
if (dryRun) {
Path path = java.nio.file.Paths.get(filename);
DryRunStatus status = new DryRunStatus(path);
if (getEnableMinimalUpdate()) {
status.setState(DryRunStatus.State.WriteIfNewer);
} else {
status.setState(DryRunStatus.State.Write);
}
dryRunStatusMap.put(filename, status);
return path.toFile();
} else {
return super.writeToFile(filename, contents);
}
}
} }

View File

@ -0,0 +1,160 @@
package org.openapitools.codegen;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Locale;
/**
* Holds details about a file's write status for display via the --dry-run option of CLI
*/
class DryRunStatus {
private Path path;
private State state;
private String reason;
/**
* Constructs a new instance of {@link DryRunStatus} for a given path and status of {@link State#Write}
*
* @param path The target path where the file would write
*/
public DryRunStatus(Path path) {
this(path, State.Write);
}
/**
* Constructs a new instance of {@link DryRunStatus} for a path and a target state
*
* @param path The target path where the file would write
* @param state The evaluated state as determined by the generation workflow
*/
public DryRunStatus(Path path, State state) {
this.path = path;
setState(state);
}
/**
* Constructs a new instance of {@link DryRunStatus} for a path, target state, and presenting a specific reason for that state
*
* @param path The target path where the file would write
* @param state The evaluated state as determined by the generation workflow
* @param reason A reason for the state, beyond any generic reason
*/
public DryRunStatus(Path path, State state, String reason) {
this.path = path;
this.state = state;
this.reason = reason;
}
/**
* Append a user display text to the {@link Appendable} instance
*
* @param appendable An object implementing {@link Appendable} (such as {@link StringBuilder}
* @throws IOException via contract of {@link Path#toAbsolutePath()}
*/
public void appendTo(Appendable appendable) throws IOException {
appendable.append(String.format(Locale.ROOT, "%s %s", this.state.getShortDisplay(), this.path.toAbsolutePath()));
}
/**
* Gets the target path of the file write operation
*
* @return a {@link Path} instance
*/
public Path getPath() {
return path;
}
/**
* Gets the reason for the file's {@link State}
*
* @return A human-readable string which explains why this file's dry-run resulted in the defined {@link State}
*/
public String getReason() {
return reason;
}
/**
* Gets the {@link State} as determined by the generator's workflow
*
* @return A {@link State} enum detailing the expected operation of the generator's workflow
*/
public State getState() {
return state;
}
/**
* Sets the {@link State} as determined by the generator's workflow.
* <p>
* This method will provide a default reason. To explicitly provide a reason for the {@link State}, use {@link DryRunStatus#DryRunStatus(Path, State, String)}
*
* @param state A {@link State} enum detailing the expected operation of the generator's workflow
*/
public void setState(State state) {
if (state != this.state) {
switch (state) {
case Write:
this.reason = "File will be written.";
break;
case WriteIfNewer:
this.reason = "File will be written only if it is new or if contents differ from an existing file.";
break;
case Ignored:
this.reason = "Ignored via rules defined in codegen ignore file.";
break;
case SkippedOverwrite:
this.reason = "File is configured not to overwrite an existing file of the same name.";
break;
case Error:
this.reason = "File error: template does not exist, or file is not accessible.";
break;
}
}
this.state = state;
}
/**
* Represents the possible states of a file write operation as determined by the Generator
*/
enum State {
Write("w", "Write"),
WriteIfNewer("n", "Write if New/Updated"),
Ignored("i", "Ignored"),
SkippedOverwrite("s", "Skipped Overwrite"),
Skipped("k", "Skipped by user option(s)"),
Error("e", "Error evaluating file write state");
private final String shortDisplay;
private final String description;
/**
* Constructs a new {@link State} with required short value and human-readable description
*
* @param shortDisplay The short value used for display
* @param description A description of the state which is more human-readable than the enum's name
*/
State(String shortDisplay, String description) {
this.shortDisplay = shortDisplay;
this.description = description;
}
/**
* Gets a description of the state which is more human-readable than the enum's name
*
* @return A human-readable description
*/
public String getDescription() {
return description;
}
/**
* Gets the short value used for display
*
* @return A character representing this state
*/
public String getShortDisplay() {
return shortDisplay;
}
}
}

View File

@ -439,6 +439,9 @@ public class CodegenConfigurator {
GlobalSettings.setProperty("debugModels", ""); GlobalSettings.setProperty("debugModels", "");
GlobalSettings.setProperty("debugOperations", ""); GlobalSettings.setProperty("debugOperations", "");
GlobalSettings.setProperty("debugSupportingFiles", ""); GlobalSettings.setProperty("debugSupportingFiles", "");
GlobalSettings.setProperty("verbose", "true");
} else {
GlobalSettings.setProperty("verbose", "false");
} }
for (Map.Entry<String, String> entry : workflowSettings.getSystemProperties().entrySet()) { for (Map.Entry<String, String> entry : workflowSettings.getSystemProperties().entrySet()) {