diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml index fb183eb1..73b8563e 100644 --- a/multiapi-engine/pom.xml +++ b/multiapi-engine/pom.xml @@ -4,7 +4,7 @@ com.sngular multiapi-engine - 6.6.2 + 6.6.3 jar diff --git a/multiapi-engine/src/main/resources/templates/openapi/templateReactive.ftlh b/multiapi-engine/src/main/resources/templates/openapi/templateReactive.ftlh index c8b2c869..7f56c742 100644 --- a/multiapi-engine/src/main/resources/templates/openapi/templateReactive.ftlh +++ b/multiapi-engine/src/main/resources/templates/openapi/templateReactive.ftlh @@ -1,4 +1,5 @@ <#ftl output_format="plainText"> +<#macro formDataParts request><#list request.contentObjects[0].schemaObject.fieldObjectList as part> @Parameter(name = "${part.baseName}", required = ${part.required?c}, schema = @Schema(description = "")) @RequestPart(value = "${part.baseName}", required = ${part.required?c}) <#if part.dataType.baseType == "multipartfile">FilePart<#elseif part.dataType.baseType == "array" && part.dataType.innerType.baseType == "multipartfile">Flux<#else>${part.dataType} ${part.baseName}<#if part?has_next>, package <#if packageApi??>${packageApi}<#elseif package??>${package}; import java.util.List; @@ -21,11 +22,33 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import reactor.core.publisher.Flux; import springfox.documentation.annotations.ApiIgnore; +<#assign needsFilePart=false> +<#list pathObjects as path> + <#list path.operationObjects as operation> + <#list operation.requestObjects as request> + <#if request.isFormData> + <#list request.contentObjects as content> + <#if content.schemaObject??> + <#list content.schemaObject.fieldObjectList as field> + <#if "${field.dataType}"?contains("MultipartFile")> + <#assign needsFilePart=true> + + + + + + + + +<#if needsFilePart> +import org.springframework.http.codec.multipart.FilePart; + <#assign imports=[]> <#list pathObjects as path> <#list path.operationObjects as operation> <#list operation.requestObjects as request> + <#if !request.isFormData> <#list request.contentObjects as content> <#if content.importName?? && (!imports?seq_contains(content.importName))> <#if (!checkBasicTypes?seq_contains(content.importName))> @@ -33,6 +56,7 @@ import springfox.documentation.annotations.ApiIgnore; + <#list operation.responseObjects as response> <#list response.contentObjects as content> @@ -70,7 +94,7 @@ public interface ${className?cap_first}Api { * @param <#list path.parameterObjects as parameter> ${parameter.name} ${parameter.description} ${parameter.required?c} <#if operation.requestObjects?has_content> - *<#list operation.requestObjects as request> @param <#list request.contentObjects as content>${content.dataType?api.getVariableNameString()}<#if content?has_next>, ${request.description! ""}<#if request.required == true> (required) + *<#list operation.requestObjects as request> @param <#if request.isFormData><#list request.contentObjects[0].schemaObject.fieldObjectList as part>${part.baseName}<#if part?has_next>, <#else><#list request.contentObjects as content>${content.dataType?api.getVariableNameString()}<#if content?has_next>, ${request.description! ""}<#if request.required == true> (required) * @return<#list operation.responseObjects as response><#if response.responseName != "default"> ${response.description}; (status code ${response.responseName}) @@ -88,22 +112,23 @@ public interface ${className?cap_first}Api { @RequestMapping( method = RequestMethod.${operation.operationType}, value = "${path.pathName}", - produces = {"application/json"} + produces = {"application/json"}<#if operation.requestObjects?has_content && operation.requestObjects[0].isFormData>, + consumes = {MediaType.MULTIPART_FORM_DATA_VALUE} ) <#if operation.securities?has_content> default Mono<<#if operation.responseObjects[0].contentObjects[0]??><#if operation.responseObjects[0].contentObjects[0].dataType.baseType == "array" || operation.responseObjects[0].contentObjects[0].dataType.baseType == "map">Flux<${operation.responseObjects[0].contentObjects[0].dataType.innerType}><#else>${operation.responseObjects[0].contentObjects[0].dataType}><#else>Void> ${operation.operationId}(<@compress single_line=true> <#if operation.parameterObjects?has_content><#list operation.parameterObjects as parameter> @Parameter(name = "${parameter.name}", description = "${parameter.description}", required = ${parameter.required?c}, schema = @Schema(description = "")) <#if parameter.in == "path"> @PathVariable("${parameter.name}") <#elseif parameter.in == "query"> @RequestParam(required = ${parameter.required?c}) ${parameter.dataType} ${parameter.name} <#if parameter?has_next || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content><#list path.parameterObjects as parameter> @Parameter(name = "${parameter.name}", description = "${parameter.description}", required = ${parameter.required?c}, schema = @Schema(description = "")) <#if parameter.in == "path"> @PathVariable("${parameter.name}") <#elseif parameter.in == "query"> @RequestParam(required = ${parameter.required?c}) ${parameter.dataType} ${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, - <#if operation.requestObjects?has_content><#list operation.requestObjects as request> @Parameter(name = "${request.contentObjects[0].dataType?api.getVariableNameString()}", description = "${request.description! ""}", required = ${request.required?c}, schema = @Schema(description = "${request.contentObjects[0].description! ""}")) @Valid @RequestBody + <#if operation.requestObjects?has_content><#list operation.requestObjects as request><#if request.isFormData><@formDataParts request/><#else> @Parameter(name = "${request.contentObjects[0].dataType?api.getVariableNameString()}", description = "${request.description! ""}", required = ${request.required?c}, schema = @Schema(description = "${request.contentObjects[0].description! ""}")) @Valid @RequestBody <#if request.contentObjects[0].dataType.baseType == "array" || request.contentObjects[0].dataType.baseType == "map"> Flux<${request.contentObjects[0].dataType.innerType}> - <#else> Mono<${request.contentObjects[0].dataType}> ${request.contentObjects[0].dataType?api.getVariableNameString()}<#if request?has_next>, <#if operation.parameterObjects?has_content || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content || path.requestObjects?has_content>, @ApiIgnore final ServerWebExchange exchange) { + <#else> Mono<${request.contentObjects[0].dataType}> ${request.contentObjects[0].dataType?api.getVariableNameString()}<#if request?has_next>, <#if operation.parameterObjects?has_content || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content || path.requestObjects?has_content>, @ApiIgnore final ServerWebExchange exchange) { return Mono.just(new ResponseEntity(HttpStatus.NOT_IMPLEMENTED)); } <#else> default ResponseEntity<@compress single_line=true><<#if operation.responseObjects[0].contentObjects[0]??><#if operation.responseObjects[0].contentObjects[0].dataType.baseType == "array" || operation.responseObjects[0].contentObjects[0].dataType.baseType == "map">Flux<${operation.responseObjects[0].contentObjects[0].dataType.innerType}><#else>Mono<${operation.responseObjects[0].contentObjects[0].dataType}><#else>Void> ${operation.operationId}(<@compress single_line=true> <#if operation.parameterObjects?has_content><#list operation.parameterObjects as parameter> @Parameter(name = "${parameter.name}", description = "${parameter.description}", required = ${parameter.required?c}, schema = @Schema(description = "")) <#if parameter.in == "path"> @PathVariable("${parameter.name}") <#elseif parameter.in == "query"> @RequestParam(required = ${parameter.required?c}) ${parameter.dataType} ${parameter.name} <#if parameter?has_next || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content><#list path.parameterObjects as parameter> @Parameter(name = "${parameter.name}", description = "${parameter.description}", required = ${parameter.required?c}, schema = @Schema(description = "")) <#if parameter.in == "path"> @PathVariable("${parameter.name}") <#elseif parameter.in == "query"> @RequestParam(required = ${parameter.required?c}) ${parameter.dataType} ${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, - <#if operation.requestObjects?has_content><#list operation.requestObjects as request> @Parameter(name = "${request.contentObjects[0].dataType?api.getVariableNameString()}", description = "${request.description! ""}", required = ${request.required?c}, schema = @Schema(description = "${request.contentObjects[0].description! ""}")) @Valid @RequestBody + <#if operation.requestObjects?has_content><#list operation.requestObjects as request><#if request.isFormData><@formDataParts request/><#else> @Parameter(name = "${request.contentObjects[0].dataType?api.getVariableNameString()}", description = "${request.description! ""}", required = ${request.required?c}, schema = @Schema(description = "${request.contentObjects[0].description! ""}")) @Valid @RequestBody <#if request.contentObjects[0].dataType.baseType == "array" || request.contentObjects[0].dataType.baseType == "map"> Flux<${request.contentObjects[0].dataType.innerType}> - <#else> Mono<${request.contentObjects[0].dataType}> ${request.contentObjects[0].dataType?api.getVariableNameString()}<#if request?has_next>, <#if operation.parameterObjects?has_content || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content || path.requestObjects?has_content>, @ApiIgnore final ServerWebExchange exchange) { + <#else> Mono<${request.contentObjects[0].dataType}> ${request.contentObjects[0].dataType?api.getVariableNameString()}<#if request?has_next>, <#if operation.parameterObjects?has_content || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content || path.requestObjects?has_content>, @ApiIgnore final ServerWebExchange exchange) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); } diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java index c3892474..3b66654a 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java @@ -81,6 +81,11 @@ public final class OpenApiGeneratorFixtures { .modelPackage("com.sngular.multifileplugin.reactivegeneration.model").modelNamePrefix("Api") .modelNameSuffix("DTO").useLombokModelAnnotation(false).isReactive(true).build()); + static final List TEST_REACTIVE_FORM_DATA_MULTIPART = List + .of(SpecFile.builder().filePath("openapigenerator/testReactiveFormDataMultipart/api-test.yml") + .apiPackage("com.sngular.multifileplugin.testreactiveformdatamultipart") + .useLombokModelAnnotation(false).isReactive(true).build()); + static final List TEST_API_TAGS_GENERATION = List .of(SpecFile.builder().filePath("openapigenerator/testApiTagsGeneration/api-tags-test.yml") .apiPackage("com.sngular.multifileplugin.tagsgeneration") @@ -602,6 +607,22 @@ static Function validateApiReactiveGeneration(int springBootVersi } + static Function validateReactiveFormDataMultipart() { + + final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/testreactiveformdatamultipart"; + + final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/testreactiveformdatamultipart/model"; + + final String COMMON_PATH = "openapigenerator/testReactiveFormDataMultipart/"; + + final String ASSETS_PATH = COMMON_PATH + "assets/"; + + final List expectedTestApiFile = List.of(ASSETS_PATH + "UploadApi.java"); + + return path -> commonTest(path, expectedTestApiFile, Collections.emptyList(), DEFAULT_TARGET_API, + DEFAULT_MODEL_API, Collections.emptyList(), null); + } + static Function validateTagsGeneration() { final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/tagsgeneration"; diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java index 18d9edcf..4bfa1dac 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java @@ -63,6 +63,8 @@ static Stream fileSpecToProcess() { OpenApiGeneratorFixtures.validatePathWithSpecialCharGeneration()), Arguments.of("testApiReactiveGeneration", OpenApiGeneratorFixtures.TEST_API_REACTIVE_GENERATION, OpenApiGeneratorFixtures.validateApiReactiveGeneration(SPRING_BOOT_VERSION)), + Arguments.of("testReactiveFormDataMultipart", OpenApiGeneratorFixtures.TEST_REACTIVE_FORM_DATA_MULTIPART, + OpenApiGeneratorFixtures.validateReactiveFormDataMultipart()), Arguments.of("testApiTagsGeneration", OpenApiGeneratorFixtures.TEST_API_TAGS_GENERATION, OpenApiGeneratorFixtures.validateTagsGeneration()), Arguments.of("testMultipleRefGeneration", OpenApiGeneratorFixtures.TEST_MULTIPLE_REF_GENERATION, diff --git a/multiapi-engine/src/test/resources/openapigenerator/testReactiveFormDataMultipart/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testReactiveFormDataMultipart/api-test.yml new file mode 100644 index 00000000..b33d19d9 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testReactiveFormDataMultipart/api-test.yml @@ -0,0 +1,33 @@ +openapi: 3.0.2 +info: + title: Testing reactive multipart file upload + version: 1.0.0 +servers: +- url: http://localhost/v1 +paths: + /upload: + post: + tags: + - test + operationId: uploadMultipart + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + someString: + type: string + someFile: + type: string + format: binary + someFiles: + type: array + items: + type: string + format: binary + responses: + '200': + description: + OK diff --git a/multiapi-engine/src/test/resources/openapigenerator/testReactiveFormDataMultipart/assets/UploadApi.java b/multiapi-engine/src/test/resources/openapigenerator/testReactiveFormDataMultipart/assets/UploadApi.java new file mode 100644 index 00000000..8b4e0d13 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testReactiveFormDataMultipart/assets/UploadApi.java @@ -0,0 +1,51 @@ +package com.sngular.multifileplugin.testreactiveformdatamultipart; + +import java.util.List; +import java.util.Map; +import java.nio.charset.StandardCharsets; +import javax.validation.Valid; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Flux; +import springfox.documentation.annotations.ApiIgnore; +import org.springframework.http.codec.multipart.FilePart; + + +public interface UploadApi { + + /** + * POST /upload + * @param someFile, someFiles, someString (required) + * @return OK; (status code 200) + * @throws WebClientResponseException if an error occurs while attempting to invoke the API + */ + @Operation( + operationId = "uploadMultipart", + tags = {"test"}, + responses = { + @ApiResponse(responseCode = "200", description = "OK") + } + ) + @RequestMapping( + method = RequestMethod.POST, + value = "/upload", + produces = {"application/json"}, + consumes = {MediaType.MULTIPART_FORM_DATA_VALUE} + ) + default ResponseEntity uploadMultipart(@Parameter(name = "someFile", required = false, schema = @Schema(description = "")) @RequestPart(value = "someFile", required = false) FilePart someFile, @Parameter(name = "someFiles", required = false, schema = @Schema(description = "")) @RequestPart(value = "someFiles", required = false) Flux someFiles, @Parameter(name = "someString", required = false, schema = @Schema(description = "")) @RequestPart(value = "someString", required = false) String someString, @ApiIgnore final ServerWebExchange exchange) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + +} \ No newline at end of file diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle index a28a2352..68e961a3 100644 --- a/scs-multiapi-gradle-plugin/build.gradle +++ b/scs-multiapi-gradle-plugin/build.gradle @@ -21,7 +21,7 @@ repositories { } group = 'com.sngular' -version = '6.6.2' +version = '6.6.3' def SCSMultiApiPluginGroupId = group def SCSMultiApiPluginVersion = version @@ -31,7 +31,7 @@ dependencies { shadow localGroovy() shadow gradleApi() - implementation 'com.sngular:multiapi-engine:6.6.2' + implementation 'com.sngular:multiapi-engine:6.6.3' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3' testImplementation 'org.junit.platform:junit-platform-launcher:1.9.2' @@ -100,7 +100,7 @@ testing { integrationTest(JvmTestSuite) { dependencies { - implementation 'com.sngular:scs-multiapi-gradle-plugin:6.6.2' + implementation 'com.sngular:scs-multiapi-gradle-plugin:6.6.3' implementation 'org.assertj:assertj-core:3.24.2' } diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml index c26471b9..27bc47e7 100644 --- a/scs-multiapi-maven-plugin/pom.xml +++ b/scs-multiapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ com.sngular scs-multiapi-maven-plugin - 6.6.2 + 6.6.3 maven-plugin AsyncApi - OpenApi Code Generator Maven Plugin @@ -271,7 +271,7 @@ com.sngular multiapi-engine - 6.6.2 + 6.6.3 org.apache.maven