diff --git a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/ConfigHelp.java b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/ConfigHelp.java index ba6c86466a2..b6c4f9b89bc 100644 --- a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/ConfigHelp.java +++ b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/ConfigHelp.java @@ -72,4 +72,4 @@ public class ConfigHelp implements Runnable { System.exit(1); } } -} \ No newline at end of file +} diff --git a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java index 7382a6d5595..8c4db2af9e4 100644 --- a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java +++ b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java @@ -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); } diff --git a/modules/openapi-generator-gradle-plugin/README.adoc b/modules/openapi-generator-gradle-plugin/README.adoc index e3752a5a627..b379a9f6130 100644 --- a/modules/openapi-generator-gradle-plugin/README.adoc +++ b/modules/openapi-generator-gradle-plugin/README.adoc @@ -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 diff --git a/modules/openapi-generator-gradle-plugin/samples/local-spec/README.md b/modules/openapi-generator-gradle-plugin/samples/local-spec/README.md index 2f6b2a5168c..b3d5dfc4f0b 100644 --- a/modules/openapi-generator-gradle-plugin/samples/local-spec/README.md +++ b/modules/openapi-generator-gradle-plugin/samples/local-spec/README.md @@ -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: diff --git a/modules/openapi-generator-gradle-plugin/samples/local-spec/build.gradle b/modules/openapi-generator-gradle-plugin/samples/local-spec/build.gradle index 4a79c8e6f9a..e9a5bb42e0e 100644 --- a/modules/openapi-generator-gradle-plugin/samples/local-spec/build.gradle +++ b/modules/openapi-generator-gradle-plugin/samples/local-spec/build.gradle @@ -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" + ] +} diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt index dda1bfc2310..b4a2089c11b 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt @@ -80,6 +80,7 @@ class OpenApiGeneratorPlugin : Plugin { 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) diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt index a62efd0bc55..fa7726542cc 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt @@ -32,6 +32,11 @@ open class OpenApiGeneratorGenerateExtension(project: Project) { */ val verbose = project.objects.property() + /** + * Whether or not an input specification should be validated upon generation. + */ + val validateSpec = project.objects.property() + /** * 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>() 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) } } \ No newline at end of file diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt index dbe0424dbca..db901f021c7 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt @@ -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() + /** + * Whether or not an input specification should be validated upon generation. + */ + @get:Internal + val validateSpec = project.objects.property() + /** * 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 -> diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskDslTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskDslTest.kt index fe84a903bd7..c2a6e024420 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskDslTest.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/GenerateTaskDslTest.kt @@ -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) diff --git a/modules/openapi-generator-maven-plugin/README.md b/modules/openapi-generator-maven-plugin/README.md index af7ec01fb8d..2a541850bcb 100644 --- a/modules/openapi-generator-maven-plugin/README.md +++ b/modules/openapi-generator-maven-plugin/README.md @@ -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 +``` diff --git a/modules/openapi-generator-maven-plugin/examples/java-client.xml b/modules/openapi-generator-maven-plugin/examples/java-client.xml index b538b3545e7..475f9cfa665 100644 --- a/modules/openapi-generator-maven-plugin/examples/java-client.xml +++ b/modules/openapi-generator-maven-plugin/examples/java-client.xml @@ -12,7 +12,7 @@ org.openapitools openapi-generator-maven-plugin - 3.0.1-SNAPSHOT + 3.1.1-SNAPSHOT diff --git a/modules/openapi-generator-maven-plugin/examples/non-java-invalid-spec.xml b/modules/openapi-generator-maven-plugin/examples/non-java-invalid-spec.xml new file mode 100644 index 00000000000..31da5677d95 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/examples/non-java-invalid-spec.xml @@ -0,0 +1,34 @@ + + 4.0.0 + org.openapitools + sample-project + jar + 1.0-SNAPSHOT + sample-project + http://maven.apache.org + + + + + org.openapitools + openapi-generator-maven-plugin + 3.1.1-SNAPSHOT + + + + generate + + + false + petstore-v3.0-invalid.yaml + aspnetcore + + optionalProjectFile=true + + + + + + + + diff --git a/modules/openapi-generator-maven-plugin/examples/non-java.xml b/modules/openapi-generator-maven-plugin/examples/non-java.xml index 2210a3b686b..8592217bdce 100644 --- a/modules/openapi-generator-maven-plugin/examples/non-java.xml +++ b/modules/openapi-generator-maven-plugin/examples/non-java.xml @@ -12,7 +12,7 @@ org.openapitools openapi-generator-maven-plugin - 3.0.1-SNAPSHOT + 3.1.1-SNAPSHOT diff --git a/modules/openapi-generator-maven-plugin/examples/petstore-v3.0-invalid.yaml b/modules/openapi-generator-maven-plugin/examples/petstore-v3.0-invalid.yaml new file mode 100644 index 00000000000..0f5c6fc2982 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/examples/petstore-v3.0-invalid.yaml @@ -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 diff --git a/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java index 178caa62750..ffc4c843300 100644 --- a/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java +++ b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java @@ -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); } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/SpecValidationException.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/SpecValidationException.java new file mode 100644 index 00000000000..1578918ba8c --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/SpecValidationException.java @@ -0,0 +1,133 @@ +package org.openapitools.codegen; + +import java.util.Set; + +public class SpecValidationException extends RuntimeException { + + private Set errors; + private Set 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.

Note that the detail message associated with + * {@code cause} is not 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 null 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 (cause==null ? null : cause.toString()) + * (which typically contains the class and detail message of + * cause). 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 null 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 getErrors() { + return errors; + } + + public Set getWarnings() { + return warnings; + } + + public void setErrors(Set errors) { + this.errors = errors; + } + + public void setWarnings(Set 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(); + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java index db72abdf88b..96492e5251c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java @@ -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 dynamicProperties = new HashMap(); //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 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 warnings = new HashSet<>(); + if (specification != null) { + List 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; }