diff --git a/bin/configs/kotlin-spring-boot-delegate.yaml b/bin/configs/kotlin-spring-boot-delegate.yaml index c09fcf345fdb..b887a2eb1721 100644 --- a/bin/configs/kotlin-spring-boot-delegate.yaml +++ b/bin/configs/kotlin-spring-boot-delegate.yaml @@ -1,8 +1,13 @@ generatorName: kotlin-spring outputDir: samples/server/petstore/kotlin-springboot-delegate library: spring-boot -inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml templateDir: modules/openapi-generator/src/main/resources/kotlin-spring +injectOperationVendorExtensions: + placeOrder.x-request-body-extra-annotation: "@NonNull" + placeOrder.x-extra-imports: org.springframework.lang.NonNull + getOrderById.orderId.x-field-extra-annotation: "@NonNull" + getOrderById.orderId.x-extra-imports: org.springframework.lang.NonNull additionalProperties: generateJsonIncludeAnnotations: "true" generateJsonSetterNullsAnnotations: "true" diff --git a/bin/configs/kotlin-spring-boot-reactive.yaml b/bin/configs/kotlin-spring-boot-reactive.yaml index a8272859ce07..dd7c9d6ccc30 100644 --- a/bin/configs/kotlin-spring-boot-reactive.yaml +++ b/bin/configs/kotlin-spring-boot-reactive.yaml @@ -1,8 +1,13 @@ generatorName: kotlin-spring outputDir: samples/server/petstore/kotlin-springboot-reactive library: spring-boot -inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml templateDir: modules/openapi-generator/src/main/resources/kotlin-spring +injectOperationVendorExtensions: + placeOrder.x-request-body-extra-annotation: "@NonNull" + placeOrder.x-extra-imports: org.springframework.lang.NonNull + getOrderById.orderId.x-field-extra-annotation: "@NonNull" + getOrderById.orderId.x-extra-imports: org.springframework.lang.NonNull additionalProperties: generateJsonIncludeAnnotations: "true" generateJsonSetterNullsAnnotations: "true" diff --git a/bin/configs/spring-boot-reactive.yaml b/bin/configs/spring-boot-reactive.yaml index 338a4ebaeedf..164719b6d418 100644 --- a/bin/configs/spring-boot-reactive.yaml +++ b/bin/configs/spring-boot-reactive.yaml @@ -1,7 +1,10 @@ generatorName: spring outputDir: samples/server/petstore/springboot-reactive -inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml templateDir: modules/openapi-generator/src/main/resources/JavaSpring +injectOperationVendorExtensions: + placeOrder.x-request-body-extra-annotation: "@org.springframework.lang.NonNull" + getOrderById.order_id.x-field-extra-annotation: "@org.springframework.lang.NonNull" additionalProperties: generateJsonIncludeAnnotations: "true" generateJsonSetterNullsAnnotations: "true" diff --git a/bin/configs/spring-boot-useoptional.yaml b/bin/configs/spring-boot-useoptional.yaml index 428935c9bdd7..dba0e460064e 100644 --- a/bin/configs/spring-boot-useoptional.yaml +++ b/bin/configs/spring-boot-useoptional.yaml @@ -1,7 +1,10 @@ generatorName: spring outputDir: samples/server/petstore/springboot-useoptional -inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml templateDir: modules/openapi-generator/src/main/resources/JavaSpring +injectOperationVendorExtensions: + placeOrder.x-request-body-extra-annotation: "@org.springframework.lang.NonNull" + getOrderById.order_id.x-field-extra-annotation: "@org.springframework.lang.NonNull" additionalProperties: generateJsonIncludeAnnotations: "true" generateJsonSetterNullsAnnotations: "true" diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index e4a0074ee562..bee6a3964858 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -91,6 +91,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-discriminator-value|Used with model inheritance to specify value for discriminator that identifies current model|MODEL| |x-field-extra-annotation|Custom annotation(s) to be added to property; accepts a string or list of strings|FIELD, OPERATION_PARAMETER|null |x-operation-extra-annotation|Custom annotation(s) to be added to operation; accepts a string or list of strings|OPERATION|null +|x-request-body-extra-annotation|Custom annotation(s) to be added to the request body parameter; accepts a string or list of strings. Declared on the operation because the request body typically `$ref`s a shared model (so the annotation cannot be placed next to the `$ref`); the value is rendered by being merged into the body parameter's `x-field-extra-annotation`|OPERATION|null |x-extra-imports|Custom import(s) to add to the generated file that declares the annotated model, property, operation, or parameter (e.g. so custom annotations can be referenced by their short name); accepts a string or list of strings. Values are emitted verbatim (Kotlin alias imports supported) and only exact duplicates are removed|MODEL, FIELD, OPERATION, OPERATION_PARAMETER|null |x-pattern-message|Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable|FIELD, OPERATION_PARAMETER|null |x-size-message|Add this property whenever you need to customize the invalidation error message for the size or length of a variable|FIELD, OPERATION_PARAMETER|null diff --git a/docs/generators/spring.md b/docs/generators/spring.md index adf5726a9d8a..92e7da29bd20 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -145,6 +145,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-class-extra-annotation|Custom annotation(s) to be added to model; accepts a string or list of strings|MODEL|null |x-field-extra-annotation|Custom annotation(s) to be added to property; accepts a string or list of strings|FIELD, OPERATION_PARAMETER|null |x-operation-extra-annotation|Custom annotation(s) to be added to operation; accepts a string or list of strings|OPERATION|null +|x-request-body-extra-annotation|Custom annotation(s) to be added to the request body parameter; accepts a string or list of strings. Declared on the operation because the request body typically `$ref`s a shared model (so the annotation cannot be placed next to the `$ref`); the value is rendered by being merged into the body parameter's `x-field-extra-annotation`|OPERATION|null |x-spring-paginated|Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object. Applies when `library=spring-boot` or `library=spring-cloud`; ignored for other (client) libraries.|OPERATION|false |x-version-param|Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false|OPERATION_PARAMETER|null |x-pattern-message|Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable|FIELD, OPERATION_PARAMETER|null 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 d0707f90388b..d72176238c08 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 @@ -265,9 +265,26 @@ public class Generate extends OpenApiGeneratorCommand { description = "injects vendor extensions into model classes or their properties." + " Class-level format: ModelName.x-extension-name=value." + " Property-level format: ModelName.propertyBaseName.x-extension-name=value." + + " Values are strings, applied at render time, and overwrite existing values." + + " For the extra-annotation extensions, separate multiple annotations in a single" + + " value with spaces (e.g. ModelName.x-class-extra-annotation=@Foo @Bar), not" + + " commas, since an unquoted comma separates different injection targets." + " You can also have multiple occurrences of this option.") private List injectModelVendorExtensions = new ArrayList<>(); + @Option( + name = {"--inject-operation-vendor-extensions"}, + title = "inject operation vendor extensions", + description = "injects vendor extensions into operations or their parameters." + + " Operation-level format: operationId.x-extension-name=value." + + " Parameter-level format: operationId.paramBaseName.x-extension-name=value." + + " Values are strings, applied at render time, and overwrite existing values." + + " For the extra-annotation extensions, separate multiple annotations in a single" + + " value with spaces (e.g. operationId.x-operation-extra-annotation=@Foo @Bar), not" + + " commas, since an unquoted comma separates different injection targets." + + " You can also have multiple occurrences of this option.") + private List injectOperationVendorExtensions = new ArrayList<>(); + @Option( name = {"--openapi-normalizer"}, title = "OpenAPI normalizer rules", @@ -616,6 +633,7 @@ public void execute() { applyEnumNameMappingsKvpList(enumNameMappings, configurator); applyOperationIdNameMappingsKvpList(operationIdNameMappings, configurator); applyInjectModelVendorExtensionsKvpList(injectModelVendorExtensions, configurator); + applyInjectOperationVendorExtensionsKvpList(injectOperationVendorExtensions, configurator); applyOpenapiNormalizerKvpList(openapiNormalizer, configurator); applyTypeMappingsKvpList(typeMappings, configurator); applyAdditionalPropertiesKvpList(additionalProperties, configurator); diff --git a/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java b/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java index 9f18c8b85afb..2697306e8866 100644 --- a/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java +++ b/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java @@ -60,6 +60,7 @@ public final class GeneratorSettings implements Serializable { private final Map enumNameMappings; private final Map operationIdNameMappings; private final Map injectModelVendorExtensions; + private final Map injectOperationVendorExtensions; private final Map openapiNormalizer; private final Set languageSpecificPrimitives; private final Set openapiGeneratorIgnoreList; @@ -337,6 +338,15 @@ public Map getInjectModelVendorExtensions() { return injectModelVendorExtensions; } + /** + * Gets inject operation vendor extensions. + * + * @return a map of operationId.x-extension-name or operationId.paramBaseName.x-extension-name to extension value + */ + public Map getInjectOperationVendorExtensions() { + return injectOperationVendorExtensions; + } + /** * Gets OpenAPI normalizer rules * @@ -480,6 +490,7 @@ private GeneratorSettings(Builder builder) { enumNameMappings = Collections.unmodifiableMap(builder.enumNameMappings); operationIdNameMappings = Collections.unmodifiableMap(builder.operationIdNameMappings); injectModelVendorExtensions = Collections.unmodifiableMap(builder.injectModelVendorExtensions); + injectOperationVendorExtensions = Collections.unmodifiableMap(builder.injectOperationVendorExtensions); openapiNormalizer = Collections.unmodifiableMap(builder.openapiNormalizer); languageSpecificPrimitives = Collections.unmodifiableSet(builder.languageSpecificPrimitives); openapiGeneratorIgnoreList = Collections.unmodifiableSet(builder.openapiGeneratorIgnoreList); @@ -562,6 +573,7 @@ public GeneratorSettings() { enumNameMappings = Collections.unmodifiableMap(new HashMap<>(0)); operationIdNameMappings = Collections.unmodifiableMap(new HashMap<>(0)); injectModelVendorExtensions = Collections.unmodifiableMap(new HashMap<>(0)); + injectOperationVendorExtensions = Collections.unmodifiableMap(new HashMap<>(0)); openapiNormalizer = Collections.unmodifiableMap(new HashMap<>(0)); languageSpecificPrimitives = Collections.unmodifiableSet(new HashSet<>(0)); openapiGeneratorIgnoreList = Collections.unmodifiableSet(new HashSet<>(0)); @@ -645,6 +657,9 @@ public static Builder newBuilder(GeneratorSettings copy) { if (copy.getInjectModelVendorExtensions() != null) { builder.injectModelVendorExtensions.putAll(copy.getInjectModelVendorExtensions()); } + if (copy.getInjectOperationVendorExtensions() != null) { + builder.injectOperationVendorExtensions.putAll(copy.getInjectOperationVendorExtensions()); + } if (copy.getOpenapiNormalizer() != null) { builder.openapiNormalizer.putAll(copy.getOpenapiNormalizer()); } @@ -700,6 +715,7 @@ public static final class Builder { private Map enumNameMappings; private Map operationIdNameMappings; private Map injectModelVendorExtensions; + private Map injectOperationVendorExtensions; private Map openapiNormalizer; private Set languageSpecificPrimitives; private Set openapiGeneratorIgnoreList; @@ -729,6 +745,7 @@ public Builder() { enumNameMappings = new HashMap<>(); operationIdNameMappings = new HashMap<>(); injectModelVendorExtensions = new HashMap<>(); + injectOperationVendorExtensions = new HashMap<>(); openapiNormalizer = new HashMap<>(); languageSpecificPrimitives = new HashSet<>(); openapiGeneratorIgnoreList = new HashSet<>(); @@ -1236,6 +1253,32 @@ public Builder withInjectModelVendorExtension(String key, String value) { return this; } + /** + * Sets the {@code injectOperationVendorExtensions} and returns a reference to this Builder so that the methods can be chained together. + * + * @param injectOperationVendorExtensions the {@code injectOperationVendorExtensions} to set + * @return a reference to this Builder + */ + public Builder withInjectOperationVendorExtensions(Map injectOperationVendorExtensions) { + this.injectOperationVendorExtensions = injectOperationVendorExtensions; + return this; + } + + /** + * Sets a single {@code injectOperationVendorExtension} and returns a reference to this Builder so that the methods can be chained together. + * + * @param key A key in the format operationId.x-extension-name or operationId.paramBaseName.x-extension-name + * @param value The extension value + * @return a reference to this Builder + */ + public Builder withInjectOperationVendorExtension(String key, String value) { + if (this.injectOperationVendorExtensions == null) { + this.injectOperationVendorExtensions = new HashMap<>(); + } + this.injectOperationVendorExtensions.put(key, value); + return this; + } + /** * Sets the {@code openapiNormalizer} and returns a reference to this Builder so that the methods can be chained together. * @@ -1482,6 +1525,8 @@ public boolean equals(Object o) { Objects.equals(getModelNameMappings(), that.getModelNameMappings()) && Objects.equals(getEnumNameMappings(), that.getEnumNameMappings()) && Objects.equals(getOperationIdNameMappings(), that.getOperationIdNameMappings()) && + Objects.equals(getInjectModelVendorExtensions(), that.getInjectModelVendorExtensions()) && + Objects.equals(getInjectOperationVendorExtensions(), that.getInjectOperationVendorExtensions()) && Objects.equals(getOpenapiNormalizer(), that.getOpenapiNormalizer()) && Objects.equals(getLanguageSpecificPrimitives(), that.getLanguageSpecificPrimitives()) && Objects.equals(getOpenapiGeneratorIgnoreList(), that.getOpenapiGeneratorIgnoreList()) && @@ -1521,6 +1566,8 @@ public int hashCode() { getModelNameMappings(), getEnumNameMappings(), getOperationIdNameMappings(), + getInjectModelVendorExtensions(), + getInjectOperationVendorExtensions(), getOpenapiNormalizer(), getLanguageSpecificPrimitives(), getOpenapiGeneratorIgnoreList(), diff --git a/modules/openapi-generator-gradle-plugin/README.adoc b/modules/openapi-generator-gradle-plugin/README.adoc index 66ee6acb357c..dc3dd0cf39f7 100644 --- a/modules/openapi-generator-gradle-plugin/README.adoc +++ b/modules/openapi-generator-gradle-plugin/README.adoc @@ -200,6 +200,16 @@ apply plugin: 'org.openapi.generator' |None |Sets specified global properties. +|injectModelVendorExtensions +|Map / Provider +|None +|Injects vendor extensions into models or their properties without editing the input spec. Keys use the form `modelName.x-extension-name` (model) or `modelName.propertyBaseName.x-extension-name` (model property). See the note below on supplying multiple annotations in a single value. + +|injectOperationVendorExtensions +|Map / Provider +|None +|Injects vendor extensions into operations or their parameters without editing the input spec. Keys use the form `operationId.x-extension-name` (operation) or `operationId.paramBaseName.x-extension-name` (parameter, matched by its raw spec name). See the note below on supplying multiple annotations in a single value. + |configFile |String / Provider |None @@ -564,6 +574,21 @@ models: "User:Pet" ---- ==== +[NOTE] +==== +`injectModelVendorExtensions` and `injectOperationVendorExtensions` are a generic mechanism for +setting *any* vendor extension on a model, property, operation, or parameter without editing the spec. +Keep in mind: + +* Values are always strings, applied *late* (at codegen/render time) and **overwrite** any existing + value; missing targets are a silent no-op. It is best suited to extensions consumed by templates, + such as the Spring extra-annotation extensions. +* For the extra-annotation extensions, put multiple annotations in one value separated by *spaces* + (as in source), e.g. `"addPet.x-request-body-extra-annotation": "@com.example.MyValidation @Valid"`. + In the Gradle map form commas inside a value need no escaping, so `@Size(min = 0, max = 10)` works + directly (unlike the comma-separated CLI/Maven form). +==== + === openApiValidate .Options 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 62e2a439c481..bea647b27bd3 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 @@ -127,6 +127,8 @@ class OpenApiGeneratorPlugin : Plugin { templateResourcePath.set(generate.templateResourcePath) auth.set(generate.auth) globalProperties.set(generate.globalProperties) + injectModelVendorExtensions.set(generate.injectModelVendorExtensions) + injectOperationVendorExtensions.set(generate.injectOperationVendorExtensions) configFile.set(generate.configFile) skipOverwrite.set(generate.skipOverwrite) packageName.set(generate.packageName) 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 c2b2e9a23a17..cbd87c26436f 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 @@ -163,6 +163,24 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) { */ val globalProperties = project.objects.mapProperty() + /** + * Injects vendor extensions into models or their properties without editing the input spec. + * + * Keys use the form {@code modelName.x-extension-name} for a model or + * {@code modelName.propertyBaseName.x-extension-name} for a model property; the value is the + * extension value. + */ + val injectModelVendorExtensions = project.objects.mapProperty() + + /** + * Injects vendor extensions into operations or their parameters without editing the input spec. + * + * Keys use the form {@code operationId.x-extension-name} for an operation or + * {@code operationId.paramBaseName.x-extension-name} for a parameter (matched by its raw spec + * name); the value is the extension value. + */ + val injectOperationVendorExtensions = project.objects.mapProperty() + /** * Path to json configuration file. * File content should be in a json format { "optionKey":"optionValue", "optionKey1":"optionValue1"...} 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 913ab08c96ed..efa9196b30d2 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 @@ -89,6 +89,8 @@ interface OpenApiWorkParameters : WorkParameters { val dryRun: Property val globalProperties: MapProperty + val injectModelVendorExtensions: MapProperty + val injectOperationVendorExtensions: MapProperty val instantiationTypes: MapProperty val importMappings: MapProperty val schemaMappings: MapProperty @@ -212,6 +214,8 @@ abstract class OpenApiWorkAction : WorkAction { // Maps and Lists params.globalProperties.orNull?.forEach { (k, v) -> configurator.addGlobalProperty(k, v) } + params.injectModelVendorExtensions.orNull?.forEach { (k, v) -> configurator.addInjectModelVendorExtension(k, v) } + params.injectOperationVendorExtensions.orNull?.forEach { (k, v) -> configurator.addInjectOperationVendorExtension(k, v) } params.instantiationTypes.orNull?.forEach { (k, v) -> configurator.addInstantiationType(k, v) } params.importMappings.orNull?.forEach { (k, v) -> configurator.addImportMapping(k, v) } params.schemaMappings.orNull?.forEach { (k, v) -> configurator.addSchemaMapping(k, v) } @@ -523,6 +527,28 @@ abstract class GenerateTask : DefaultTask() { @get:Input abstract val globalProperties: MapProperty + /** + * Injects vendor extensions into models or their properties without editing the input spec. + * + * Keys use the form {@code modelName.x-extension-name} for a model or + * {@code modelName.propertyBaseName.x-extension-name} for a model property; the value is the + * extension value. + */ + @get:Optional + @get:Input + abstract val injectModelVendorExtensions: MapProperty + + /** + * Injects vendor extensions into operations or their parameters without editing the input spec. + * + * Keys use the form {@code operationId.x-extension-name} for an operation or + * {@code operationId.paramBaseName.x-extension-name} for a parameter (matched by its raw spec + * name); the value is the extension value. + */ + @get:Optional + @get:Input + abstract val injectOperationVendorExtensions: MapProperty + /** * Path to json configuration file. * File content should be in a json format { "optionKey":"optionValue", "optionKey1":"optionValue1"...} @@ -1234,6 +1260,8 @@ abstract class GenerateTask : DefaultTask() { parameters.dryRun.set(dryRun) parameters.globalProperties.set(globalProperties) + parameters.injectModelVendorExtensions.set(injectModelVendorExtensions) + parameters.injectOperationVendorExtensions.set(injectOperationVendorExtensions) parameters.instantiationTypes.set(instantiationTypes) parameters.importMappings.set(importMappings) parameters.schemaMappings.set(schemaMappings) diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/ParameterWiringRegressionTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/ParameterWiringRegressionTest.kt index d24892023f78..21ae5a32c3af 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/ParameterWiringRegressionTest.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/ParameterWiringRegressionTest.kt @@ -226,4 +226,41 @@ class ParameterWiringRegressionTest : TestBase() { assertEquals(TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome) } + + // ------------------------------------------------------------------------- + // injectOperationVendorExtensions (side-loading, was not exposed on the plugin) + // ------------------------------------------------------------------------- + + @Test + fun `injectOperationVendorExtensions is wired from extension to task`() { + // Before the fix injectOperationVendorExtensions was not exposed on the Gradle plugin, + // so vendor extensions could only be side-loaded via a configFile. We inject + // x-operation-extra-annotation onto the listPets operation and verify the Spring + // generator renders the injected annotation into the generated API interface. + val result = runOpenApiGenerate(""" + plugins { id 'org.openapi.generator' } + openApiGenerate { + generatorName = "spring" + inputSpec = file("spec.yaml").absolutePath + outputDir = file("build/spring").absolutePath + configOptions = ["interfaceOnly": "true"] + injectOperationVendorExtensions = ["listPets.x-operation-extra-annotation": "@Deprecated"] + } + """.trimIndent(), "spec.yaml" to "specs/petstore-v3.0.yaml") + + assertEquals( + TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome, + "Generation failed after wiring injectOperationVendorExtensions — check plugin wiring" + ) + + val generatedSrcRoot = File(temp, "build/spring/src/main/java") + val allJavaSources = generatedSrcRoot.walkTopDown().filter { it.extension == "java" }.toList() + assertTrue(allJavaSources.isNotEmpty(), "No Java source files were generated") + + val allSourceText = allJavaSources.joinToString("\n") { it.readText() } + assertTrue( + allSourceText.contains("@Deprecated"), + "Expected injected operation annotation '@Deprecated' not found in generated sources — injectOperationVendorExtensions may not be wired" + ) + } } diff --git a/modules/openapi-generator-maven-plugin/README.md b/modules/openapi-generator-maven-plugin/README.md index 4c5637e16a74..64ab488a4d2b 100644 --- a/modules/openapi-generator-maven-plugin/README.md +++ b/modules/openapi-generator-maven-plugin/README.md @@ -94,6 +94,8 @@ mvn clean compile | `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 | +| `injectModelVendorExtensions` | `openapi.generator.maven.plugin.injectModelVendorExtensions` | sets vendor extensions on a model or its properties without editing the spec, as `modelName.x-extension-name=value` (model) or `modelName.propertyBaseName.x-extension-name=value` (property). Values are strings, applied at render time, and overwrite existing values. For the extra-annotation extensions, separate multiple annotations in one value with spaces (`@Foo @Bar`), not commas (an unquoted comma separates injection targets). You can also have multiple occurrences of this option | +| `injectOperationVendorExtensions` | `openapi.generator.maven.plugin.injectOperationVendorExtensions` | sets vendor extensions on an operation or its parameters without editing the spec, as `operationId.x-extension-name=value` (operation) or `operationId.paramBaseName.x-extension-name=value` (parameter, matched by its raw spec name). Values are strings, applied at render time, and overwrite existing values. For the extra-annotation extensions, separate multiple annotations in one value with spaces (`@Foo @Bar`), not commas (an unquoted comma separates injection targets). You can also have multiple occurrences of this option | | `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 | 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 084fab18b5c4..92b78b0625e4 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 @@ -446,6 +446,23 @@ public class CodeGenMojo extends AbstractMojo { @Parameter(name = "operationIdNameMappings", property = "openapi.generator.maven.plugin.operationIdNameMappings") private List operationIdNameMappings; + /** + * A map of vendor extensions to inject into models or their properties, without editing the input + * spec. Each entry is of the form {@code modelName.x-extension-name=value} for a model or + * {@code modelName.propertyBaseName.x-extension-name=value} for a model property. + */ + @Parameter(name = "injectModelVendorExtensions", property = "openapi.generator.maven.plugin.injectModelVendorExtensions") + private List injectModelVendorExtensions; + + /** + * A map of vendor extensions to inject into operations or their parameters, without editing the + * input spec. Each entry is of the form {@code operationId.x-extension-name=value} for an operation + * or {@code operationId.paramBaseName.x-extension-name=value} for a parameter (matched by its raw + * spec name). + */ + @Parameter(name = "injectOperationVendorExtensions", property = "openapi.generator.maven.plugin.injectOperationVendorExtensions") + private List injectOperationVendorExtensions; + /** * A set of rules for OpenAPI normalizer */ @@ -1015,31 +1032,46 @@ public void execute() throws MojoExecutionException { applyInlineSchemaOptionsKvpList(inlineSchemaOptions, configurator); } - // Apply Name Mappings - if (nameMappings != null && (configOptions == null || !configOptions.containsKey("name-mappings"))) { + // Apply Name Mappings. + // These *-name-mappings options are not generator CliOptions and have no configOptions + // compatibility reader above, so a configOptions guard would protect nothing. + if (nameMappings != null) { applyNameMappingsKvpList(nameMappings, configurator); } // Apply Parameter Name Mappings - if (parameterNameMappings != null && (configOptions == null || !configOptions.containsKey("parameter-name-mappings"))) { + if (parameterNameMappings != null) { applyParameterNameMappingsKvpList(parameterNameMappings, configurator); } // Apply Model Name Mappings - if (modelNameMappings != null && (configOptions == null || !configOptions.containsKey("model-name-mappings"))) { + if (modelNameMappings != null) { applyModelNameMappingsKvpList(modelNameMappings, configurator); } // Apply Enum Name Mappings - if (enumNameMappings != null && (configOptions == null || !configOptions.containsKey("enum-name-mappings"))) { + if (enumNameMappings != null) { applyEnumNameMappingsKvpList(enumNameMappings, configurator); } // Apply Operation ID Name Mappings - if (operationIdNameMappings != null && (configOptions == null || !configOptions.containsKey("operation-id-name-mappings"))) { + if (operationIdNameMappings != null) { applyOperationIdNameMappingsKvpList(operationIdNameMappings, configurator); } + // Apply Inject Model Vendor Extensions. + // Unlike the legacy mapping options above, there is no configOptions compatibility path + // for this setting (it is not a generator CliOption), so no configOptions guard is needed. + if (injectModelVendorExtensions != null) { + applyInjectModelVendorExtensionsKvpList(injectModelVendorExtensions, configurator); + } + + // Apply Inject Operation Vendor Extensions. + // No configOptions compatibility path exists for this setting either, so no guard is needed. + if (injectOperationVendorExtensions != null) { + applyInjectOperationVendorExtensionsKvpList(injectOperationVendorExtensions, configurator); + } + // Apply OpenAPI normalizer rules if (openapiNormalizer != null && (configOptions == null || !configOptions.containsKey("openapi-normalizer"))) { applyOpenapiNormalizerKvpList(openapiNormalizer, configurator); diff --git a/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java index 18b6df86f1e8..ad8154199c20 100644 --- a/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java +++ b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java @@ -101,6 +101,81 @@ public void testMinimalUpdateConfiguration() throws Exception { assertEquals(Boolean.TRUE, getVariableValueFromObject(mojo, "minimalUpdate")); } + @SuppressWarnings("unchecked") + public void testInjectOperationVendorExtensions() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + CodeGenMojo mojo = loadMojo(tempDir, "src/test/resources/inject-vendor-extensions", null, "executionId"); + + // WHEN + mojo.execute(); + + // THEN + // The configured parameter is bound onto the mojo. + List injected = (List) getVariableValueFromObject(mojo, "injectOperationVendorExtensions"); + assertNotNull(injected); + assertEquals(1, injected.size()); + assertEquals("addPet.x-request-body-extra-annotation=@com.example.MyValidation", injected.get(0)); + + // The injected request-body annotation is merged into the body parameter and rendered on the + // generated Spring API interface. Assert it lands specifically on addPet's body parameter and + // is NOT applied to a control operation (updatePet) that also has a request body but no + // injection, so the test fails if the merge became non-selective or targeted the wrong method. + final Path generatedDir = tempDir.resolve("target/generated-sources/inject-vendor-extensions"); + final Path petApi; + try (Stream files = Files.walk(generatedDir)) { + petApi = files + .filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().equals("PetApi.java")) + .findFirst() + .orElseThrow(() -> new AssertionError("PetApi.java was not generated under " + generatedDir)); + } + final String petApiSource = Files.readString(petApi); + + final String addPetSignature = extractMethodSignature(petApiSource, "addPet"); + assertTrue( + "Injected annotation should be rendered on addPet's request body parameter", + addPetSignature.contains("@com.example.MyValidation") + && addPetSignature.contains("@RequestBody") + ); + + final String updatePetSignature = extractMethodSignature(petApiSource, "updatePet"); + assertFalse( + "Injected annotation must not leak onto the control operation updatePet", + updatePetSignature.contains("@com.example.MyValidation") + ); + + // Belt and suspenders: the injected annotation must appear exactly once in the whole + // interface, proving it was not applied to every operation. + int occurrences = petApiSource.split("@com.example.MyValidation", -1).length - 1; + assertEquals("Injected annotation should appear exactly once (only on addPet)", 1, occurrences); + } + + /** + * Extracts a method's declaration up to and including its balanced parameter-list parentheses, + * so parameter annotations can be asserted per-method. Assumes {@code methodName(} appears only + * at the method declaration (true for the generated Spring interface). + */ + private static String extractMethodSignature(String source, String methodName) { + int start = source.indexOf(methodName + "("); + assertTrue("Method '" + methodName + "' not found in generated source", start >= 0); + int depth = 0; + int i = start + methodName.length(); + for (; i < source.length(); i++) { + char c = source.charAt(i); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + i++; + break; + } + } + } + return source.substring(start, i); + } + public void testHashGenerationFileContainsExecutionId() throws Exception { // GIVEN final Path tempDir = newTempFolder(); diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/inject-vendor-extensions/pom.xml b/modules/openapi-generator-maven-plugin/src/test/resources/inject-vendor-extensions/pom.xml new file mode 100644 index 000000000000..48c9954bf4fa --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/inject-vendor-extensions/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + inject.vendor.extensions.test + inject-vendor-extensions-test + jar + 1.0.0-SNAPSHOT + OpenAPI Generator Inject Vendor Extensions Test + https://openapi-generator.tech/ + + inject-vendor-extensions-test + + + org.openapitools + openapi-generator-maven-plugin + + petstore-on-classpath.yaml + spring + ${basedir}/target/generated-sources/inject-vendor-extensions + + true + true + + + addPet.x-request-body-extra-annotation=@com.example.MyValidation + + + + + executionId + generate-sources + + generate + + + + + + + diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java index d9b3d8550e53..2e34ef5b5bac 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java @@ -190,6 +190,8 @@ default List divideOperationsByContentType(OpenAPI openAPI, String pa Map injectModelVendorExtensions(); + Map injectOperationVendorExtensions(); + Map openapiNormalizer(); Map apiTemplateFiles(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 085f1d981597..6dd59dd02e39 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -203,6 +203,8 @@ public class DefaultCodegen implements CodegenConfig { protected Map operationIdNameMapping = new HashMap<>(); // a map to inject vendor extensions into model classes or their properties: key=ModelName.x-extension-name or ModelName.propertyBaseName.x-extension-name, value=extensionValue protected Map injectModelVendorExtensions = new HashMap<>(); + // a map to inject vendor extensions into operations or their parameters: key=operationId.x-extension-name or operationId.paramName.x-extension-name, value=extensionValue + protected Map injectOperationVendorExtensions = new HashMap<>(); // a map to store the rules in OpenAPI Normalizer protected Map openapiNormalizer = new HashMap<>(); @Setter @@ -556,7 +558,7 @@ public Map postProcessAllModels(Map objs) } } - // Inject vendor extensions from --inject-property-extensions into matching schema properties + // Inject vendor extensions from --inject-model-vendor-extensions into matching schema properties if (!injectModelVendorExtensions.isEmpty()) { for (Map.Entry entry : objs.entrySet()) { CodegenModel model = ModelUtils.getModelByName(entry.getKey(), objs); @@ -1674,6 +1676,11 @@ public Map injectModelVendorExtensions() { return injectModelVendorExtensions; } + @Override + public Map injectOperationVendorExtensions() { + return injectOperationVendorExtensions; + } + @Override public Map openapiNormalizer() { return openapiNormalizer; @@ -5132,9 +5139,63 @@ public CodegenOperation fromOperation(String path, // legacy support op.nickname = op.operationId; + injectOperationVendorExtensions(op); + return op; } + /** + * Injects vendor extensions supplied via {@code injectOperationVendorExtensions} onto the given + * operation or its parameters. Keys are dotted: {@code operationId.x-extension-name} targets the + * operation, while {@code operationId.paramBaseName.x-extension-name} targets a parameter matched + * by its spec name ({@code baseName}). The {@code operationId} segment is matched against the + * spec-authored operationId when present, falling back to the generated operationId only when the + * spec omits one. This runs during operation construction, before + * {@code postProcessOperationsWithModels}, so an injected operation-level extension is available + * to any later normalization of that extension (for example, request-body annotation handling in + * the Spring generators). A parameter is represented by more than one object across the + * operation's collections, so the value is set on every matching instance. + * + * @param op the operation to update + */ + private void injectOperationVendorExtensions(CodegenOperation op) { + if (injectOperationVendorExtensions.isEmpty()) { + return; + } + // The operation always has a non-blank operationId at this point: getOrGenerateOperationId + // synthesizes one from the path and HTTP method when the spec omits or blanks it. + Objects.requireNonNull(op.operationId, "operationId must be set before injecting operation vendor extensions"); + // Prefer the spec-authored operationId; only fall back to the generated one when the spec + // omits operationId (in which case operationIdOriginal is null or blank). + String matchOperationId = StringUtils.isNotBlank(op.operationIdOriginal) ? op.operationIdOriginal : op.operationId; + for (Map.Entry extEntry : injectOperationVendorExtensions.entrySet()) { + String[] parts = extEntry.getKey().split("\\.", 3); + if (parts.length < 2) continue; + if (!parts[0].equals(matchOperationId)) continue; + String extensionValue = extEntry.getValue(); + + if (parts.length == 2) { + // operation-level extension: operationId.x-extension-name + op.vendorExtensions.put(parts[1], extensionValue); + } else { + // parameter-level extension: operationId.paramBaseName.x-extension-name + String paramBaseName = parts[1]; + String extensionName = parts[2]; + List> allParameterLists = Arrays.asList( + op.allParams, op.bodyParams, op.pathParams, op.queryParams, op.headerParams, + op.cookieParams, op.formParams, op.requiredParams, op.optionalParams, + op.requiredAndNotNullableParams, op.notNullableParams); + for (List parameters : allParameterLists) { + for (CodegenParameter parameter : parameters) { + if (paramBaseName.equals(parameter.baseName)) { + parameter.vendorExtensions.put(extensionName, extensionValue); + } + } + } + } + } + } + /** * Helper method to add an import for a data type if it exists in the importMapping. * @@ -5971,7 +6032,7 @@ protected boolean isReservedWord(String word) { * @param operation the operation object * @param path the path of the operation * @param httpMethod the HTTP method of the operation - * @return the (generated) operationId + * @return the (generated) operationId; never null or blank */ protected String getOrGenerateOperationId(Operation operation, String path, String httpMethod) { String operationId = operation.getOperationId(); @@ -7399,6 +7460,68 @@ public static void normalizeVendorExtensionWithStringList(Map ve vendorExtensions.put(name, new ArrayList<>(getObjectAsStringList(vendorExtensions.get(name)))); } + /** + * Normalizes a vendor extension across all of an operation's parameter collections into a mutable + * {@code List}, so that a value authored as either a single string or a list is handled + * uniformly and further values can be appended. The same parameter can appear in several of the + * operation's parameter collections, so they are de-duplicated by identity before being updated. + * + * @param operation operation whose parameters should be updated + * @param name vendor extension name + */ + protected void normalizeOperationParameterVendorExtensions(CodegenOperation operation, String name) { + Set parameters = Collections.newSetFromMap(new IdentityHashMap<>()); + parameters.addAll(operation.allParams); + parameters.addAll(operation.bodyParams); + parameters.addAll(operation.pathParams); + parameters.addAll(operation.queryParams); + parameters.addAll(operation.headerParams); + parameters.addAll(operation.implicitHeadersParams); + parameters.addAll(operation.constantParams); + parameters.addAll(operation.formParams); + parameters.addAll(operation.cookieParams); + parameters.addAll(operation.requiredParams); + parameters.addAll(operation.optionalParams); + parameters.addAll(operation.requiredAndNotNullableParams); + parameters.addAll(operation.notNullableParams); + for (CodegenParameter parameter : parameters) { + normalizeVendorExtensionWithStringList(parameter.vendorExtensions, name); + } + } + + /** + * Merges the values of an operation-level vendor extension into a list-valued vendor extension on + * each request body parameter. This lets an annotation authored on the operation be applied to the + * request body parameter, which is needed because a request body typically {@code $ref}s a shared + * model and so cannot carry the annotation itself. Any values already present on the body parameter + * are preserved (they appear first), then the operation-level values are appended. This is a no-op + * when the source extension is absent or empty, leaving existing body-parameter extensions untouched. + * + * @param operation operation whose {@code bodyParams} should receive the merged values + * @param sourceName operation-level vendor extension name to read the values from + * @param targetName body-parameter vendor extension name to merge the values into + */ + public static void mergeOperationVendorExtensionIntoBodyParams(CodegenOperation operation, String sourceName, String targetName) { + List sourceValues = getObjectAsStringList(operation.vendorExtensions.get(sourceName)); + if (sourceValues.isEmpty()) { + return; + } + // A request body parameter may be represented by more than one object; update every + // instance so the merged values are applied consistently. + Set bodyParameters = Collections.newSetFromMap(new IdentityHashMap<>()); + bodyParameters.addAll(operation.bodyParams); + for (CodegenParameter param : operation.allParams) { + if (param.isBodyParam) { + bodyParameters.add(param); + } + } + for (CodegenParameter bodyParam : bodyParameters) { + List merged = new ArrayList<>(getObjectAsStringList(bodyParam.vendorExtensions.get(targetName))); + merged.addAll(sourceValues); + bodyParam.vendorExtensions.put(targetName, merged); + } + } + public Map getPropertyAsStringMap(String propertyKey) { final Object value = additionalProperties.get(propertyKey); return getObjectAsStringMap(value); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java index ac8bfbe87b3b..afde725c185f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java @@ -26,6 +26,7 @@ public enum VendorExtension { X_CLASS_EXTRA_ANNOTATION("x-class-extra-annotation", ExtensionLevel.MODEL, "Custom annotation(s) to be added to model; accepts a string or list of strings", null), X_FIELD_EXTRA_ANNOTATION("x-field-extra-annotation", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "Custom annotation(s) to be added to property; accepts a string or list of strings", null), X_OPERATION_EXTRA_ANNOTATION("x-operation-extra-annotation", ExtensionLevel.OPERATION, "Custom annotation(s) to be added to operation; accepts a string or list of strings", null), + X_REQUEST_BODY_EXTRA_ANNOTATION("x-request-body-extra-annotation", ExtensionLevel.OPERATION, "Custom annotation(s) to be added to the request body parameter; accepts a string or list of strings. Declared on the operation because the request body typically `$ref`s a shared model (so the annotation cannot be placed next to the `$ref`); the value is rendered by being merged into the body parameter's `x-field-extra-annotation`", null), X_EXTRA_IMPORTS("x-extra-imports", Arrays.asList(ExtensionLevel.MODEL, ExtensionLevel.FIELD, ExtensionLevel.OPERATION, ExtensionLevel.OPERATION_PARAMETER), "Custom import(s) to add to the generated file that declares the annotated model, property, operation, or parameter (e.g. so custom annotations can be referenced by their short name); accepts a string or list of strings. Values are emitted verbatim (Kotlin alias imports supported) and only exact duplicates are removed", null), X_VERSION_PARAM("x-version-param", ExtensionLevel.OPERATION_PARAMETER, "Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false", null), X_PATTERN_MESSAGE("x-pattern-message", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable", null), 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 ab15c320b2cd..dac3ac307fd3 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 @@ -75,6 +75,7 @@ public class CodegenConfigurator { private Map nameMappings = new HashMap<>(); private Map parameterNameMappings = new HashMap<>(); private Map modelNameMappings = new HashMap<>(); + private Map injectOperationVendorExtensions = new HashMap<>(); private Map enumNameMappings = new HashMap<>(); private Map operationIdNameMappings = new HashMap<>(); private Map injectModelVendorExtensions = new HashMap<>(); @@ -153,6 +154,9 @@ public static CodegenConfigurator fromFile(String configFile, Module... modules) if (generatorSettings.getInjectModelVendorExtensions() != null) { configurator.injectModelVendorExtensions.putAll(generatorSettings.getInjectModelVendorExtensions()); } + if (generatorSettings.getInjectOperationVendorExtensions() != null) { + configurator.injectOperationVendorExtensions.putAll(generatorSettings.getInjectOperationVendorExtensions()); + } if (generatorSettings.getOpenapiNormalizer() != null) { configurator.openapiNormalizer.putAll(generatorSettings.getOpenapiNormalizer()); } @@ -311,6 +315,18 @@ public CodegenConfigurator setInjectModelVendorExtensions(Map ex return this; } + public CodegenConfigurator addInjectOperationVendorExtension(String key, String value) { + this.injectOperationVendorExtensions.put(key, value); + generatorSettingsBuilder.withInjectOperationVendorExtension(key, value); + return this; + } + + public CodegenConfigurator setInjectOperationVendorExtensions(Map extensions) { + this.injectOperationVendorExtensions = extensions; + generatorSettingsBuilder.withInjectOperationVendorExtensions(extensions); + return this; + } + public CodegenConfigurator addOpenapiNormalizer(String key, String value) { this.openapiNormalizer.put(key, value); generatorSettingsBuilder.withOpenapiNormalizer(key, value); @@ -832,6 +848,7 @@ public ClientOptInput toClientOptInput() { config.enumNameMapping().putAll(generatorSettings.getEnumNameMappings()); config.operationIdNameMapping().putAll(generatorSettings.getOperationIdNameMappings()); config.injectModelVendorExtensions().putAll(generatorSettings.getInjectModelVendorExtensions()); + config.injectOperationVendorExtensions().putAll(generatorSettings.getInjectOperationVendorExtensions()); config.openapiNormalizer().putAll(generatorSettings.getOpenapiNormalizer()); config.languageSpecificPrimitives().addAll(generatorSettings.getLanguageSpecificPrimitives()); config.openapiGeneratorIgnoreList().addAll(generatorSettings.getOpenapiGeneratorIgnoreList()); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java index eabae28f67b6..1d8b6e5dff00 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java @@ -217,6 +217,19 @@ public static void applyInjectModelVendorExtensionsKvp(String injectModelVendorE } } + public static void applyInjectOperationVendorExtensionsKvpList(List injectOperationVendorExtensions, CodegenConfigurator configurator) { + for (String propString : injectOperationVendorExtensions) { + applyInjectOperationVendorExtensionsKvp(propString, configurator); + } + } + + public static void applyInjectOperationVendorExtensionsKvp(String injectOperationVendorExtensions, CodegenConfigurator configurator) { + final Map map = createMapFromKeyValuePairs(injectOperationVendorExtensions); + for (Map.Entry entry : map.entrySet()) { + configurator.addInjectOperationVendorExtension(entry.getKey().trim(), entry.getValue().trim()); + } + } + public static void applyTypeMappingsKvpList(List typeMappings, CodegenConfigurator configurator) { for (String propString : typeMappings) { applyTypeMappingsKvp(propString, configurator); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index 554f98183fc7..61cced77eb7b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -2303,36 +2303,6 @@ private void normalizeModelPropertyVendorExtensions(CodegenModel model, String n } } - /** - * Normalizes an operation parameter vendor extension across all parameter collections. - * In this context, normalization means converting a missing value, a single string, or a list value - * into a predictable mutable {@code List} on each parameter. The same parameter can appear in - * several operation collections, so the collections are de-duplicated before updating the extension map. - * - * @param operation operation whose parameters should be updated - * @param name vendor extension name - */ - protected void normalizeOperationParameterVendorExtensions(CodegenOperation operation, String name) { - Set parameters = Collections.newSetFromMap(new IdentityHashMap<>()); - parameters.addAll(operation.allParams); - parameters.addAll(operation.bodyParams); - parameters.addAll(operation.pathParams); - parameters.addAll(operation.queryParams); - parameters.addAll(operation.headerParams); - parameters.addAll(operation.implicitHeadersParams); - parameters.addAll(operation.constantParams); - parameters.addAll(operation.formParams); - parameters.addAll(operation.cookieParams); - parameters.addAll(operation.requiredParams); - parameters.addAll(operation.optionalParams); - parameters.addAll(operation.requiredAndNotNullableParams); - parameters.addAll(operation.notNullableParams); - - for (CodegenParameter parameter : parameters) { - normalizeVendorExtensionWithStringList(parameter.vendorExtensions, name); - } - } - @Override public void preprocessOpenAPI(OpenAPI openAPI) { super.preprocessOpenAPI(openAPI); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCamelServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCamelServerCodegen.java index 831c083aa0b1..eb8a34859149 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCamelServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCamelServerCodegen.java @@ -20,6 +20,7 @@ import org.openapitools.codegen.CliOption; import org.openapitools.codegen.CodegenOperation; import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.VendorExtension; import org.openapitools.codegen.languages.features.BeanValidationFeatures; import org.openapitools.codegen.languages.features.OptionalFeatures; import org.openapitools.codegen.languages.features.PerformBeanValidationFeatures; @@ -190,4 +191,13 @@ private T manageAdditionalProperty(String propertyName, T defaultValue) { private Boolean manageBooleanAdditionalProperty(String propertyValue) { return Boolean.parseBoolean(propertyValue); } + + @Override + public List getSupportedVendorExtensions() { + List extensions = super.getSupportedVendorExtensions(); + // The Camel REST DSL templates do not render a request-body parameter annotation, + // so this generator does not support the extension inherited from SpringCodegen. + extensions.remove(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION); + return extensions; + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java index 12047a88e12f..c0625a9f2215 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java @@ -1706,6 +1706,14 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List ops = operations.getOperation(); ops.forEach(operation -> { + // Normalize x-field-extra-annotation on each parameter (including the body) so a + // string or list value is handled uniformly and further values can be appended. + normalizeOperationParameterVendorExtensions(operation, VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName()); + // x-request-body-extra-annotation is authored on the operation (the request body usually + // $refs a shared model, so it cannot be placed next to the $ref). Merge its values into the + // body parameter's x-field-extra-annotation so it renders through the same template path. + normalizeVendorExtensionWithStringList(operation.vendorExtensions, VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION.getName()); + mergeOperationVendorExtensionIntoBodyParams(operation, VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION.getName(), VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName()); List responses = operation.responses; if (responses != null) { responses.forEach(resp -> { @@ -1948,6 +1956,7 @@ public List getSupportedVendorExtensions() { extensions.add(VendorExtension.X_DISCRIMINATOR_VALUE); extensions.add(VendorExtension.X_FIELD_EXTRA_ANNOTATION); extensions.add(VendorExtension.X_OPERATION_EXTRA_ANNOTATION); + extensions.add(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION); extensions.add(VendorExtension.X_EXTRA_IMPORTS); extensions.add(VendorExtension.X_PATTERN_MESSAGE); extensions.add(VendorExtension.X_SIZE_MESSAGE); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 4d8b7f462fcc..60806230efc7 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -1102,6 +1102,11 @@ public void setIsVoid(boolean isVoid) { handleImplicitHeaders(operation); normalizeVendorExtensionWithStringList(operation.vendorExtensions, VendorExtension.X_OPERATION_EXTRA_ANNOTATION.getName()); normalizeOperationParameterVendorExtensions(operation, VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName()); + // x-request-body-extra-annotation is authored on the operation (the request body usually + // $refs a shared model, so it cannot be placed next to the $ref). Merge its values into the + // body parameter's x-field-extra-annotation so it renders through the same template path. + normalizeVendorExtensionWithStringList(operation.vendorExtensions, VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION.getName()); + mergeOperationVendorExtensionIntoBodyParams(operation, VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION.getName(), VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName()); if (useSpringSecurityPreAuthorize) { addSpringSecurityPreAuthorize(operation); @@ -1722,6 +1727,7 @@ public void setUseSwaggerUI(boolean useSwaggerUI) { public List getSupportedVendorExtensions() { List extensions = super.getSupportedVendorExtensions(); extensions.add(VendorExtension.X_OPERATION_EXTRA_ANNOTATION); + extensions.add(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION); extensions.add(VendorExtension.X_SPRING_PAGINATED); extensions.add(VendorExtension.X_VERSION_PARAM); extensions.add(VendorExtension.X_PATTERN_MESSAGE); diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache index 26bea8c9b706..258d3425292e 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache @@ -1 +1 @@ -{{#isBodyParam}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}@Valid{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{>nullableAnnotation}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}} \ No newline at end of file +{{#isBodyParam}}{{>paramDoc}}{{#vendorExtensions.x-field-extra-annotation}} {{{.}}}{{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}} {{>beanValidationBodyParams}}@Valid{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{>nullableAnnotation}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/cookieParams.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/cookieParams.mustache index a255b5c7daf2..87865ce82f1b 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/cookieParams.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/cookieParams.mustache @@ -1 +1 @@ -{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isCookieParam}} \ No newline at end of file +{{#isCookieParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isCookieParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/bodyParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/bodyParams.mustache index b4f03475cd0b..c44f1a1740bc 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/bodyParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/bodyParams.mustache @@ -1 +1 @@ -{{#isBodyParam}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"], defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"]){{/defaultValue}}{{/allowableValues}}{{/isContainer}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}, allowableValues = "{{{.}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{>beanValidationBodyParams}}{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}} \ No newline at end of file +{{#isBodyParam}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"], defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"]){{/defaultValue}}{{/allowableValues}}{{/isContainer}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}, allowableValues = "{{{.}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}@Valid{{>beanValidationBodyParams}}{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/cookieParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/cookieParams.mustache index 028264a18bcf..acce5461e358 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/cookieParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/cookieParams.mustache @@ -1 +1 @@ -{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}@CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{{paramName}}}: {{>optionalDataType}}{{/isCookieParam}} \ No newline at end of file +{{#isCookieParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}@CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{{paramName}}}: {{>optionalDataType}}{{/isCookieParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/formParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/formParams.mustache index 456af893718f..4cc39f195fb8 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/formParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/formParams.mustache @@ -1 +1 @@ -{{#isFormParam}}{{^isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}@RequestParam{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{paramName}}}: {{>optionalDataType}}{{/isFile}}{{#isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}") {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "file detail") {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart("{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{paramName}}}: {{>optionalDataType}}{{/isFile}}{{/isFormParam}} \ No newline at end of file +{{#isFormParam}}{{^isFile}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}@RequestParam{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{paramName}}}: {{>optionalDataType}}{{/isFile}}{{#isFile}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}") {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "file detail") {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart("{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{paramName}}}: {{>optionalDataType}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/headerParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/headerParams.mustache index 0c2678f1bf67..a29b151e1c64 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/headerParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/headerParams.mustache @@ -1 +1 @@ -{{#isHeaderParam}}{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}", `in` = ParameterIn.HEADER{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}@RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}{{#defaultValue}}, defaultValue = {{^isString}}"{{{.}}}"{{/isString}}{{#isString}}{{#isEnum}}"{{{.}}}"{{/isEnum}}{{^isEnum}}{{{.}}}{{/isEnum}}{{/isString}}{{/defaultValue}}) {{{paramName}}}: {{>optionalDataType}}{{/isHeaderParam}} \ No newline at end of file +{{#isHeaderParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}", `in` = ParameterIn.HEADER{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}@RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}{{#defaultValue}}, defaultValue = {{^isString}}"{{{.}}}"{{/isString}}{{#isString}}{{#isEnum}}"{{{.}}}"{{/isEnum}}{{^isEnum}}{{{.}}}{{/isEnum}}{{/isString}}{{/defaultValue}}) {{{paramName}}}: {{>optionalDataType}}{{/isHeaderParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/pathParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/pathParams.mustache index 2e28d18c78fa..3f22fecbbd9b 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/pathParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/pathParams.mustache @@ -1 +1 @@ -{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}@PathVariable("{{baseName}}") {{{paramName}}}: {{>optionalDataType}}{{/isPathParam}} \ No newline at end of file +{{#isPathParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}@PathVariable("{{baseName}}") {{{paramName}}}: {{>optionalDataType}}{{/isPathParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/queryParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/queryParams.mustache index 27d7e286bb33..551811ddbe23 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/queryParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/queryParams.mustache @@ -1 +1 @@ -{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{{paramName}}}: {{>optionalDataType}}{{/isQueryParam}} \ No newline at end of file +{{#isQueryParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{{paramName}}}: {{>optionalDataType}}{{/isQueryParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 72624f257e5a..da713d79d333 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -37,6 +37,7 @@ import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.parser.core.models.ParseOptions; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; import org.openapitools.codegen.config.CodegenConfigurator; import org.openapitools.codegen.config.GlobalSettings; @@ -101,6 +102,68 @@ public void testEnumImports() { assertEquals(1, Sets.intersection(operation.imports, Sets.newHashSet("PetByType")).size()); } + @Test + public void testInjectOperationVendorExtensions() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openApi = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + codegen.setOpenAPI(openApi); + codegen.injectOperationVendorExtensions().put("createEmployee.x-request-body-extra-annotation", "@com.example.MyValidation"); + codegen.injectOperationVendorExtensions().put("createEmployee.orgId.x-field-extra-annotation", "@com.example.ValidOrgId"); + // non-matching operationId is a no-op + codegen.injectOperationVendorExtensions().put("noSuchOperation.x-foo", "bar"); + + PathItem path = openApi.getPaths().get("/orgs/{orgId}/employees"); + CodegenOperation operation = codegen.fromOperation("/orgs/{orgId}/employees", "post", path.getPost(), path.getServers()); + + // operation-level extension landed on the operation + assertEquals("@com.example.MyValidation", operation.vendorExtensions.get("x-request-body-extra-annotation")); + assertNull(operation.vendorExtensions.get("x-foo")); + + // parameter-level extension landed on the matching parameter across collections + CodegenParameter orgId = operation.allParams.stream() + .filter(p -> "orgId".equals(p.baseName)).findFirst().orElseThrow(); + assertEquals("@com.example.ValidOrgId", orgId.vendorExtensions.get("x-field-extra-annotation")); + CodegenParameter orgIdPath = operation.pathParams.stream() + .filter(p -> "orgId".equals(p.baseName)).findFirst().orElseThrow(); + assertEquals("@com.example.ValidOrgId", orgIdPath.vendorExtensions.get("x-field-extra-annotation")); + } + + @Test + public void testInjectOperationVendorExtensionsMatchesSpecOperationId() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openApi = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + codegen.setOpenAPI(openApi); + // key uses the spec-authored (snake_case) operationId, not the generated/camelized one + codegen.injectOperationVendorExtensions().put("create_employee_snake.x-request-body-extra-annotation", "@com.example.MyValidation"); + // the generated/camelized operationId must not match when the spec provides an operationId + codegen.injectOperationVendorExtensions().put("createEmployeeSnake.x-foo", "bar"); + + PathItem path = openApi.getPaths().get("/orgs/{orgId}/employees/snake"); + CodegenOperation operation = codegen.fromOperation("/orgs/{orgId}/employees/snake", "post", path.getPost(), path.getServers()); + + assertEquals("create_employee_snake", operation.operationIdOriginal); + assertEquals("@com.example.MyValidation", operation.vendorExtensions.get("x-request-body-extra-annotation")); + assertNull(operation.vendorExtensions.get("x-foo")); + } + + @Test + public void testInjectOperationVendorExtensionsFallsBackToGeneratedIdWhenBlank() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openApi = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + codegen.setOpenAPI(openApi); + + PathItem path = openApi.getPaths().get("/orgs/{orgId}/employees/blank"); + // first pass discovers the generated operationId (the spec leaves operationId blank) + CodegenOperation discovered = codegen.fromOperation("/orgs/{orgId}/employees/blank", "post", path.getPost(), path.getServers()); + assertTrue(discovered.operationIdOriginal == null || discovered.operationIdOriginal.isEmpty()); + assertTrue(StringUtils.isNotBlank(discovered.operationId)); + + // injecting via the generated operationId must apply, since the blank original cannot be matched + codegen.injectOperationVendorExtensions().put(discovered.operationId + ".x-request-body-extra-annotation", "@com.example.MyValidation"); + CodegenOperation operation = codegen.fromOperation("/orgs/{orgId}/employees/blank", "post", path.getPost(), path.getServers()); + assertEquals("@com.example.MyValidation", operation.vendorExtensions.get("x-request-body-extra-annotation")); + } + @Test public void testHasBodyParameter() { final Schema refSchema = new Schema<>().$ref("#/components/schemas/Pet"); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index b7184f853396..2a657cb654db 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -3959,7 +3959,126 @@ public void testHasOperationParameterExtraAnnotation_issue18224() throws IOExcep .toMethod() .assertParameter("clientId") .assertParameterAnnotations() - .containsWithName("com.test.MyAnnotationInHeader"); + .containsWithName("com.test.MyAnnotationInHeader") + .toParameter() + .toMethod() + .assertParameter("sessionId") + .assertParameterAnnotations() + .containsWithName("com.test.MyAnnotationInCookie"); + } + + @Test + public void testRequestBodyExtraAnnotation() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/request-body-extra-annotation.yaml"); + final SpringCodegen codegen = new SpringCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(INTERFACE_ONLY, "true"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + JavaFileAssert.assertThat(files.get("EmployeesApi.java")) + // single string value renders before the body param + .assertMethod("createEmployee") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.MyValidation") + .containsWithName("RequestBody") + .toParameter().toMethod().toFileAssert() + // list value renders all annotations before the body param + .assertMethod("createEmployeeBulk") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.MyValidation") + .containsWithName("com.example.AuditLogged") + .toParameter().toMethod().toFileAssert() + // selectivity: operation without the extension referencing the same model is unaffected + .assertMethod("createEmployeePlain") + .assertParameter("employee") + .assertParameterAnnotations() + .doesNotContainWithName("com.example.MyValidation") + .doesNotContainWithName("com.example.AuditLogged") + .toParameter().toMethod().toFileAssert() + // annotation placed directly on the inline requestBody object renders before the body param + .assertMethod("createEmployeeInlineAnnotation") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.InlineBodyValidation") + .containsWithName("RequestBody") + .toParameter().toMethod().toFileAssert() + // reusable components.requestBodies annotation applies to every operation that refs it + .assertMethod("createEmployeeReusableA") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.ReusableBodyValidation") + .containsWithName("com.example.ReusableAuditLogged") + .toParameter().toMethod().toFileAssert() + .assertMethod("createEmployeeReusableB") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.ReusableBodyValidation") + .containsWithName("com.example.ReusableAuditLogged"); + } + + @Test + public void testInjectOperationVendorExtensions() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + final SpringCodegen codegen = new SpringCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(INTERFACE_ONLY, "true"); + + // operation-level injection drives the request-body annotation; parameter-level injection + // annotates the path param, both without editing the spec + codegen.injectOperationVendorExtensions().put("createEmployee.x-request-body-extra-annotation", "@com.example.MyValidation"); + codegen.injectOperationVendorExtensions().put("createEmployee.orgId.x-field-extra-annotation", "@com.example.ValidOrgId"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + JavaFileAssert.assertThat(files.get("OrgsApi.java")) + .assertMethod("createEmployee") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.MyValidation") + .containsWithName("RequestBody") + .toParameter().toMethod() + .assertParameter("orgId") + .assertParameterAnnotations() + .containsWithName("com.example.ValidOrgId") + .toParameter().toMethod().toFileAssert() + // selectivity: the second operation referencing the same model/param is unaffected + .assertMethod("createEmployeePlain") + .assertParameter("employee") + .assertParameterAnnotations() + .doesNotContainWithName("com.example.MyValidation") + .toParameter().toMethod() + .assertParameter("orgId") + .assertParameterAnnotations() + .doesNotContainWithName("com.example.ValidOrgId"); } @Test diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index 7671d2683121..9d61a6ef586a 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -4482,6 +4482,81 @@ public void declarativeReactorArrayOfStringReturnsMonoResponseEntity() throws Ex "kotlin.collections.Set<", "Mono files = generateFromContract( + "src/test/resources/3_0/kotlin/kotlin-spring-param-extra-annotation.yaml", + Map.of(INTERFACE_ONLY, true)); + + Path locationApi = files.get("OrgsApi.kt").toPath(); + // path param (single value), path param (list value -> both render), query param, + // header param (single value), cookie param (list value -> both render) + assertFileContains(locationApi, + "@com.example.ValidOrgId ", + "@com.example.ValidLocId @com.example.Trimmed ", + "@com.example.ValidFilter ", + "@com.example.ValidTrace ", + "@com.example.ValidSession @com.example.Trimmed "); + + // form param + assertFileContains(files.get("DevicesApi.kt").toPath(), "@com.example.ValidDeviceId "); + } + + @Test + public void testRequestBodyExtraAnnotation() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/request-body-extra-annotation.yaml", + Map.of(INTERFACE_ONLY, true)); + + Path employeeApi = files.get("EmployeesApi.kt").toPath(); + // single string value and list value both render before the body binding + assertFileContains(employeeApi, + "@com.example.MyValidation ", + "@com.example.AuditLogged "); + + // selectivity: the annotation is applied per-operation only, even though all three + // operations reference the same shared Employee model. It must render exactly twice + // (createEmployee + createEmployeeBulk), never for createEmployeePlain. + String content = Files.readString(employeeApi); + int myValidationCount = content.split("@com.example.MyValidation", -1).length - 1; + assertThat(myValidationCount).isEqualTo(2); + + // annotation placed directly on the inline requestBody object renders before the body binding + assertFileContains(employeeApi, "@com.example.InlineBodyValidation "); + // reusable components.requestBodies annotation applies to every operation that refs it (2 ops) + assertFileContains(employeeApi, + "@com.example.ReusableBodyValidation ", + "@com.example.ReusableAuditLogged "); + assertThat(content.split("@com.example.ReusableBodyValidation", -1).length - 1).isEqualTo(2); + } + + @Test + public void testInjectOperationVendorExtensions() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/inject-operation-vendor-extensions.yaml", + Map.of(INTERFACE_ONLY, true), + new HashMap<>(), + configurator -> { + // operation-level injection drives the request-body annotation; parameter-level + // injection annotates the path param, both without editing the spec + configurator.addInjectOperationVendorExtension( + "createEmployee.x-request-body-extra-annotation", "@com.example.MyValidation"); + configurator.addInjectOperationVendorExtension( + "createEmployee.orgId.x-field-extra-annotation", "@com.example.ValidOrgId"); + }); + + Path employeesApi = files.get("OrgsApi.kt").toPath(); + assertFileContains(employeesApi, + "@com.example.MyValidation ", + "@com.example.ValidOrgId "); + + // selectivity: only createEmployee is annotated, not createEmployeePlain, even though + // both reference the same shared Employee model and OrgId parameter schema + String content = Files.readString(employeesApi); + assertThat(content.split("@com.example.MyValidation", -1).length - 1).isEqualTo(1); + assertThat(content.split("@com.example.ValidOrgId", -1).length - 1).isEqualTo(1); + } + private Map generateFromContract(String url) throws IOException { return generateFromContract(url, new HashMap<>(), new HashMap<>()); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/languages/JavaCamelServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/languages/JavaCamelServerCodegenTest.java new file mode 100644 index 000000000000..6610e1863b7e --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/languages/JavaCamelServerCodegenTest.java @@ -0,0 +1,21 @@ +package org.openapitools.codegen.languages; + +import org.openapitools.codegen.VendorExtension; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class JavaCamelServerCodegenTest { + + @Test + public void doesNotAdvertiseRequestBodyExtraAnnotation() { + // The Camel REST DSL templates do not render a request-body parameter annotation, so this + // generator must not advertise the extension it inherits from SpringCodegen. + assertFalse(new JavaCamelServerCodegen().getSupportedVendorExtensions() + .contains(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION)); + // SpringCodegen itself still supports it. + assertTrue(new SpringCodegen().getSupportedVendorExtensions() + .contains(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION)); + } +} diff --git a/modules/openapi-generator/src/test/resources/3_0/inject-operation-vendor-extensions.yaml b/modules/openapi-generator/src/test/resources/3_0/inject-operation-vendor-extensions.yaml new file mode 100644 index 000000000000..b59f3c24ad23 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/inject-operation-vendor-extensions.yaml @@ -0,0 +1,95 @@ +openapi: 3.0.3 +info: + title: inject operation vendor extensions + version: 1.0.0 +paths: + /orgs/{orgId}/employees: + post: + tags: + - employee + operationId: createEmployee + parameters: + - name: orgId + in: path + required: true + schema: + $ref: '#/components/schemas/OrgId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /orgs/{orgId}/employees/plain: + post: + tags: + - employee + operationId: createEmployeePlain + parameters: + - name: orgId + in: path + required: true + schema: + $ref: '#/components/schemas/OrgId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /orgs/{orgId}/employees/snake: + post: + tags: + - employee + operationId: create_employee_snake + parameters: + - name: orgId + in: path + required: true + schema: + $ref: '#/components/schemas/OrgId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /orgs/{orgId}/employees/blank: + post: + tags: + - employee + operationId: "" + parameters: + - name: orgId + in: path + required: true + schema: + $ref: '#/components/schemas/OrgId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created +components: + schemas: + OrgId: + type: string + format: uuid + Employee: + type: object + properties: + name: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_18224.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_18224.yaml index ea8a4db8adfd..8267a2b94a89 100644 --- a/modules/openapi-generator/src/test/resources/3_0/issue_18224.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/issue_18224.yaml @@ -12,6 +12,7 @@ paths: - $ref: '#/components/parameters/groupObj' - $ref: '#/components/parameters/token' - $ref: '#/components/parameters/clientId' + - $ref: '#/components/parameters/sessionId' requestBody: content: application/json: @@ -49,6 +50,13 @@ components: pattern: "\\d" x-pattern-message: "Only numbers" x-field-extra-annotation: '@com.test.MyAnnotationInHeader' + sessionId: + in: cookie + name: sessionId + required: true + schema: + type: string + x-field-extra-annotation: '@com.test.MyAnnotationInCookie' schemas: ObjTest: description: A model to return diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-spring-param-extra-annotation.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-spring-param-extra-annotation.yaml new file mode 100644 index 000000000000..b43b3a3f3f33 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-spring-param-extra-annotation.yaml @@ -0,0 +1,83 @@ +openapi: 3.0.3 +info: + title: kotlin-spring param extra annotation + version: 1.0.0 +paths: + /orgs/{orgId}/locations/{locId}: + get: + tags: + - location + operationId: getLocation + parameters: + - name: orgId + in: path + required: true + x-field-extra-annotation: "@com.example.ValidOrgId" + schema: + $ref: '#/components/schemas/OrgId' + - name: locId + in: path + required: true + x-field-extra-annotation: + - "@com.example.ValidLocId" + - "@com.example.Trimmed" + schema: + $ref: '#/components/schemas/LocId' + - name: filter + in: query + required: false + x-field-extra-annotation: "@com.example.ValidFilter" + schema: + $ref: '#/components/schemas/Filter' + - name: X-Trace-Id + in: header + required: true + x-field-extra-annotation: "@com.example.ValidTrace" + schema: + $ref: '#/components/schemas/TraceId' + - name: session + in: cookie + required: true + x-field-extra-annotation: + - "@com.example.ValidSession" + - "@com.example.Trimmed" + schema: + $ref: '#/components/schemas/SessionId' + responses: + '200': + description: ok + /devices: + post: + tags: + - device + operationId: registerDevice + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - deviceId + properties: + deviceId: + x-field-extra-annotation: "@com.example.ValidDeviceId" + allOf: + - $ref: '#/components/schemas/DeviceId' + responses: + '200': + description: ok +components: + schemas: + OrgId: + type: string + format: uuid + LocId: + type: string + DeviceId: + type: string + Filter: + type: string + TraceId: + type: string + SessionId: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml new file mode 100644 index 000000000000..a3c6c88fbd5f --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml @@ -0,0 +1,753 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: OpenAPI Petstore + license: + name: Apache-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + x-extra-imports: org.springframework.lang.NonNull + x-request-body-extra-annotation: "@NonNull" + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + externalDocs: + url: "http://petstore.swagger.io/v2/doc/updatePet" + description: "API documentation for the updatePet operation" + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: false + deprecated: true + x-extra-imports: + - org.springframework.lang.NonNull + - org.springframework.lang.Nullable + x-field-extra-annotation: + - "@NonNull" + - "@Nullable" + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + x-extra-imports: org.springframework.lang.NonNull + x-field-extra-annotation: "@NonNull" + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + x-extra-imports: org.springframework.lang.NonNull + x-field-extra-annotation: "@NonNull" + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value <= 5 or > 10. Other values + will generate exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + Set-Cookie: + description: >- + Cookie authentication key for use with the `api_key` + apiKey authentication. + schema: + type: string + example: AUTH_KEY=abcde12345; Path=/; HttpOnly + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + security: + - api_key: [] + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing. + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + security: + - api_key: [] +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + title: Pet category + description: A category for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + xml: + name: Category + User: + title: a User + description: A User who is purchasing from the pet store + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + title: Pet Tag + description: A tag for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + deprecated: true + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + title: An uploaded response + description: Describes the result of uploading an image resource + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/request-body-extra-annotation.yaml b/modules/openapi-generator/src/test/resources/3_0/request-body-extra-annotation.yaml new file mode 100644 index 000000000000..f42f48bddccf --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/request-body-extra-annotation.yaml @@ -0,0 +1,105 @@ +openapi: 3.0.3 +info: + title: request body extra annotation + version: 1.0.0 +paths: + /employees: + post: + tags: + - employee + operationId: createEmployee + x-request-body-extra-annotation: "@com.example.MyValidation" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /employees/bulk: + post: + tags: + - employee + operationId: createEmployeeBulk + x-request-body-extra-annotation: + - "@com.example.MyValidation" + - "@com.example.AuditLogged" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /employees/plain: + post: + tags: + - employee + operationId: createEmployeePlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /employees/inline-annotation: + post: + tags: + - employee + operationId: createEmployeeInlineAnnotation + # annotation placed directly on the requestBody object (per-operation, selective) + requestBody: + x-field-extra-annotation: "@com.example.InlineBodyValidation" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /employees/reusable-a: + post: + tags: + - employee + operationId: createEmployeeReusableA + requestBody: + $ref: '#/components/requestBodies/AnnotatedEmployeeBody' + responses: + '201': + description: created + /employees/reusable-b: + post: + tags: + - employee + operationId: createEmployeeReusableB + requestBody: + $ref: '#/components/requestBodies/AnnotatedEmployeeBody' + responses: + '201': + description: created +components: + requestBodies: + # reusable request body: the annotation applies to every operation that $refs it + AnnotatedEmployeeBody: + x-field-extra-annotation: + - "@com.example.ReusableBodyValidation" + - "@com.example.ReusableAuditLogged" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + schemas: + Employee: + type: object + properties: + name: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml new file mode 100644 index 000000000000..82f149cd9c4e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml @@ -0,0 +1,2101 @@ +openapi: 3.0.0 +info: + description: 'This spec is mainly for testing Petstore server and contains fake + endpoints, models. Please do not use this for any other purpose. Special + characters: " \' + version: 1.0.0 + title: OpenAPI Petstore + license: + name: Apache-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + x-request-body-extra-annotation: "@org.springframework.lang.NonNull" + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + description: successful operation + "405": + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + put: + tags: + - pet + summary: Update an existing pet + description: "" + operationId: updatePet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + description: successful operation + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: false + x-field-extra-annotation: "@org.springframework.lang.NonNull" + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + explode: false + schema: + type: array + items: + type: string + uniqueItems: true + responses: + "200": + description: successful operation + content: + application/xml: + schema: + type: array + uniqueItems: true + items: + $ref: "#/components/schemas/Pet" + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid tag value + security: + - petstore_auth: + - write:pets + - read:pets + deprecated: true + "/pet/{petId}": + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + x-field-extra-annotation: "@org.springframework.lang.NonNull" + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/Pet" + application/json: + schema: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid ID supplied + "404": + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: "" + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + responses: + "405": + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet + description: "" + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + "400": + description: Invalid pet value + security: + - petstore_auth: + - write:pets + - read:pets + "/pet/{petId}/uploadImage": + post: + tags: + - pet + summary: uploads an image + description: "" + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiResponse" + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: "" + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: order placed for purchasing the pet + required: true + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/Order" + application/json: + schema: + $ref: "#/components/schemas/Order" + "400": + description: Invalid Order + "/store/order/{order_id}": + get: + tags: + - store + summary: Find purchase order by ID + description: For valid response try integer IDs with value <= 5 or > 10. Other values + will generate exceptions + operationId: getOrderById + parameters: + - name: order_id + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/Order" + application/json: + schema: + $ref: "#/components/schemas/Order" + "400": + description: Invalid ID supplied + "404": + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: order_id + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + responses: + "400": + description: Invalid ID supplied + "404": + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Created user object + required: true + responses: + default: + description: successful operation + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: "" + operationId: createUsersWithArrayInput + requestBody: + $ref: "#/components/requestBodies/UserArray" + responses: + default: + description: successful operation + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: "" + operationId: createUsersWithListInput + requestBody: + $ref: "#/components/requestBodies/UserArray" + responses: + default: + description: successful operation + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: "" + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: "" + operationId: logoutUser + responses: + default: + description: successful operation + "/user/{username}": + get: + tags: + - user + summary: Get user by user name + description: "" + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing. + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/User" + application/json: + schema: + $ref: "#/components/schemas/User" + "400": + description: Invalid username supplied + "404": + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Updated user object + required: true + responses: + "400": + description: Invalid user supplied + "404": + description: User not found + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "400": + description: Invalid username supplied + "404": + description: User not found + /fake_classname_test: + patch: + tags: + - fake_classname_tags 123#$%^ + summary: To test class name in snake case + description: To test class name in snake case + operationId: testClassname + requestBody: + $ref: "#/components/requestBodies/Client" + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + security: + - api_key_query: [] + /fake: + patch: + tags: + - fake + summary: To test "client" model + description: To test "client" model + operationId: testClientModel + requestBody: + $ref: "#/components/requestBodies/Client" + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + get: + tags: + - fake + summary: To test enum parameters + description: To test enum parameters + operationId: testEnumParameters + parameters: + - name: enum_header_string_array + in: header + description: Header parameter enum test (string array) + schema: + type: array + items: + type: string + default: $ + enum: + - ">" + - $ + - name: enum_header_string + in: header + description: Header parameter enum test (string) + schema: + type: string + enum: + - _abc + - -efg + - (xyz) + default: -efg + - name: enum_query_string_array + in: query + description: Query parameter enum test (string array) + schema: + type: array + items: + type: string + default: $ + enum: + - ">" + - $ + - name: enum_query_string + in: query + description: Query parameter enum test (string) + schema: + type: string + enum: + - _abc + - -efg + - (xyz) + default: -efg + - name: enum_query_integer + in: query + description: Query parameter enum test (double) + schema: + type: integer + format: int32 + enum: + - 1 + - -2 + - name: enum_query_double + in: query + description: Query parameter enum test (double) + schema: + type: number + format: double + enum: + - 1.1 + - -1.2 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + enum_form_string_array: + description: Form parameter enum test (string array) + type: array + items: + type: string + default: $ + enum: + - ">" + - $ + enum_form_string: + description: Form parameter enum test (string) + type: string + enum: + - _abc + - -efg + - (xyz) + default: -efg + responses: + "400": + description: Invalid request + "404": + description: Not found + post: + tags: + - fake + summary: |- + Fake endpoint for testing various parameters + 假端點 + 偽のエンドポイント + 가짜 엔드 포인트 + description: |- + Fake endpoint for testing various parameters + 假端點 + 偽のエンドポイント + 가짜 엔드 포인트 + operationId: testEndpointParameters + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + integer: + description: None + type: integer + minimum: 10 + maximum: 100 + int32: + description: None + type: integer + format: int32 + minimum: 20 + maximum: 200 + int64: + description: None + type: integer + format: int64 + number: + description: None + type: number + minimum: 32.1 + maximum: 543.2 + float: + description: None + type: number + format: float + maximum: 987.6 + double: + description: None + type: number + format: double + minimum: 67.8 + maximum: 123.4 + string: + description: None + type: string + pattern: "[a-zA-Z]" + pattern_without_delimiter: + description: None + type: string + pattern: ^[A-Z].* + byte: + description: None + type: string + format: byte + binary: + description: None + type: string + format: binary + date: + description: None + type: string + format: date + dateTime: + description: None + type: string + format: date-time + password: + description: None + type: string + format: password + minLength: 10 + maxLength: 64 + callback: + description: None + type: string + required: + - number + - double + - pattern_without_delimiter + - byte + responses: + "400": + description: Invalid username supplied + "404": + description: User not found + security: + - http_basic_test: [] + delete: + tags: + - fake + summary: Fake endpoint to test group parameters (optional) + description: Fake endpoint to test group parameters (optional) + operationId: testGroupParameters + x-group-parameters: true + parameters: + - name: required_string_group + in: query + description: Required String in group parameters + required: true + schema: + type: integer + - name: required_boolean_group + in: header + description: Required Boolean in group parameters + required: true + schema: + type: boolean + - name: required_int64_group + in: query + description: Required Integer in group parameters + required: true + schema: + type: integer + format: int64 + - name: string_group + in: query + description: String in group parameters + schema: + type: integer + - name: boolean_group + in: header + description: Boolean in group parameters + schema: + type: boolean + - name: int64_group + in: query + description: Integer in group parameters + schema: + type: integer + format: int64 + responses: + "400": + description: Something wrong + /fake/outer/number: + post: + tags: + - fake + description: Test serialization of outer number types + operationId: fakeOuterNumberSerialize + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OuterNumber" + description: Input number as post body + responses: + "200": + description: Output number + content: + "*/*": + schema: + $ref: "#/components/schemas/OuterNumber" + /fake/outer/string: + post: + tags: + - fake + description: Test serialization of outer string types + operationId: fakeOuterStringSerialize + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OuterString" + description: Input string as post body + responses: + "200": + description: Output string + content: + "*/*": + schema: + $ref: "#/components/schemas/OuterString" + /fake/outer/boolean: + post: + tags: + - fake + description: Test serialization of outer boolean types + operationId: fakeOuterBooleanSerialize + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OuterBoolean" + description: Input boolean as post body + responses: + "200": + description: Output boolean + content: + "*/*": + schema: + $ref: "#/components/schemas/OuterBoolean" + /fake/outer/composite: + post: + tags: + - fake + description: Test serialization of object with outer number type + operationId: fakeOuterCompositeSerialize + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OuterComposite" + description: Input composite as post body + responses: + "200": + description: Output composite + content: + "*/*": + schema: + $ref: "#/components/schemas/OuterComposite" + /fake/jsonFormData: + get: + tags: + - fake + summary: test json serialization of form data + description: "" + operationId: testJsonFormData + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + param: + description: field1 + type: string + param2: + description: field2 + type: string + required: + - param + - param2 + responses: + "200": + description: successful operation + /fake/inline-additionalProperties: + post: + tags: + - fake + summary: test inline additionalProperties + description: "" + operationId: testInlineAdditionalProperties + requestBody: + content: + application/json: + schema: + type: object + additionalProperties: + type: string + description: request body + required: true + responses: + "200": + description: successful operation + /fake/nullable: + post: + tags: + - fake + summary: test nullable parent property + description: "" + operationId: testNullable + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ChildWithNullable' + description: request body + required: true + responses: + "200": + description: successful operation + /fake/body-with-query-params: + put: + tags: + - fake + operationId: testBodyWithQueryParams + parameters: + - name: query + in: query + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + required: true + responses: + "200": + description: Success + /fake/create_xml_item: + post: + tags: + - fake + operationId: createXmlItem + summary: creates an XmlItem + description: this route creates an XmlItem + requestBody: + content: + application/xml: + schema: + $ref: "#/components/schemas/XmlItem" + application/xml; charset=utf-8: + schema: + $ref: "#/components/schemas/XmlItem" + application/xml; charset=utf-16: + schema: + $ref: "#/components/schemas/XmlItem" + text/xml: + schema: + $ref: "#/components/schemas/XmlItem" + text/xml; charset=utf-8: + schema: + $ref: "#/components/schemas/XmlItem" + text/xml; charset=utf-16: + schema: + $ref: "#/components/schemas/XmlItem" + description: XmlItem Body + required: true + responses: + "200": + description: successful operation + /another-fake/dummy: + patch: + tags: + - $another-fake? + summary: To test special tags + description: To test special tags and operation ID starting with number + operationId: 123_test_@#$%_special_tags + requestBody: + $ref: "#/components/requestBodies/Client" + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + /fake/body-with-file-schema: + put: + tags: + - fake + description: For this test, the body for this request much reference a schema named + `File`. + operationId: testBodyWithFileSchema + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/FileSchemaTestClass" + required: true + responses: + "200": + description: Success + /fake/response-with-example: + get: + tags: + - fake + description: This endpoint defines an example value for its response schema. + operationId: testWithResultExample + responses: + "200": + content: + application/json: + schema: + example: 42 + type: integer + description: Success + /fake/test-query-parameters: + put: + tags: + - fake + description: To test the collection format in query parameters + operationId: testQueryParameterCollectionFormat + parameters: + - name: pipe + in: query + required: true + schema: + type: array + items: + type: string + - name: http + in: query + required: true + style: spaceDelimited + schema: + type: array + items: + type: string + - name: url + in: query + required: true + style: form + explode: false + schema: + type: array + items: + type: string + - name: context + in: query + required: true + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: Success + "/fake/{petId}/uploadImageWithRequiredFile": + post: + tags: + - pet + summary: uploads an image (required) + description: "" + operationId: uploadFileWithRequiredFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + requiredFile: + description: file to upload + type: string + format: binary + required: + - requiredFile + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiResponse" + security: + - petstore_auth: + - write:pets + - read:pets + /fake/{petId}/response-object-different-names: + get: + tags: + - pet + operationId: responseObjectDifferentNames + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseObjectWithDifferentFieldNames" +servers: + - url: http://petstore.swagger.io:80/v2 +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + description: List of user object + required: true + Client: + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + description: client model + required: true + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + application/xml: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://petstore.swagger.io/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header + api_key_query: + type: apiKey + name: api_key_query + in: query + http_basic_test: + type: http + scheme: basic + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + type: object + required: + - name + properties: + id: + type: integer + format: int64 + name: + type: string + default: default-name + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + x-is-unique: true + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + x-is-unique: true + category: + $ref: "#/components/schemas/Category" + name: + type: string + example: doggie + photoUrls: + type: array + uniqueItems: true + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: "#/components/schemas/Tag" + status: + type: string + description: pet status in the store + deprecated: true + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + Return: + description: Model for testing reserved words + properties: + return: + type: integer + format: int32 + xml: + name: Return + Name: + description: Model for testing model name same as property name + required: + - name + properties: + name: + type: integer + format: int32 + snake_case: + readOnly: true + type: integer + format: int32 + property: + type: string + 123Number: + type: integer + readOnly: true + xml: + name: Name + 200_response: + description: Model for testing model name starting with number + properties: + name: + type: integer + format: int32 + class: + type: string + xml: + name: Name + ClassModel: + description: Model for testing model with "_class" property + properties: + _class: + type: string + Dog: + allOf: + - $ref: "#/components/schemas/Animal" + - type: object + properties: + breed: + type: string + Cat: + allOf: + - $ref: "#/components/schemas/Animal" + - type: object + properties: + declawed: + type: boolean + BigCat: + allOf: + - $ref: "#/components/schemas/Cat" + - type: object + properties: + kind: + type: string + enum: + - lions + - tigers + - leopards + - jaguars + Animal: + type: object + discriminator: + propertyName: className + required: + - className + properties: + className: + type: string + color: + type: string + default: red + AnimalFarm: + type: array + items: + $ref: "#/components/schemas/Animal" + format_test: + type: object + required: + - number + - byte + - date + - password + properties: + integer: + type: integer + maximum: 100 + minimum: 10 + int32: + type: integer + format: int32 + maximum: 200 + minimum: 20 + int64: + type: integer + format: int64 + number: + maximum: 543.2 + minimum: 32.1 + type: number + float: + type: number + format: float + maximum: 987.6 + minimum: 54.3 + double: + type: number + format: double + maximum: 123.4 + minimum: 67.8 + string: + type: string + pattern: "[a-zA-Z]" + byte: + type: string + format: byte + binary: + type: string + format: binary + date: + type: string + format: date + dateTime: + type: string + format: date-time + uuid: + type: string + format: uuid + maxLength: 36 + example: 72f98069-206d-4f12-9f12-3d1e525a8e84 + password: + type: string + format: password + maxLength: 64 + minLength: 10 + BigDecimal: + type: string + format: number + EnumClass: + type: string + default: -efg + enum: + - _abc + - -efg + - (xyz) + Enum_Test: + type: object + required: + - enum_string_required + properties: + enum_string: + type: string + enum: + - UPPER + - lower + - "" + enum_string_required: + type: string + enum: + - UPPER + - lower + - "" + enum_integer: + type: integer + format: int32 + enum: + - 1 + - -1 + enum_number: + type: number + format: double + enum: + - 1.1 + - -1.2 + outerEnum: + $ref: "#/components/schemas/OuterEnum" + AdditionalPropertiesClass: + type: object + properties: + map_string: + type: object + additionalProperties: + type: string + map_number: + type: object + additionalProperties: + type: number + map_integer: + type: object + additionalProperties: + type: integer + map_boolean: + type: object + additionalProperties: + type: boolean + map_array_integer: + type: object + additionalProperties: + type: array + items: + type: integer + map_array_anytype: + type: object + additionalProperties: + type: array + items: + type: object + map_map_string: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + map_map_anytype: + type: object + additionalProperties: + type: object + additionalProperties: + type: object + anytype_1: + type: object + anytype_2: {} + anytype_3: + type: object + properties: {} + AdditionalPropertiesString: + type: object + properties: + name: + type: string + additionalProperties: + type: string + AdditionalPropertiesInteger: + type: object + properties: + name: + type: string + additionalProperties: + type: integer + AdditionalPropertiesNumber: + type: object + properties: + name: + type: string + additionalProperties: + type: number + AdditionalPropertiesBoolean: + type: object + properties: + name: + type: string + additionalProperties: + type: boolean + AdditionalPropertiesArray: + type: object + properties: + name: + type: string + additionalProperties: + type: array + items: + type: object + AdditionalPropertiesObject: + type: object + properties: + name: + type: string + additionalProperties: + type: object + additionalProperties: + type: object + AdditionalPropertiesAnyType: + type: object + properties: + name: + type: string + additionalProperties: + type: object + MixedPropertiesAndAdditionalPropertiesClass: + type: object + properties: + uuid: + type: string + format: uuid + dateTime: + type: string + format: date-time + map: + type: object + additionalProperties: + $ref: "#/components/schemas/Animal" + List: + type: object + properties: + 123-list: + type: string + Client: + type: object + properties: + client: + type: string + ReadOnlyFirst: + type: object + properties: + bar: + type: string + readOnly: true + baz: + type: string + hasOnlyReadOnly: + type: object + properties: + bar: + type: string + readOnly: true + foo: + type: string + readOnly: true + Capitalization: + type: object + properties: + smallCamel: + type: string + CapitalCamel: + type: string + small_Snake: + type: string + Capital_Snake: + type: string + SCA_ETH_Flow_Points: + type: string + ATT_NAME: + description: | + Name of the pet + type: string + MapTest: + type: object + properties: + map_map_of_string: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + map_of_enum_string: + type: object + additionalProperties: + type: string + enum: + - UPPER + - lower + direct_map: + type: object + additionalProperties: + type: boolean + indirect_map: + $ref: "#/components/schemas/StringBooleanMap" + ArrayTest: + type: object + properties: + array_of_string: + type: array + items: + type: string + array_array_of_integer: + type: array + items: + type: array + items: + type: integer + format: int64 + array_array_of_model: + type: array + items: + type: array + items: + $ref: "#/components/schemas/ReadOnlyFirst" + NumberOnly: + type: object + properties: + JustNumber: + type: number + ArrayOfNumberOnly: + type: object + properties: + ArrayNumber: + type: array + items: + type: number + ArrayOfArrayOfNumberOnly: + type: object + properties: + ArrayArrayNumber: + type: array + items: + type: array + items: + type: number + EnumArrays: + type: object + properties: + just_symbol: + type: string + enum: + - ">=" + - $ + array_enum: + type: array + items: + type: string + enum: + - fish + - crab + OuterEnum: + type: string + enum: + - placed + - approved + - delivered + OuterComposite: + type: object + properties: + my_number: + $ref: "#/components/schemas/OuterNumber" + my_string: + $ref: "#/components/schemas/OuterString" + my_boolean: + $ref: "#/components/schemas/OuterBoolean" + OuterNumber: + type: number + OuterString: + type: string + OuterBoolean: + type: boolean + x-codegen-body-parameter-name: boolean_post_body + ParentWithNullable: + type: object + discriminator: + propertyName: type + properties: + type: + type: string + enum: + - ChildWithNullable + nullableProperty: + type: string + nullable: true + ChildWithNullable: + allOf: + - $ref: '#/components/schemas/ParentWithNullable' + - type: object + properties: + otherProperty: + type: string + StringBooleanMap: + additionalProperties: + type: boolean + FileSchemaTestClass: + type: object + properties: + file: + $ref: "#/components/schemas/File" + files: + type: array + items: + $ref: "#/components/schemas/File" + File: + type: object + description: Must be named `File` for test. + properties: + sourceURI: + description: Test capitalization + type: string + TypeHolderDefault: + type: object + required: + - string_item + - number_item + - integer_item + - bool_item + - array_item + properties: + string_item: + type: string + default: what + number_item: + type: number + default: 1.234 + integer_item: + type: integer + default: -2 + bool_item: + type: boolean + default: true + array_item: + type: array + items: + type: integer + default: + - 0 + - 1 + - 2 + - 3 + TypeHolderExample: + type: object + required: + - string_item + - number_item + - float_item + - integer_item + - bool_item + - array_item + properties: + string_item: + type: string + example: what + number_item: + type: number + example: 1.234 + float_item: + type: number + example: 1.234 + format: float + integer_item: + type: integer + example: -2 + bool_item: + type: boolean + example: true + array_item: + type: array + items: + type: integer + example: + - 0 + - 1 + - 2 + - 3 + XmlItem: + type: object + xml: + namespace: http://a.com/schema + prefix: pre + properties: + attribute_string: + type: string + example: string + xml: + attribute: true + attribute_number: + type: number + example: 1.234 + xml: + attribute: true + attribute_integer: + type: integer + example: -2 + xml: + attribute: true + attribute_boolean: + type: boolean + example: true + xml: + attribute: true + wrapped_array: + type: array + xml: + wrapped: true + items: + type: integer + name_string: + type: string + example: string + xml: + name: xml_name_string + name_number: + type: number + example: 1.234 + xml: + name: xml_name_number + name_integer: + type: integer + example: -2 + xml: + name: xml_name_integer + name_boolean: + type: boolean + example: true + xml: + name: xml_name_boolean + name_array: + type: array + items: + type: integer + xml: + name: xml_name_array_item + name_wrapped_array: + type: array + xml: + wrapped: true + name: xml_name_wrapped_array + items: + type: integer + xml: + name: xml_name_wrapped_array_item + prefix_string: + type: string + example: string + xml: + prefix: ab + prefix_number: + type: number + example: 1.234 + xml: + prefix: cd + prefix_integer: + type: integer + example: -2 + xml: + prefix: ef + prefix_boolean: + type: boolean + example: true + xml: + prefix: gh + prefix_array: + type: array + items: + type: integer + xml: + prefix: ij + prefix_wrapped_array: + type: array + xml: + wrapped: true + prefix: kl + items: + type: integer + xml: + prefix: mn + namespace_string: + type: string + example: string + xml: + namespace: http://a.com/schema + namespace_number: + type: number + example: 1.234 + xml: + namespace: http://b.com/schema + namespace_integer: + type: integer + example: -2 + xml: + namespace: http://c.com/schema + namespace_boolean: + type: boolean + example: true + xml: + namespace: http://d.com/schema + namespace_array: + type: array + items: + type: integer + xml: + namespace: http://e.com/schema + namespace_wrapped_array: + type: array + xml: + wrapped: true + namespace: http://f.com/schema + items: + type: integer + xml: + namespace: http://g.com/schema + prefix_ns_string: + type: string + example: string + xml: + namespace: http://a.com/schema + prefix: a + prefix_ns_number: + type: number + example: 1.234 + xml: + namespace: http://b.com/schema + prefix: b + prefix_ns_integer: + type: integer + example: -2 + xml: + namespace: http://c.com/schema + prefix: c + prefix_ns_boolean: + type: boolean + example: true + xml: + namespace: http://d.com/schema + prefix: d + prefix_ns_array: + type: array + items: + type: integer + xml: + namespace: http://e.com/schema + prefix: e + prefix_ns_wrapped_array: + type: array + xml: + wrapped: true + namespace: http://f.com/schema + prefix: f + items: + type: integer + xml: + namespace: http://g.com/schema + prefix: g + _special_model.name_: + properties: + "$special[property.name]": + type: integer + format: int64 + xml: + name: $special[model.name] + ContainerDefaultValue: + type: object + required: + - required_array + - nullable_required_array + properties: + nullable_array: + type: array + nullable: true + items: + type: string + nullable_required_array: + type: array + nullable: true + items: + type: string + required_array: + type: array + nullable: false + items: + type: string + nullable_array_with_default: + type: array + nullable: true + items: + type: string + default: ["foo", "bar"] + ResponseObjectWithDifferentFieldNames: + type: object + properties: + normalPropertyName: + type: string + UPPER_CASE_PROPERTY_SNAKE: + type: string + lower-case-property-dashes: + type: string + property name with spaces: + type: string + NullableMapProperty: + type: object + properties: + languageValues: + nullable: true + type: object + additionalProperties: + type: string diff --git a/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt index 8acb5303e965..e94244bcd9da 100644 --- a/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt +++ b/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt @@ -7,6 +7,8 @@ package org.openapitools.api import org.openapitools.model.ModelApiResponse import org.openapitools.model.Pet +import org.springframework.lang.NonNull +import org.springframework.lang.Nullable import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* @@ -59,7 +61,7 @@ interface PetApi { consumes = ["application/json", "application/xml"] ) fun addPet( - @Parameter(description = "Pet object that needs to be added to the store", required = true) @Valid @RequestBody pet: Pet + @Parameter(description = "Pet object that needs to be added to the store", required = true) @NonNull @Valid @RequestBody pet: Pet ): ResponseEntity { return getDelegate().addPet(pet) } @@ -104,7 +106,7 @@ interface PetApi { produces = ["application/xml", "application/json"] ) fun findPetsByStatus( - @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List + @NonNull @Nullable @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List ): ResponseEntity> { return getDelegate().findPetsByStatus(status) } @@ -152,7 +154,7 @@ interface PetApi { produces = ["application/xml", "application/json"] ) fun getPetById( - @Parameter(description = "ID of pet to return", required = true) @PathVariable("petId") petId: kotlin.Long + @NonNull @Parameter(description = "ID of pet to return", required = true) @PathVariable("petId") petId: kotlin.Long ): ResponseEntity { return getDelegate().getPetById(petId) } @@ -227,7 +229,7 @@ interface PetApi { fun uploadFile( @Parameter(description = "ID of pet to update", required = true) @PathVariable("petId") petId: kotlin.Long, @Parameter(description = "Additional data to pass to server") @Valid @RequestParam(value = "additionalMetadata", required = false) additionalMetadata: kotlin.String?, - @Parameter(description = "file to upload") @Valid @RequestPart("file", required = false) file: org.springframework.web.multipart.MultipartFile + @NonNull @Parameter(description = "file to upload") @Valid @RequestPart("file", required = false) file: org.springframework.web.multipart.MultipartFile ): ResponseEntity { return getDelegate().uploadFile(petId, additionalMetadata, file) } diff --git a/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/StoreApi.kt b/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/StoreApi.kt index 485509730aff..9d821b638239 100644 --- a/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/StoreApi.kt +++ b/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/StoreApi.kt @@ -6,6 +6,7 @@ package org.openapitools.api import org.openapitools.model.Order +import org.springframework.lang.NonNull import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* @@ -98,7 +99,7 @@ interface StoreApi { produces = ["application/xml", "application/json"] ) fun getOrderById( - @Min(value=1L) @Max(value=5L) @Parameter(description = "ID of pet that needs to be fetched", required = true) @PathVariable("orderId") orderId: kotlin.Long + @NonNull @Min(value=1L) @Max(value=5L) @Parameter(description = "ID of pet that needs to be fetched", required = true) @PathVariable("orderId") orderId: kotlin.Long ): ResponseEntity { return getDelegate().getOrderById(orderId) } @@ -121,7 +122,7 @@ interface StoreApi { consumes = ["application/json"] ) fun placeOrder( - @Parameter(description = "order placed for purchasing the pet", required = true) @Valid @RequestBody order: Order + @Parameter(description = "order placed for purchasing the pet", required = true) @NonNull @Valid @RequestBody order: Order ): ResponseEntity { return getDelegate().placeOrder(order) } diff --git a/samples/server/petstore/kotlin-springboot-delegate/src/main/resources/openapi.yaml b/samples/server/petstore/kotlin-springboot-delegate/src/main/resources/openapi.yaml index 885b64671054..218e008f9bad 100644 --- a/samples/server/petstore/kotlin-springboot-delegate/src/main/resources/openapi.yaml +++ b/samples/server/petstore/kotlin-springboot-delegate/src/main/resources/openapi.yaml @@ -45,6 +45,8 @@ paths: summary: Add a new pet to the store tags: - pet + x-extra-imports: org.springframework.lang.NonNull + x-request-body-extra-annotation: '@NonNull' put: description: "" externalDocs: @@ -97,6 +99,12 @@ paths: type: string type: array style: form + x-extra-imports: + - org.springframework.lang.NonNull + - org.springframework.lang.Nullable + x-field-extra-annotation: + - '@NonNull' + - '@Nullable' responses: "200": content: @@ -202,6 +210,8 @@ paths: format: int64 type: integer style: simple + x-extra-imports: org.springframework.lang.NonNull + x-field-extra-annotation: '@NonNull' responses: "200": content: @@ -795,6 +805,8 @@ components: description: file to upload format: binary type: string + x-extra-imports: org.springframework.lang.NonNull + x-field-extra-annotation: '@NonNull' type: object securitySchemes: petstore_auth: diff --git a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiController.kt b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiController.kt index 9e04cc7c297a..aa54d9ceecb3 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiController.kt +++ b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiController.kt @@ -2,6 +2,8 @@ package org.openapitools.api import org.openapitools.model.ModelApiResponse import org.openapitools.model.Pet +import org.springframework.lang.NonNull +import org.springframework.lang.Nullable import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* @@ -52,7 +54,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) { consumes = ["application/json", "application/xml"] ) suspend fun addPet( - @Parameter(description = "Pet object that needs to be added to the store", required = true) @Valid @RequestBody pet: Pet + @Parameter(description = "Pet object that needs to be added to the store", required = true) @NonNull @Valid @RequestBody pet: Pet ): ResponseEntity { return ResponseEntity(service.addPet(pet), HttpStatus.valueOf(200)) } @@ -93,7 +95,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) { produces = ["application/xml", "application/json"] ) fun findPetsByStatus( - @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List + @NonNull @Nullable @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List ): ResponseEntity> { return ResponseEntity(service.findPetsByStatus(status), HttpStatus.valueOf(200)) } @@ -137,7 +139,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) { produces = ["application/xml", "application/json"] ) suspend fun getPetById( - @Parameter(description = "ID of pet to return", required = true) @PathVariable("petId") petId: kotlin.Long + @NonNull @Parameter(description = "ID of pet to return", required = true) @PathVariable("petId") petId: kotlin.Long ): ResponseEntity { return ResponseEntity(service.getPetById(petId), HttpStatus.valueOf(200)) } @@ -206,7 +208,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) { suspend fun uploadFile( @Parameter(description = "ID of pet to update", required = true) @PathVariable("petId") petId: kotlin.Long, @Parameter(description = "Additional data to pass to server") @Valid @RequestParam(value = "additionalMetadata", required = false) additionalMetadata: kotlin.String?, - @Parameter(description = "file to upload") @Valid @RequestPart("file", required = false) file: org.springframework.http.codec.multipart.Part? + @NonNull @Parameter(description = "file to upload") @Valid @RequestPart("file", required = false) file: org.springframework.http.codec.multipart.Part? ): ResponseEntity { return ResponseEntity(service.uploadFile(petId, additionalMetadata, file), HttpStatus.valueOf(200)) } diff --git a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/StoreApiController.kt b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/StoreApiController.kt index 03750f1b7577..f156825366fe 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/StoreApiController.kt +++ b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/StoreApiController.kt @@ -1,6 +1,7 @@ package org.openapitools.api import org.openapitools.model.Order +import org.springframework.lang.NonNull import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* @@ -87,7 +88,7 @@ class StoreApiController(@Autowired(required = true) val service: StoreApiServic produces = ["application/xml", "application/json"] ) suspend fun getOrderById( - @Min(value=1L) @Max(value=5L) @Parameter(description = "ID of pet that needs to be fetched", required = true) @PathVariable("orderId") orderId: kotlin.Long + @NonNull @Min(value=1L) @Max(value=5L) @Parameter(description = "ID of pet that needs to be fetched", required = true) @PathVariable("orderId") orderId: kotlin.Long ): ResponseEntity { return ResponseEntity(service.getOrderById(orderId), HttpStatus.valueOf(200)) } @@ -108,7 +109,7 @@ class StoreApiController(@Autowired(required = true) val service: StoreApiServic consumes = ["application/json"] ) suspend fun placeOrder( - @Parameter(description = "order placed for purchasing the pet", required = true) @Valid @RequestBody order: Order + @Parameter(description = "order placed for purchasing the pet", required = true) @NonNull @Valid @RequestBody order: Order ): ResponseEntity { return ResponseEntity(service.placeOrder(order), HttpStatus.valueOf(200)) } diff --git a/samples/server/petstore/kotlin-springboot-reactive/src/main/resources/openapi.yaml b/samples/server/petstore/kotlin-springboot-reactive/src/main/resources/openapi.yaml index 885b64671054..218e008f9bad 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/src/main/resources/openapi.yaml +++ b/samples/server/petstore/kotlin-springboot-reactive/src/main/resources/openapi.yaml @@ -45,6 +45,8 @@ paths: summary: Add a new pet to the store tags: - pet + x-extra-imports: org.springframework.lang.NonNull + x-request-body-extra-annotation: '@NonNull' put: description: "" externalDocs: @@ -97,6 +99,12 @@ paths: type: string type: array style: form + x-extra-imports: + - org.springframework.lang.NonNull + - org.springframework.lang.Nullable + x-field-extra-annotation: + - '@NonNull' + - '@Nullable' responses: "200": content: @@ -202,6 +210,8 @@ paths: format: int64 type: integer style: simple + x-extra-imports: org.springframework.lang.NonNull + x-field-extra-annotation: '@NonNull' responses: "200": content: @@ -795,6 +805,8 @@ components: description: file to upload format: binary type: string + x-extra-imports: org.springframework.lang.NonNull + x-field-extra-annotation: '@NonNull' type: object securitySchemes: petstore_auth: diff --git a/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/PetApi.java index fb35140489a7..e78bdca9637d 100644 --- a/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/PetApi.java +++ b/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/PetApi.java @@ -73,7 +73,7 @@ default PetApiDelegate getDelegate() { consumes = { "application/json", "application/xml" } ) default Mono> addPet( - @Parameter(name = "Pet", description = "Pet object that needs to be added to the store", required = true) @Valid @RequestBody Mono pet, + @Parameter(name = "Pet", description = "Pet object that needs to be added to the store", required = true) @org.springframework.lang.NonNull @Valid @RequestBody Mono pet, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().addPet(pet, exchange); @@ -147,7 +147,7 @@ default Mono> deletePet( produces = { "application/xml", "application/json" } ) default Mono>> findPetsByStatus( - @NotNull @Parameter(name = "status", description = "Status values that need to be considered for filter", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "status", required = true) List status, + @org.springframework.lang.NonNull @NotNull @Parameter(name = "status", description = "Status values that need to be considered for filter", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "status", required = true) List status, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().findPetsByStatus(status, exchange); @@ -228,7 +228,7 @@ default Mono>> findPetsByTags( produces = { "application/xml", "application/json" } ) default Mono> getPetById( - @Parameter(name = "petId", description = "ID of pet to return", required = true, in = ParameterIn.PATH) @PathVariable("petId") Long petId, + @org.springframework.lang.NonNull @Parameter(name = "petId", description = "ID of pet to return", required = true, in = ParameterIn.PATH) @PathVariable("petId") Long petId, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().getPetById(petId, exchange); diff --git a/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/StoreApi.java b/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/StoreApi.java index 2c52ce3dddf3..08ef69a3df87 100644 --- a/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/StoreApi.java +++ b/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/StoreApi.java @@ -137,7 +137,7 @@ default Mono>> getInventory( produces = { "application/xml", "application/json" } ) default Mono> getOrderById( - @Min(value = 1L) @Max(value = 5L) @Parameter(name = "order_id", description = "ID of pet that needs to be fetched", required = true, in = ParameterIn.PATH) @PathVariable("order_id") Long orderId, + @org.springframework.lang.NonNull @Min(value = 1L) @Max(value = 5L) @Parameter(name = "order_id", description = "ID of pet that needs to be fetched", required = true, in = ParameterIn.PATH) @PathVariable("order_id") Long orderId, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().getOrderById(orderId, exchange); @@ -173,7 +173,7 @@ default Mono> getOrderById( consumes = { "application/json" } ) default Mono> placeOrder( - @Parameter(name = "Order", description = "order placed for purchasing the pet", required = true) @Valid @RequestBody Mono order, + @Parameter(name = "Order", description = "order placed for purchasing the pet", required = true) @org.springframework.lang.NonNull @Valid @RequestBody Mono order, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().placeOrder(order, exchange); diff --git a/samples/server/petstore/springboot-reactive/src/main/resources/openapi.yaml b/samples/server/petstore/springboot-reactive/src/main/resources/openapi.yaml index 53ec8083c77d..28da4d940572 100644 --- a/samples/server/petstore/springboot-reactive/src/main/resources/openapi.yaml +++ b/samples/server/petstore/springboot-reactive/src/main/resources/openapi.yaml @@ -36,6 +36,7 @@ paths: summary: Add a new pet to the store tags: - pet + x-request-body-extra-annotation: '@org.springframework.lang.NonNull' x-content-type: application/json x-accepts: - application/json @@ -87,6 +88,7 @@ paths: type: string type: array style: form + x-field-extra-annotation: '@org.springframework.lang.NonNull' responses: "200": content: @@ -213,6 +215,7 @@ paths: format: int64 type: integer style: simple + x-field-extra-annotation: '@org.springframework.lang.NonNull' responses: "200": content: diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/PetApi.java index 433ff63e9a9f..cf708161f613 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/PetApi.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/PetApi.java @@ -73,7 +73,7 @@ default Optional getRequest() { consumes = { "application/json", "application/xml" } ) default ResponseEntity addPet( - @Parameter(name = "Pet", description = "Pet object that needs to be added to the store", required = true) @Valid @RequestBody Pet pet + @Parameter(name = "Pet", description = "Pet object that needs to be added to the store", required = true) @org.springframework.lang.NonNull @Valid @RequestBody Pet pet ) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); @@ -147,7 +147,7 @@ default ResponseEntity deletePet( produces = { "application/xml", "application/json" } ) default ResponseEntity> findPetsByStatus( - @NotNull @Parameter(name = "status", description = "Status values that need to be considered for filter", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "status", required = true) List status + @org.springframework.lang.NonNull @NotNull @Parameter(name = "status", description = "Status values that need to be considered for filter", required = true, in = ParameterIn.QUERY) @Valid @RequestParam(value = "status", required = true) List status ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { @@ -256,7 +256,7 @@ default ResponseEntity> findPetsByTags( produces = { "application/xml", "application/json" } ) default ResponseEntity getPetById( - @Parameter(name = "petId", description = "ID of pet to return", required = true, in = ParameterIn.PATH) @PathVariable("petId") Long petId + @org.springframework.lang.NonNull @Parameter(name = "petId", description = "ID of pet to return", required = true, in = ParameterIn.PATH) @PathVariable("petId") Long petId ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/StoreApi.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/StoreApi.java index 0813a3064040..f394ffcaf309 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/StoreApi.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/StoreApi.java @@ -138,7 +138,7 @@ default ResponseEntity> getInventory( produces = { "application/xml", "application/json" } ) default ResponseEntity getOrderById( - @Min(value = 1L) @Max(value = 5L) @Parameter(name = "order_id", description = "ID of pet that needs to be fetched", required = true, in = ParameterIn.PATH) @PathVariable("order_id") Long orderId + @org.springframework.lang.NonNull @Min(value = 1L) @Max(value = 5L) @Parameter(name = "order_id", description = "ID of pet that needs to be fetched", required = true, in = ParameterIn.PATH) @PathVariable("order_id") Long orderId ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { @@ -188,7 +188,7 @@ default ResponseEntity getOrderById( consumes = { "application/json" } ) default ResponseEntity placeOrder( - @Parameter(name = "Order", description = "order placed for purchasing the pet", required = true) @Valid @RequestBody Order order + @Parameter(name = "Order", description = "order placed for purchasing the pet", required = true) @org.springframework.lang.NonNull @Valid @RequestBody Order order ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { diff --git a/samples/server/petstore/springboot-useoptional/src/main/resources/openapi.yaml b/samples/server/petstore/springboot-useoptional/src/main/resources/openapi.yaml index 53ec8083c77d..28da4d940572 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/resources/openapi.yaml +++ b/samples/server/petstore/springboot-useoptional/src/main/resources/openapi.yaml @@ -36,6 +36,7 @@ paths: summary: Add a new pet to the store tags: - pet + x-request-body-extra-annotation: '@org.springframework.lang.NonNull' x-content-type: application/json x-accepts: - application/json @@ -87,6 +88,7 @@ paths: type: string type: array style: form + x-field-extra-annotation: '@org.springframework.lang.NonNull' responses: "200": content: @@ -213,6 +215,7 @@ paths: format: int64 type: integer style: simple + x-field-extra-annotation: '@org.springframework.lang.NonNull' responses: "200": content: