From 35bb92c5df99eefc5fd5d3f3b59c247d27a784e5 Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Tue, 15 Jul 2025 13:06:21 -0700 Subject: [PATCH 01/11] [kotlin-spring] remove file type specific templating in optionalDataType.mustache --- .../src/main/resources/kotlin-spring/optionalDataType.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/optionalDataType.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/optionalDataType.mustache index a6b059b8632a..ed2a4468e9d3 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/optionalDataType.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/optionalDataType.mustache @@ -1 +1 @@ -{{^isFile}}{{{dataType}}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isFile}}{{#isFile}}{{#isArray}}Array<{{/isArray}}org.springframework.web.multipart.MultipartFile{{#isArray}}>{{/isArray}}{{^isArray}}{{^required}}?{{/required}}{{/isArray}}{{/isFile}} \ No newline at end of file +{{{dataType}}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}} \ No newline at end of file From 8fe8490a719b86e3a17d1884ec95972c777df310 Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Wed, 16 Jul 2025 09:18:53 -0700 Subject: [PATCH 02/11] [kotlin-spring] move non-reactive file param template into its own partial template file. --- .../main/resources/kotlin-spring/nonReactiveFileParam.mustache | 1 + 1 file changed, 1 insertion(+) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-spring/nonReactiveFileParam.mustache diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/nonReactiveFileParam.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/nonReactiveFileParam.mustache new file mode 100644 index 000000000000..454b6e6ac4a3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/nonReactiveFileParam.mustache @@ -0,0 +1 @@ +{{^reactive}}{{#isFile}}{{#isArray}}Array<{{/isArray}}org.springframework.web.multipart.MultipartFile{{#isArray}}>{{/isArray}}{{^required}}?{{/required}}{{/isFile}}{{/reactive}} \ No newline at end of file From 9665ea91b870a5588a91d645e042c21d864d7c24 Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Wed, 16 Jul 2025 09:20:03 -0700 Subject: [PATCH 03/11] [kotlin-spring] reference non-reactive fil param template in delegate, formParams & service templates --- .../src/main/resources/kotlin-spring/apiDelegate.mustache | 2 +- .../src/main/resources/kotlin-spring/formParams.mustache | 2 +- .../src/main/resources/kotlin-spring/service.mustache | 2 +- .../src/main/resources/kotlin-spring/serviceImpl.mustache | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache index cb76cbcc7dda..d9b5e3297e44 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache @@ -33,7 +33,7 @@ interface {{classname}}Delegate { /** * @see {{classname}}#{{operationId}} */ - {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{^-last}}, + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{>nonReactiveFileParam}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{^-last}}, {{/-last}}{{/allParams}}): {{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}}{{^skipDefaultDelegateInterface}} { {{>methodBody}}{{! prevent indent}} }{{/skipDefaultDelegateInterface}} 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 c6eb0799686a..fab1cb916f49 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}}{{#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}}}: {{>nonReactiveFileParam}}{{#reactive}}{{>optionalDataType}}{{/reactive}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache index 309be55743b4..b6439150538d 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache @@ -30,7 +30,7 @@ interface {{classname}}Service { {{/externalDocs}} * @see {{classname}}#{{operationId}} */ - {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{^reactive}}{{>nonReactiveFileParam}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/reactive}}{{#reactive}}{{>optionalDataType}}{{/reactive}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} {{/operation}} } {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache index 181bfa52991c..1872b59a7318 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service class {{classname}}ServiceImpl : {{classname}}Service { {{#operation}} - override {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} { + override {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{^reactive}}{{>nonReactiveFileParam}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/reactive}}{{#reactive}}{{>optionalDataType}}{{/reactive}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} { TODO("Implement me") } {{/operation}} From 2c296feb455b99b36b5d217530cce138aec8da5c Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Wed, 16 Jul 2025 09:20:36 -0700 Subject: [PATCH 04/11] [kotlin-spring] fix spec errors in petstore-with-tags.yaml --- .../resources/3_0/kotlin/petstore-with-tags.yaml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-tags.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-tags.yaml index 062ab023e2d0..f8eef9225bd6 100644 --- a/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-tags.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-tags.yaml @@ -267,16 +267,15 @@ paths: content: multipart/form-data: schema: + type: object properties: additionalMetadata: - type: string - description: Additional data to pass to server - required: false + type: string + description: Additional data to pass to server image: - type: string - description: image to upload - format: binary - required: true + type: string + description: image to upload + format: binary responses: 200: description: successful operation @@ -306,18 +305,17 @@ paths: content: multipart/form-data: schema: + type: object properties: additionalMetadata: type: string description: Additional data to pass to server - required: false images: type: array items: type: string description: image to upload format: binary - required: true responses: 200: description: successful operation From ffd69fed7a8d541bb2dca226f5f6712abf9e5772 Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Wed, 16 Jul 2025 09:21:20 -0700 Subject: [PATCH 05/11] [kotlin-spring] add form params specific unit test cribbed from SpringServerCodegenTest.java --- .../spring/KotlinSpringServerCodegenTest.java | 97 +++++++++++++++++-- 1 file changed, 90 insertions(+), 7 deletions(-) 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 5a0d24cdcf1a..3aa6dfa8c6d5 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 @@ -5,8 +5,10 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.parser.core.models.ParseOptions; +import java.util.Arrays; import java.util.HashMap; import java.util.function.Consumer; +import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.assertj.core.api.Assertions; import org.jetbrains.annotations.NotNull; @@ -789,7 +791,7 @@ public void contractWithResolvedInnerEnumContainsEnumConverter() throws IOExcept } @Test - public void givenMultipartFormArray_whenGenerateDelegateAndService_thenParameterIsCreatedAsListOfMultipartFile() throws IOException { + public void givenNonRequiredMultipartFileArray_whenGenerateDelegateAndService_thenParameterIsCreatedAsNullableListOfMultipartFile() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); output.deleteOnExit(); String outputPath = output.getAbsolutePath().replace('\\', '/'); @@ -799,7 +801,8 @@ public void givenMultipartFormArray_whenGenerateDelegateAndService_thenParameter codegen.setOpenAPI(openAPI); codegen.setOutputDir(output.getAbsolutePath()); codegen.setDelegatePattern(true); - codegen.setServiceInterface(true); + // this will generate the service interface & implementation files + codegen.setServiceImplementation(true); ClientOptInput input = new ClientOptInput(); input.openAPI(openAPI); @@ -817,14 +820,94 @@ public void givenMultipartFormArray_whenGenerateDelegateAndService_thenParameter Path delegateFile = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt"); assertFileContains(delegateFile, "additionalMetadata: kotlin.String?"); - assertFileContains(delegateFile, "images: Array"); + assertFileContains(delegateFile, "images: Array?)"); Path controllerFile = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApi.kt"); - assertFileContains(controllerFile, "images: Array"); - + assertFileContains(controllerFile, "additionalMetadata: kotlin.String?"); + assertFileContains(controllerFile, "images: Array?)"); Path serviceFile = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApiService.kt"); - assertFileContains(serviceFile, "images: Array"); + assertFileContains(serviceFile, "additionalMetadata: kotlin.String?"); + assertFileContains(serviceFile, "images: Array?)"); + + Path serviceImplFile = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt"); + assertFileContains(serviceImplFile, "additionalMetadata: kotlin.String?"); + assertFileContains(serviceImplFile, "images: Array?)"); + } + + @Test + public void givenMultipartBinaryArray_whenGenerateDelegateAndService_correctMultipartFileIsCreated() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/form-multipart-binary-array.yaml"); + final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setDelegatePattern(true); + // this will generate the service interface & implementation files + codegen.setServiceImplementation(true); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + + generator.opts(input).generate(); + + validateMultipartFiles( + outputPath + "/src/main/kotlin/org/openapitools/api/MultipartArray", + "files: Array?)" + ); + + validateMultipartFiles( + outputPath + "/src/main/kotlin/org/openapitools/api/MultipartMixed", + "file: org.springframework.web.multipart.MultipartFile,", + "marker: MultipartMixedRequestMarker?", + "statusArray: kotlin.collections.List?" + ); + + validateMultipartFiles( + outputPath + "/src/main/kotlin/org/openapitools/api/MultipartSingle", + "file: org.springframework.web.multipart.MultipartFile?" + ); + } + + private void validateMultipartFiles( + String filePrefix, + String... lines + ) { + Stream.of( + filePrefix + "ApiDelegate.kt", + filePrefix + "ApiService.kt", + filePrefix + "ApiServiceImpl.kt", + filePrefix + "Api.kt" + ) + .map(Paths::get) + .forEach(path -> { + try { + validateMultipartFile(path, lines); + } catch (AssertionError e) { + throw new AssertionError(path.toString() + " does not contain a line", e); + } + }); + } + + private void validateMultipartFile( + Path filePath, + String... lines + ) { + for(String line : lines) { + assertFileContains(filePath, line); + } } @Test @@ -887,7 +970,7 @@ public void givenMultipartForm_whenGenerateReactiveServer_thenParameterAreCreate assertFileContains(outputFilepath, "@Parameter(description = \"Additional data to pass to server\") @Valid @RequestParam(value = \"additionalMetadata\", required = false) additionalMetadata: kotlin.String?"); assertFileContains(outputFilepath, - "@Parameter(description = \"image to upload\") @Valid @RequestPart(\"image\", required = false) image: org.springframework.web.multipart.MultipartFile"); + "@Parameter(description = \"image to upload\") @Valid @RequestPart(\"image\", required = false) image: org.springframework.web.multipart.MultipartFile?)"); } From 6449fd6d2312a7e725d5a35048c1892f5c2bf242 Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Wed, 16 Jul 2025 14:03:46 -0700 Subject: [PATCH 06/11] [kotlin-spring] add file parameter template which supports reactive files --- .../kotlin-spring/apiDelegate.mustache | 3 +- .../kotlin-spring/fileParamType.mustache | 1 + .../kotlin-spring/formParams.mustache | 2 +- .../nonReactiveFileParam.mustache | 1 - .../resources/kotlin-spring/service.mustache | 2 +- .../kotlin-spring/serviceImpl.mustache | 2 +- .../spring/KotlinSpringServerCodegenTest.java | 47 +++++++++++++++++++ 7 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-spring/fileParamType.mustache delete mode 100644 modules/openapi-generator/src/main/resources/kotlin-spring/nonReactiveFileParam.mustache diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache index d9b5e3297e44..730c1dd34bec 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache @@ -33,8 +33,7 @@ interface {{classname}}Delegate { /** * @see {{classname}}#{{operationId}} */ - {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{>nonReactiveFileParam}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{^-last}}, - {{/-last}}{{/allParams}}): {{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}}{{^skipDefaultDelegateInterface}} { + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/reactive}}{{#reactive}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/isFile}}{{/reactive}}{{^-last}}, {{/-last}}{{/allParams}}): {{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}}{{^skipDefaultDelegateInterface}} { {{>methodBody}}{{! prevent indent}} }{{/skipDefaultDelegateInterface}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/fileParamType.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/fileParamType.mustache new file mode 100644 index 000000000000..a6daa389e383 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/fileParamType.mustache @@ -0,0 +1 @@ +{{#isFile}}{{#reactive}}{{#isArray}}Flux<{{/isArray}}org.springframework.http.codec.multipart.Part{{#isArray}}>{{/isArray}}{{^required}}?{{/required}}{{/reactive}}{{^reactive}}{{#isArray}}Array<{{/isArray}}org.springframework.web.multipart.MultipartFile{{#isArray}}>{{/isArray}}{{^required}}?{{/required}}{{/reactive}}{{/isFile}} \ 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 fab1cb916f49..b5d333863f45 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}}}: {{>nonReactiveFileParam}}{{#reactive}}{{>optionalDataType}}{{/reactive}}{{/isFile}}{{/isFormParam}} \ No newline at end of file +{{#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}}}: {{>fileParamType}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/nonReactiveFileParam.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/nonReactiveFileParam.mustache deleted file mode 100644 index 454b6e6ac4a3..000000000000 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/nonReactiveFileParam.mustache +++ /dev/null @@ -1 +0,0 @@ -{{^reactive}}{{#isFile}}{{#isArray}}Array<{{/isArray}}org.springframework.web.multipart.MultipartFile{{#isArray}}>{{/isArray}}{{^required}}?{{/required}}{{/isFile}}{{/reactive}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache index b6439150538d..1b4cde03f8c7 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/service.mustache @@ -30,7 +30,7 @@ interface {{classname}}Service { {{/externalDocs}} * @see {{classname}}#{{operationId}} */ - {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{^reactive}}{{>nonReactiveFileParam}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/reactive}}{{#reactive}}{{>optionalDataType}}{{/reactive}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} {{/operation}} } {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache index 1872b59a7318..e4730f488bb5 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/serviceImpl.mustache @@ -11,7 +11,7 @@ import org.springframework.stereotype.Service class {{classname}}ServiceImpl : {{classname}}Service { {{#operation}} - override {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{^reactive}}{{>nonReactiveFileParam}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/reactive}}{{#reactive}}{{>optionalDataType}}{{/reactive}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} { + override {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} { TODO("Implement me") } {{/operation}} 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 3aa6dfa8c6d5..3bf08f8b780d 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 @@ -881,6 +881,53 @@ public void givenMultipartBinaryArray_whenGenerateDelegateAndService_correctMult ); } + @Test + public void givenMultipartBinaryArray_whenGenerateReactiveDelegateAndService_correctPartIsCreated() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/form-multipart-binary-array.yaml"); + final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setDelegatePattern(true); + // this will generate the service interface & implementation files + codegen.setServiceImplementation(true); + codegen.setReactive(true); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); +// generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + + generator.opts(input).generate(); + + validateMultipartFiles( + outputPath + "/src/main/kotlin/org/openapitools/api/MultipartArray", + "files: Flux?)" + ); + + validateMultipartFiles( + outputPath + "/src/main/kotlin/org/openapitools/api/MultipartMixed", + "file: org.springframework.http.codec.multipart.Part,", + "marker: MultipartMixedRequestMarker?", + "statusArray: kotlin.collections.List?" + ); + + validateMultipartFiles( + outputPath + "/src/main/kotlin/org/openapitools/api/MultipartSingle", + "file: org.springframework.http.codec.multipart.Part?" + ); + } + private void validateMultipartFiles( String filePrefix, String... lines From a8e2cd8c734a461929502c5adc0a09a6de357c2c Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Mon, 28 Jul 2025 19:27:04 -0700 Subject: [PATCH 07/11] Streamline tests --- .../spring/KotlinSpringServerCodegenTest.java | 194 ++++++++---------- 1 file changed, 83 insertions(+), 111 deletions(-) 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 3bf08f8b780d..11b6b4008d94 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 @@ -7,6 +7,7 @@ import io.swagger.v3.parser.core.models.ParseOptions; import java.util.Arrays; import java.util.HashMap; +import java.util.Objects; import java.util.function.Consumer; import java.util.stream.Stream; import org.apache.commons.io.FileUtils; @@ -40,6 +41,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.openapitools.codegen.TestUtils.assertFileContains; import static org.openapitools.codegen.TestUtils.assertFileNotContains; +import static org.openapitools.codegen.languages.KotlinSpringServerCodegen.REACTIVE; +import static org.openapitools.codegen.languages.KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION; +import static org.openapitools.codegen.languages.SpringCodegen.DELEGATE_PATTERN; import static org.openapitools.codegen.languages.SpringCodegen.SPRING_BOOT; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.ANNOTATION_LIBRARY; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.DOCUMENTATION_PROVIDER; @@ -210,7 +214,7 @@ public void testSettersForConfigValues() throws Exception { Assert.assertTrue(codegen.getServiceInterface()); Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.SERVICE_INTERFACE), true); Assert.assertTrue(codegen.getServiceImplementation()); - Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION), true); + Assert.assertEquals(codegen.additionalProperties().get(SERVICE_IMPLEMENTATION), true); Assert.assertFalse(codegen.getUseBeanValidation()); Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.USE_BEANVALIDATION), false); Assert.assertFalse(codegen.isReactive()); @@ -229,7 +233,7 @@ public void testAdditionalPropertiesPutForConfigValues() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.EXCEPTION_HANDLER, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.GRADLE_BUILD_FILE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_INTERFACE, true); - codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_BEANVALIDATION, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, false); codegen.processOpts(); @@ -255,7 +259,7 @@ public void testAdditionalPropertiesPutForConfigValues() throws Exception { Assert.assertTrue(codegen.getServiceInterface()); Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.SERVICE_INTERFACE), true); Assert.assertTrue(codegen.getServiceImplementation()); - Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION), true); + Assert.assertEquals(codegen.additionalProperties().get(SERVICE_IMPLEMENTATION), true); Assert.assertFalse(codegen.getUseBeanValidation()); Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.USE_BEANVALIDATION), false); Assert.assertFalse(codegen.isReactive()); @@ -792,143 +796,118 @@ public void contractWithResolvedInnerEnumContainsEnumConverter() throws IOExcept @Test public void givenNonRequiredMultipartFileArray_whenGenerateDelegateAndService_thenParameterIsCreatedAsNullableListOfMultipartFile() throws IOException { - File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); - output.deleteOnExit(); - String outputPath = output.getAbsolutePath().replace('\\', '/'); - - final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/kotlin/petstore-with-tags.yaml"); - final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); - codegen.setOpenAPI(openAPI); - codegen.setOutputDir(output.getAbsolutePath()); - codegen.setDelegatePattern(true); - // this will generate the service interface & implementation files - codegen.setServiceImplementation(true); - - ClientOptInput input = new ClientOptInput(); - input.openAPI(openAPI); - input.config(codegen); - - DefaultGenerator generator = new DefaultGenerator(); - - generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); - generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); - - generator.opts(input).generate(); + Map additionalProperties = new HashMap<>(); + additionalProperties.put(DELEGATE_PATTERN, true); + additionalProperties.put(SERVICE_IMPLEMENTATION, true); - Path delegateFile = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApiDelegate.kt"); - assertFileContains(delegateFile, "additionalMetadata: kotlin.String?"); - assertFileContains(delegateFile, "images: Array?)"); + Map generatorPropertyDefaults = new HashMap<>(); + generatorPropertyDefaults.put(CodegenConstants.MODELS, "false"); + generatorPropertyDefaults.put(CodegenConstants.MODEL_TESTS, "false"); + generatorPropertyDefaults.put(CodegenConstants.MODEL_DOCS, "false"); + generatorPropertyDefaults.put(CodegenConstants.APIS, "true"); + generatorPropertyDefaults.put(CodegenConstants.SUPPORTING_FILES, "false"); - Path controllerFile = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApi.kt"); - assertFileContains(controllerFile, "additionalMetadata: kotlin.String?"); - assertFileContains(controllerFile, "images: Array?)"); - - Path serviceFile = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApiService.kt"); - assertFileContains(serviceFile, "additionalMetadata: kotlin.String?"); - assertFileContains(serviceFile, "images: Array?)"); + Map files = generateFromContract( + "src/test/resources/3_0/kotlin/petstore-with-tags.yaml", + additionalProperties, + generatorPropertyDefaults + ); - Path serviceImplFile = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt"); - assertFileContains(serviceImplFile, "additionalMetadata: kotlin.String?"); - assertFileContains(serviceImplFile, "images: Array?)"); + validateMultipartFiles( + files, + "Pet", + "additionalMetadata: kotlin.String?", + "images: Array?)" + ); } @Test public void givenMultipartBinaryArray_whenGenerateDelegateAndService_correctMultipartFileIsCreated() throws IOException { - File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); - output.deleteOnExit(); - String outputPath = output.getAbsolutePath().replace('\\', '/'); - - final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/form-multipart-binary-array.yaml"); - final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); - codegen.setOpenAPI(openAPI); - codegen.setOutputDir(output.getAbsolutePath()); - codegen.setDelegatePattern(true); - // this will generate the service interface & implementation files - codegen.setServiceImplementation(true); + Map additionalProperties = new HashMap<>(); + additionalProperties.put(DELEGATE_PATTERN, true); + additionalProperties.put(SERVICE_IMPLEMENTATION, true); - ClientOptInput input = new ClientOptInput(); - input.openAPI(openAPI); - input.config(codegen); + Map generatorPropertyDefaults = new HashMap<>(); + generatorPropertyDefaults.put(CodegenConstants.MODELS, "false"); + generatorPropertyDefaults.put(CodegenConstants.MODEL_TESTS, "false"); + generatorPropertyDefaults.put(CodegenConstants.MODEL_DOCS, "false"); + generatorPropertyDefaults.put(CodegenConstants.APIS, "true"); + generatorPropertyDefaults.put(CodegenConstants.SUPPORTING_FILES, "false"); - DefaultGenerator generator = new DefaultGenerator(); - - generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); - generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); - - generator.opts(input).generate(); + Map files = generateFromContract( + "src/test/resources/3_0/form-multipart-binary-array.yaml", + additionalProperties, + generatorPropertyDefaults + ); validateMultipartFiles( - outputPath + "/src/main/kotlin/org/openapitools/api/MultipartArray", + files, + "MultipartArray", "files: Array?)" ); validateMultipartFiles( - outputPath + "/src/main/kotlin/org/openapitools/api/MultipartMixed", + files, + "MultipartMixed", "file: org.springframework.web.multipart.MultipartFile,", "marker: MultipartMixedRequestMarker?", "statusArray: kotlin.collections.List?" ); validateMultipartFiles( - outputPath + "/src/main/kotlin/org/openapitools/api/MultipartSingle", + files, + "MultipartSingle", "file: org.springframework.web.multipart.MultipartFile?" ); } @Test public void givenMultipartBinaryArray_whenGenerateReactiveDelegateAndService_correctPartIsCreated() throws IOException { - File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); - output.deleteOnExit(); - String outputPath = output.getAbsolutePath().replace('\\', '/'); - - final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/form-multipart-binary-array.yaml"); - final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); - codegen.setOpenAPI(openAPI); - codegen.setOutputDir(output.getAbsolutePath()); - codegen.setDelegatePattern(true); - // this will generate the service interface & implementation files - codegen.setServiceImplementation(true); - codegen.setReactive(true); + Map additionalProperties = new HashMap<>(); + additionalProperties.put(DELEGATE_PATTERN, true); + additionalProperties.put(SERVICE_IMPLEMENTATION, true); + additionalProperties.put(REACTIVE, true); + + Map generatorPropertyDefaults = new HashMap<>(); + generatorPropertyDefaults.put(CodegenConstants.MODELS, "false"); + generatorPropertyDefaults.put(CodegenConstants.MODEL_TESTS, "false"); + generatorPropertyDefaults.put(CodegenConstants.MODEL_DOCS, "false"); + generatorPropertyDefaults.put(CodegenConstants.APIS, "true"); + generatorPropertyDefaults.put(CodegenConstants.SUPPORTING_FILES, "false"); - ClientOptInput input = new ClientOptInput(); - input.openAPI(openAPI); - input.config(codegen); - - DefaultGenerator generator = new DefaultGenerator(); - - generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); -// generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); - - generator.opts(input).generate(); + Map files = generateFromContract( + "src/test/resources/3_0/form-multipart-binary-array.yaml", + additionalProperties, + generatorPropertyDefaults + ); validateMultipartFiles( - outputPath + "/src/main/kotlin/org/openapitools/api/MultipartArray", + files, + "MultipartArray", "files: Flux?)" ); validateMultipartFiles( - outputPath + "/src/main/kotlin/org/openapitools/api/MultipartMixed", + files, + "MultipartMixed", "file: org.springframework.http.codec.multipart.Part,", "marker: MultipartMixedRequestMarker?", "statusArray: kotlin.collections.List?" ); validateMultipartFiles( - outputPath + "/src/main/kotlin/org/openapitools/api/MultipartSingle", + files, + "MultipartSingle", "file: org.springframework.http.codec.multipart.Part?" ); } + /** + * Utility function to help validate that all delegate, service & api code generated for + * schemas with multipart-form-data have the same lines. + */ private void validateMultipartFiles( + Map files, String filePrefix, String... lines ) { @@ -938,25 +917,18 @@ private void validateMultipartFiles( filePrefix + "ApiServiceImpl.kt", filePrefix + "Api.kt" ) - .map(Paths::get) + .map(files::get) + .map(Objects::requireNonNull) + .map(File::toPath) .forEach(path -> { try { - validateMultipartFile(path, lines); + TestUtils.assertFileContains(path, lines); } catch (AssertionError e) { throw new AssertionError(path.toString() + " does not contain a line", e); } }); } - private void validateMultipartFile( - Path filePath, - String... lines - ) { - for(String line : lines) { - assertFileContains(filePath, line); - } - } - @Test public void givenOctetStreamResponseType_whenGenerateServer_thenReturnTypeIsResource() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); @@ -1090,7 +1062,7 @@ public void reactiveWithoutFlow() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() @@ -1135,7 +1107,7 @@ public void reactiveWithFlow() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() @@ -1181,7 +1153,7 @@ public void reactiveWithDefaultValueFlow() throws Exception { // should use default 'true' instead // codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() @@ -1226,7 +1198,7 @@ public void nonReactiveWithoutFlow() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() @@ -1271,7 +1243,7 @@ public void nonReactiveWithFlow() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() From 8ba6f0cd4458a0e8a9dd932090bbbdfdbd1536ad Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Mon, 28 Jul 2025 19:32:55 -0700 Subject: [PATCH 08/11] Fixup imports --- .../spring/KotlinSpringServerCodegenTest.java | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) 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 11b6b4008d94..425768f02891 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 @@ -41,10 +41,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.openapitools.codegen.TestUtils.assertFileContains; import static org.openapitools.codegen.TestUtils.assertFileNotContains; -import static org.openapitools.codegen.languages.KotlinSpringServerCodegen.REACTIVE; -import static org.openapitools.codegen.languages.KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION; -import static org.openapitools.codegen.languages.SpringCodegen.DELEGATE_PATTERN; -import static org.openapitools.codegen.languages.SpringCodegen.SPRING_BOOT; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.ANNOTATION_LIBRARY; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.DOCUMENTATION_PROVIDER; @@ -214,7 +210,7 @@ public void testSettersForConfigValues() throws Exception { Assert.assertTrue(codegen.getServiceInterface()); Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.SERVICE_INTERFACE), true); Assert.assertTrue(codegen.getServiceImplementation()); - Assert.assertEquals(codegen.additionalProperties().get(SERVICE_IMPLEMENTATION), true); + Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION), true); Assert.assertFalse(codegen.getUseBeanValidation()); Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.USE_BEANVALIDATION), false); Assert.assertFalse(codegen.isReactive()); @@ -233,7 +229,7 @@ public void testAdditionalPropertiesPutForConfigValues() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.EXCEPTION_HANDLER, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.GRADLE_BUILD_FILE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_INTERFACE, true); - codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_BEANVALIDATION, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, false); codegen.processOpts(); @@ -259,7 +255,7 @@ public void testAdditionalPropertiesPutForConfigValues() throws Exception { Assert.assertTrue(codegen.getServiceInterface()); Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.SERVICE_INTERFACE), true); Assert.assertTrue(codegen.getServiceImplementation()); - Assert.assertEquals(codegen.additionalProperties().get(SERVICE_IMPLEMENTATION), true); + Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION), true); Assert.assertFalse(codegen.getUseBeanValidation()); Assert.assertEquals(codegen.additionalProperties().get(KotlinSpringServerCodegen.USE_BEANVALIDATION), false); Assert.assertFalse(codegen.isReactive()); @@ -797,8 +793,8 @@ public void contractWithResolvedInnerEnumContainsEnumConverter() throws IOExcept @Test public void givenNonRequiredMultipartFileArray_whenGenerateDelegateAndService_thenParameterIsCreatedAsNullableListOfMultipartFile() throws IOException { Map additionalProperties = new HashMap<>(); - additionalProperties.put(DELEGATE_PATTERN, true); - additionalProperties.put(SERVICE_IMPLEMENTATION, true); + additionalProperties.put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); + additionalProperties.put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); Map generatorPropertyDefaults = new HashMap<>(); generatorPropertyDefaults.put(CodegenConstants.MODELS, "false"); @@ -824,8 +820,8 @@ public void givenNonRequiredMultipartFileArray_whenGenerateDelegateAndService_th @Test public void givenMultipartBinaryArray_whenGenerateDelegateAndService_correctMultipartFileIsCreated() throws IOException { Map additionalProperties = new HashMap<>(); - additionalProperties.put(DELEGATE_PATTERN, true); - additionalProperties.put(SERVICE_IMPLEMENTATION, true); + additionalProperties.put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); + additionalProperties.put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); Map generatorPropertyDefaults = new HashMap<>(); generatorPropertyDefaults.put(CodegenConstants.MODELS, "false"); @@ -864,9 +860,9 @@ public void givenMultipartBinaryArray_whenGenerateDelegateAndService_correctMult @Test public void givenMultipartBinaryArray_whenGenerateReactiveDelegateAndService_correctPartIsCreated() throws IOException { Map additionalProperties = new HashMap<>(); - additionalProperties.put(DELEGATE_PATTERN, true); - additionalProperties.put(SERVICE_IMPLEMENTATION, true); - additionalProperties.put(REACTIVE, true); + additionalProperties.put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); + additionalProperties.put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); + additionalProperties.put(KotlinSpringServerCodegen.REACTIVE, true); Map generatorPropertyDefaults = new HashMap<>(); generatorPropertyDefaults.put(CodegenConstants.MODELS, "false"); @@ -1062,7 +1058,7 @@ public void reactiveWithoutFlow() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() @@ -1107,7 +1103,7 @@ public void reactiveWithFlow() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() @@ -1153,7 +1149,7 @@ public void reactiveWithDefaultValueFlow() throws Exception { // should use default 'true' instead // codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() @@ -1198,7 +1194,7 @@ public void nonReactiveWithoutFlow() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() @@ -1243,7 +1239,7 @@ public void nonReactiveWithFlow() throws Exception { codegen.additionalProperties().put(KotlinSpringServerCodegen.REACTIVE, false); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_TAGS, true); - codegen.additionalProperties().put(SERVICE_IMPLEMENTATION, true); + codegen.additionalProperties().put(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true); codegen.additionalProperties().put(KotlinSpringServerCodegen.DELEGATE_PATTERN, true); List files = new DefaultGenerator() @@ -1371,7 +1367,7 @@ private Map generateFromContract( .setAdditionalProperties(additionalProperties) .setValidateSpec(false) .setInputSpec(url) - .setLibrary(SPRING_BOOT) + .setLibrary(KotlinSpringServerCodegen.SPRING_BOOT) .setOutputDir(output.getAbsolutePath()); consumer.accept(configurator); From 9a67862e018fb5501efdda1906ec0a0ae0b8ff99 Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Tue, 29 Jul 2025 08:46:21 -0700 Subject: [PATCH 09/11] Preserve newlines in delegate interfaces --- .../src/main/resources/kotlin-spring/apiDelegate.mustache | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache index 730c1dd34bec..9497ccb22e49 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache @@ -33,7 +33,8 @@ interface {{classname}}Delegate { /** * @see {{classname}}#{{operationId}} */ - {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/reactive}}{{#reactive}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/isFile}}{{/reactive}}{{^-last}}, {{/-last}}{{/allParams}}): {{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}}{{^skipDefaultDelegateInterface}} { + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/reactive}}{{#reactive}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/isFile}}{{/reactive}}{{^-last}}, + {{/-last}}{{/allParams}}): {{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}}{{^skipDefaultDelegateInterface}} { {{>methodBody}}{{! prevent indent}} }{{/skipDefaultDelegateInterface}} From 8f0819c6b65bc9d7de95350c92773e9579ac042f Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Tue, 29 Jul 2025 08:46:34 -0700 Subject: [PATCH 10/11] Update samples --- .../src/main/kotlin/org/openapitools/api/PetApiController.kt | 2 +- .../src/main/kotlin/org/openapitools/api/PetApiService.kt | 2 +- .../src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt | 2 +- .../src/main/kotlin/org/openapitools/api/PetApiController.kt | 2 +- .../src/main/kotlin/org/openapitools/api/PetApiService.kt | 2 +- .../src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiController.kt b/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiController.kt index 05575e31959f..3d04601aec1c 100644 --- a/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiController.kt +++ b/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiController.kt @@ -177,7 +177,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) { produces = ["application/json"], consumes = ["multipart/form-data"] ) - 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.web.multipart.MultipartFile?): ResponseEntity { + 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?): ResponseEntity { return ResponseEntity(service.uploadFile(petId, additionalMetadata, file), HttpStatus.valueOf(200)) } } diff --git a/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiService.kt b/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiService.kt index 9a6c4b569f00..64e01a90bafa 100644 --- a/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiService.kt +++ b/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiService.kt @@ -100,5 +100,5 @@ interface PetApiService { * @return successful operation (status code 200) * @see PetApi#uploadFile */ - suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile?): ModelApiResponse + suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.http.codec.multipart.Part?): ModelApiResponse } diff --git a/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt b/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt index 2590f434f52c..5b580f565b0d 100644 --- a/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt +++ b/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt @@ -35,7 +35,7 @@ class PetApiServiceImpl : PetApiService { TODO("Implement me") } - override suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile?): ModelApiResponse { + override suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.http.codec.multipart.Part?): ModelApiResponse { TODO("Implement me") } } 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 3a9a2760dd62..46e216272b1a 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 @@ -177,7 +177,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) { produces = ["application/json"], consumes = ["multipart/form-data"] ) - 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.web.multipart.MultipartFile?): ResponseEntity { + 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?): 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/PetApiService.kt b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiService.kt index 29ff5bc85617..4e8184ec8327 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiService.kt +++ b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiService.kt @@ -100,5 +100,5 @@ interface PetApiService { * @return successful operation (status code 200) * @see PetApi#uploadFile */ - suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile?): ModelApiResponse + suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.http.codec.multipart.Part?): ModelApiResponse } diff --git a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt index 58b6df4cc091..4565b2cbadf0 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt +++ b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiServiceImpl.kt @@ -35,7 +35,7 @@ class PetApiServiceImpl : PetApiService { TODO("Implement me") } - override suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.web.multipart.MultipartFile?): ModelApiResponse { + override suspend fun uploadFile(petId: kotlin.Long, additionalMetadata: kotlin.String?, file: org.springframework.http.codec.multipart.Part?): ModelApiResponse { TODO("Implement me") } } From 4bdd27538a4a990c20c28e9f45218dba54ba1b94 Mon Sep 17 00:00:00 2001 From: Chris Gual Date: Tue, 29 Jul 2025 10:01:00 -0700 Subject: [PATCH 11/11] Cleanup template logic in apiDelegate.mustache for readability --- .../src/main/resources/kotlin-spring/apiDelegate.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache index 9497ccb22e49..219e156168c3 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiDelegate.mustache @@ -33,7 +33,7 @@ interface {{classname}}Delegate { /** * @see {{classname}}#{{operationId}} */ - {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{>optionalDataType}}{{/isFile}}{{/reactive}}{{#reactive}}{{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/isFile}}{{/reactive}}{{^-last}}, + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{#isFile}}{{>fileParamType}}{{/isFile}}{{^isFile}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{/isFile}}{{^-last}}, {{/-last}}{{/allParams}}): {{#responseWrapper}}{{.}}<{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}}{{^skipDefaultDelegateInterface}} { {{>methodBody}}{{! prevent indent}} }{{/skipDefaultDelegateInterface}}