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

* :shirt:🎨 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
7 changed files with 597 additions and 193 deletions

View File

@@ -17,7 +17,7 @@
package org.openapitools.codegen;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
@@ -28,12 +28,16 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Pattern;
public abstract class AbstractGenerator implements TemplatingGenerator {
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?
*
@@ -50,19 +54,19 @@ public abstract class AbstractGenerator implements TemplatingGenerator {
* @throws IOException If file cannot be written.
*/
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
*
* @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.
* @throws IOException If file cannot be written.
*/
@SuppressWarnings("static-method")
public File writeToFile(String filename, byte contents[]) throws IOException {
public File writeToFile(String filename, byte[] contents) throws IOException {
if (getEnableMinimalUpdate()) {
String tempFilename = filename + ".tmp";
// 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.util.*;
import static org.openapitools.codegen.utils.OnceLogger.once;
@SuppressWarnings("rawtypes")
public class DefaultGenerator extends AbstractGenerator implements Generator {
protected final Logger LOGGER = LoggerFactory.getLogger(DefaultGenerator.class);
protected CodegenConfig config;
@@ -73,11 +76,21 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
private String contextPath;
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
public boolean getEnableMinimalUpdate() {
return config.isEnableMinimalUpdate();
}
@SuppressWarnings("deprecation")
@Override
public Generator opts(ClientOptInput opts) {
this.opts = opts;
@@ -285,17 +298,35 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
for (String templateName : config.modelTestTemplateFiles().keySet()) {
String suffix = config.modelTestTemplateFiles().get(templateName);
String filename = config.modelTestFileFolder() + File.separator + config.toModelTestFilename(modelName) + suffix;
// do not overwrite test file that already exists
if (new File(filename).exists()) {
LOGGER.info("File exists. Skipped overwriting " + filename);
continue;
}
File written = processTemplateToFile(models, templateName, filename);
if (written != null) {
files.add(written);
if (config.isEnablePostProcessFile()) {
config.postProcessFile(written, "model-test");
if (generateModelTests) {
// do not overwrite test file that already exists (regardless of config's skipOverwrite setting)
if (new File(filename).exists()) {
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;
}
File written = processTemplateToFile(models, templateName, filename);
if (written != null) {
files.add(written);
if (config.isEnablePostProcessFile() && !dryRun) {
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 suffix = docExtension != null ? docExtension : config.modelDocTemplateFiles().get(templateName);
String filename = config.modelDocFileFolder() + File.separator + config.toModelDocFilename(modelName) + suffix;
if (!config.shouldOverwrite(filename)) {
LOGGER.info("Skipped overwriting " + filename);
continue;
}
File written = processTemplateToFile(models, templateName, filename);
if (written != null) {
files.add(written);
if (config.isEnablePostProcessFile()) {
config.postProcessFile(written, "model-doc");
if (generateModelDocumentation) {
if (!config.shouldOverwrite(filename)) {
LOGGER.info("Skipped overwriting {}", filename);
if (dryRun) {
dryRunStatusMap.put(filename, new DryRunStatus(java.nio.file.Paths.get(filename), DryRunStatus.State.SkippedOverwrite));
}
continue;
}
File written = processTemplateToFile(models, templateName, filename);
if (written != null) {
files.add(written);
if (config.isEnablePostProcessFile() && !dryRun) {
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 {
for (String templateName : config.modelTemplateFiles().keySet()) {
String suffix = config.modelTemplateFiles().get(templateName);
String filename = config.modelFileFolder() + File.separator + config.toModelFilename(modelName) + suffix;
String filename = getModelFilenameByTemplate(modelName, templateName);
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;
}
File written = processTemplateToFile(models, templateName, filename);
if (written != null) {
files.add(written);
if (config.isEnablePostProcessFile()) {
if (config.isEnablePostProcessFile() && !dryRun) {
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) {
if (!generateModels) {
// TODO: Process these anyway and add to dryRun info
LOGGER.info("Skipping generation of models.");
return;
}
final Map<String, Schema> schemas = ModelUtils.getSchemas(this.openAPI);
if (schemas == null) {
LOGGER.warn("Skipping generation of models because specification document has no schemas.");
return;
}
String modelNames = GlobalSettings.getProperty("models");
Set<String> modelsToGenerate = null;
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();
if (modelsToGenerate != null && !modelsToGenerate.isEmpty()) {
Set<String> updatedKeys = new HashSet<String>();
Set<String> updatedKeys = new HashSet<>();
for (String m : modelKeys) {
if (modelsToGenerate.contains(m)) {
updatedKeys.add(m);
@@ -366,59 +426,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
}
// store all processed models
Map<String, Object> allProcessedModels = new TreeMap<String, Object>(new Comparator<String>() {
@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;
} */
});
Map<String, Object> allProcessedModels = new TreeMap<>((o1, o2) -> ObjectUtils.compare(config.toModelName(o1), config.toModelName(o2)));
Boolean skipFormModel = GlobalSettings.getProperty(CodegenConstants.SKIP_FORM_MODEL) != null ?
Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.SKIP_FORM_MODEL)) :
@@ -429,7 +437,18 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
try {
//don't generate models that have an import mapping
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;
}
@@ -437,9 +456,10 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
if (unusedModels.contains(name)) {
if (Boolean.FALSE.equals(skipFormModel)) {
// 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 {
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;
}
}
@@ -447,7 +467,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
Schema schema = schemas.get(name);
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;
} 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
@@ -455,13 +475,13 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
// in the inner schemas, and the outer schema does not have properties.
if (!ModelUtils.isGenerateAliasAsModel() && !ModelUtils.isComposedSchema(schema) && (schema.getProperties() == null || schema.getProperties().isEmpty())) {
// 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;
}
} else if (ModelUtils.isArraySchema(schema)) { // check to see if it's an "array" model
if (!ModelUtils.isGenerateAliasAsModel() && (schema.getProperties() == null || schema.getProperties().isEmpty())) {
// 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;
}
}
@@ -509,14 +529,12 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
// to generate model files
generateModel(files, models, modelName);
if (generateModelTests) {
// to generate model test files
generateModelTests(files, models, modelName);
}
if (generateModelDocumentation) {
// to generate model documentation files
generateModelDocumentation(files, models, modelName);
}
// to generate model test files
generateModelTests(files, models, modelName);
// to generate model documentation files
generateModelDocumentation(files, models, modelName);
} catch (Exception 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) {
if (!generateApis) {
// TODO: Process these anyway and present info via dryRun?
LOGGER.info("Skipping generation of APIs.");
return;
}
Map<String, List<CodegenOperation>> paths = processPaths(this.openAPI.getPaths());
Set<String> apisToGenerate = null;
String apiNames = GlobalSettings.getProperty("apis");
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()) {
Map<String, List<CodegenOperation>> updatedPaths = new TreeMap<String, List<CodegenOperation>>();
Map<String, List<CodegenOperation>> updatedPaths = new TreeMap<>();
for (String m : paths.keySet()) {
if (apisToGenerate.contains(m)) {
updatedPaths.put(m, paths.get(m));
@@ -550,12 +571,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
for (String tag : paths.keySet()) {
try {
List<CodegenOperation> ops = paths.get(tag);
Collections.sort(ops, new Comparator<CodegenOperation>() {
@Override
public int compare(CodegenOperation one, CodegenOperation another) {
return ObjectUtils.compare(one.operationId, another.operationId);
}
});
ops.sort((one, another) -> ObjectUtils.compare(one.operationId, another.operationId));
Map<String, Object> operation = processOperations(config, tag, ops, allModels);
URL url = URLPathUtils.getServerURL(openAPI, config.serverVariableOverrides());
operation.put("basePath", basePath);
@@ -583,7 +599,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
// process top-level 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");
@SuppressWarnings("unchecked")
@@ -598,7 +614,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
// Pass sortParamsByRequiredFlag through to the Mustache template...
boolean sortParamsByRequiredFlag = true;
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);
@@ -607,7 +623,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
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++) {
Map<String, Object> oo = (Map<String, Object>) allOperations.get(i);
if (i < (allOperations.size() - 1)) {
@@ -617,56 +633,81 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
for (String templateName : config.apiTemplateFiles().keySet()) {
String filename = config.apiFilename(templateName, tag);
if (!config.shouldOverwrite(filename) && new File(filename).exists()) {
LOGGER.info("Skipped overwriting " + filename);
File apiFile = new File(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;
}
File written = processTemplateToFile(operation, templateName, filename);
if (written != null) {
files.add(written);
if (config.isEnablePostProcessFile()) {
if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(written, "api");
}
}
}
if (generateApiTests) {
// to generate api test files
for (String templateName : config.apiTestTemplateFiles().keySet()) {
String filename = config.apiTestFilename(templateName, tag);
// to generate api test files
for (String templateName : config.apiTestTemplateFiles().keySet()) {
String filename = config.apiTestFilename(templateName, tag);
File apiTestFile = new File(filename);
if (generateApiTests) {
// do not overwrite test file that already exists
if (new File(filename).exists()) {
LOGGER.info("File exists. Skipped overwriting " + filename);
if (apiTestFile.exists()) {
LOGGER.info("File exists. Skipped overwriting {}", filename);
if (dryRun) {
dryRunStatusMap.put(filename, new DryRunStatus(apiTestFile.toPath(), DryRunStatus.State.SkippedOverwrite));
}
continue;
}
File written = processTemplateToFile(operation, templateName, filename);
if (written != null) {
files.add(written);
if (config.isEnablePostProcessFile()) {
if (config.isEnablePostProcessFile() && !dryRun) {
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
for (String templateName : config.apiDocTemplateFiles().keySet()) {
String filename = config.apiDocFilename(templateName, tag);
if (!config.shouldOverwrite(filename) && new File(filename).exists()) {
LOGGER.info("Skipped overwriting " + filename);
// to generate api documentation files
for (String templateName : config.apiDocTemplateFiles().keySet()) {
String filename = config.apiDocFilename(templateName, tag);
File apiDocFile = new File(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;
}
File written = processTemplateToFile(operation, templateName, filename);
if (written != null) {
files.add(written);
if (config.isEnablePostProcessFile()) {
if (config.isEnablePostProcessFile() && !dryRun) {
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) {
if (!generateSupportingFiles) {
// TODO: process these anyway and report via dryRun?
LOGGER.info("Skipping generation of supporting files.");
return;
}
Set<String> supportingFilesToGenerate = null;
String supportingFiles = GlobalSettings.getProperty(CodegenConstants.SUPPORTING_FILES);
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()) {
@@ -699,13 +742,22 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
}
File of = new File(outputFolder);
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
? support.destinationFilename
: outputFolder + File.separator + support.destinationFilename.replace('/', File.separatorChar);
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;
}
String templateFile;
@@ -719,6 +771,15 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
shouldGenerate = supportingFilesToGenerate.contains(support.destinationFilename);
}
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;
}
@@ -744,12 +805,16 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
}
File outputFile = writeInputStreamToFile(outputFilename, in, templateFile);
files.add(outputFile);
if (config.isEnablePostProcessFile()) {
if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(outputFile, "supporting-common");
}
}
} 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) {
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);
}
files.add(ignoreFile);
if (config.isEnablePostProcessFile()) {
if (config.isEnablePostProcessFile() && !dryRun) {
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) {
final String versionMetadata = config.outputFolder() + File.separator + ".openapi-generator" + File.separator + "VERSION";
File versionMetadataFile = new File(versionMetadata);
try {
writeToFile(versionMetadata, ImplementationVersion.read());
files.add(versionMetadataFile);
if (config.isEnablePostProcessFile()) {
if (config.isEnablePostProcessFile() && !dryRun) {
config.postProcessFile(ignoreFile, "openapi-generator-version");
}
} catch (IOException 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 {
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;
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> buildSupportFileBundle(List<Object> allOperations, List<Object> allModels) {
Map<String, Object> bundle = new HashMap<String, Object>();
bundle.putAll(config.additionalProperties());
Map<String, Object> bundle = new HashMap<>(config.additionalProperties());
bundle.put("apiPackage", config.apiPackage());
Map<String, Object> apis = new HashMap<String, Object>();
Map<String, Object> apis = new HashMap<>();
apis.put("apis", allOperations);
URL url = URLPathUtils.getServerURL(openAPI, config.serverVariableOverrides());
@@ -903,7 +970,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
}
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 {
GeneratorMetadata generatorMetadata = config.getGeneratorMetadata();
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.
configPostProcessMustacheCompiler();
List<File> files = new ArrayList<File>();
List<File> files = new ArrayList<>();
// models
List<String> filteredSchemas = ModelUtils.getSchemasUsedOnlyInFormParam(openAPI);
List<Object> allModels = new ArrayList<Object>();
List<Object> allModels = new ArrayList<>();
generateModels(files, allModels, filteredSchemas);
// apis
List<Object> allOperations = new ArrayList<Object>();
List<Object> allOperations = new ArrayList<>();
generateApis(files, allOperations, allModels);
// supporting files
@@ -943,6 +1010,43 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
generateSupportingFiles(files, bundle);
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
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 {
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);
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;
}
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()) {
PathItem path = paths.get(resourcePath);
processOperation(resourcePath, "get", path.getGet(), ops, path);
@@ -1000,10 +1108,10 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
}
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<Tag> swaggerTags = openAPI.getTags();
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"
i'm assuming "location" == "in"
*/
Set<String> operationParameters = new HashSet<String>();
Set<String> operationParameters = new HashSet<>();
if (operation.getParameters() != null) {
for (Parameter parameter : operation.getParameters()) {
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 List<SecurityRequirement> globalSecurities = openAPI.getSecurity();
for (Tag tag : tags) {
@@ -1102,14 +1209,15 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
return parameter.getName() + ":" + parameter.getIn();
}
@SuppressWarnings("unchecked")
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> objs = new HashMap<String, Object>();
Map<String, Object> operations = new HashMap<>();
Map<String, Object> objs = new HashMap<>();
objs.put("classname", config.toApiName(tag));
objs.put("pathPrefix", config.toApiVarName(tag));
// check for operationId uniqueness
Set<String> opIds = new HashSet<String>();
Set<String> opIds = new HashSet<>();
int counter = 0;
for (CodegenOperation op : ops) {
String opId = op.nickname;
@@ -1124,15 +1232,15 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
operations.put("operations", objs);
operations.put("package", config.apiPackage());
Set<String> allImports = new TreeSet<String>();
Set<String> allImports = new TreeSet<>();
for (CodegenOperation op : ops) {
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<>();
for (String nextImport : allImports) {
Map<String, String> im = new LinkedHashMap<String, String>();
Map<String, String> im = new LinkedHashMap<>();
String mapping = config.importMapping().get(nextImport);
if (mapping == null) {
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) {
Map<String, Object> objs = new HashMap<String, Object>();
Map<String, Object> objs = new HashMap<>();
objs.put("package", config.modelPackage());
List<Object> models = new ArrayList<Object>();
Set<String> allImports = new LinkedHashSet<String>();
List<Object> models = new ArrayList<>();
Set<String> allImports = new LinkedHashSet<>();
for (String key : definitions.keySet()) {
Schema schema = definitions.get(key);
if (schema == null)
throw new RuntimeException("schema cannot be null in processModels");
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("importPath", config.toModelImport(cm.classname));
models.add(mo);
@@ -1187,7 +1295,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
allImports.addAll(cm.imports);
}
objs.put("models", models);
Set<String> importSet = new TreeSet<String>();
Set<String> importSet = new TreeSet<>();
for (String nextImport : allImports) {
String mapping = config.importMapping().get(nextImport);
if (mapping == null) {
@@ -1202,9 +1310,9 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
importSet.add(mapping);
}
}
List<Map<String, String>> imports = new ArrayList<Map<String, String>>();
List<Map<String, String>> imports = new ArrayList<>();
for (String s : importSet) {
Map<String, String> item = new HashMap<String, String>();
Map<String, String> item = new HashMap<>();
item.put("import", s);
imports.add(item);
}
@@ -1295,7 +1403,7 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
return authMethods;
}
List<CodegenSecurity> result = new ArrayList<CodegenSecurity>();
List<CodegenSecurity> result = new ArrayList<>();
for (CodegenSecurity security : authMethods) {
boolean filtered = false;
@@ -1356,4 +1464,51 @@ public class DefaultGenerator extends AbstractGenerator implements Generator {
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("debugOperations", "");
GlobalSettings.setProperty("debugSupportingFiles", "");
GlobalSettings.setProperty("verbose", "true");
} else {
GlobalSettings.setProperty("verbose", "false");
}
for (Map.Entry<String, String> entry : workflowSettings.getSystemProperties().entrySet()) {