Skip to content

Commit

Permalink
Divide operations by content type
Browse files Browse the repository at this point in the history
Fixed #17877
  • Loading branch information
altro3 committed Sep 9, 2024
1 parent 0026e15 commit 7710ce5
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -453,4 +453,6 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case,
public static final String WAIT_TIME_OF_THREAD = "waitTimeMillis";

public static final String USE_DEFAULT_VALUES_FOR_REQUIRED_VARS = "useDefaultValuesForRequiredVars";

public static final String DIVIDE_OPERATIONS_BY_CONTENT_TYPE = "divideOperationsByContentType";
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.openapitools.codegen.CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE;
import static org.openapitools.codegen.CodegenConstants.UNSUPPORTED_V310_SPEC_MSG;
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
import static org.openapitools.codegen.utils.OnceLogger.once;
Expand Down Expand Up @@ -386,7 +387,7 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, this::setDisallowAdditionalPropertiesIfNotPresent);
convertPropertyToBooleanAndWriteBack(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, this::setEnumUnknownDefaultCase);
convertPropertyToBooleanAndWriteBack(CodegenConstants.AUTOSET_CONSTANTS, this::setAutosetConstants);
}
}


/***
Expand Down Expand Up @@ -993,6 +994,49 @@ public void postProcessParameter(CodegenParameter parameter) {
@Override
@SuppressWarnings("unused")
public void preprocessOpenAPI(OpenAPI openAPI) {

var divideOperationsByContentType = Boolean.parseBoolean(GlobalSettings.getProperty(DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "false"));

if (divideOperationsByContentType && openAPI.getPaths() != null && !openAPI.getPaths().isEmpty()) {

for (Map.Entry<String, PathItem> entry : openAPI.getPaths().entrySet()) {
String pathStr = entry.getKey();
PathItem path = entry.getValue();
List<Operation> getOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.GET, path.getGet());
if (!getOps.isEmpty()) {
path.addExtension("x-get", getOps);
}
List<Operation> putOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.PUT, path.getPut());
if (!putOps.isEmpty()) {
path.addExtension("x-put", putOps);
}
List<Operation> postOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.POST, path.getPost());
if (!postOps.isEmpty()) {
path.addExtension("x-post", postOps);
}
List<Operation> deleteOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.DELETE, path.getDelete());
if (!deleteOps.isEmpty()) {
path.addExtension("x-delete", deleteOps);
}
List<Operation> optionsOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.OPTIONS, path.getOptions());
if (!optionsOps.isEmpty()) {
path.addExtension("x-options", optionsOps);
}
List<Operation> headOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.HEAD, path.getHead());
if (!headOps.isEmpty()) {
path.addExtension("x-head", headOps);
}
List<Operation> patchOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.PATCH, path.getPatch());
if (!patchOps.isEmpty()) {
path.addExtension("x-patch", patchOps);
}
List<Operation> traceOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.TRACE, path.getTrace());
if (!traceOps.isEmpty()) {
path.addExtension("x-trace", traceOps);
}
}
}

if (useOneOfInterfaces && openAPI.getComponents() != null) {
// we process the openapi schema here to find oneOf schemas and create interface models for them
Map<String, Schema> schemas = new HashMap<>(openAPI.getComponents().getSchemas());
Expand Down Expand Up @@ -1074,6 +1118,61 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
}
}

private List<Operation> divideOperationsByContentType(String path, PathItem.HttpMethod httpMethod, Operation op) {

if (op == null) {
return Collections.emptyList();
}

var additionalOps = new ArrayList<Operation>();

RequestBody body = op.getRequestBody();
if (body == null || body.getContent() == null) {
return Collections.emptyList();
}
Content content = body.getContent();
if (content.size() <= 1) {
return Collections.emptyList();
}
var mediaTypesToRemove = new ArrayList<String>();
for (var entry : content.entrySet()) {
if (mediaTypesToRemove.contains(entry.getKey())) {
continue;
}
for (var entry2 : content.entrySet()) {
if (entry.getKey().equals(entry2.getKey()) || entry.getValue().equals(entry2.getValue())) {
continue;
}
additionalOps.add(new Operation()
.deprecated(op.getDeprecated())
.callbacks(op.getCallbacks())
.description(op.getDescription())
.extensions(op.getExtensions())
.externalDocs(op.getExternalDocs())
.operationId(getOrGenerateOperationId(op, path, httpMethod.name()))
.parameters(op.getParameters())
.responses(op.getResponses())
.security(op.getSecurity())
.servers(op.getServers())
.summary(op.getSummary())
.tags(op.getTags())
.requestBody(new RequestBody()
.description(body.getDescription())
.extensions(body.getExtensions())
.content(new Content()
.addMediaType(entry2.getKey(), entry2.getValue()))
)
);
mediaTypesToRemove.add(entry2.getKey());
}
}
if (!mediaTypesToRemove.isEmpty()) {
content.entrySet().removeIf(stringMediaTypeEntry -> mediaTypesToRemove.contains(stringMediaTypeEntry.getKey()));
}

return additionalOps;
}

// override with any special handling of the entire OpenAPI spec document
@Override
@SuppressWarnings("unused")
Expand Down Expand Up @@ -1158,8 +1257,7 @@ public String encodePath(String input) {
*/
@Override
public String escapeUnsafeCharacters(String input) {
LOGGER.warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape " +
"unsafe characters");
LOGGER.warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape unsafe characters");
// doing nothing by default and code generator should implement
// the logic to prevent code injection
// later we'll make this method abstract to make sure
Expand All @@ -1175,8 +1273,7 @@ public String escapeUnsafeCharacters(String input) {
*/
@Override
public String escapeQuotationMark(String input) {
LOGGER.warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape " +
"single/double quote");
LOGGER.warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape single/double quote");
return input.replace("\"", "\\\"");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,10 +608,10 @@ private void generateModelsForVariable(List<File> files, List<ModelMap> allModel
if (!processedModels.contains(key) && allSchemas.containsKey(key)) {
generateModels(files, allModels, unusedModels, aliasModels, processedModels, () -> Set.of(key));
} else {
LOGGER.info("Type " + variable.getComplexType()+" of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getComplexType(), variable.getName());
}
} else {
LOGGER.info("Type " + variable.getOpenApiType()+" of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getComplexType(), variable.getName());
}
}

Expand Down Expand Up @@ -1001,7 +1001,7 @@ private void generateOpenapiGeneratorIgnoreFile() {
File ignoreFile = new File(ignoreFileNameTarget);
// use the entries provided by the users to pre-populate .openapi-generator-ignore
try {
LOGGER.info("Writing file " + ignoreFileNameTarget + " (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)");
LOGGER.info("Writing file {} (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)", ignoreFileNameTarget);
new File(config.outputFolder()).mkdirs();
if (!ignoreFile.createNewFile()) {
// file may already exist, do nothing
Expand Down Expand Up @@ -1457,6 +1457,9 @@ public Map<String, List<CodegenOperation>> processPaths(Paths paths) {
if (paths == null) {
return ops;
}

var divideOperationsByContentType = Boolean.parseBoolean(GlobalSettings.getProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "false"));

for (Map.Entry<String, PathItem> pathsEntry : paths.entrySet()) {
String resourcePath = pathsEntry.getKey();
PathItem path = pathsEntry.getValue();
Expand All @@ -1468,10 +1471,34 @@ public Map<String, List<CodegenOperation>> processPaths(Paths paths) {
processOperation(resourcePath, "patch", path.getPatch(), ops, path);
processOperation(resourcePath, "options", path.getOptions(), ops, path);
processOperation(resourcePath, "trace", path.getTrace(), ops, path);

if (divideOperationsByContentType) {
processAdditionalOperations(resourcePath, "x-get", "get", ops, path);
processAdditionalOperations(resourcePath, "x-head", "head", ops, path);
processAdditionalOperations(resourcePath, "x-put", "put", ops, path);
processAdditionalOperations(resourcePath, "x-post", "post", ops, path);
processAdditionalOperations(resourcePath, "x-delete", "delete", ops, path);
processAdditionalOperations(resourcePath, "x-patch", "patch", ops, path);
processAdditionalOperations(resourcePath, "x-options", "options", ops, path);
processAdditionalOperations(resourcePath, "x-trace", "trace", ops, path);
}
}
return ops;
}

protected void processAdditionalOperations(String resourcePath, String extName, String httpMethod, Map<String, List<CodegenOperation>> ops, PathItem path) {
if (path.getExtensions() == null || !path.getExtensions().containsKey(extName)) {
return;
}
var xOps = (List<Operation>) path.getExtensions().get(extName);
if (xOps == null) {
return;
}
for (Operation op : xOps) {
processOperation(resourcePath, httpMethod, op, ops, path);
}
}

public Map<String, List<CodegenOperation>> processWebhooks(Map<String, PathItem> webhooks) {
Map<String, List<CodegenOperation>> ops = new TreeMap<>();
// when input file is not valid and doesn't contain any paths
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures;
import org.openapitools.codegen.meta.features.*;
Expand Down Expand Up @@ -194,6 +195,8 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
public AbstractJavaCodegen() {
super();

GlobalSettings.setProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "true");

modifyFeatureSet(features -> features
.includeDocumentationFeatures(DocumentationFeature.Readme)
.wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.templating.mustache.EscapeChar;
Expand Down Expand Up @@ -89,6 +90,7 @@ public abstract class AbstractKotlinCodegen extends DefaultCodegen implements Co
public AbstractKotlinCodegen() {
super();

GlobalSettings.setProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "true");
supportsInheritance = true;
setSortModelPropertiesByRequiredFlag(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.java.assertions.JavaFileAssert;
import org.openapitools.codegen.languages.JavaMicronautClientCodegen;
import org.openapitools.codegen.testutils.ConfigAssert;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.io.File;
Expand All @@ -21,7 +21,6 @@
import static org.openapitools.codegen.TestUtils.newTempFolder;
import static org.testng.Assert.assertEquals;


public class JavaMicronautClientCodegenTest extends AbstractMicronautCodegenTest {
@Test
public void clientOptsUnicity() {
Expand Down Expand Up @@ -457,4 +456,27 @@ public void testConfigurePathSeparator() {
.hasAnnotation("JacksonXmlProperty", Map.of("localName", "\"item\""))
.hasAnnotation("JacksonXmlElementWrapper", Map.of("localName", "\"activities-array\""));
}

@Test
public void testMultipleContentTypesToPath() {

GlobalSettings.setProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "true");

var codegen = new JavaMicronautClientCodegen();
String outputPath = generateFiles(codegen, "src/test/resources/3_0/java/multiple-content-types.yaml", CodegenConstants.APIS, CodegenConstants.MODELS);

// Micronaut declarative http client should use the provided path separator
assertFileContains(outputPath + "/src/main/java/org/openapitools/api/DefaultApi.java",
" @Post(uri=\"/multiplecontentpath\")\n" +
" @Produces({\"application/json\"})\n" +
" Mono<Void> myOp(\n" +
" @Body @Nullable @Valid Coordinates coordinates\n" +
" );\n",
" @Post(uri=\"/multiplecontentpath\")\n" +
" @Produces({\"multipart/form-data\"})\n" +
" Mono<Void> myOp_1(\n" +
" @Body @Nullable @Valid MyOpRequest myOpRequest\n" +
" );"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.openapitools.codegen.java.assertions.JavaFileAssert;
import org.openapitools.codegen.languages.JavaMicronautServerCodegen;
import org.openapitools.codegen.testutils.ConfigAssert;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.io.File;
Expand Down Expand Up @@ -487,4 +486,35 @@ public void doRepeatOperationForAllTags() {
.hasAnnotation("JacksonXmlProperty", Map.of("localName", "\"item\""))
.hasAnnotation("JacksonXmlElementWrapper", Map.of("localName", "\"activities-array\""));
}

@Test
public void testMultipleContentTypesToPath() {

var codegen = new JavaMicronautServerCodegen();
String outputPath = generateFiles(codegen, "src/test/resources/3_0/java/multiple-content-types.yaml", CodegenConstants.APIS, CodegenConstants.MODELS);

// Micronaut declarative http client should use the provided path separator
assertFileContains(outputPath + "/src/main/java/org/openapitools/controller/DefaultController.java",
" @Post(uri=\"/multiplecontentpath\")\n" +
" @Produces(value = {})\n" +
" @Consumes(value = {\"application/json\"})\n" +
" @Secured({SecurityRule.IS_ANONYMOUS})\n" +
" public Mono<Void> myOp(\n" +
" @Body @Nullable @Valid Coordinates coordinates\n" +
" ) {\n" +
" // TODO implement myOp();\n" +
" return Mono.error(new HttpStatusException(HttpStatus.NOT_IMPLEMENTED, null));\n" +
" }",
" @Post(uri=\"/multiplecontentpath\")\n" +
" @Produces(value = {})\n" +
" @Consumes(value = {\"multipart/form-data\"})\n" +
" @Secured({SecurityRule.IS_ANONYMOUS})\n" +
" public Mono<Void> myOp_1(\n" +
" @Body @Nullable @Valid MyOpRequest myOpRequest\n" +
" ) {\n" +
" // TODO implement myOp_1();\n" +
" return Mono.error(new HttpStatusException(HttpStatus.NOT_IMPLEMENTED, null));\n" +
" }"
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
openapi: 3.0.3
info:
version: "1"
title: Multiple Content Types for same request
paths:
/multiplecontentpath:
post:
operationId: myOp
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/coordinates'
multipart/form-data:
schema:
type: object
properties:
coordinates:
$ref: '#/components/schemas/coordinates'
file:
type: string
format: binary
responses:
201:
description: Successfully created
headers:
Location:
schema:
type: string
components:
schemas:
coordinates:
type: object
required:
- lat
- long
properties:
lat:
type: number
long:
type: number

0 comments on commit 7710ce5

Please sign in to comment.