Skip to content

Commit 127a9ae

Browse files
committed
Divide operations by content type
Fixed #17877
1 parent e9c3c63 commit 127a9ae

File tree

8 files changed

+287
-13
lines changed

8 files changed

+287
-13
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,4 +453,6 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case,
453453
public static final String WAIT_TIME_OF_THREAD = "waitTimeMillis";
454454

455455
public static final String USE_DEFAULT_VALUES_FOR_REQUIRED_VARS = "useDefaultValuesForRequiredVars";
456+
457+
public static final String DIVIDE_OPERATIONS_BY_CONTENT_TYPE = "divideOperationsByContentType";
456458
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
import java.util.stream.Collectors;
8787
import java.util.stream.Stream;
8888

89+
import static org.openapitools.codegen.CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE;
8990
import static org.openapitools.codegen.CodegenConstants.UNSUPPORTED_V310_SPEC_MSG;
9091
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
9192
import static org.openapitools.codegen.utils.OnceLogger.once;
@@ -392,7 +393,7 @@ public void processOpts() {
392393
convertPropertyToBooleanAndWriteBack(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, this::setDisallowAdditionalPropertiesIfNotPresent);
393394
convertPropertyToBooleanAndWriteBack(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, this::setEnumUnknownDefaultCase);
394395
convertPropertyToBooleanAndWriteBack(CodegenConstants.AUTOSET_CONSTANTS, this::setAutosetConstants);
395-
}
396+
}
396397

397398

398399
/***
@@ -999,6 +1000,49 @@ public void postProcessParameter(CodegenParameter parameter) {
9991000
@Override
10001001
@SuppressWarnings("unused")
10011002
public void preprocessOpenAPI(OpenAPI openAPI) {
1003+
1004+
var divideOperationsByContentType = Boolean.parseBoolean(GlobalSettings.getProperty(DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "false"));
1005+
1006+
if (divideOperationsByContentType && openAPI.getPaths() != null && !openAPI.getPaths().isEmpty()) {
1007+
1008+
for (Map.Entry<String, PathItem> entry : openAPI.getPaths().entrySet()) {
1009+
String pathStr = entry.getKey();
1010+
PathItem path = entry.getValue();
1011+
List<Operation> getOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.GET, path.getGet());
1012+
if (!getOps.isEmpty()) {
1013+
path.addExtension("x-get", getOps);
1014+
}
1015+
List<Operation> putOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.PUT, path.getPut());
1016+
if (!putOps.isEmpty()) {
1017+
path.addExtension("x-put", putOps);
1018+
}
1019+
List<Operation> postOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.POST, path.getPost());
1020+
if (!postOps.isEmpty()) {
1021+
path.addExtension("x-post", postOps);
1022+
}
1023+
List<Operation> deleteOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.DELETE, path.getDelete());
1024+
if (!deleteOps.isEmpty()) {
1025+
path.addExtension("x-delete", deleteOps);
1026+
}
1027+
List<Operation> optionsOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.OPTIONS, path.getOptions());
1028+
if (!optionsOps.isEmpty()) {
1029+
path.addExtension("x-options", optionsOps);
1030+
}
1031+
List<Operation> headOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.HEAD, path.getHead());
1032+
if (!headOps.isEmpty()) {
1033+
path.addExtension("x-head", headOps);
1034+
}
1035+
List<Operation> patchOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.PATCH, path.getPatch());
1036+
if (!patchOps.isEmpty()) {
1037+
path.addExtension("x-patch", patchOps);
1038+
}
1039+
List<Operation> traceOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.TRACE, path.getTrace());
1040+
if (!traceOps.isEmpty()) {
1041+
path.addExtension("x-trace", traceOps);
1042+
}
1043+
}
1044+
}
1045+
10021046
if (useOneOfInterfaces && openAPI.getComponents() != null) {
10031047
// we process the openapi schema here to find oneOf schemas and create interface models for them
10041048
Map<String, Schema> schemas = new HashMap<>(openAPI.getComponents().getSchemas());
@@ -1080,6 +1124,77 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
10801124
}
10811125
}
10821126

1127+
private List<Operation> divideOperationsByContentType(String path, PathItem.HttpMethod httpMethod, Operation op) {
1128+
1129+
if (op == null) {
1130+
return Collections.emptyList();
1131+
}
1132+
1133+
var additionalOps = new ArrayList<Operation>();
1134+
1135+
RequestBody body = op.getRequestBody();
1136+
if (body == null || body.getContent() == null) {
1137+
return Collections.emptyList();
1138+
}
1139+
Content content = body.getContent();
1140+
if (content.size() <= 1) {
1141+
return Collections.emptyList();
1142+
}
1143+
var firstEntry = content.entrySet().iterator().next();
1144+
var mediaTypesToRemove = new ArrayList<String>();
1145+
for (var entry : content.entrySet()) {
1146+
if (mediaTypesToRemove.contains(entry.getKey()) || entry.getKey().equals(firstEntry.getKey()) || entry.getValue().equals(firstEntry.getValue())) {
1147+
continue;
1148+
}
1149+
var foundSameOpSignature = false;
1150+
for (var additionalOp : additionalOps) {
1151+
RequestBody additionalBody = additionalOp.getRequestBody();
1152+
if (additionalBody == null || additionalBody.getContent() == null) {
1153+
return Collections.emptyList();
1154+
}
1155+
for (var addContentEntry : additionalBody.getContent().entrySet()) {
1156+
if (addContentEntry.getValue().equals(entry.getValue())) {
1157+
foundSameOpSignature = true;
1158+
break;
1159+
}
1160+
}
1161+
if (foundSameOpSignature) {
1162+
additionalBody.getContent().put(entry.getKey(), entry.getValue());
1163+
break;
1164+
}
1165+
}
1166+
mediaTypesToRemove.add(entry.getKey());
1167+
if (foundSameOpSignature) {
1168+
continue;
1169+
}
1170+
additionalOps.add(new Operation()
1171+
.deprecated(op.getDeprecated())
1172+
.callbacks(op.getCallbacks())
1173+
.description(op.getDescription())
1174+
.extensions(op.getExtensions())
1175+
.externalDocs(op.getExternalDocs())
1176+
.operationId(getOrGenerateOperationId(op, path, httpMethod.name()))
1177+
.parameters(op.getParameters())
1178+
.responses(op.getResponses())
1179+
.security(op.getSecurity())
1180+
.servers(op.getServers())
1181+
.summary(op.getSummary())
1182+
.tags(op.getTags())
1183+
.requestBody(new RequestBody()
1184+
.description(body.getDescription())
1185+
.extensions(body.getExtensions())
1186+
.content(new Content()
1187+
.addMediaType(entry.getKey(), entry.getValue()))
1188+
)
1189+
);
1190+
}
1191+
if (!mediaTypesToRemove.isEmpty()) {
1192+
content.entrySet().removeIf(stringMediaTypeEntry -> mediaTypesToRemove.contains(stringMediaTypeEntry.getKey()));
1193+
}
1194+
1195+
return additionalOps;
1196+
}
1197+
10831198
// override with any special handling of the entire OpenAPI spec document
10841199
@Override
10851200
@SuppressWarnings("unused")
@@ -1164,8 +1279,7 @@ public String encodePath(String input) {
11641279
*/
11651280
@Override
11661281
public String escapeUnsafeCharacters(String input) {
1167-
LOGGER.warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape " +
1168-
"unsafe characters");
1282+
LOGGER.warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape unsafe characters");
11691283
// doing nothing by default and code generator should implement
11701284
// the logic to prevent code injection
11711285
// later we'll make this method abstract to make sure
@@ -1181,8 +1295,7 @@ public String escapeUnsafeCharacters(String input) {
11811295
*/
11821296
@Override
11831297
public String escapeQuotationMark(String input) {
1184-
LOGGER.warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape " +
1185-
"single/double quote");
1298+
LOGGER.warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape single/double quote");
11861299
return input.replace("\"", "\\\"");
11871300
}
11881301

modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -616,10 +616,10 @@ private void generateModelsForVariable(List<File> files, List<ModelMap> allModel
616616
if (!processedModels.contains(key) && allSchemas.containsKey(key)) {
617617
generateModels(files, allModels, unusedModels, aliasModels, processedModels, () -> Set.of(key));
618618
} else {
619-
LOGGER.info("Type " + variable.getComplexType()+" of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
619+
LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getComplexType(), variable.getName());
620620
}
621621
} else {
622-
LOGGER.info("Type " + variable.getOpenApiType()+" of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
622+
LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getComplexType(), variable.getName());
623623
}
624624
}
625625

@@ -1012,7 +1012,7 @@ private void generateOpenapiGeneratorIgnoreFile() {
10121012
File ignoreFile = new File(ignoreFileNameTarget);
10131013
// use the entries provided by the users to pre-populate .openapi-generator-ignore
10141014
try {
1015-
LOGGER.info("Writing file " + ignoreFileNameTarget + " (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)");
1015+
LOGGER.info("Writing file {} (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)", ignoreFileNameTarget);
10161016
new File(config.outputFolder()).mkdirs();
10171017
if (!ignoreFile.createNewFile()) {
10181018
// file may already exist, do nothing
@@ -1475,6 +1475,9 @@ public Map<String, List<CodegenOperation>> processPaths(Paths paths) {
14751475
if (paths == null) {
14761476
return ops;
14771477
}
1478+
1479+
var divideOperationsByContentType = Boolean.parseBoolean(GlobalSettings.getProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "false"));
1480+
14781481
for (Map.Entry<String, PathItem> pathsEntry : paths.entrySet()) {
14791482
String resourcePath = pathsEntry.getKey();
14801483
PathItem path = pathsEntry.getValue();
@@ -1486,10 +1489,34 @@ public Map<String, List<CodegenOperation>> processPaths(Paths paths) {
14861489
processOperation(resourcePath, "patch", path.getPatch(), ops, path);
14871490
processOperation(resourcePath, "options", path.getOptions(), ops, path);
14881491
processOperation(resourcePath, "trace", path.getTrace(), ops, path);
1492+
1493+
if (divideOperationsByContentType) {
1494+
processAdditionalOperations(resourcePath, "x-get", "get", ops, path);
1495+
processAdditionalOperations(resourcePath, "x-head", "head", ops, path);
1496+
processAdditionalOperations(resourcePath, "x-put", "put", ops, path);
1497+
processAdditionalOperations(resourcePath, "x-post", "post", ops, path);
1498+
processAdditionalOperations(resourcePath, "x-delete", "delete", ops, path);
1499+
processAdditionalOperations(resourcePath, "x-patch", "patch", ops, path);
1500+
processAdditionalOperations(resourcePath, "x-options", "options", ops, path);
1501+
processAdditionalOperations(resourcePath, "x-trace", "trace", ops, path);
1502+
}
14891503
}
14901504
return ops;
14911505
}
14921506

1507+
protected void processAdditionalOperations(String resourcePath, String extName, String httpMethod, Map<String, List<CodegenOperation>> ops, PathItem path) {
1508+
if (path.getExtensions() == null || !path.getExtensions().containsKey(extName)) {
1509+
return;
1510+
}
1511+
var xOps = (List<Operation>) path.getExtensions().get(extName);
1512+
if (xOps == null) {
1513+
return;
1514+
}
1515+
for (Operation op : xOps) {
1516+
processOperation(resourcePath, httpMethod, op, ops, path);
1517+
}
1518+
}
1519+
14931520
public Map<String, List<CodegenOperation>> processWebhooks(Map<String, PathItem> webhooks) {
14941521
Map<String, List<CodegenOperation>> ops = new TreeMap<>();
14951522
// when input file is not valid and doesn't contain any paths

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.apache.commons.lang3.StringUtils;
3939
import org.apache.commons.text.StringEscapeUtils;
4040
import org.openapitools.codegen.*;
41+
import org.openapitools.codegen.config.GlobalSettings;
4142
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
4243
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures;
4344
import org.openapitools.codegen.meta.features.*;
@@ -193,6 +194,8 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
193194
public AbstractJavaCodegen() {
194195
super();
195196

197+
GlobalSettings.setProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "true");
198+
196199
modifyFeatureSet(features -> features
197200
.includeDocumentationFeatures(DocumentationFeature.Readme)
198201
.wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML))

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.apache.commons.io.FilenameUtils;
2828
import org.apache.commons.lang3.StringUtils;
2929
import org.openapitools.codegen.*;
30+
import org.openapitools.codegen.config.GlobalSettings;
3031
import org.openapitools.codegen.model.ModelMap;
3132
import org.openapitools.codegen.model.ModelsMap;
3233
import org.openapitools.codegen.templating.mustache.EscapeChar;
@@ -88,6 +89,7 @@ public abstract class AbstractKotlinCodegen extends DefaultCodegen implements Co
8889
public AbstractKotlinCodegen() {
8990
super();
9091

92+
GlobalSettings.setProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "true");
9193
supportsInheritance = true;
9294
setSortModelPropertiesByRequiredFlag(true);
9395

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautClientCodegenTest.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
import org.openapitools.codegen.CodegenConstants;
88
import org.openapitools.codegen.DefaultGenerator;
99
import org.openapitools.codegen.config.CodegenConfigurator;
10+
import org.openapitools.codegen.config.GlobalSettings;
1011
import org.openapitools.codegen.java.assertions.JavaFileAssert;
1112
import org.openapitools.codegen.languages.JavaMicronautClientCodegen;
1213
import org.openapitools.codegen.testutils.ConfigAssert;
13-
import org.testng.Assert;
1414
import org.testng.annotations.Test;
1515

1616
import java.io.File;
@@ -21,7 +21,6 @@
2121
import static org.openapitools.codegen.TestUtils.newTempFolder;
2222
import static org.testng.Assert.assertEquals;
2323

24-
2524
public class JavaMicronautClientCodegenTest extends AbstractMicronautCodegenTest {
2625
@Test
2726
public void clientOptsUnicity() {
@@ -323,7 +322,8 @@ public void testConfigurePathSeparator() {
323322
* Includes regression tests for:
324323
* - <a href="https://github.com/OpenAPITools/openapi-generator/issues/2417">Correct Jackson annotation when `wrapped: false`</a>
325324
*/
326-
@Test public void shouldGenerateCorrectXmlAnnotations() {
325+
@Test
326+
public void shouldGenerateCorrectXmlAnnotations() {
327327
// Arrange
328328
final CodegenConfigurator config = new CodegenConfigurator()
329329
.addAdditionalProperty(CodegenConstants.WITH_XML, true)
@@ -333,7 +333,7 @@ public void testConfigurePathSeparator() {
333333
.setGeneratorName(JavaMicronautClientCodegen.NAME)
334334
.setInputSpec("src/test/resources/3_0/java/xml-annotations-test.yaml")
335335
.setOutputDir(newTempFolder().toString());
336-
336+
337337
// Act
338338
final List<File> files = new DefaultGenerator().opts(config.toClientOptInput()).generate();
339339

@@ -457,4 +457,33 @@ public void testConfigurePathSeparator() {
457457
.hasAnnotation("JacksonXmlProperty", Map.of("localName", "\"item\""))
458458
.hasAnnotation("JacksonXmlElementWrapper", Map.of("localName", "\"activities-array\""));
459459
}
460+
461+
@Test
462+
public void testMultipleContentTypesToPath() {
463+
464+
GlobalSettings.setProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "true");
465+
466+
var codegen = new JavaMicronautClientCodegen();
467+
String outputPath = generateFiles(codegen, "src/test/resources/3_0/java/multiple-content-types.yaml", CodegenConstants.APIS, CodegenConstants.MODELS);
468+
469+
// Micronaut declarative http client should use the provided path separator
470+
assertFileContains(outputPath + "/src/main/java/org/openapitools/api/DefaultApi.java",
471+
" @Post(uri=\"/multiplecontentpath\")\n" +
472+
" @Produces({\"application/json\", \"application/xml\"})\n" +
473+
" Mono<Void> myOp(\n" +
474+
" @Body @Nullable @Valid Coordinates coordinates\n" +
475+
" );\n",
476+
" @Post(uri=\"/multiplecontentpath\")\n" +
477+
" @Produces({\"multipart/form-data\"})\n" +
478+
" Mono<Void> myOp_1(\n" +
479+
" @Nullable @Valid Coordinates coordinates, \n" +
480+
" @Nullable File _file\n" +
481+
" );",
482+
" @Post(uri=\"/multiplecontentpath\")\n" +
483+
" @Produces({\"application/yaml\", \"text/json\"})\n" +
484+
" Mono<Void> myOp_2(\n" +
485+
" @Body @Nullable @Valid MySchema mySchema\n" +
486+
" );"
487+
);
488+
}
460489
}

0 commit comments

Comments
 (0)