From 0bf44a6f4c3848b1cda8601dce7964d57ce478b1 Mon Sep 17 00:00:00 2001 From: "istvan.verhas" Date: Sun, 31 May 2026 13:50:18 +0200 Subject: [PATCH 1/2] Add ValidateMojo as a new target to enable OpenAPI definition validation with the Maven plugin Add validation harness both unit and integration tests for the 'validate' goal --- .../openapi-generator-maven-plugin/README.md | 207 ++++++--- .../openapi-generator-maven-plugin/pom.xml | 16 +- .../invoker.properties | 3 + .../cli-comma-separated-override/pom.xml | 17 + .../unused_model_api.yaml | 27 ++ .../valid_api.yaml | 25 + .../input-directory-merge/invoker.properties | 2 + .../input-directory-merge/pom.xml | 28 ++ .../input-directory-merge/specs/part1.yaml | 10 + .../input-directory-merge/specs/part2.yaml | 10 + .../multi-file-xml-list/api_first.yaml | 10 + .../multi-file-xml-list/api_second.yaml | 10 + .../multi-file-xml-list/invoker.properties | 2 + .../multi-file-xml-list/pom.xml | 31 ++ .../strict-spec-failure/invalid_api.yaml | 10 + .../strict-spec-failure/invoker.properties | 3 + .../strict-spec-failure/pom.xml | 28 ++ .../strict-spec-failure/verify.groovy | 16 + .../codegen/plugin/ValidateMojo.java | 427 ++++++++++++++++++ .../codegen/plugin/ValidateMojoTest.java | 381 ++++++++++++++++ .../validate-harness/broken_syntax_api.yaml | 24 + .../test/resources/validate-harness/pom.xml | 43 ++ .../specs-to-merge/file1.yaml | 10 + .../specs-to-merge/file2.yaml | 10 + .../validate-harness/unused_model_api.yaml | 27 ++ .../resources/validate-harness/valid_api.yaml | 25 + 26 files changed, 1329 insertions(+), 73 deletions(-) create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/invoker.properties create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/pom.xml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/unused_model_api.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/valid_api.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/invoker.properties create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/pom.xml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/specs/part1.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/specs/part2.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/api_first.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/api_second.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/invoker.properties create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/pom.xml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/invalid_api.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/invoker.properties create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/pom.xml create mode 100644 modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/verify.groovy create mode 100644 modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/ValidateMojo.java create mode 100644 modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/ValidateMojoTest.java create mode 100644 modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/broken_syntax_api.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/pom.xml create mode 100644 modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/specs-to-merge/file1.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/specs-to-merge/file2.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/unused_model_api.yaml create mode 100644 modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/valid_api.yaml diff --git a/modules/openapi-generator-maven-plugin/README.md b/modules/openapi-generator-maven-plugin/README.md index 4119b1c615ce..7484ef65a781 100644 --- a/modules/openapi-generator-maven-plugin/README.md +++ b/modules/openapi-generator-maven-plugin/README.md @@ -41,80 +41,143 @@ mvn clean compile :bulb: These **general** configurations should be in the same level +| Option | Property | Description | +|-----------------------------------|-------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `verbose` | `openapi.generator.maven.plugin.verbose` | verbose mode (`false` by default) | +| `inputSpec` | `openapi.generator.maven.plugin.inputSpec` | OpenAPI Spec file path | +| `inputSpecRootDirectory` | `openapi.generator.maven.plugin.inputSpecRootDirectory` | Local root folder with spec file(s) | +| `mergedFileName` | `openapi.generator.maven.plugin.mergedFileName` | Name of the file that will contain all merged specs | +| `language` | `openapi.generator.maven.plugin.language` | target generation language (deprecated, replaced by `generatorName` as values here don't represent only 'language' any longer) | +| `generatorName` | `openapi.generator.maven.plugin.generatorName` | target generator name | +| `cleanupOutput` | `openapi.generator.maven.plugin.cleanupOutput` | Defines whether the output directory should be cleaned up before generating the output (`false` by default). | +| `output` | `openapi.generator.maven.plugin.output` | target output path (default is `${project.build.directory}/generated-sources/openapi`. Can also be set globally through the `openapi.generator.maven.plugin.output` property) | +| `gitHost` | `openapi.generator.maven.plugin.gitHost` | The git host, e.g. gitlab.com | +| `gitUserId` | `openapi.generator.maven.plugin.gitUserId` | sets git information of the project | +| `gitRepoId` | `openapi.generator.maven.plugin.gitRepoId` | sets the repo ID (e.g. openapi-generator) | +| `collapsedSpec` | `openapi.generator.maven.plugin.collapsedSpec` | sets the path to the collapsed single-file representation of the OpenAPI spec | +| `includeCollapsedSpecInArtifacts` | `openapi.generator.maven.plugin.publishCollapsedSpec` | includes the collapsed spec in the Maven artifacts (`false` by default) | +| `templateDirectory` | `openapi.generator.maven.plugin.templateDirectory` | directory with mustache templates | +| `templateResourcePath` | `openapi.generator.maven.plugin.templateResourcePath` | directory with mustache templates via resource path. This option will overwrite any option defined in `templateDirectory`. | +| `engine` | `openapi.generator.maven.plugin.engine` | The name of templating engine to use, "mustache" (default) or "handlebars" (beta) | +| `auth` | `openapi.generator.maven.plugin.auth` | adds authorization headers when fetching the OpenAPI definitions remotely. Pass in a URL-encoded string of `name:header` with a comma separating multiple values | +| `configurationFile` | `openapi.generator.maven.plugin.configurationFile` | Path to separate json configuration file. File content should be in a json format {"optionKey":"optionValue", "optionKey1":"optionValue1"...} Supported options can be different for each generator. Run `config-help -g {generator name}` command for generator-specific config options | +| `skipOverwrite` | `openapi.generator.maven.plugin.skipOverwrite` | Specifies if the existing files should be overwritten during the generation. (`false` by default) | +| `apiPackage` | `openapi.generator.maven.plugin.apiPackage` | the package to use for generated api objects/classes | +| `modelPackage` | `openapi.generator.maven.plugin.modelPackage` | the package to use for generated model objects/classes | +| `invokerPackage` | `openapi.generator.maven.plugin.invokerPackage` | the package to use for the generated invoker objects | +| `packageName` | `openapi.generator.maven.plugin.packageName` | the default package name to use for the generated objects | +| `groupId` | `openapi.generator.maven.plugin.groupId` | sets project information in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators | +| `artifactId` | `openapi.generator.maven.plugin.artifactId` | sets project information in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators | +| `artifactVersion` | `openapi.generator.maven.plugin.artifactVersion` | sets project information in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators | +| `library` | `openapi.generator.maven.plugin.library` | library template (sub-template) | +| `modelNamePrefix` | `openapi.generator.maven.plugin.modelNamePrefix` | Sets the prefix for model classes and enums | +| `modelNameSuffix` | `openapi.generator.maven.plugin.modelNameSuffix` | Sets the suffix for model classes and enums | +| `apiNameSuffix` | `openapi.generator.maven.plugin.apiNameSuffix` | Sets the suffix for api classes | +| `ignoreFileOverride` | `openapi.generator.maven.plugin.ignoreFileOverride` | specifies the full path to a `.openapi-generator-ignore` used for pattern based overrides of generated outputs | +| `httpUserAgent` | `openapi.generator.maven.plugin.httpUserAgent` | Sets custom User-Agent header value | +| `removeOperationIdPrefix` | `openapi.generator.maven.plugin.removeOperationIdPrefix` | remove operationId prefix (e.g. user_getName => getName) | +| `skipOperationExample` | `openapi.generator.maven.plugin.skipOperationExample` | skip examples defined in the operation | +| `logToStderr` | `openapi.generator.maven.plugin.logToStderr` | write all log messages (not just errors) to STDERR | +| `enablePostProcessFile` | `openapi.generator.maven.plugin.` | enable file post-processing hook | +| `skipValidateSpec` | `openapi.generator.maven.plugin.skipValidateSpec` | Whether or not to skip validating the input spec prior to generation. By default, invalid specifications will result in an error. | +| `strictSpec` | `openapi.generator.maven.plugin.strictSpec` | Whether or not to treat an input document strictly against the spec. 'MUST' and 'SHALL' wording in OpenAPI spec is strictly adhered to. e.g. when false, no fixes will be applied to documents which pass validation but don't follow the spec. | +| `openapiNormalizer` | `openapi.generator.maven.plugin.openapiNormalizer` | specifies the rules to be enabled in OpenAPI normalizer in the form of RULE_1=true,RULE_2=original. | +| `generateAliasAsModel` | `openapi.generator.maven.plugin.generateAliasAsModel` | generate alias (array, map) as model | +| `configOptions` | N/A | a **map** of generator-specific parameters. To show a full list of generator-specified parameters (options), please use `configHelp` (explained below) | +| `instantiationTypes` | `openapi.generator.maven.plugin.instantiationTypes` | sets instantiation type mappings in the format of type=instantiatedType,type=instantiatedType. For example (in Java): `array=ArrayList,map=HashMap`. In other words array types will get instantiated as ArrayList in generated code. You can also have multiple occurrences of this option | +| `importMappings` | `openapi.generator.maven.plugin.importMappings` | specifies mappings between a given class and the import that should be used for that class in the format of type=import,type=import. You can also have multiple occurrences of this option | +| `typeMappings` | `openapi.generator.maven.plugin.typeMappings` | sets mappings between OpenAPI spec types and generated code types in the format of OpenAPIType=generatedType,OpenAPIType=generatedType. For example: `array=List,map=Map,string=String`. You can also have multiple occurrences of this option. To map a specified format, use type+format, e.g. string+password=EncryptedString will map `type: string, format: password` to `EncryptedString`. | +| `schemaMappings` | `openapi.generator.maven.plugin.schemaMappings` | specifies mappings between the schema and the new name in the format of schema_a=Cat,schema_b=Bird. https://openapi-generator.tech/docs/customization/#schema-mapping | +| `nameMappings` | `openapi.generator.maven.plugin.nameMappings` | specifies mappings between the property name and the new name in the format of property_a=firstProperty,property_b=secondProperty. https://openapi-generator.tech/docs/customization/#name-mapping | +| `modelNameMappings` | `openapi.generator.maven.plugin.modelNameMappings` | specifies mappings between the model name and the new name in the format of model_a=FirstModel,model_b=SecondModel. https://openapi-generator.tech/docs/customization/#name-mapping | +| `parameterNameMappings` | `openapi.generator.maven.plugin.parameterNameMappings` | specifies mappings between the parameter name and the new name in the format of param_a=first_parameter,param_b=second_parameter. https://openapi-generator.tech/docs/customization/#name-mapping | +| `inlineSchemaNameMappings` | `openapi.generator.maven.plugin.inlineSchemaNameMappings` | specifies mappings between the inline schema name and the new name in the format of inline_object_2=Cat,inline_object_5=Bird. | +| `inlineSchemaOptions` | `openapi.generator.maven.plugin.inlineSchemaOptions` | specifies the options used when naming inline schema in inline model resolver | +| `languageSpecificPrimitives` | `openapi.generator.maven.plugin.languageSpecificPrimitives` | specifies additional language specific primitive types in the format of type1,type2,type3,type3. For example: `String,boolean,Boolean,Double`. You can also have multiple occurrences of this option | +| `additionalProperties` | `openapi.generator.maven.plugin.additionalProperties` | sets additional properties that can be referenced by the mustache templates in the format of name=value,name=value. You can also have multiple occurrences of this option | +| `serverVariableOverrides` | `openapi.generator.maven.plugin.serverVariableOverrides` | A map of server variable overrides for specs that support server URL templating | +| `reservedWordsMappings` | `openapi.generator.maven.plugin.reservedWordsMappings` | specifies how a reserved name should be escaped to. Otherwise, the default `_` is used. For example `id=identifier`. You can also have multiple occurrences of this option | +| `generateApis` | `openapi.generator.maven.plugin.generateApis` | generate the apis (`true` by default). Specific apis may be defined as a CSV via `apisToGenerate`. | +| `apisToGenerate` | `openapi.generator.maven.plugin.apisToGenerate` | A comma separated list of apis to generate. All apis is the default. | +| `generateModels` | `openapi.generator.maven.plugin.generateModels` | generate the models (`true` by default). Specific models may be defined as a CSV via `modelsToGenerate`. | +| `modelsToGenerate` | `openapi.generator.maven.plugin.modelsToGenerate` | A comma separated list of models to generate. All models is the default. | +| `generateSupportingFiles` | `openapi.generator.maven.plugin.generateSupportingFiles` | generate the supporting files (`true` by default) | +| `supportingFilesToGenerate` | `openapi.generator.maven.plugin.supportingFilesToGenerate` | A comma separated list of supporting files to generate. All files is the default. | +| `generateModelTests` | `openapi.generator.maven.plugin.generateModelTests` | generate the model tests (`true` by default. Only available if `generateModels` is `true`) | +| `generateModelDocumentation` | `openapi.generator.maven.plugin.generateModelDocumentation` | generate the model documentation (`true` by default. Only available if `generateModels` is `true`) | +| `generateApiTests` | `openapi.generator.maven.plugin.generateApiTests` | generate the api tests (`true` by default. Only available if `generateApis` is `true`) | +| `generateApiDocumentation` | `openapi.generator.maven.plugin.generateApiDocumentation` | generate the api documentation (`true` by default. Only available if `generateApis` is `true`) | +| `skip` | `codegen.skip` | skip code generation (`false` by default. Can also be set globally through the `codegen.skip` property) | +| `skipIfSpecIsUnchanged` | `codegen.skipIfSpecIsUnchanged` | Skip the execution if the source file is older than the output folder (`false` by default. Can also be set globally through the `codegen.skipIfSpecIsUnchanged` property) | +| `addCompileSourceRoot` | `openapi.generator.maven.plugin.addCompileSourceRoot` | Add the output directory to the project as a source root, so that the generated java types are compiled and included in the project artifact (`true` by default). Mutually exclusive with `addTestCompileSourceRoot`. | +| `addTestCompileSourceRoot` | `openapi.generator.maven.plugin.addTestCompileSourceRoot` | Add the output directory to the project as a test source root, so that the generated java types are compiled only for the test classpath of the project (`false` by default). Mutually exclusive with `addCompileSourceRoot`. | +| `dryRun` | `openapi.generator.maven.plugin.dryRun` | Defines whether the generator should run in dry-run mode. In dry-run mode no files are written and a summary about file states is output ( `false` by default). | +| `environmentVariables` | N/A | deprecated. Use globalProperties instead. | +| `globalProperties` | N/A | A **map** of items conceptually similar to "environment variables" or "system properties". These are available to all aspects of the generation flow. See [Global Properties](https://openapi-generator.tech/docs/globals/) for list of available properties. | +| `configHelp` | `codegen.configHelp` | dumps the configuration help for the specified library (generates no sources) | + +### Validate goal + +The `validate` goal allows you to validate one or more OpenAPI specifications. It supports multi-file validation via XML list configuration, comma-separated strings (useful for CLI overrides), or directory scanning. + +#### Usage + +```xml + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator-maven-plugin-version} + + + + validate + + + ${project.basedir}/src/main/resources/api.yaml + + + + +``` + +#### Multi-file configuration + +You can validate multiple files by providing them as a list in the XML configuration: + +```xml + + + ${project.basedir}/src/main/resources/api-v1.yaml + ${project.basedir}/src/main/resources/api-v2.yaml + + +``` + +Alternatively, you can scan an entire directory for OpenAPI specifications: + +```xml + + ${project.basedir}/src/main/resources/specs + +``` + +#### Validation Parameters + | Option | Property | Description | |--------|----------|-------------| -| `verbose` | `openapi.generator.maven.plugin.verbose` | verbose mode (`false` by default) -| `inputSpec` | `openapi.generator.maven.plugin.inputSpec` | OpenAPI Spec file path -| `inputSpecRootDirectory` | `openapi.generator.maven.plugin.inputSpecRootDirectory` | Local root folder with spec file(s) -| `mergedFileName` | `openapi.generator.maven.plugin.mergedFileName` | Name of the file that will contain all merged specs -| `language` | `openapi.generator.maven.plugin.language` | target generation language (deprecated, replaced by `generatorName` as values here don't represent only 'language' any longer) -| `generatorName` | `openapi.generator.maven.plugin.generatorName` | target generator name -| `cleanupOutput` | `openapi.generator.maven.plugin.cleanupOutput` | Defines whether the output directory should be cleaned up before generating the output (`false` by default). -| `output` | `openapi.generator.maven.plugin.output` | target output path (default is `${project.build.directory}/generated-sources/openapi`. Can also be set globally through the `openapi.generator.maven.plugin.output` property) -| `gitHost` | `openapi.generator.maven.plugin.gitHost` | The git host, e.g. gitlab.com -| `gitUserId` | `openapi.generator.maven.plugin.gitUserId` | sets git information of the project -| `gitRepoId` | `openapi.generator.maven.plugin.gitRepoId` | sets the repo ID (e.g. openapi-generator) -| `collapsedSpec` | `openapi.generator.maven.plugin.collapsedSpec` | sets the path to the collapsed single-file representation of the OpenAPI spec -| `includeCollapsedSpecInArtifacts` | `openapi.generator.maven.plugin.publishCollapsedSpec` | includes the collapsed spec in the Maven artifacts (`false` by default) -| `templateDirectory` | `openapi.generator.maven.plugin.templateDirectory` | directory with mustache templates -| `templateResourcePath` | `openapi.generator.maven.plugin.templateResourcePath` | directory with mustache templates via resource path. This option will overwrite any option defined in `templateDirectory`. -| `engine` | `openapi.generator.maven.plugin.engine` | The name of templating engine to use, "mustache" (default) or "handlebars" (beta) -| `auth` | `openapi.generator.maven.plugin.auth` | adds authorization headers when fetching the OpenAPI definitions remotely. Pass in a URL-encoded string of `name:header` with a comma separating multiple values -| `configurationFile` | `openapi.generator.maven.plugin.configurationFile` | Path to separate json configuration file. File content should be in a json format {"optionKey":"optionValue", "optionKey1":"optionValue1"...} Supported options can be different for each generator. Run `config-help -g {generator name}` command for generator-specific config options -| `skipOverwrite` | `openapi.generator.maven.plugin.skipOverwrite` | Specifies if the existing files should be overwritten during the generation. (`false` by default) -| `apiPackage` | `openapi.generator.maven.plugin.apiPackage` | the package to use for generated api objects/classes -| `modelPackage` | `openapi.generator.maven.plugin.modelPackage` | the package to use for generated model objects/classes -| `invokerPackage` | `openapi.generator.maven.plugin.invokerPackage` | the package to use for the generated invoker objects -| `packageName` | `openapi.generator.maven.plugin.packageName` | the default package name to use for the generated objects -| `groupId` | `openapi.generator.maven.plugin.groupId` | sets project information in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators -| `artifactId` | `openapi.generator.maven.plugin.artifactId` | sets project information in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators -| `artifactVersion` | `openapi.generator.maven.plugin.artifactVersion` | sets project information in generated pom.xml/build.gradle or other build script. Language-specific conversions occur in non-jvm generators -| `library` | `openapi.generator.maven.plugin.library` | library template (sub-template) -| `modelNamePrefix` | `openapi.generator.maven.plugin.modelNamePrefix` | Sets the prefix for model classes and enums -| `modelNameSuffix` | `openapi.generator.maven.plugin.modelNameSuffix` | Sets the suffix for model classes and enums -| `apiNameSuffix` | `openapi.generator.maven.plugin.apiNameSuffix` | Sets the suffix for api classes -| `ignoreFileOverride` | `openapi.generator.maven.plugin.ignoreFileOverride` | specifies the full path to a `.openapi-generator-ignore` used for pattern based overrides of generated outputs -| `httpUserAgent` | `openapi.generator.maven.plugin.httpUserAgent` | Sets custom User-Agent header value -| `removeOperationIdPrefix` | `openapi.generator.maven.plugin.removeOperationIdPrefix` | remove operationId prefix (e.g. user_getName => getName) -| `skipOperationExample` | `openapi.generator.maven.plugin.skipOperationExample` | skip examples defined in the operation -| `logToStderr` | `openapi.generator.maven.plugin.logToStderr` | write all log messages (not just errors) to STDERR -| `enablePostProcessFile` | `openapi.generator.maven.plugin.` | enable file post-processing hook -| `skipValidateSpec` | `openapi.generator.maven.plugin.skipValidateSpec` | Whether or not to skip validating the input spec prior to generation. By default, invalid specifications will result in an error. -| `strictSpec` | `openapi.generator.maven.plugin.strictSpec` | Whether or not to treat an input document strictly against the spec. 'MUST' and 'SHALL' wording in OpenAPI spec is strictly adhered to. e.g. when false, no fixes will be applied to documents which pass validation but don't follow the spec. -| `openapiNormalizer` | `openapi.generator.maven.plugin.openapiNormalizer` | specifies the rules to be enabled in OpenAPI normalizer in the form of RULE_1=true,RULE_2=original. -| `generateAliasAsModel` | `openapi.generator.maven.plugin.generateAliasAsModel` | generate alias (array, map) as model -| `configOptions` | N/A | a **map** of generator-specific parameters. To show a full list of generator-specified parameters (options), please use `configHelp` (explained below) -| `instantiationTypes` | `openapi.generator.maven.plugin.instantiationTypes` | sets instantiation type mappings in the format of type=instantiatedType,type=instantiatedType. For example (in Java): `array=ArrayList,map=HashMap`. In other words array types will get instantiated as ArrayList in generated code. You can also have multiple occurrences of this option -| `importMappings` | `openapi.generator.maven.plugin.importMappings` | specifies mappings between a given class and the import that should be used for that class in the format of type=import,type=import. You can also have multiple occurrences of this option -| `typeMappings` | `openapi.generator.maven.plugin.typeMappings` | sets mappings between OpenAPI spec types and generated code types in the format of OpenAPIType=generatedType,OpenAPIType=generatedType. For example: `array=List,map=Map,string=String`. You can also have multiple occurrences of this option. To map a specified format, use type+format, e.g. string+password=EncryptedString will map `type: string, format: password` to `EncryptedString`. -| `schemaMappings` | `openapi.generator.maven.plugin.schemaMappings` | specifies mappings between the schema and the new name in the format of schema_a=Cat,schema_b=Bird. https://openapi-generator.tech/docs/customization/#schema-mapping -| `nameMappings` | `openapi.generator.maven.plugin.nameMappings` | specifies mappings between the property name and the new name in the format of property_a=firstProperty,property_b=secondProperty. https://openapi-generator.tech/docs/customization/#name-mapping -| `modelNameMappings` | `openapi.generator.maven.plugin.modelNameMappings` | specifies mappings between the model name and the new name in the format of model_a=FirstModel,model_b=SecondModel. https://openapi-generator.tech/docs/customization/#name-mapping -| `parameterNameMappings` | `openapi.generator.maven.plugin.parameterNameMappings` | specifies mappings between the parameter name and the new name in the format of param_a=first_parameter,param_b=second_parameter. https://openapi-generator.tech/docs/customization/#name-mapping -| `inlineSchemaNameMappings` | `openapi.generator.maven.plugin.inlineSchemaNameMappings` | specifies mappings between the inline schema name and the new name in the format of inline_object_2=Cat,inline_object_5=Bird. -| `inlineSchemaOptions` | `openapi.generator.maven.plugin.inlineSchemaOptions` | specifies the options used when naming inline schema in inline model resolver -| `languageSpecificPrimitives` | `openapi.generator.maven.plugin.languageSpecificPrimitives` | specifies additional language specific primitive types in the format of type1,type2,type3,type3. For example: `String,boolean,Boolean,Double`. You can also have multiple occurrences of this option -| `additionalProperties` | `openapi.generator.maven.plugin.additionalProperties` | sets additional properties that can be referenced by the mustache templates in the format of name=value,name=value. You can also have multiple occurrences of this option -| `serverVariableOverrides` | `openapi.generator.maven.plugin.serverVariableOverrides` | A map of server variable overrides for specs that support server URL templating -| `reservedWordsMappings` | `openapi.generator.maven.plugin.reservedWordsMappings` | specifies how a reserved name should be escaped to. Otherwise, the default `_` is used. For example `id=identifier`. You can also have multiple occurrences of this option -| `generateApis` | `openapi.generator.maven.plugin.generateApis` | generate the apis (`true` by default). Specific apis may be defined as a CSV via `apisToGenerate`. -| `apisToGenerate` | `openapi.generator.maven.plugin.apisToGenerate` | A comma separated list of apis to generate. All apis is the default. -| `generateModels` | `openapi.generator.maven.plugin.generateModels` | generate the models (`true` by default). Specific models may be defined as a CSV via `modelsToGenerate`. -| `modelsToGenerate` | `openapi.generator.maven.plugin.modelsToGenerate` | A comma separated list of models to generate. All models is the default. -| `generateSupportingFiles` | `openapi.generator.maven.plugin.generateSupportingFiles` | generate the supporting files (`true` by default) -| `supportingFilesToGenerate` | `openapi.generator.maven.plugin.supportingFilesToGenerate` | A comma separated list of supporting files to generate. All files is the default. -| `generateModelTests` | `openapi.generator.maven.plugin.generateModelTests` | generate the model tests (`true` by default. Only available if `generateModels` is `true`) -| `generateModelDocumentation` | `openapi.generator.maven.plugin.generateModelDocumentation` | generate the model documentation (`true` by default. Only available if `generateModels` is `true`) -| `generateApiTests` | `openapi.generator.maven.plugin.generateApiTests` | generate the api tests (`true` by default. Only available if `generateApis` is `true`) -| `generateApiDocumentation` | `openapi.generator.maven.plugin.generateApiDocumentation` | generate the api documentation (`true` by default. Only available if `generateApis` is `true`) -| `skip` | `codegen.skip` | skip code generation (`false` by default. Can also be set globally through the `codegen.skip` property) -| `skipIfSpecIsUnchanged` | `codegen.skipIfSpecIsUnchanged` | Skip the execution if the source file is older than the output folder (`false` by default. Can also be set globally through the `codegen.skipIfSpecIsUnchanged` property) -| `addCompileSourceRoot` | `openapi.generator.maven.plugin.addCompileSourceRoot` | Add the output directory to the project as a source root, so that the generated java types are compiled and included in the project artifact (`true` by default). Mutually exclusive with `addTestCompileSourceRoot`. -| `addTestCompileSourceRoot` | `openapi.generator.maven.plugin.addTestCompileSourceRoot` | Add the output directory to the project as a test source root, so that the generated java types are compiled only for the test classpath of the project (`false` by default). Mutually exclusive with `addCompileSourceRoot`. -| `dryRun` | `openapi.generator.maven.plugin.dryRun` | Defines whether the generator should run in dry-run mode. In dry-run mode no files are written and a summary about file states is output ( `false` by default). -| `environmentVariables` | N/A | deprecated. Use globalProperties instead. -| `globalProperties` | N/A | A **map** of items conceptually similar to "environment variables" or "system properties". These are available to all aspects of the generation flow. See [Global Properties](https://openapi-generator.tech/docs/globals/) for list of available properties. -| `configHelp` | `codegen.configHelp` | dumps the configuration help for the specified library (generates no sources) +| `inputSpec` | `openapi.generator.maven.plugin.inputSpec` | Path(s) to OpenAPI Spec file(s). Supports CSV string via CLI or `` list in XML. | +| `inputSpecRootDirectory` | `openapi.generator.maven.plugin.inputSpecRootDirectory` | Local root folder to scan for spec files. | +| `mergedFileName` | `openapi.generator.maven.plugin.mergedFileName` | Name of the file that will contain all merged specs (default: `_merged_spec`). | +| `mergedFileInfoName` | `openapi.generator.maven.plugin.mergedFileInfoName` | Name in the info section of the merged spec (default: `merged spec`). | +| `mergedFileInfoDescription` | `openapi.generator.maven.plugin.mergedFileInfoDescription` | Description in the info section of the merged spec (default: `merged spec`). | +| `mergedFileInfoVersion` | `openapi.generator.maven.plugin.mergedFileInfoVersion` | Version in the info section of the merged spec (default: `1.0.0`). | +| `collapsedSpec` | `openapi.generator.maven.plugin.collapsedSpec` | Path to the collapsed single-file representation of the OpenAPI spec. | +| `includeCollapsedSpecInArtifacts` | `openapi.generator.maven.plugin.publishCollapsedSpec` | Includes the collapsed spec in the Maven artifacts (default: `false`). | +| `auth` | `openapi.generator.maven.plugin.auth` | Adds authorization headers when fetching specs remotely. Format: `name:header` (URL-encoded). | +| `strictSpec` | `openapi.generator.maven.plugin.strictSpec` | If `true`, treats recommendations (e.g. unused models) as errors (default: `false`). | +| `skipValidateSpec` | `openapi.generator.maven.plugin.skipValidateSpec` | Skips the validation goal execution. | +| `skip` | `codegen.skip` | Global skip for all plugin goals (default: `false`). | +| `dryRun` | `openapi.generator.maven.plugin.dryRun` | If `true`, reports validation issues without failing the build (default: `false`). | ### Configuring **map** structures diff --git a/modules/openapi-generator-maven-plugin/pom.xml b/modules/openapi-generator-maven-plugin/pom.xml index 8b47a20b035b..fdc1caa8b5af 100644 --- a/modules/openapi-generator-maven-plugin/pom.xml +++ b/modules/openapi-generator-maven-plugin/pom.xml @@ -30,6 +30,11 @@ ${maven.version} provided + + org.apache.maven.shared + maven-shared-utils + 3.3.4 + org.apache.maven maven-artifact @@ -102,7 +107,12 @@ 4.8.0 test - + + org.mockito + mockito-core + ${mockito.version} + test + @@ -174,6 +184,10 @@ true verify true + + */pom.xml + validate-harness/*/pom.xml + diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/invoker.properties b/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/invoker.properties new file mode 100644 index 000000000000..2e7797cfa9ab --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/invoker.properties @@ -0,0 +1,3 @@ +invoker.goals = validate +invoker.systemProperties = openapi.generator.inputSpec=valid_api.yaml,unused_model_api.yaml +invoker.buildResult = success diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/pom.xml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/pom.xml new file mode 100644 index 000000000000..fb51a37d07f6 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + org.openapitools.it + cli-comma-separated-override + 1.0.0-SNAPSHOT + + + + org.openapitools + openapi-generator-maven-plugin + @project.version@ + + + + \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/unused_model_api.yaml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/unused_model_api.yaml new file mode 100644 index 000000000000..f14fb3446c0e --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/unused_model_api.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.3 +info: + title: API with Style Recommendations + version: 1.0.0 + description: Valid spec syntax, but contains an isolated component model that triggers an unused schema warning. +paths: + /users: + get: + summary: Get active users + operationId: getUsers + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + type: string +components: + schemas: + UnusedModel: + type: object + properties: + legacyField: + type: integer + description: This model is declared but never referenced anywhere in any operation or path parameter. \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/valid_api.yaml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/valid_api.yaml new file mode 100644 index 000000000000..9f56ecc6850b --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/cli-comma-separated-override/valid_api.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.3 +info: + title: Healthy Test API + version: 1.0.0 + description: A completely valid specification with no errors or recommendations. +paths: + /ping: + get: + summary: Simple healthcheck endpoint + operationId: pingCheck + responses: + '200': + description: Server is operational + content: + application/json: + schema: + $ref: '#/components/schemas/Pong' +components: + schemas: + Pong: + type: object + properties: + status: + type: string + example: healthy \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/invoker.properties b/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/invoker.properties new file mode 100644 index 000000000000..7369d207daa4 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals = validate +invoker.buildResult = success \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/pom.xml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/pom.xml new file mode 100644 index 000000000000..fbea6df37cb3 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + org.openapitools.it + input-directory-merge + 1.0.0-SNAPSHOT + + + + org.openapitools + openapi-generator-maven-plugin + @project.version@ + + + validate + + validate + + + ${project.basedir}/specs + + + + + + + \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/specs/part1.yaml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/specs/part1.yaml new file mode 100644 index 000000000000..0296f17c14b5 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/specs/part1.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.3 +info: + title: Part 1 + version: 1.0.0 +paths: + /part1: + get: + responses: + '200': + description: OK \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/specs/part2.yaml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/specs/part2.yaml new file mode 100644 index 000000000000..9606df4530fe --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/input-directory-merge/specs/part2.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.3 +info: + title: Part 2 + version: 1.0.0 +paths: + /part2: + get: + responses: + '200': + description: OK \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/api_first.yaml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/api_first.yaml new file mode 100644 index 000000000000..4677612f4511 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/api_first.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.3 +info: + title: First XML API Element + version: 1.0.0 +paths: + /first: + get: + responses: + '200': + description: OK \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/api_second.yaml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/api_second.yaml new file mode 100644 index 000000000000..ff66c008b80c --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/api_second.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.3 +info: + title: Second XML API Element + version: 1.0.0 +paths: + /second: + get: + responses: + '200': + description: OK \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/invoker.properties b/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/invoker.properties new file mode 100644 index 000000000000..7369d207daa4 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals = validate +invoker.buildResult = success \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/pom.xml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/pom.xml new file mode 100644 index 000000000000..02704e479496 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/multi-file-xml-list/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + org.openapitools.it + multi-file-xml-list + 1.0.0-SNAPSHOT + + + + org.openapitools + openapi-generator-maven-plugin + @project.version@ + + + validate + + validate + + + + api_first.yaml + api_second.yaml + + + + + + + + \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/invalid_api.yaml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/invalid_api.yaml new file mode 100644 index 000000000000..17fb467ae580 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/invalid_api.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.3 +info: + title: Completely Broken Spec + # Missing required 'version' field! +paths: + /broken-endpoint: + get: + responses: + # Invalid response code type syntax or completely missing details + 'invalid_code': \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/invoker.properties b/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/invoker.properties new file mode 100644 index 000000000000..32d660b0c0f2 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/invoker.properties @@ -0,0 +1,3 @@ +invoker.goals = validate +# Mandate that the plugin properly drops the build execution lifecycle hammer! +invoker.buildResult = failure \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/pom.xml b/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/pom.xml new file mode 100644 index 000000000000..0a6ad3f1357f --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + org.openapitools.it + strict-spec-failure + 1.0.0-SNAPSHOT + + + + org.openapitools + openapi-generator-maven-plugin + @project.version@ + + + validate + + validate + + + invalid_api.yaml + true + + + + + + \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/verify.groovy b/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/verify.groovy new file mode 100644 index 000000000000..84b26babae53 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/it/validate-harness/strict-spec-failure/verify.groovy @@ -0,0 +1,16 @@ +import java.io.* + +// 1. Locate the build log file +File logFile = new File(basedir, "build.log") +assert logFile.exists() : "The build.log file was not found!" + +String logContent = logFile.text + +// 2. Assert the true root cause strings +// Check for the exact exception message thrown by the Mojo wrapper +assert logContent.contains("Validation has error(s). See above for the details.") : "Build failed, but not due to the expected validation exception wrapper!" + +// Check for the swagger/openapi parser's structural complaint about our broken yaml +assert logContent.contains("attribute info.version is missing") || logContent.contains("missing") : "The logs do not contain the specific OpenAPI specification structural violations!" + +return true \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/ValidateMojo.java b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/ValidateMojo.java new file mode 100644 index 000000000000..6e9d010e7b91 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/ValidateMojo.java @@ -0,0 +1,427 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * Copyright 2018 SmartBear Software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.plugin; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; + +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.parser.OpenAPIResolver; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.AuthorizationValue; +import io.swagger.v3.parser.core.models.ParseOptions; +import io.swagger.v3.parser.core.models.SwaggerParseResult; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; +import lombok.Setter; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.shared.utils.logging.MessageUtils; +import org.jspecify.annotations.NonNull; +import org.openapitools.codegen.auth.AuthParser; +import org.openapitools.codegen.config.GlobalSettings; +import org.openapitools.codegen.config.MergedSpecBuilder; +import org.openapitools.codegen.validation.Invalid; +import org.openapitools.codegen.validation.Severity; +import org.openapitools.codegen.validation.ValidationResult; +import org.openapitools.codegen.validation.ValidationRule; +import org.openapitools.codegen.validations.oas.OpenApiEvaluator; +import org.openapitools.codegen.validations.oas.RuleConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonatype.plexus.build.incremental.BuildContext; +import org.sonatype.plexus.build.incremental.DefaultBuildContext; + +/** + * Goal that validates the OpenAPI json/yaml definition. + */ +@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection"}) +@Mojo(name = "validate", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true) +public class ValidateMojo extends AbstractMojo { + + private final Logger LOGGER = LoggerFactory.getLogger(ValidateMojo.class); + /** + * Location of the OpenAPI spec(s), as URL(s) and/or file(s). Standard maven style comma separated string list or list + * of tags inside. + */ + @Parameter(property = "openapi.generator.maven.plugin.inputSpec") + protected String[] inputSpec;//the name is intentionally singular to remain backward compatible in the pom xml file. + /** + * Local root folder with spec files + */ + @Parameter(name = "inputSpecRootDirectory", property = "openapi.generator.maven.plugin.inputSpecRootDirectory") + protected String inputSpecRootDirectory; + /** + * The Maven project context. + */ + @Parameter(defaultValue = "${project}", required = true, readonly = true) + MavenProject mavenProject; + /** + * Maven ProjectHelper used to manage build artifacts. + */ + @Component + MavenProjectHelper mavenProjectHelper; + + /** + * The build context is only avail when running from within eclipse. It is used to update the eclipse-m2e-layer when + * the plugin is executed inside the IDE. + */ + @Setter + @Component + private BuildContext buildContext = new DefaultBuildContext(); + /** + * Name of the file that will contain all merged specs + */ + @Parameter(name = "mergedFileName", property = "openapi.generator.maven.plugin.mergedFileName", defaultValue = "_merged_spec") + private String mergedFileName; + /** + * Name that will appear in the info section of the merged spec + */ + @Parameter(name = "mergedFileInfoName", property = "openapi.generator.maven.plugin.mergedFileInfoName", defaultValue = "merged spec") + private String mergedFileInfoName; + /** + * Description that will appear in the info section of the merged spec + */ + @Parameter(name = "mergedFileInfoDescription", property = "openapi.generator.maven.plugin.mergedFileInfoDescription", defaultValue = "merged spec") + private String mergedFileInfoDescription; + /** + * Version that will appear in the info section of the merged spec + */ + @Parameter(name = "mergedFileInfoVersion", property = "openapi.generator.maven.plugin.mergedFileInfoVersion", defaultValue = "1.0.0") + private String mergedFileInfoVersion; + /** + * The path to the collapsed single-file representation of the OpenAPI spec. + */ + @Parameter(name = "collapsedSpec", property = "openapi.generator.maven.plugin.collapsedSpec") + private String collapsedSpec; + /** + * Includes the collapsed spec in the Maven artifacts. + */ + @Parameter(name = "includeCollapsedSpecInArtifacts", property = "openapi.generator.maven.plugin.publishCollapsedSpec", defaultValue = "false") + private boolean includeCollapsedSpecInArtifacts; + /** + * Adds authorization headers when fetching the swagger definitions remotely. " Pass in a URL-encoded string of + * name:header with a comma separating multiple values + */ + @Parameter(name = "auth", property = "openapi.generator.maven.plugin.auth") + private String auth; + /** + * To skip spec validation + */ + @Parameter(name = "skipValidateSpec", property = "openapi.generator.maven.plugin.skipValidateSpec") + private Boolean skipValidateSpec; + /** + * To treat a document strictly against the spec. + */ + @Parameter(name = "strictSpec", property = "openapi.generator.maven.plugin.strictSpec", defaultValue = "false") + private Boolean strictSpec; + /** + * Skip the execution. + */ + @Parameter(name = "skip", property = "codegen.skip", defaultValue = "false") + private Boolean skip; + @Parameter(defaultValue = "false", property = "openapi.generator.maven.plugin.dryRun") + private Boolean dryRun; + @Parameter(defaultValue = "${mojoExecution}", readonly = true) + private MojoExecution mojo; + /** + * The project being built. + */ + @Parameter(readonly = true, required = true, defaultValue = "${project}") + MavenProject project; + + boolean hasError; + boolean hasWarning; + + @Override + public void execute() throws MojoExecutionException { + validateInputSpecInput(); + + if (shouldWeSkip()) { + return; + } + + if(inputSpec != null && inputSpec.length == 1 && inputSpec[0] != null) { + inputSpec = inputSpec[0].split("\\s*,\\s*"); + } + + mergeInDirectory().ifPresent(mergedSpec -> { + inputSpec = new String[1]; + inputSpec[0] = mergedSpec; + }); + + try { + for (String oneInputSpec : inputSpec) { + if(isEmpty(oneInputSpec)) { + continue; + } + File inputSpecFile = new File(oneInputSpec); + try { + if (shouldWeSkipDeltaBuild(inputSpecFile)) { + continue; + } + execute(oneInputSpec); + } catch (Exception e) { + if (isNotEmpty(e.getMessage())) { + getLog().error(e.getMessage()); + if (buildContext != null) { + buildContext.addMessage(inputSpecFile, 0, 0, e.getMessage(), BuildContext.SEVERITY_WARNING, null); + } + } + } + } + if (dryRun) { + if (hasError || (strictSpec && hasWarning)) { + getLog().warn("Validation issues detected in dry-run mode. Please review the results."); + getLog().info("The build will not fail because dryRun is active."); + } + return; + } + + if (hasError || (strictSpec && hasWarning)) { + throw new Exception(); + } + } catch (Exception e) { + throw new MojoExecutionException( + "Validation has error(s). See above for the details."); + } finally { + GlobalSettings.log(); + } + } + + private void validateInputSpecInput() throws MojoExecutionException { + boolean isInputSpecEmpty = (inputSpec == null || inputSpec.length == 0 || isBlank(inputSpec[0])); + + if (isInputSpecEmpty && isBlank(inputSpecRootDirectory)) { + LOGGER.error("inputSpec or inputSpecRootDirectory must be specified"); + throw new MojoExecutionException("inputSpec or inputSpecRootDirectory must be specified"); + } + } + + private boolean shouldWeSkip() { + if (Boolean.TRUE.equals(skip) || Boolean.TRUE.equals(skipValidateSpec)) { + getLog().info("Validation is skipped."); + return true; + } + return false; + } + + private Optional mergeInDirectory() { + Optional mergedSpec = Optional.empty(); + if (StringUtils.isNotBlank(inputSpecRootDirectory)) { + inputSpecRootDirectory = replaceBackslashesToSlashes(inputSpecRootDirectory); + + mergedSpec = Optional.of(new MergedSpecBuilder(inputSpecRootDirectory, mergedFileName, + mergedFileInfoName, mergedFileInfoDescription, mergedFileInfoVersion, auth) + .buildMergedSpec()); + LOGGER.info("Merge input spec would be used - {}", mergedSpec.get()); + } + return mergedSpec; + } + + private boolean shouldWeSkipDeltaBuild(File inputSpecFile) { + if (buildContext != null && inputSpec[0] != null) { + if (buildContext.isIncremental() && + inputSpecFile.exists() && + !buildContext.hasDelta(inputSpecFile)) { + getLog().info( + "Validation is skipped in delta-build because source-json/yaml was not modified."); + return true; + } + } + return false; + } + + private void execute(final String oneInputSpec) throws MojoExecutionException { + String theInputSpec = replaceBackslashesToSlashes(oneInputSpec); + + theInputSpec = collapseSpecIfNeeded(oneInputSpec).orElse(theInputSpec); + getLog().info(MessageUtils.buffer().a("Validating spec (").strong(theInputSpec).a(")").toString()); + ParseOptions options = new ParseOptions(); + options.setResolve(true); + final List authorizationValues = AuthParser.parse(auth); + SwaggerParseResult result = new OpenAPIParser().readLocation(theInputSpec, authorizationValues, options); + + Set parseErrors = new HashSet<>(result.getMessages()); + Set errors = new HashSet<>(); + + result.getMessages().forEach(message -> addParseErrorToErrors(message, errors)); + + OpenAPI specification = result.getOpenAPI(); + + RuleConfiguration ruleConfiguration = new RuleConfiguration(); + ruleConfiguration.setEnableRecommendations(false); + + OpenApiEvaluator evaluator = new OpenApiEvaluator(ruleConfiguration); + ValidationResult validationResult = evaluator.validate(specification); + + Set warnings = new HashSet<>(validationResult.getWarnings()); + errors.addAll(validationResult.getErrors()); + + logWarnings(warnings); + logErrors(errors); + + hasError = hasError || !errors.isEmpty(); + hasWarning = hasWarning || !warnings.isEmpty(); + + if (errors.isEmpty() && warnings.isEmpty()) { + getLog().info(MessageUtils.buffer().success("No validation issues detected.").toString()); + } + } + + /** + * Replaces all backslashes ('\') in the provided string with forward slashes ('/'). This makes sure the path can be + * processed correct under Windows OS + * + * @param input the input string in which backslashes should be replaced; must not be null + * @return a new string with all backslashes replaced by forward slashes + */ + private @NonNull String replaceBackslashesToSlashes(String input) { + return input.replaceAll("\\\\", "/"); + } + + private Optional collapseSpecIfNeeded(String inputFile) throws MojoExecutionException { + Optional collapsedSpecString = Optional.empty(); + if (collapsedSpec != null) { + final var collapsedSpecPath = createCollapsedSpec(inputFile); + collapsedSpecString = Optional.of(collapsedSpecPath.toString()); + if (includeCollapsedSpecInArtifacts) { + mavenProjectHelper.attachArtifact( + mavenProject, + collapsedSpecPath.toString().toLowerCase(Locale.ROOT).endsWith(".json") ? "json" : "yaml", + collapsedSpec, + collapsedSpecPath.toFile()); + } + } + return collapsedSpecString; + } + + private void addParseErrorToErrors(String message, Set errors) { + ValidationRule failedRule = + ValidationRule.error("Failed parsing the descriptor.", ignore -> ValidationRule.Fail.empty()); + Invalid invalidParseResult = new Invalid(failedRule, "Descriptor parsing failed.", message); + errors.add(invalidParseResult); + } + + private void logWarnings(Set warnings) { + warnings.forEach(this::logInvalid); + if (warnings.isEmpty()) { + getLog().info("Spec has no recommendation(s)."); + } else { + getLog().warn("Spec has " + warnings.size() + " recommendation(s)."); + } + } + + private void logErrors(Set errors) { + errors.forEach(this::logInvalid); + if (errors.isEmpty()) { + getLog().info("Spec has no errors."); + } else { + getLog().error("Spec has " + errors.size() + " error(s)."); + } + } + + private Path createCollapsedSpec(String inputFile) throws MojoExecutionException { + // Merge the OpenAPI spec file. + final var parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + final List authorizationValues = AuthParser.parse(this.auth); + + final var openApiMerged = new OpenAPIResolver( + new OpenAPIV3Parser().readLocation(inputFile, authorizationValues, parseOptions).getOpenAPI()).resolve(); + + // Switch based on JSON or YAML. + final var extension = inputFile.toLowerCase(Locale.ROOT).endsWith(".json") ? ".json" : ".yaml"; + final var mapper = inputFile.toLowerCase(Locale.ROOT).endsWith(".json") ? Json.mapper() : Yaml.mapper(); + + // Write the merged spec to the output file. + final var collapsedSpecPath = + Paths.get(project.getBuild().getOutputDirectory(), collapsedSpec + extension).toAbsolutePath(); + try { + final var openApiString = mapper.writeValueAsString(openApiMerged); + FileUtils.writeStringToFile(collapsedSpecPath.toFile(), openApiString, StandardCharsets.UTF_8); + } catch (final IOException e) { + throw new MojoExecutionException( + new MessageFormat("Failed to write collapsed spec {0}", Locale.ROOT).format(collapsedSpecPath), e); + } + + // Return the path to the collapsed spec file. + return collapsedSpecPath; + } + + private void logInvalid(Invalid invalid) { + LOGLEVEL loglevel = invalid.getSeverity() == Severity.ERROR ? LOGLEVEL.ERROR : LOGLEVEL.WARNING; + loglevel.logColored(getLog(), invalid.getMessage()); + loglevel.log(getLog(), MessageUtils.buffer().format("Based on rule: %s", invalid.getRule()).toString()); + loglevel.log(getLog(), MessageUtils.buffer() + .format(isNotEmpty(invalid.getDetails()) ? "Details: %s" : "No details available.", invalid.getDetails()) + .toString()); + loglevel.logColored(getLog(), "----------"); + } + + private enum LOGLEVEL { + WARNING { + @Override + void log(Log logger, String message) { + logger.warn(message); + } + + @Override + void logColored(Log logger, String message) { + log(logger, MessageUtils.buffer().warning(message).toString()); + } + }, ERROR { + @Override + void log(Log logger, String message) { + logger.error(message); + } + + @Override + void logColored(Log logger, String message) { + log(logger, MessageUtils.buffer().failure(message).toString()); + } + }; + + abstract void log(Log logger, String message); + + abstract void logColored(Log logger, String message); + } +} diff --git a/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/ValidateMojoTest.java b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/ValidateMojoTest.java new file mode 100644 index 000000000000..094ed754d34a --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/ValidateMojoTest.java @@ -0,0 +1,381 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * Copyright 2018 SmartBear Software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.plugin; + +import java.nio.file.Paths; +import org.apache.commons.io.FileUtils; +import org.apache.maven.execution.DefaultMavenExecutionRequest; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.MojoExecution; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.ProjectBuilder; +import org.apache.maven.project.ProjectBuildingRequest; +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.internal.impl.DefaultLocalPathComposer; +import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory; +import org.eclipse.aether.repository.LocalRepository; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit and integration tests for ValidateMojo to guarantee enforcement + * of validation precedence gates and multi-file tracking matrices. + */ +public class ValidateMojoTest extends BaseTestCase { + + private static final String HARNESS_PATH = "src/test/resources/validate-harness"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + /** + * Test 1: Happy Path. Verifies a completely valid specification passes + * seamlessly without raising exceptions. + */ + public void testExecute_WithValidSpec_ShouldPassNatively() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + // Point specifically to a fully compliant valid spec + mojo.inputSpec = new String[]{ tempDir.resolve("valid_api.yaml").toString() }; + + // WHEN & THEN (Expect no exception) + mojo.execute(); + assertFalse("Mojo state tracking shouldn't flag global error", mojo.hasError); + assertFalse("Mojo state tracking shouldn't flag global warning", mojo.hasWarning); + } + + /** + * Test 2: Multi-File Error Isolation. Verifies that if multiple files have errors, + * a failure in the first file does not crash the loop before checking the remaining files. + */ + public void testExecute_WithMultipleSpecs_ShouldCollectAllBeforeFailing() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + mojo.inputSpec = new String[]{ + tempDir.resolve("broken_syntax_api.yaml").toString(), + tempDir.resolve("unused_model_api.yaml").toString() + }; + + // WHEN & THEN (Should collect across files, then throw execution crash at termination) + assertThrows(MojoExecutionException.class, mojo::execute, + "Should fail build globally due to aggregated specifications errors."); + + assertTrue("Mojo must have flagged the error from file 1", mojo.hasError); + assertTrue("Mojo must have flagged the warning from file 2", mojo.hasWarning); + } + + /** + * Test 3: Strict Spec Mode Gates. Verifies recommendations behave as non-blocking + * items when strictSpec is false, but trigger hard failures when strictSpec is turned on. + */ + public void testExecute_WithWarningsOnly_ShouldEnforceStrictSpecGates() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + + // Scenario A: strictSpec is false -> Warnings should be logged, but build passes + ValidateMojo softMojo = loadMojo(tempDir, "default"); + setVariableValueToObject(softMojo, "strictSpec", Boolean.FALSE); + softMojo.inputSpec = new String[]{ tempDir.resolve("unused_model_api.yaml").toString() }; + + softMojo.execute(); + assertTrue("Should catch warning state", softMojo.hasWarning); + assertFalse("Should not mark hard error state", softMojo.hasError); + + // Scenario B: strictSpec is true -> Same warnings must result in a hard build crash + ValidateMojo strictMojo = loadMojo(tempDir, "default"); + setVariableValueToObject(strictMojo, "strictSpec", Boolean.TRUE); + strictMojo.inputSpec = new String[]{ tempDir.resolve("unused_model_api.yaml").toString() }; + + assertThrows(MojoExecutionException.class, strictMojo::execute, + "Strict compliance configuration must fail build if specs contain recommendations."); + } + + /** + * Test 4: Dry Run Override Supremacy. Verifies that if dryRun is active, the build + * passes completely despite severe specification errors and strict flags being active. + */ + public void testExecute_WithErrorsInDryRunMode_ShouldPassNatively() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + // Activate both strict configurations and point to broken specification layouts + setVariableValueToObject(mojo, "dryRun", Boolean.TRUE); + setVariableValueToObject(mojo, "strictSpec", Boolean.TRUE); + mojo.inputSpec = new String[]{ tempDir.resolve("broken_syntax_api.yaml").toString() }; + + // WHEN & THEN (The dryRun shield guarantees no MojoExecutionException is thrown) + mojo.execute(); + + assertTrue("Mojo must successfully record that severe errors were found", mojo.hasError); + } + + /** + * Test 5: Comma-Separated Input Processing. Verifies inline comma-delimited definitions + * work seamlessly for CLI invocation overrides (-Dopenapi.generator.maven.plugin.inputSpec=a,b). + */ + public void testExecute_WithCommaSeparatedInputSpec_ShouldParseIndividualFiles() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + // Pass a single string containing comma-separated paths mirroring direct terminal arguments + String path1 = tempDir.resolve("valid_api.yaml").toString(); + String path2 = tempDir.resolve("unused_model_api.yaml").toString(); + mojo.inputSpec = new String[]{ path1 + " , " + path2 }; + + // WHEN + mojo.execute(); + + // THEN + assertTrue("Must successfully split string elements and traverse to check file 2", mojo.hasWarning); + } + + /** + * Test 6: Input Validation Edge Case. Verifies fallback errors raise correctly + * if parameters are left completely blank. + */ + public void testExecute_WithMissingConfigurationInputs_ShouldThrowExplicitException() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + mojo.inputSpec = new String[]{ "" }; + mojo.inputSpecRootDirectory = null; + + // WHEN & THEN + MojoExecutionException e = assertThrows(MojoExecutionException.class, mojo::execute); + assertEquals("inputSpec or inputSpecRootDirectory must be specified", e.getMessage()); + } + + /** + * Test 7: Directory Merging Path. Verifies that configuring inputSpecRootDirectory + * triggers MergedSpecBuilder, cleanly overrides the inputSpec array, and runs validation on the result. + */ + public void testExecute_WithInputSpecRootDirectory_ShouldMergeAndValidate() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + // Point to our subfolder containing files to combine, and clear out explicit inputSpec + setVariableValueToObject(mojo, "inputSpecRootDirectory", tempDir.resolve("specs-to-merge").toString()); + mojo.inputSpec = new String[]{ "" }; + + // WHEN + mojo.execute(); + + // THEN + assertNotNull("inputSpec should have been automatically populated via the merge engine", mojo.inputSpec); + assertEquals("Should contain exactly 1 merged path pointer", 1, mojo.inputSpec.length); + assertTrue("The path should point to the generated merged spec file", mojo.inputSpec[0].contains("_merged_spec")); + assertFalse("The aggregated spec should pass validation cleanly", mojo.hasError); + } + + /** + * Test 8: Collapsed Spec Generation (YAML flavor). Verifies that when collapsedSpec is set, + * the plugin successfully resolves inline $refs, compiles a single flat representation, + * and dumps it directly into the project's build target output path. + */ + public void testExecute_WithCollapsedSpec_ShouldCreateFlatRepresentationFile() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + // Target an explicit singular input file and configure a collapse name definition + mojo.inputSpec = new String[]{ tempDir.resolve("valid_api.yaml").toString() }; + setVariableValueToObject(mojo, "collapsedSpec", "flattened-output-api"); + + // Mock the maven project output path structure so createCollapsedSpec knows where to save it + String mockOutputDir = tempDir.resolve("target/classes").toString(); + Files.createDirectories(Paths.get(mockOutputDir)); + mojo.project.getBuild().setOutputDirectory(mockOutputDir); + + // WHEN + mojo.execute(); + + // THEN + Path expectedFile = Paths.get(mockOutputDir, "flattened-output-api.yaml"); + assertTrue("The Mojo must physically write out the collapsed flat definition to disk", Files.exists(expectedFile)); + + String content = Files.readString(expectedFile); + assertTrue("The written file must be a real parsed OpenAPI file", content.contains("openapi: 3.0.3")); + } + + /** + * Test 9: Collapsed Spec Attached to Maven Artifact Matrix. Verifies that when + * includeCollapsedSpecInArtifacts is flagged true, the plugin cleanly calls Maven's ProjectHelper + * to bind the new file to the lifecycle deployment queue. + */ + public void testExecute_WithIncludeCollapsedSpecInArtifacts_ShouldRegisterMavenArtifact() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + mojo.inputSpec = new String[]{ tempDir.resolve("valid_api.yaml").toString() }; + setVariableValueToObject(mojo, "collapsedSpec", "artifact-bound-spec"); + setVariableValueToObject(mojo, "includeCollapsedSpecInArtifacts", Boolean.TRUE); + + // Standard build output paths setups + String mockOutputDir = tempDir.resolve("target/classes").toString(); + Files.createDirectories(Paths.get(mockOutputDir)); + mojo.project.getBuild().setOutputDirectory(mockOutputDir); + + // WHEN + mojo.execute(); + + // THEN + // Dig into the mocked MavenProject artifact array to verify our binding exists + java.util.List attachedArtifacts = mojo.mavenProject.getAttachedArtifacts(); + boolean artifactFound = attachedArtifacts.stream() + .anyMatch(artifact -> artifact.getClassifier().equals("artifact-bound-spec") + && artifact.getType().equals("yaml")); + + assertTrue("The collapsed specification file must be registered directly into the Maven artifact attachment queue", + artifactFound); + } + + /** + * Test 10: Global Skip Toggle. Verifies that when codegen.skip (skip) is set to true, + * the Mojo exits immediately and logs that validation is skipped, without running any file parsing. + */ + public void testExecute_WithGlobalSkipTrue_ShouldReturnImmediately() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + // Explicitly set the global skip configuration to true + setVariableValueToObject(mojo, "skip", Boolean.TRUE); + + // Point to a completely broken file path that would normally throw an exception + mojo.inputSpec = new String[]{ "non_existent_file_that_would_crash.yaml" }; + + // WHEN & THEN + // If the skip gate works perfectly, this will execute smoothly without throwing any MojoExecutionException + mojo.execute(); + + assertFalse("Mojo should not have processed any files to find errors", mojo.hasError); + } + + /** + * Test 11: Goal-Specific Skip Toggle. Verifies that when skipValidateSpec is true, + * the plugin behaves identically to the global skip, bypassing execution rules cleanly. + */ + public void testExecute_WithSkipValidateSpecTrue_ShouldReturnImmediately() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + // Activate the plugin-specific skip flag instead of the global one + setVariableValueToObject(mojo, "skipValidateSpec", Boolean.TRUE); + mojo.inputSpec = new String[]{ "another_broken_path.yaml" }; + + // WHEN & THEN + mojo.execute(); + + assertFalse("Mojo should have exited before evaluating any file loops", mojo.hasError); + } + + /** + * Test 12: Delta Build Incremental Skip. Verifies that when running in an incremental + * workspace build context, if the input spec file exists but has no change modifications (no delta), + * the execution skips parsing and optimization steps gracefully. + */ + public void testExecute_WithIncrementalBuildContextAndNoDelta_ShouldSkipFile() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + ValidateMojo mojo = loadMojo(tempDir, "default"); + + File validFile = tempDir.resolve("valid_api.yaml").toFile(); + mojo.inputSpec = new String[]{ validFile.getAbsolutePath() }; + + // Create a mocked BuildContext that simulates an active incremental IDE compilation + // where the file in question has *not* changed. + org.sonatype.plexus.build.incremental.BuildContext mockContext = + mock(org.sonatype.plexus.build.incremental.BuildContext.class); + + when(mockContext.isIncremental()).thenReturn(true); + when(mockContext.hasDelta(validFile)).thenReturn(false); + + // Inject our mock context directly into the Mojo + setVariableValueToObject(mojo, "buildContext", mockContext); + + // WHEN + mojo.execute(); + + // THEN + // Since it skipped validation inside the delta gate loop, the validation stats will stay completely untouched + assertFalse("Mojo should have skipped validation tracking loops due to matching cache delta rules", mojo.hasError); + } + // --- Harness Infrastructure Factories adapted from CodeGenMojoTest --- + + protected ValidateMojo loadMojo(Path temporaryFolder, String profile) throws Exception { + FileUtils.copyDirectory(new File(HARNESS_PATH), temporaryFolder.toFile()); + MavenProject project = readMavenProject(temporaryFolder, profile); + MavenSession session = newMavenSession(project); + // 1. Generate the base mojo execution frame + MojoExecution baseExecution = newMojoExecution("validate"); + + // 2. Clone it safely with the execution id using the helper + MojoExecution executionWithId = copyWithExecutionId("default", baseExecution); + return (ValidateMojo) lookupConfiguredMojo(session, executionWithId); + } + + private MojoExecution copyWithExecutionId(String executionId, MojoExecution execution) { + MojoExecution executionWithId = new MojoExecution(execution.getMojoDescriptor(), executionId); + executionWithId.setConfiguration(execution.getConfiguration()); + return executionWithId; + } + protected MavenProject readMavenProject(Path basedir, String profile) throws Exception { + LocalRepository localRepo = new LocalRepository(basedir.resolve("local-repo").toFile()); + DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); + session.setLocalRepositoryManager( + new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()).newInstance(session, localRepo) + ); + MavenExecutionRequest request = new DefaultMavenExecutionRequest().setBaseDirectory(basedir.toFile()); + if (profile != null) { + request.addActiveProfile(profile); + } + ProjectBuildingRequest configuration = request.getProjectBuildingRequest() + .setRepositorySession(session) + .setResolveDependencies(true); + return lookup(ProjectBuilder.class) + .build(basedir.resolve("pom.xml").toFile(), configuration) + .getProject(); + } + + private static Path newTempFolder() throws IOException { + final Path tempDir = Files.createTempDirectory("validate-test"); + tempDir.toFile().deleteOnExit(); + return tempDir; + } +} \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/broken_syntax_api.yaml b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/broken_syntax_api.yaml new file mode 100644 index 000000000000..a11ee3442396 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/broken_syntax_api.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.3 +info: + title: Broken Structural API + version: 1.0.0 + description: This file fails basic descriptor parsing due to missing structural schema bindings. +paths: + /configuration: + get: + summary: Get system configuration + operationId: getConfig + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Configuratio' +components: + schemas: + Configuration: + type: object + properties: + activeProfile: + type: string \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/pom.xml b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/pom.xml new file mode 100644 index 000000000000..1deaf53bdc82 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + org.openapitools.test + validate-test-harness + 1.0.0-SNAPSHOT + jar + + Validate Mojo Test Harness Stub + + + 11 + 11 + UTF-8 + + + + + + org.openapitools + openapi-generator-maven-plugin + @project.version@ + + + validate-test + validate + + validate + + + ${project.basedir}/valid_api.yaml + false + false + + + + + + + \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/specs-to-merge/file1.yaml b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/specs-to-merge/file1.yaml new file mode 100644 index 000000000000..0296f17c14b5 --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/specs-to-merge/file1.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.3 +info: + title: Part 1 + version: 1.0.0 +paths: + /part1: + get: + responses: + '200': + description: OK \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/specs-to-merge/file2.yaml b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/specs-to-merge/file2.yaml new file mode 100644 index 000000000000..9606df4530fe --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/specs-to-merge/file2.yaml @@ -0,0 +1,10 @@ +openapi: 3.0.3 +info: + title: Part 2 + version: 1.0.0 +paths: + /part2: + get: + responses: + '200': + description: OK \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/unused_model_api.yaml b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/unused_model_api.yaml new file mode 100644 index 000000000000..f14fb3446c0e --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/unused_model_api.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.3 +info: + title: API with Style Recommendations + version: 1.0.0 + description: Valid spec syntax, but contains an isolated component model that triggers an unused schema warning. +paths: + /users: + get: + summary: Get active users + operationId: getUsers + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + type: string +components: + schemas: + UnusedModel: + type: object + properties: + legacyField: + type: integer + description: This model is declared but never referenced anywhere in any operation or path parameter. \ No newline at end of file diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/valid_api.yaml b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/valid_api.yaml new file mode 100644 index 000000000000..9f56ecc6850b --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/validate-harness/valid_api.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.3 +info: + title: Healthy Test API + version: 1.0.0 + description: A completely valid specification with no errors or recommendations. +paths: + /ping: + get: + summary: Simple healthcheck endpoint + operationId: pingCheck + responses: + '200': + description: Server is operational + content: + application/json: + schema: + $ref: '#/components/schemas/Pong' +components: + schemas: + Pong: + type: object + properties: + status: + type: string + example: healthy \ No newline at end of file From ad01fc6ef8e15469c99f84c90bc2758f99f8dc1c Mon Sep 17 00:00:00 2001 From: "istvan.verhas" Date: Mon, 1 Jun 2026 09:38:08 +0200 Subject: [PATCH 2/2] Add missing 'validate' goal to lifecycle mapping and refine execution ordering in ValidateMojo to fix "Skip flags are checked too late; input validation should come after the skip check." issue --- .../java/org/openapitools/codegen/plugin/ValidateMojo.java | 4 ++-- .../resources/META-INF/m2e/lifecycle-mapping-metadata.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/ValidateMojo.java b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/ValidateMojo.java index 6e9d010e7b91..27a44219edf4 100644 --- a/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/ValidateMojo.java +++ b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/ValidateMojo.java @@ -173,12 +173,12 @@ public class ValidateMojo extends AbstractMojo { @Override public void execute() throws MojoExecutionException { - validateInputSpecInput(); - if (shouldWeSkip()) { return; } + validateInputSpecInput(); + if(inputSpec != null && inputSpec.length == 1 && inputSpec[0] != null) { inputSpec = inputSpec[0].split("\\s*,\\s*"); } diff --git a/modules/openapi-generator-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml b/modules/openapi-generator-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml index bbb0518d4f52..8ed362c62f12 100644 --- a/modules/openapi-generator-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml +++ b/modules/openapi-generator-maven-plugin/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml @@ -4,6 +4,7 @@ generate + validate