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}#if> ${part.baseName}<#if part?has_next>, #if>#list>#macro>
package <#if packageApi??>${packageApi}<#elseif package??>${package}#if>;
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>
+ #list>
+ #if>
+ #list>
+ #if>
+ #list>
+ #list>
+#list>
+<#if needsFilePart>
+import org.springframework.http.codec.multipart.FilePart;
+#if>
<#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;
#if>
#if>
#list>
+ #if>
#list>
<#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}#list>
#if>
<#if operation.requestObjects?has_content>
- *<#list operation.requestObjects as request> @param <#list request.contentObjects as content>${content.dataType?api.getVariableNameString()}<#if content?has_next>, #if>#list>${request.description! ""}<#if request.required == true> (required)#if>#list>
+ *<#list operation.requestObjects as request> @param <#if request.isFormData><#list request.contentObjects[0].schemaObject.fieldObjectList as part>${part.baseName}<#if part?has_next>, #if>#list><#else><#list request.contentObjects as content>${content.dataType?api.getVariableNameString()}<#if content?has_next>, #if>#list>#if>${request.description! ""}<#if request.required == true> (required)#if>#list>
#if>
* @return<#list operation.responseObjects as response><#if response.responseName != "default"> ${response.description}; (status code ${response.responseName})#if>#list>
#if>
@@ -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>
)
<#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}>#if><#else>Void#if>>@compress> ${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}) #if> ${parameter.dataType} ${parameter.name} <#if parameter?has_next || operation.requestObjects?has_content>, #if>#list>#if><#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}) #if> ${parameter.dataType} ${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, #if>#list>#if>
- <#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}> #if> ${request.contentObjects[0].dataType?api.getVariableNameString()}<#if request?has_next>, #if>#list>#if>@compress><#if operation.parameterObjects?has_content || operation.requestObjects?has_content>, #if><#if path.parameterObjects?has_content || path.requestObjects?has_content>, #if>@ApiIgnore final ServerWebExchange exchange) {
+ <#else> Mono<${request.contentObjects[0].dataType}> #if> ${request.contentObjects[0].dataType?api.getVariableNameString()}<#if request?has_next>, #if>#if>#list>#if>@compress><#if operation.parameterObjects?has_content || operation.requestObjects?has_content>, #if><#if path.parameterObjects?has_content || path.requestObjects?has_content>, #if>@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}>#if><#else>Void#if>>@compress> ${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}) #if> ${parameter.dataType} ${parameter.name} <#if parameter?has_next || operation.requestObjects?has_content>, #if>#list>#if><#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}) #if> ${parameter.dataType} ${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, #if>#list>#if>
- <#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}> #if> ${request.contentObjects[0].dataType?api.getVariableNameString()}<#if request?has_next>, #if>#list>#if>@compress><#if operation.parameterObjects?has_content || operation.requestObjects?has_content>, #if><#if path.parameterObjects?has_content || path.requestObjects?has_content>, #if>@ApiIgnore final ServerWebExchange exchange) {
+ <#else> Mono<${request.contentObjects[0].dataType}> #if> ${request.contentObjects[0].dataType?api.getVariableNameString()}<#if request?has_next>, #if>#if>#list>#if>@compress><#if operation.parameterObjects?has_content || operation.requestObjects?has_content>, #if><#if path.parameterObjects?has_content || path.requestObjects?has_content>, #if>@ApiIgnore final ServerWebExchange exchange) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
#if>
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