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 619d44868dc4..86cf4300e508 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 @@ -87,6 +87,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen public static final String SKIP_DEFAULT_API_INTERFACE = "skipDefaultApiInterface"; public static final String SKIP_DEFAULT_DELEGATE_INTERFACE = "skipDefaultDelegateInterface"; public static final String REACTIVE = "reactive"; + private static final String REACTIVE_MULTIPART = "reactiveMultipart"; public static final String INTERFACE_ONLY = "interfaceOnly"; public static final String USE_FEIGN_CLIENT_URL = "useFeignClientUrl"; public static final String USE_FEIGN_CLIENT = "useFeignClient"; @@ -725,6 +726,7 @@ public void processOpts() { writePropertyBack(SKIP_DEFAULT_INTERFACE, skipDefaultInterface); } writePropertyBack(REACTIVE, reactive); + writePropertyBack(REACTIVE_MULTIPART, reactive && SPRING_BOOT.equals(library)); writePropertyBack(EXCEPTION_HANDLER, exceptionHandler); writePropertyBack(USE_FLOW_FOR_ARRAY_RETURN_TYPE, useFlowForArrayReturnType); 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 1a4578db7733..2ea8b749deb4 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}}{{#isNullable}}?{{/isNullable}}{{/isFile}} \ No newline at end of file +{{^isFile}}{{{dataType}}}{{^required}}{{^defaultValue}}?{{/defaultValue}}{{/required}}{{/isFile}}{{#isFile}}{{#reactiveMultipart}}{{#isArray}}reactor.core.publisher.Flux<{{/isArray}}org.springframework.http.codec.multipart.Part{{#isArray}}>{{/isArray}}{{/reactiveMultipart}}{{^reactiveMultipart}}{{#isArray}}Array<{{/isArray}}org.springframework.web.multipart.MultipartFile{{#isArray}}>{{/isArray}}{{/reactiveMultipart}}{{#isNullable}}?{{/isNullable}}{{/isFile}} \ No newline at end of file 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 c99cf7a0aae1..e0e398aed1a4 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 @@ -56,6 +56,8 @@ public class KotlinSpringServerCodegenTest { + private static final String MULTIPART_SPEC = "src/test/resources/3_0/form-multipart-binary-array.yaml"; + @Test(description = "test embedded enum array") public void embeddedEnumArrayTest() throws Exception { String baseModelPackage = "zz"; @@ -432,6 +434,26 @@ public void testNullableMultipartFile() throws IOException { + " )"); } + @Test + public void testNullableMultipartFileReactive() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/kotlin/feat-multipartfile_nullable.yaml", + Map.of(KotlinSpringServerCodegen.REACTIVE, true)); + + assertFileContains(files.get("NullableMultipartfileApiController.kt").toPath(), + "file: org.springframework.http.codec.multipart.Part?" + + " )"); + assertFileContains(files.get("NullableMultipartfileArrayApiController.kt").toPath(), + "files: reactor.core.publisher.Flux?" + + " )"); + assertFileContains(files.get("NonNullableMultipartfileApiController.kt").toPath(), + "file: org.springframework.http.codec.multipart.Part" + + " )"); + assertFileContains(files.get("NonNullableMultipartfileArrayApiController.kt").toPath(), + "files: reactor.core.publisher.Flux" + + " )"); + } + @Test public void arrayItemsCanBeNullable() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); @@ -984,35 +1006,98 @@ public void givenOctetStreamResponseType_whenGenerateServer_thenReturnTypeIsReso @Test public void givenMultipartForm_whenGenerateReactiveServer_thenParameterAreCreatedAsRequestPart() throws IOException { - File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); - output.deleteOnExit(); - String outputPath = output.getAbsolutePath().replace('\\', '/'); + Map files = generateFromContract( + MULTIPART_SPEC, + Map.of( + KotlinSpringServerCodegen.REACTIVE, true, + KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true)); - 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()); + assertReactiveMultipartParameters(files, "Controller.kt"); + assertReactiveMultipartParameters(files, "Service.kt"); + assertReactiveMultipartParameters(files, "ServiceImpl.kt"); + assertReactiveMultipartParameters(files, "Test.kt"); + } - ClientOptInput input = new ClientOptInput(); - input.openAPI(openAPI); - input.config(codegen); + @Test + public void givenMultipartForm_whenGenerateReactiveDelegate_thenParametersUseRequestPart() throws IOException { + Map files = generateFromContract( + MULTIPART_SPEC, + Map.of( + KotlinSpringServerCodegen.REACTIVE, true, + KotlinSpringServerCodegen.DELEGATE_PATTERN, true)); - DefaultGenerator generator = new DefaultGenerator(); + assertReactiveMultipartParameters(files, ".kt"); + assertReactiveMultipartParameters(files, "Delegate.kt"); + } - 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"); + @Test + public void givenMultipartForm_whenGenerateBlockingServer_thenMultipartFileTypesRemain() throws IOException { + Map files = generateFromContract( + MULTIPART_SPEC, + Map.of(KotlinSpringServerCodegen.SERVICE_IMPLEMENTATION, true)); - generator.opts(input).generate(); + assertBlockingMultipartParameters(files, "Controller.kt"); + assertBlockingMultipartParameters(files, "Service.kt"); + assertBlockingMultipartParameters(files, "ServiceImpl.kt"); + assertBlockingMultipartParameters(files, "Test.kt"); + } - Path outputFilepath = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/api/PetApiController.kt"); + @Test + public void givenMultipartForm_whenGenerateReactiveDeclarativeHttpInterface_thenMultipartFileTypesRemain() throws IOException { + Map files = generateFromContract( + MULTIPART_SPEC, + Map.of( + KotlinSpringServerCodegen.REACTIVE, true, + KotlinSpringServerCodegen.USE_FLOW_FOR_ARRAY_RETURN_TYPE, false), + new HashMap<>(), + configurator -> configurator.setLibrary(SPRING_DECLARATIVE_HTTP_INTERFACE_LIBRARY)); - 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"); + Path apiFile = files.get("MultipartApi.kt").toPath(); + assertFileContains(apiFile, + "files: Array", + "file: org.springframework.web.multipart.MultipartFile", + "status: MultipartMixedStatus", + "marker: MultipartMixedRequestMarker?", + "statusArray: kotlin.collections.List?"); + assertFileNotContains(apiFile, "org.springframework.http.codec.multipart.Part"); + } + + private void assertReactiveMultipartParameters(Map files, String fileSuffix) { + Path arrayFile = files.get("MultipartArrayApi" + fileSuffix).toPath(); + Path singleFile = files.get("MultipartSingleApi" + fileSuffix).toPath(); + Path mixedFile = files.get("MultipartMixedApi" + fileSuffix).toPath(); + + assertFileContains(arrayFile, + "files: reactor.core.publisher.Flux"); + assertFileContains(singleFile, + "file: org.springframework.http.codec.multipart.Part"); + assertFileContains(mixedFile, + "status: MultipartMixedStatus", + "file: org.springframework.http.codec.multipart.Part", + "marker: MultipartMixedRequestMarker?", + "statusArray: kotlin.collections.List?"); + assertFileNotContains(arrayFile, "org.springframework.web.multipart.MultipartFile"); + assertFileNotContains(singleFile, "org.springframework.web.multipart.MultipartFile"); + assertFileNotContains(mixedFile, "org.springframework.web.multipart.MultipartFile"); + } + + private void assertBlockingMultipartParameters(Map files, String fileSuffix) { + Path arrayFile = files.get("MultipartArrayApi" + fileSuffix).toPath(); + Path singleFile = files.get("MultipartSingleApi" + fileSuffix).toPath(); + Path mixedFile = files.get("MultipartMixedApi" + fileSuffix).toPath(); + + assertFileContains(arrayFile, + "files: Array"); + assertFileContains(singleFile, + "file: org.springframework.web.multipart.MultipartFile"); + assertFileContains(mixedFile, + "status: MultipartMixedStatus", + "file: org.springframework.web.multipart.MultipartFile", + "marker: MultipartMixedRequestMarker?", + "statusArray: kotlin.collections.List?"); + assertFileNotContains(arrayFile, "org.springframework.http.codec.multipart.Part"); + assertFileNotContains(singleFile, "org.springframework.http.codec.multipart.Part"); + assertFileNotContains(mixedFile, "org.springframework.http.codec.multipart.Part"); } 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 165920c56390..3bb47cd5712f 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 @@ -206,7 +206,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.web.multipart.MultipartFile + @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 ed714a425820..77b25feace88 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 @@ -101,5 +101,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 751d8c0b477e..58f2d3fcfb5a 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-without-flow/src/test/kotlin/org/openapitools/api/PetApiTest.kt b/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/test/kotlin/org/openapitools/api/PetApiTest.kt index 401b2fc8829f..2bf0ad9fa15c 100644 --- a/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/test/kotlin/org/openapitools/api/PetApiTest.kt +++ b/samples/server/petstore/kotlin-springboot-reactive-without-flow/src/test/kotlin/org/openapitools/api/PetApiTest.kt @@ -123,7 +123,7 @@ class PetApiTest { fun uploadFileTest() = runBlockingTest { val petId: kotlin.Long = TODO() val additionalMetadata: kotlin.String? = TODO() - val file: org.springframework.web.multipart.MultipartFile? = TODO() + val file: org.springframework.http.codec.multipart.Part = TODO() val response: ResponseEntity = api.uploadFile(petId, additionalMetadata, file) // TODO: test validations 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 682a15ef48e1..b3c53ae479c6 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 @@ -206,7 +206,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.web.multipart.MultipartFile + @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 79151790a6ca..b2ac669dfb7f 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 @@ -101,5 +101,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 d1fce342257d..6efce8cb9beb 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") } } diff --git a/samples/server/petstore/kotlin-springboot-reactive/src/test/kotlin/org/openapitools/api/PetApiTest.kt b/samples/server/petstore/kotlin-springboot-reactive/src/test/kotlin/org/openapitools/api/PetApiTest.kt index 26f1a1f9e0b6..50b9ced32062 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/src/test/kotlin/org/openapitools/api/PetApiTest.kt +++ b/samples/server/petstore/kotlin-springboot-reactive/src/test/kotlin/org/openapitools/api/PetApiTest.kt @@ -123,7 +123,7 @@ class PetApiTest { fun uploadFileTest() = runBlockingTest { val petId: kotlin.Long = TODO() val additionalMetadata: kotlin.String? = TODO() - val file: org.springframework.web.multipart.MultipartFile? = TODO() + val file: org.springframework.http.codec.multipart.Part = TODO() val response: ResponseEntity = api.uploadFile(petId, additionalMetadata, file) // TODO: test validations