Validate spec on generation by default (#251)

* Validate spec on generation by default

Adds a validation parameter to CodegenConfigurator, and passes through
options from CLI, Maven Plugin and Gradle Plugin to that property.

Default is to validate the spec during generation. If spec has errors,
we will output errors as well as warnings to the user.

Option can be disabled by passing false to validateSpec (Maven/Gradle)
or --validate-spec (CLI).

* Prepare version 3.1.1-SNAPSHOT

* fix version

* Use last prod version for the sample

* Update README.md

Fix

* [cli] Option parser does not support true/false for boolean options
This commit is contained in:
Jim Schubert 2018-07-26 05:36:08 -04:00 committed by William Cheng
parent a8e8acead7
commit 77df3d6770
17 changed files with 391 additions and 15 deletions

View File

@ -72,4 +72,4 @@ public class ConfigHelp implements Runnable {
System.exit(1);
}
}
}
}

View File

@ -194,6 +194,11 @@ public class Generate implements Runnable {
description = CodegenConstants.REMOVE_OPERATION_ID_PREFIX_DESC)
private Boolean removeOperationIdPrefix;
@Option(name = {"--skip-validate-spec"},
title = "skip spec validation",
description = "Skips the default behavior of validating an input specification.")
private Boolean skipValidateSpec;
@Override
public void run() {
@ -207,6 +212,10 @@ public class Generate implements Runnable {
}
// now override with any specified parameters
if (skipValidateSpec != null) {
configurator.setValidateSpec(false);
}
if (verbose != null) {
configurator.setVerbose(verbose);
}

View File

@ -59,6 +59,11 @@ The gradle plugin is not currently published to https://plugins.gradle.org/m2/.
|false
|The verbosity of generation
|validateSpec
|Boolean
|true
|Whether or not we should validate the input spec before generation. Invalid specs result in an error.
|generatorName
|String
|None

View File

@ -11,6 +11,7 @@ gradle openApiGenerate
gradle openApiMeta
gradle openApiValidate
gradle buildGoSdk
gradle generateGoWithInvalidSpec
```
The samples can be tested against other versions of the plugin using the `openApiGeneratorVersion` property. For example:

View File

@ -54,3 +54,16 @@ task buildGoSdk(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTas
dateLibrary: "threetenp"
]
}
task generateGoWithInvalidSpec(type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask){
validateSpec = true
generatorName = "go"
inputSpec = "$rootDir/petstore-v3.0-invalid.yaml".toString()
additionalProperties = [
packageName: "petstore"
]
outputDir = "$buildDir/go".toString()
configOptions = [
dateLibrary: "threetenp"
]
}

View File

@ -80,6 +80,7 @@ class OpenApiGeneratorPlugin : Plugin<Project> {
description = "Generate code via Open API Tools Generator for Open API 2.0 or 3.x specification documents."
verbose.set(generate.verbose)
validateSpec.set(generate.validateSpec)
generatorName.set(generate.generatorName)
outputDir.set(generate.outputDir)
inputSpec.set(generate.inputSpec)

View File

@ -32,6 +32,11 @@ open class OpenApiGeneratorGenerateExtension(project: Project) {
*/
val verbose = project.objects.property<Boolean>()
/**
* Whether or not an input specification should be validated upon generation.
*/
val validateSpec = project.objects.property<Boolean>()
/**
* The name of the generator which will handle codegen. (see "openApiGenerators" task)
*/
@ -262,6 +267,11 @@ open class OpenApiGeneratorGenerateExtension(project: Project) {
val configOptions = project.objects.property<Map<String, String>>()
init {
applyDefaults()
}
@Suppress("MemberVisibilityCanBePrivate")
fun applyDefaults(){
releaseNote.set("Minor update")
modelNamePrefix.set("")
modelNameSuffix.set("")
@ -271,5 +281,6 @@ open class OpenApiGeneratorGenerateExtension(project: Project) {
generateApiDocumentation.set(true)
withXml.set(false)
configOptions.set(mapOf())
validateSpec.set(true)
}
}

View File

@ -28,7 +28,6 @@ import org.gradle.kotlin.dsl.property
import org.openapitools.codegen.CodegenConstants
import org.openapitools.codegen.DefaultGenerator
import org.openapitools.codegen.config.CodegenConfigurator
import org.openapitools.codegen.config.CodegenConfiguratorUtils.*
/**
@ -48,6 +47,12 @@ open class GenerateTask : DefaultTask() {
@get:Internal
val verbose = project.objects.property<Boolean>()
/**
* Whether or not an input specification should be validated upon generation.
*/
@get:Internal
val validateSpec = project.objects.property<Boolean>()
/**
* The name of the generator which will handle codegen. (see "openApiGenerators" task)
*/
@ -382,6 +387,10 @@ open class GenerateTask : DefaultTask() {
configurator.isVerbose = value
}
validateSpec.ifNotEmpty { value ->
configurator.isValidateSpec = value
}
skipOverwrite.ifNotEmpty { value ->
configurator.isSkipOverwrite = value ?: false
}
@ -528,8 +537,7 @@ open class GenerateTask : DefaultTask() {
out.println("Successfully generated code to ${configurator.outputDir}")
} catch (e: RuntimeException) {
logger.error(e.message)
throw GradleException("Code generation failed.")
throw GradleException("Code generation failed.", e)
}
} finally {
originalEnvironmentVariables.forEach { entry ->

View File

@ -31,7 +31,7 @@ class GenerateTaskDslTest : TestBase() {
fun `openApiGenerate should create an expected file structure from DSL config`() {
// Arrange
val projectFiles = mapOf(
"spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0-invalid.yaml")
"spec.yaml" to javaClass.classLoader.getResourceAsStream("specs/petstore-v3.0.yaml")
)
withProject(defaultBuildGradle, projectFiles)

View File

@ -38,6 +38,7 @@ mvn clean compile
### General Configuration parameters
- `inputSpec` - OpenAPI Spec file path
- `validateSpec` - Whether or not to validate the input spec prior to generation. Invalid specifications will result in an error.
- `language` - target generation language (deprecated, replaced by `generatorName` as values here don't represent only 'language' any longer)
- `generatorName` - target generator name
- `output` - target output path (default is `${project.build.directory}/generated-sources/swagger`)
@ -102,4 +103,8 @@ Specifying a custom generator is a bit different. It doesn't support the classpa
### Sample configuration
- Please see [an example configuration](examples) for using the plugin
Please see [an example configuration](examples) for using the plugin. To run these examples, explicitly pass the file to maven. Example:
```bash
mvn -f non-java.xml compile
```

View File

@ -12,7 +12,7 @@
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>3.0.1-SNAPSHOT</version>
<version>3.1.1-SNAPSHOT</version>
<executions>
<execution>
<goals>

View File

@ -0,0 +1,34 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.openapitools</groupId>
<artifactId>sample-project</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>sample-project</name>
<url>http://maven.apache.org</url>
<build>
<plugins>
<!-- activate the plugin -->
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>3.1.1-SNAPSHOT</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<validateSpec>false</validateSpec>
<inputSpec>petstore-v3.0-invalid.yaml</inputSpec>
<generatorName>aspnetcore</generatorName>
<configOptions>
<additional-properties>optionalProjectFile=true</additional-properties>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -12,7 +12,7 @@
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>3.0.1-SNAPSHOT</version>
<version>3.1.1-SNAPSHOT</version>
<executions>
<execution>
<goals>

View File

@ -0,0 +1,103 @@
openapi: "3.0.0"
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
tags:
- pets
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Pet:
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: "#/components/schemas/Pet"
Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

View File

@ -60,6 +60,9 @@ public class CodeGenMojo extends AbstractMojo {
private static final Logger LOGGER = LoggerFactory.getLogger(CodeGenMojo.class);
@Parameter(name="validateSpec", required = false, defaultValue = "true")
private Boolean validateSpec;
@Parameter(name = "verbose", required = false, defaultValue = "false")
private boolean verbose;
@ -348,6 +351,11 @@ public class CodeGenMojo extends AbstractMojo {
configurator.setVerbose(verbose);
// now override with any specified parameters
if (validateSpec != null) {
configurator.setValidateSpec(validateSpec);
}
if (skipOverwrite != null) {
configurator.setSkipOverwrite(skipOverwrite);
}

View File

@ -0,0 +1,133 @@
package org.openapitools.codegen;
import java.util.Set;
public class SpecValidationException extends RuntimeException {
private Set<String> errors;
private Set<String> warnings;
/**
* Constructs a new runtime exception with {@code null} as its
* detail message. The cause is not initialized, and may subsequently be
* initialized by a call to {@link #initCause}.
*/
public SpecValidationException() {
}
/**
* Constructs a new runtime exception with the specified detail message.
* The cause is not initialized, and may subsequently be initialized by a
* call to {@link #initCause}.
*
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
*/
public SpecValidationException(String message) {
super(message);
}
/**
* Constructs a new runtime exception with the specified detail message and
* cause. <p>Note that the detail message associated with
* {@code cause} is <i>not</i> automatically incorporated in
* this runtime exception's detail message.
*
* @param message the detail message (which is saved for later retrieval
* by the {@link #getMessage()} method).
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public SpecValidationException(String message, Throwable cause) {
super(message, cause);
}
/**
* Constructs a new runtime exception with the specified cause and a
* detail message of <tt>(cause==null ? null : cause.toString())</tt>
* (which typically contains the class and detail message of
* <tt>cause</tt>). This constructor is useful for runtime exceptions
* that are little more than wrappers for other throwables.
*
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @since 1.4
*/
public SpecValidationException(Throwable cause) {
super(cause);
}
/**
* Constructs a new runtime exception with the specified detail
* message, cause, suppression enabled or disabled, and writable
* stack trace enabled or disabled.
*
* @param message the detail message.
* @param cause the cause. (A {@code null} value is permitted,
* and indicates that the cause is nonexistent or unknown.)
* @param enableSuppression whether or not suppression is enabled
* or disabled
* @param writableStackTrace whether or not the stack trace should
* be writable
* @since 1.7
*/
public SpecValidationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public Set<String> getErrors() {
return errors;
}
public Set<String> getWarnings() {
return warnings;
}
public void setErrors(Set<String> errors) {
this.errors = errors;
}
public void setWarnings(Set<String> warnings) {
this.warnings = warnings;
}
/**
* Returns the detail message string of this throwable.
*
* @return the detail message string of this {@code Throwable} instance
* (which may be {@code null}).
*/
@Override
public String getMessage() {
int errorCount = 0;
if (errors != null) {
errorCount = errors.size();
}
int warningCount = 0;
if (warnings != null) {
warningCount = warnings.size();
}
StringBuilder sb = new StringBuilder();
sb.append(System.lineSeparator())
.append("Errors: ")
.append(System.lineSeparator());
errors.forEach(msg ->
sb.append("\t-").append(msg).append(System.lineSeparator())
);
if (!warnings.isEmpty()) {
sb.append("Warnings: ").append(System.lineSeparator());
warnings.forEach(msg ->
sb.append("\t-").append(msg).append(System.lineSeparator())
);
}
return super.getMessage() + " | " +
"Error count: " + errorCount + ", Warning count: " + warningCount + sb.toString();
}
}

View File

@ -19,12 +19,8 @@ package org.openapitools.codegen.config;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.ClientOpts;
import org.openapitools.codegen.CodegenConfig;
import org.openapitools.codegen.CodegenConfigLoader;
import org.openapitools.codegen.CodegenConstants;
import io.swagger.v3.oas.models.OpenAPI;
import org.openapitools.codegen.*;
import org.openapitools.codegen.auth.AuthParser;
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.core.util.Json;
@ -33,6 +29,7 @@ import io.swagger.v3.parser.core.models.ParseOptions;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import org.apache.commons.lang3.Validate;
import org.openapitools.codegen.languages.*;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -80,6 +77,7 @@ public class CodegenConfigurator implements Serializable {
private boolean verbose;
private boolean skipOverwrite;
private boolean removeOperationIdPrefix;
private boolean validateSpec;
private String templateDir;
private String auth;
private String apiPackage;
@ -108,6 +106,7 @@ public class CodegenConfigurator implements Serializable {
private final Map<String, Object> dynamicProperties = new HashMap<String, Object>(); //the map that holds the JsonAnySetter/JsonAnyGetter values
public CodegenConfigurator() {
this.validateSpec = true;
this.setOutputDir(".");
}
@ -211,6 +210,15 @@ public class CodegenConfigurator implements Serializable {
return this;
}
public boolean isValidateSpec() {
return validateSpec;
}
public CodegenConfigurator setValidateSpec(final boolean validateSpec) {
this.validateSpec = validateSpec;
return this;
}
public boolean isSkipOverwrite() {
return skipOverwrite;
}
@ -514,8 +522,45 @@ public class CodegenConfigurator implements Serializable {
options.setResolve(true);
options.setFlatten(true);
SwaggerParseResult result = new OpenAPIParser().readLocation(inputSpec, authorizationValues, options);
Set<String> validationMessages = new HashSet<>(result.getMessages());
OpenAPI specification = result.getOpenAPI();
// NOTE: We will only expose errors+warnings if there are already errors in the spec.
if (validationMessages.size() > 0) {
Set<String> warnings = new HashSet<>();
if (specification != null) {
List<String> unusedModels = ModelUtils.getUnusedSchemas(specification);
if (unusedModels != null) unusedModels.forEach(name -> warnings.add("Unused model: " + name));
}
if (this.isValidateSpec()) {
SpecValidationException ex = new SpecValidationException("Specification has failed validation.");
ex.setErrors(validationMessages);
ex.setWarnings(warnings);
throw ex;
} else {
StringBuilder sb = new StringBuilder();
sb.append("There were issues with the specification, but validation has been explicitly disabled.");
sb.append(System.lineSeparator());
sb.append("Errors: ").append(System.lineSeparator());
validationMessages.forEach(msg ->
sb.append("\t-").append(msg).append(System.lineSeparator())
);
if (!warnings.isEmpty()) {
sb.append("Warnings: ").append(System.lineSeparator());
warnings.forEach(msg ->
sb.append("\t-").append(msg).append(System.lineSeparator())
);
}
LOGGER.warn(sb.toString());
}
}
input.opts(new ClientOpts())
.openAPI(result.getOpenAPI());
.openAPI(specification);
return input;
}