Skip to content

Commit

Permalink
Fix support multipart form data (#1735)
Browse files Browse the repository at this point in the history
  • Loading branch information
altro3 authored Sep 5, 2024
1 parent c44da69 commit fc503bd
Show file tree
Hide file tree
Showing 13 changed files with 395 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ public abstract class AbstractMicronautJavaCodegen<T extends GeneratorOptionsBui
public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
public static final String CONTENT_TYPE_MULTIPART_FORM_DATA = "multipart/form-data";
public static final String CONTENT_TYPE_ANY = "*/*";
public static final String EXT_CONTENT_TYPE = "x-content-type";

private static final String MONO_CLASS_NAME = "reactor.core.publisher.Mono";
private static final String FLUX_CLASS_NAME = "reactor.core.publisher.Flux";
Expand Down Expand Up @@ -1185,8 +1186,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
}

// Remove the "*/*" contentType from operations as it is ambiguous
if (CONTENT_TYPE_ANY.equals(op.vendorExtensions.get("x-contentType"))) {
op.vendorExtensions.put("x-contentType", CONTENT_TYPE_APPLICATION_JSON);
if (CONTENT_TYPE_ANY.equals(op.vendorExtensions.get(EXT_CONTENT_TYPE))) {
op.vendorExtensions.put(EXT_CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON);
}
op.consumes = op.consumes == null ? null : op.consumes.stream()
.filter(contentType -> !CONTENT_TYPE_ANY.equals(contentType.get("mediaType")))
Expand All @@ -1211,18 +1212,26 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
// Force form parameters are only set if the content-type is according
// formParams correspond to urlencoded type
// bodyParams correspond to multipart body
if (CONTENT_TYPE_APPLICATION_FORM_URLENCODED.equals(op.vendorExtensions.get("x-contentType"))) {
if (CONTENT_TYPE_APPLICATION_FORM_URLENCODED.equals(op.vendorExtensions.get(EXT_CONTENT_TYPE))) {
op.formParams.addAll(op.bodyParams);
op.bodyParams.forEach(p -> {
p.isBodyParam = false;
p.isFormParam = true;
});
op.bodyParams.clear();
} else if (CONTENT_TYPE_MULTIPART_FORM_DATA.equals(op.vendorExtensions.get("x-contentType"))) {
} else if (CONTENT_TYPE_MULTIPART_FORM_DATA.equals(op.vendorExtensions.get(EXT_CONTENT_TYPE))) {
op.bodyParams.addAll(op.formParams);
for (var param : op.allParams) {
if (param.isFormParam) {
param.isBodyParam = true;
param.isFormParam = false;
param.vendorExtensions.put("isPart", true);
}
}
op.formParams.forEach(p -> {
p.isBodyParam = true;
p.isFormParam = false;
p.vendorExtensions.put("isPart", true);
});
op.formParams.clear();
}
Expand Down Expand Up @@ -1841,6 +1850,8 @@ public String getExampleValue(
example = "LocalDate.of(2001, 2, 3)";
} else if ("LocalDateTime".equals(dataType)) {
example = "LocalDateTime.of(2001, 2, 3, 4, 5)";
} else if ("MultipartBody".equals(dataType)) {
example = "MultipartBody.builder().build()";
} else if ("BigDecimal".equals(dataType)) {
example = "new BigDecimal(\"78\")";
} else if (allowableValues != null && !allowableValues.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.micronaut.openapi.generator.Formatting.ReplaceDotsWithUnderscoreLambda;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
Expand Down Expand Up @@ -131,6 +132,7 @@ public abstract class AbstractMicronautKotlinCodegen<T extends GeneratorOptionsB
public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";
public static final String CONTENT_TYPE_MULTIPART_FORM_DATA = "multipart/form-data";
public static final String CONTENT_TYPE_ANY = "*/*";
public static final String EXT_CONTENT_TYPE = "x-content-type";

private static final String MONO_CLASS_NAME = "reactor.core.publisher.Mono";
private static final String FLUX_CLASS_NAME = "reactor.core.publisher.Flux";
Expand Down Expand Up @@ -858,6 +860,35 @@ public void preprocessOpenAPI(OpenAPI openApi) {
}

super.preprocessOpenAPI(openApi);

if (openApi.getPaths() != null) {
for (Map.Entry<String, PathItem> openAPIGetPathsEntry : openApi.getPaths().entrySet()) {
String pathname = openAPIGetPathsEntry.getKey();
PathItem path = openAPIGetPathsEntry.getValue();
if (path.readOperations() == null) {
continue;
}
for (Operation operation : path.readOperations()) {
log.info("Processing operation {}", operation.getOperationId());
if (hasBodyParameter(operation) || hasFormParameter(operation)) {
var defaultContentType = hasFormParameter(operation) ? CONTENT_TYPE_APPLICATION_FORM_URLENCODED : CONTENT_TYPE_APPLICATION_JSON;
var consumes = new ArrayList<>(getConsumesInfo(openApi, operation));
String contentType = consumes.isEmpty() ? defaultContentType : consumes.get(0);
operation.addExtension(EXT_CONTENT_TYPE, contentType);
}
String[] accepts = getAccepts(openAPI, operation);
operation.addExtension("x-accepts", accepts);
}
}
}
}

private String[] getAccepts(OpenAPI openAPIArg, Operation operation) {
final Set<String> producesInfo = getProducesInfo(openAPIArg, operation);
if (producesInfo != null && !producesInfo.isEmpty()) {
return producesInfo.toArray(new String[] {});
}
return new String[] { CONTENT_TYPE_APPLICATION_JSON }; // default media type
}

@Override
Expand Down Expand Up @@ -910,8 +941,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
}

// Remove the "*/*" contentType from operations as it is ambiguous
if (CONTENT_TYPE_ANY.equals(op.vendorExtensions.get("x-contentType"))) {
op.vendorExtensions.put("x-contentType", CONTENT_TYPE_APPLICATION_JSON);
if (CONTENT_TYPE_ANY.equals(op.vendorExtensions.get(EXT_CONTENT_TYPE))) {
op.vendorExtensions.put(EXT_CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON);
}
op.consumes = op.consumes == null ? null : op.consumes.stream()
.filter(contentType -> !CONTENT_TYPE_ANY.equals(contentType.get("mediaType")))
Expand All @@ -936,18 +967,26 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
// Force form parameters are only set if the content-type is according
// formParams correspond to urlencoded type
// bodyParams correspond to multipart body
if (CONTENT_TYPE_APPLICATION_FORM_URLENCODED.equals(op.vendorExtensions.get("x-contentType"))) {
if (CONTENT_TYPE_APPLICATION_FORM_URLENCODED.equals(op.vendorExtensions.get(EXT_CONTENT_TYPE))) {
op.formParams.addAll(op.bodyParams);
op.bodyParams.forEach(p -> {
p.isBodyParam = false;
p.isFormParam = true;
});
op.bodyParams.clear();
} else if (CONTENT_TYPE_MULTIPART_FORM_DATA.equals(op.vendorExtensions.get("x-contentType"))) {
} else if (CONTENT_TYPE_MULTIPART_FORM_DATA.equals(op.vendorExtensions.get(EXT_CONTENT_TYPE))) {
op.bodyParams.addAll(op.formParams);
for (var param : op.allParams) {
if (param.isFormParam) {
param.isBodyParam = true;
param.isFormParam = false;
param.vendorExtensions.put("isPart", true);
}
}
op.formParams.forEach(p -> {
p.isBodyParam = true;
p.isFormParam = false;
p.vendorExtensions.put("isPart", true);
});
op.formParams.clear();
}
Expand Down Expand Up @@ -1850,6 +1889,8 @@ public String getExampleValue(
example = "ByteArray(10)";
} else if ("BigDecimal".equals(dataType)) {
example = "BigDecimal(\"78\")";
} else if ("MultipartBody".equals(dataType)) {
example = "MultipartBody.builder().build()";
} else if (allowableValues != null && !allowableValues.isEmpty()) {
// This is an enum
Object value = example;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,23 @@
package io.micronaut.openapi.generator;

import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.meta.GeneratorMetadata;
import org.openapitools.codegen.meta.Stability;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.OperationMap;
import org.openapitools.codegen.model.OperationsMap;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import static io.micronaut.openapi.generator.Utils.processMultipartBody;

/**
* The generator for creating Micronaut clients.
Expand Down Expand Up @@ -68,6 +78,8 @@ public class JavaMicronautClientCodegen extends AbstractMicronautJavaCodegen<Jav

typeMapping.put("responseFile", "ByteBuffer<?>");
importMapping.put("ByteBuffer<?>", "io.micronaut.core.io.buffer.ByteBuffer");

importMapping.put("MultipartBody", "io.micronaut.http.client.multipart.MultipartBody");
}

@Override
Expand All @@ -85,6 +97,52 @@ public String getHelp() {
return "Generates a Java Micronaut Client.";
}

private void postProcessMultipartParam(CodegenOperation op, List<CodegenParameter> params, Collection<String> removedParams) {
var pair = processMultipartBody(op, params, false);
var multipartParam = pair.getLeft();
if (multipartParam != null) {
setParameterExampleValue(multipartParam);
}
removedParams.addAll(pair.getRight());
}

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
objs = super.postProcessOperationsWithModels(objs, allModels);

OperationMap operations = objs.getOperations();
List<CodegenOperation> operationList = operations.getOperation();

var removedParams = new HashSet<String>();

for (CodegenOperation op : operationList) {
postProcessMultipartParam(op, op.bodyParams, removedParams);
postProcessMultipartParam(op, op.allParams, removedParams);

op.notNullableParams.removeIf(p -> removedParams.contains(p.paramName));
op.requiredParams.removeIf(p -> removedParams.contains(p.paramName));
op.optionalParams.removeIf(p -> removedParams.contains(p.paramName));
op.requiredAndNotNullableParams.removeIf(p -> removedParams.contains(p.paramName));
if (op.vendorExtensions.containsKey("originalParams")) {
((List<CodegenParameter>) op.vendorExtensions.get("originalParams")).removeIf(p -> removedParams.contains(p.paramName));
}

if (!removedParams.isEmpty()) {
objs.getImports().add(Map.of("import", "io.micronaut.http.client.multipart.MultipartBody", "classname", "MultipartBody"));
}

var hasMultipleParams = !op.allParams.isEmpty();
var hasNotBodyParam = hasMultipleParams;

for (var param : op.allParams) {
param.vendorExtensions.put("hasNotBodyParam", hasNotBodyParam);
param.vendorExtensions.put("hasMultipleParams", hasMultipleParams);
}
}

return objs;
}

@Override
public void processOpts() {
super.processOpts();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,23 @@
package io.micronaut.openapi.generator;

import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.meta.GeneratorMetadata;
import org.openapitools.codegen.meta.Stability;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.OperationMap;
import org.openapitools.codegen.model.OperationsMap;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import static io.micronaut.openapi.generator.Utils.processMultipartBody;

/**
* The generator for creating Micronaut clients.
Expand Down Expand Up @@ -82,7 +92,53 @@ public String getName() {

@Override
public String getHelp() {
return "Generates a Java Micronaut Client.";
return "Generates a Kotlin Micronaut Client.";
}

private void postProcessMultipartParam(CodegenOperation op, List<CodegenParameter> params, Collection<String> removedParams) {
var pair = processMultipartBody(op, params, true);
var multipartParam = pair.getLeft();
if (multipartParam != null) {
setParameterExampleValue(multipartParam);
}
removedParams.addAll(pair.getRight());
}

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
objs = super.postProcessOperationsWithModels(objs, allModels);

OperationMap operations = objs.getOperations();
List<CodegenOperation> operationList = operations.getOperation();

var removedParams = new HashSet<String>();

for (CodegenOperation op : operationList) {
postProcessMultipartParam(op, op.bodyParams, removedParams);
postProcessMultipartParam(op, op.allParams, removedParams);

op.notNullableParams.removeIf(p -> removedParams.contains(p.paramName));
op.requiredParams.removeIf(p -> removedParams.contains(p.paramName));
op.optionalParams.removeIf(p -> removedParams.contains(p.paramName));
op.requiredAndNotNullableParams.removeIf(p -> removedParams.contains(p.paramName));
if (op.vendorExtensions.containsKey("originalParams")) {
((List<CodegenParameter>) op.vendorExtensions.get("originalParams")).removeIf(p -> removedParams.contains(p.paramName));
}

if (!removedParams.isEmpty()) {
objs.getImports().add(Map.of("import", "io.micronaut.http.client.multipart.MultipartBody", "classname", "MultipartBody"));
}

var hasMultipleParams = !op.allParams.isEmpty();
var hasNotBodyParam = hasMultipleParams;

for (var param : op.allParams) {
param.vendorExtensions.put("hasNotBodyParam", hasNotBodyParam);
param.vendorExtensions.put("hasMultipleParams", hasMultipleParams);
}
}

return objs;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public String getName() {

@Override
public String getHelp() {
return "Generates a Java Micronaut Server.";
return "Generates a Kotlin Micronaut Server.";
}

public void setControllerPackage(String controllerPackage) {
Expand Down
Loading

0 comments on commit fc503bd

Please sign in to comment.