Skip to content

Commit

Permalink
328 support for multipart (#351)
Browse files Browse the repository at this point in the history
* 328 added removal of @RequestBody anotation when requestBody is multipart/form-data and created MultiPartFile type


---------

Co-authored-by: Óscar Ares Bascón <oscar.ares@sngular.com>
  • Loading branch information
jemacineiras and oscar-ares authored Nov 2, 2024
1 parent 94941e5 commit 74d836e
Show file tree
Hide file tree
Showing 16 changed files with 178 additions and 37 deletions.
2 changes: 1 addition & 1 deletion multiapi-engine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.sngular</groupId>
<artifactId>multiapi-engine</artifactId>
<version>6.0.1</version>
<version>6.0.2</version>
<packaging>jar</packaging>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public class SchemaFieldObjectType {
new SimpleImmutableEntry<>(TypeConstants.ZONEDDATE, ZONED_DATE_TIME),
new SimpleImmutableEntry<>(TypeConstants.ZONEDDATETIME, ZONED_DATE_TIME),
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATE, OFFSET_DATE_TIME),
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATETIME, OFFSET_DATE_TIME)
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATETIME, OFFSET_DATE_TIME),
new SimpleImmutableEntry<>(TypeConstants.MULTIPART_FILE, "MultipartFile")
);

private static final Map<String, String> IMPL_TYPE_MAPPINGS = Map.ofEntries(
Expand All @@ -53,7 +54,8 @@ public class SchemaFieldObjectType {
new SimpleImmutableEntry<>(TypeConstants.ZONEDDATE, ZONED_DATE_TIME),
new SimpleImmutableEntry<>(TypeConstants.ZONEDDATETIME, ZONED_DATE_TIME),
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATE, OFFSET_DATE_TIME),
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATETIME, OFFSET_DATE_TIME)
new SimpleImmutableEntry<>(TypeConstants.OFFSETDATETIME, OFFSET_DATE_TIME),
new SimpleImmutableEntry<>(TypeConstants.MULTIPART_FILE, "MultipartFile")
);

private SchemaFieldObjectType innerType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public final class TypeConstants {

public static final String ENUM = "enum";

public static final String BINARY = "binary";

public static final String LOCALDATE = "localdate";

public static final String LOCALDATETIME = "localdatetime";
Expand All @@ -50,6 +52,8 @@ public final class TypeConstants {

public static final String INT_64 = "int64";

public static final String MULTIPART_FILE = "multipartfile";

public static final Set<String> BASIC_OBJECT_TYPE = Set.of(NUMBER, STRING, BOOLEAN, INTEGER, ARRAY);

public static final Set<String> NO_IMPORT_TYPE = Set.of(STRING, INTEGER, OBJECT);
Expand Down Expand Up @@ -86,7 +90,8 @@ public final class TypeConstants {
ZONEDDATE,
ZONEDDATETIME,
OFFSETDATE,
OFFSETDATETIME
OFFSETDATETIME,
MULTIPART_FILE
);

private TypeConstants() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,21 @@ public static boolean isDateTime(final JsonNode schema) {
return isDateTime;
}

public static boolean isBinary(final JsonNode schema) {
final boolean isMultipartFile;
if (hasType(schema) && TypeConstants.STRING.equalsIgnoreCase(getType(schema))) {
if (hasNode(schema, FORMAT)) {
isMultipartFile = "binary".equalsIgnoreCase(getNode(schema, FORMAT).textValue());
} else {
isMultipartFile = false;
}
} else {
isMultipartFile = false;
}
return isMultipartFile;
}


public static List<JsonNode> findContentSchemas(final JsonNode schema) {
return hasNode(schema, "content") ? schema.findValues("schema") : Collections.emptyList();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at https://mozilla.org/MPL/2.0/.
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package com.sngular.api.generator.plugin.common.tools;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

package com.sngular.api.generator.plugin.common.tools;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import com.fasterxml.jackson.databind.JsonNode;
import com.sngular.api.generator.plugin.common.model.CommonSpecFile;
import com.sngular.api.generator.plugin.common.model.SchemaFieldObject;
import com.sngular.api.generator.plugin.common.model.SchemaFieldObjectType;
import com.sngular.api.generator.plugin.common.model.SchemaObject;
import com.sngular.api.generator.plugin.common.model.TypeConstants;
import com.sngular.api.generator.plugin.common.model.*;
import com.sngular.api.generator.plugin.openapi.exception.BadDefinedEnumException;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.WordUtils;

import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;

public final class ModelBuilder {

private static final String ADDITIONAL_PROPERTY_NAME = "AdditionalProperty";
Expand Down Expand Up @@ -130,6 +126,10 @@ private static void getTypeImports(
if (type.containsType(TypeConstants.OFFSETDATETIME)) {
listHashMap.computeIfAbsent(TypeConstants.OFFSETDATETIME, key -> List.of("java.time.OffsetDateTime"));
}

if (type.containsType(TypeConstants.MULTIPART_FILE)) {
listHashMap.computeIfAbsent(TypeConstants.MULTIPART_FILE, key -> List.of("org.springframework.web.multipart.MultipartFile"));
}
}

private static Set<SchemaFieldObject> getFields(final String buildingSchema,
Expand Down Expand Up @@ -321,7 +321,16 @@ private static Object getConst(final JsonNode fieldBody) {
}

private static SchemaFieldObject processStringProperty(final String propertyName, final JsonNode schema, final CommonSpecFile specFile) {
final String resultingType = ApiTool.isDateTime(schema) ? MapperUtil.getDateType(schema, specFile) : TypeConstants.STRING;
String resultingType;
if (ApiTool.isDateTime(schema)) {
resultingType = MapperUtil.getDateType(schema, specFile);
}
else if (ApiTool.isBinary(schema)) {
resultingType = TypeConstants.MULTIPART_FILE;
}
else {
resultingType = TypeConstants.STRING;
}

final SchemaFieldObject field = SchemaFieldObject
.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class RequestObject {

private List<ContentObject> contentObjects;

private Boolean isFormData;

public static final class RequestObjectBuilder {

private final List<ContentObject> contentObjects = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,15 @@ private static List<RequestObject> mapRequestObject(
if (!ApiTool.hasRef(requestBody)) {
requestObjects.add(RequestObject.builder()
.required(ApiTool.hasNode(requestBody, REQUIRED))
.isFormData(ApiTool.getNode(requestBody, CONTENT).has("multipart/form-data"))
.contentObjects(mapContentObject(specFile, ApiTool.getNode(requestBody, CONTENT),
"InlineObject" + operationIdWithCap, globalObject, baseDir))
.build());
} else {
final var requestBodyNode = globalObject.getRequestBodyNode(MapperUtil.getRefSchemaKey(requestBody)).orElseThrow();
requestObjects.add(RequestObject.builder()
.required(ApiTool.hasNode(requestBody, REQUIRED))
.isFormData(ApiTool.getNode(requestBodyNode, CONTENT).has("multipart/form-data"))
.contentObjects(mapContentObject(specFile, ApiTool.getNode(requestBodyNode, CONTENT),
operationIdWithCap, globalObject, baseDir))
.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public interface ${className?cap_first}Api {
<${operation.responseObjects[0].contentObjects[0].dataType}<#else><Void</#if>></@compress> ${operation.operationId}(<@compress single_line=true>
<#if operation.parameterObjects?has_content><#list operation.parameterObjects as parameter> @Parameter(name = "${parameter.name}", <#if parameter.description?has_content>description = "${parameter.description}", </#if>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}", <#if parameter.description?has_content>description = "${parameter.description}", </#if>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> @Parameter(name = "${request.contentObjects[0].dataType?api.getVariableNameString()}", description = "${request.description! ""}", required = ${request.required?c}, schema = @Schema(description = "${request.contentObjects[0].description! ""}")) @Valid <#if request.isFormData == false>@RequestBody</#if>
${request.contentObjects[0].dataType} ${request.contentObjects[0].dataType?api.getVariableNameString()} <#if request?has_next>, </#if></#list></#if></@compress>) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,14 @@ public final class OpenApiGeneratorFixtures {
.build()
);

static final List<SpecFile> TEST_FORM_DATA_MULTIPART_GENERATION = List.of(
SpecFile
.builder()
.filePath("openapigenerator/testFormDataMultipartGeneration/api-test.yml")
.apiPackage("com.sngular.multifileplugin.testformdatamultipartgeneration")
.useLombokModelAnnotation(true)
.build()
);

static Function<Path, Boolean> validateOneOfInResponse() {

Expand Down Expand Up @@ -1237,6 +1245,26 @@ static Function<Path, Boolean> validateSimpleBuild() {
return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, DEFAULT_MODEL_API, Collections.emptyList(), null);
}


static Function<Path, Boolean> validateDataMultipartGeneration() {

final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/testformdatamultipartgeneration";

final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/testformdatamultipartgeneration/model";

final String COMMON_PATH = "openapigenerator/testFormDataMultipartGeneration/";

final String ASSETS_PATH = COMMON_PATH + "assets/";

final List<String> expectedTestApiFile = List.of(
ASSETS_PATH + "TestApi.java"
);

final List<String> expectedTestApiModelFiles = Collections.emptyList();

return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, DEFAULT_MODEL_API, Collections.emptyList(), null);
}

static Function<Path, Boolean> validateValidationAnnotations(int springBootVersion) {

final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/testapi";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@

package com.sngular.api.generator.plugin.openapi;

import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;

import com.sngular.api.generator.plugin.exception.InvalidAPIException;
import com.sngular.api.generator.plugin.openapi.parameter.SpecFile;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -24,6 +18,12 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;

@Slf4j
class OpenApiGeneratorTest {

Expand Down Expand Up @@ -124,7 +124,9 @@ static Stream<Arguments> fileSpecToProcess() {
Arguments.of("testRestrictionSchema", OpenApiGeneratorFixtures.TEST_RESTRICTION_SCHEMA,
OpenApiGeneratorFixtures.validateRestrictionsSchema()),
Arguments.of("testSimpleBuild", OpenApiGeneratorFixtures.TEST_SIMPLE_BUILD,
OpenApiGeneratorFixtures.validateSimpleBuild())
OpenApiGeneratorFixtures.validateSimpleBuild()),
Arguments.of("testFormDataMultipartGeneration", OpenApiGeneratorFixtures.TEST_FORM_DATA_MULTIPART_GENERATION,
OpenApiGeneratorFixtures.validateDataMultipartGeneration())
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
openapi: 3.0.2
info:
title: Testing example file
version: 1.0.0
servers:
- url: http://localhost/v1
paths:
/test:
get:
tags:
- test
operationId: testMultipart
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
someString:
type: string
someFile:
type: string
format: binary

responses:
'200':
description:
OK
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.sngular.multifileplugin.testformdatamultipartgeneration;

import java.util.Optional;
import java.util.List;
import java.util.Map;
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 com.sngular.multifileplugin.testformdatamultipartgeneration.InlineObjectTestMultipart;

public interface TestApi {

/**
* GET /test
* @param inlineObjectTestMultipart (required)
* @return OK; (status code 200)
*/

@Operation(
operationId = "testMultipart",
tags = {"test"},
responses = {
@ApiResponse(responseCode = "200", description = "OK")
}
)
@RequestMapping(
method = RequestMethod.GET,
value = "/test",
produces = {"application/json"}
)

default ResponseEntity<Void> testMultipart(@Parameter(name = "inlineObjectTestMultipart", description = "", required = true, schema = @Schema(description = "")) @Valid InlineObjectTestMultipart inlineObjectTestMultipart) {
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sngular.multifileplugin.testsimplebuild.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.web.multipart.MultipartFile;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
Expand All @@ -12,12 +13,12 @@ public class DocumentDTO {
private String description;

@JsonProperty(value ="document")
private String document;
private MultipartFile document;


@Builder
@Jacksonized
private DocumentDTO(String description, String document) {
private DocumentDTO(String description, MultipartFile document) {
this.description = description;
this.document = document;

Expand Down
6 changes: 3 additions & 3 deletions scs-multiapi-gradle-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repositories {
}

group = 'com.sngular'
version = '6.0.1'
version = '6.0.2'

def SCSMultiApiPluginGroupId = group
def SCSMultiApiPluginVersion = version
Expand All @@ -30,7 +30,7 @@ dependencies {
shadow localGroovy()
shadow gradleApi()

implementation 'com.sngular:multiapi-engine:6.0.1'
implementation 'com.sngular:multiapi-engine:6.0.2'
testImplementation 'org.assertj:assertj-core:3.24.2'
testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3'
}
Expand Down Expand Up @@ -98,7 +98,7 @@ testing {

integrationTest(JvmTestSuite) {
dependencies {
implementation 'com.sngular:scs-multiapi-gradle-plugin:6.0.1'
implementation 'com.sngular:scs-multiapi-gradle-plugin:6.0.2'
implementation 'org.assertj:assertj-core:3.24.2'
}

Expand Down
Loading

0 comments on commit 74d836e

Please sign in to comment.