Skip to content

Commit b9460eb

Browse files
committed
Divide operations by request and response content types
Fixed #17877
1 parent 2fb59ff commit b9460eb

10 files changed

Lines changed: 686 additions & 23 deletions

File tree

docs/generators/groovy.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
6969
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
7070
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |false|
7171
|withXml|whether to include support for application/xml content type and include XML annotations in the model (works with libraries that provide support for JSON and XML)| |false|
72+
|groupByResponseContentType| Group server or client methods by response content types. For example, when openapi operation produces one of "application/json" and "application/xml" content types will be generated only one method for both content types. Otherwise for each content type will be generated different method. **Available only for generatos with supportsDividingOperationsByContentType** | |true|
73+
|groupByRequestAndResponseContentType| Group server or client methods by request body and response content types. For example, when openapi operation consumes "application/json" and "application/xml" content type and also api response has content with the same content types, 2 different methods will be generated. The content of the request and response types will match. Otherwise, will be generated 4 methods - for each combination of request body content type and response content type. **Available only for generatos with supportsDividingOperationsByContentType** | |true|
7274

7375
## SUPPORTED VENDOR EXTENSIONS
7476

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,4 +366,5 @@ public interface CodegenConfig {
366366

367367
Set<String> getOpenapiGeneratorIgnoreList();
368368

369+
boolean supportsDividingOperationsByContentType();
369370
}

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

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

456456
public static final String USE_DEFAULT_VALUES_FOR_REQUIRED_VARS = "useDefaultValuesForRequiredVars";
457+
458+
public static final String GROUP_BY_RESPONSE_CONTENT_TYPE = "groupByResponseContentType";
459+
public static final String GROUP_BY_RESPONSE_CONTENT_TYPE_DESC =
460+
"Group server or client methods by response content types. "
461+
+ "For example, when openapi operation produces one of \"application/json\" and \"application/xml\" content types "
462+
+ "will be generated only one method for both content types. Otherwise for each content type will be generated different method.";
463+
464+
public static final String GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE = "groupByRequestAndResponseContentType";
465+
public static final String GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE_DESC =
466+
"Group server or client methods by request body and response content types. "
467+
+ "For example, when openapi operation consumes \"application/json\" and \"application/xml\" content type and also api response "
468+
+ "has content with the same content types, 2 different methods will be generated. The content of the request and response types will match. "
469+
+ "Otherwise, will be generated 4 methods - for each combination of request body content type and response content type.";
457470
}

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

Lines changed: 250 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ apiTemplateFiles are for API outputs only (controllers/handlers).
332332

333333
// Whether to automatically hardcode params that are considered Constants by OpenAPI Spec
334334
@Setter protected boolean autosetConstants = false;
335+
@Setter
336+
protected boolean groupByRequestAndResponseContentType = true;
337+
@Setter
338+
protected boolean groupByResponseContentType = true;
335339

336340
@Override
337341
public boolean getAddSuffixToDuplicateOperationNicknames() {
@@ -391,9 +395,10 @@ public void processOpts() {
391395
convertPropertyToBooleanAndWriteBack(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, this::setDisallowAdditionalPropertiesIfNotPresent);
392396
convertPropertyToBooleanAndWriteBack(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, this::setEnumUnknownDefaultCase);
393397
convertPropertyToBooleanAndWriteBack(CodegenConstants.AUTOSET_CONSTANTS, this::setAutosetConstants);
398+
convertPropertyToBooleanAndWriteBack(CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE, this::setGroupByRequestAndResponseContentType);
399+
convertPropertyToBooleanAndWriteBack(CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE, this::setGroupByResponseContentType);
394400
}
395401

396-
397402
/***
398403
* Preset map builder with commonly used Mustache lambdas.
399404
*
@@ -907,7 +912,7 @@ public String toEnumValue(String value, String datatype) {
907912
* @return the sanitized variable name for enum
908913
*/
909914
public String toEnumVarName(String value, String datatype) {
910-
if (value.length() == 0) {
915+
if (value.isEmpty()) {
911916
return "EMPTY";
912917
}
913918

@@ -1008,6 +1013,47 @@ public void postProcessParameter(CodegenParameter parameter) {
10081013
@Override
10091014
@SuppressWarnings("unused")
10101015
public void preprocessOpenAPI(OpenAPI openAPI) {
1016+
1017+
if (supportsDividingOperationsByContentType() && openAPI.getPaths() != null && !openAPI.getPaths().isEmpty()) {
1018+
1019+
for (Map.Entry<String, PathItem> entry : openAPI.getPaths().entrySet()) {
1020+
String pathStr = entry.getKey();
1021+
PathItem path = entry.getValue();
1022+
List<Operation> getOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.GET, path.getGet());
1023+
if (!getOps.isEmpty()) {
1024+
path.addExtension("x-get", getOps);
1025+
}
1026+
List<Operation> putOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.PUT, path.getPut());
1027+
if (!putOps.isEmpty()) {
1028+
path.addExtension("x-put", putOps);
1029+
}
1030+
List<Operation> postOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.POST, path.getPost());
1031+
if (!postOps.isEmpty()) {
1032+
path.addExtension("x-post", postOps);
1033+
}
1034+
List<Operation> deleteOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.DELETE, path.getDelete());
1035+
if (!deleteOps.isEmpty()) {
1036+
path.addExtension("x-delete", deleteOps);
1037+
}
1038+
List<Operation> optionsOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.OPTIONS, path.getOptions());
1039+
if (!optionsOps.isEmpty()) {
1040+
path.addExtension("x-options", optionsOps);
1041+
}
1042+
List<Operation> headOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.HEAD, path.getHead());
1043+
if (!headOps.isEmpty()) {
1044+
path.addExtension("x-head", headOps);
1045+
}
1046+
List<Operation> patchOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.PATCH, path.getPatch());
1047+
if (!patchOps.isEmpty()) {
1048+
path.addExtension("x-patch", patchOps);
1049+
}
1050+
List<Operation> traceOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.TRACE, path.getTrace());
1051+
if (!traceOps.isEmpty()) {
1052+
path.addExtension("x-trace", traceOps);
1053+
}
1054+
}
1055+
}
1056+
10111057
if (useOneOfInterfaces && openAPI.getComponents() != null) {
10121058
// we process the openapi schema here to find oneOf schemas and create interface models for them
10131059
Map<String, Schema> schemas = new HashMap<>(openAPI.getComponents().getSchemas());
@@ -1089,6 +1135,190 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
10891135
}
10901136
}
10911137

1138+
private List<Operation> divideOperationsByContentType(String path, PathItem.HttpMethod httpMethod, Operation op) {
1139+
1140+
if (op == null) {
1141+
return Collections.emptyList();
1142+
}
1143+
1144+
var additionalOps = new ArrayList<Operation>();
1145+
divideOperationByRequestBody(path, httpMethod, op, additionalOps);
1146+
1147+
// Check responses content types and divide operations by them
1148+
1149+
var responses = op.getResponses();
1150+
if (responses == null || responses.isEmpty()) {
1151+
return additionalOps;
1152+
}
1153+
var allPossibleContentTypes = new ArrayList<String>();
1154+
for (var responseEntry : responses.entrySet()) {
1155+
var apiResponse = responseEntry.getValue();
1156+
if (apiResponse.getContent() == null) {
1157+
continue;
1158+
}
1159+
for (var contentType : apiResponse.getContent().keySet()) {
1160+
contentType = contentType.toLowerCase();
1161+
if (!allPossibleContentTypes.contains(contentType)) {
1162+
allPossibleContentTypes.add(contentType);
1163+
}
1164+
}
1165+
}
1166+
if (allPossibleContentTypes.isEmpty() || allPossibleContentTypes.size() == 1) {
1167+
return additionalOps;
1168+
}
1169+
1170+
var apiResponsesByContentType = new HashMap<String, ApiResponses>();
1171+
for (var contentType : allPossibleContentTypes) {
1172+
var apiResponses = new ApiResponses();
1173+
for (var responseEntry : responses.entrySet()) {
1174+
var code = responseEntry.getKey();
1175+
var response = responseEntry.getValue();
1176+
if (response.getContent() == null) {
1177+
continue;
1178+
}
1179+
var mediaType = response.getContent().get(contentType);
1180+
if (mediaType == null) {
1181+
continue;
1182+
}
1183+
apiResponses.addApiResponse(code, new ApiResponse()
1184+
.description(response.getDescription())
1185+
.headers(response.getHeaders())
1186+
.links(response.getLinks())
1187+
.extensions(response.getExtensions())
1188+
.$ref(response.get$ref())
1189+
.content(new Content()
1190+
.addMediaType(contentType, mediaType)
1191+
)
1192+
);
1193+
}
1194+
apiResponsesByContentType.put(contentType, apiResponses);
1195+
}
1196+
1197+
var finalAdditionalOps = new ArrayList<Operation>();
1198+
divideOperationByResponses(path, httpMethod, op, apiResponsesByContentType, finalAdditionalOps);
1199+
for (var additionalOp : additionalOps) {
1200+
finalAdditionalOps.add(additionalOp);
1201+
divideOperationByResponses(path, httpMethod, additionalOp, apiResponsesByContentType, finalAdditionalOps);
1202+
}
1203+
1204+
return finalAdditionalOps;
1205+
}
1206+
1207+
private void divideOperationByRequestBody(String path, PathItem.HttpMethod httpMethod, Operation op, List<Operation> additionalOps) {
1208+
RequestBody body = op.getRequestBody();
1209+
if (body == null || body.getContent() == null) {
1210+
return;
1211+
}
1212+
Content content = body.getContent();
1213+
if (content.size() <= 1) {
1214+
return;
1215+
}
1216+
var firstEntry = content.entrySet().iterator().next();
1217+
var mediaTypesToRemove = new ArrayList<String>();
1218+
for (var entry : content.entrySet()) {
1219+
var contentType = entry.getKey();
1220+
MediaType mediaType = entry.getValue();
1221+
if (mediaTypesToRemove.contains(contentType) || contentType.equals(firstEntry.getKey())) {
1222+
continue;
1223+
}
1224+
var foundSameOpSignature = false;
1225+
// group by response content type
1226+
if (groupByResponseContentType) {
1227+
for (var additionalOp : additionalOps) {
1228+
RequestBody additionalBody = additionalOp.getRequestBody();
1229+
if (additionalBody == null || additionalBody.getContent() == null) {
1230+
return;
1231+
}
1232+
for (var addContentEntry : additionalBody.getContent().entrySet()) {
1233+
if (addContentEntry.getValue().equals(mediaType)) {
1234+
foundSameOpSignature = true;
1235+
break;
1236+
}
1237+
}
1238+
if (foundSameOpSignature) {
1239+
additionalBody.getContent().put(contentType, mediaType);
1240+
break;
1241+
}
1242+
}
1243+
}
1244+
1245+
mediaTypesToRemove.add(contentType);
1246+
if (groupByResponseContentType && foundSameOpSignature) {
1247+
continue;
1248+
}
1249+
1250+
var apiResponsesCopy = new ApiResponses();
1251+
apiResponsesCopy.putAll(op.getResponses());
1252+
1253+
additionalOps.add(new Operation()
1254+
.deprecated(op.getDeprecated())
1255+
.callbacks(op.getCallbacks())
1256+
.description(op.getDescription())
1257+
.extensions(op.getExtensions())
1258+
.externalDocs(op.getExternalDocs())
1259+
.operationId(getOrGenerateOperationId(op, path, httpMethod.name()))
1260+
.parameters(op.getParameters())
1261+
.responses(apiResponsesCopy)
1262+
.security(op.getSecurity())
1263+
.servers(op.getServers())
1264+
.summary(op.getSummary())
1265+
.tags(op.getTags())
1266+
.requestBody(new RequestBody()
1267+
.description(body.getDescription())
1268+
.extensions(body.getExtensions())
1269+
.content(new Content()
1270+
.addMediaType(contentType, mediaType))
1271+
)
1272+
);
1273+
}
1274+
if (!mediaTypesToRemove.isEmpty()) {
1275+
content.entrySet().removeIf(stringMediaTypeEntry -> mediaTypesToRemove.contains(stringMediaTypeEntry.getKey()));
1276+
}
1277+
}
1278+
1279+
private void divideOperationByResponses(
1280+
String path,
1281+
PathItem.HttpMethod httpMethod,
1282+
Operation op,
1283+
Map<String, ApiResponses> apiResponsesByContentType,
1284+
List<Operation> additionalOps
1285+
) {
1286+
var isFirst = true;
1287+
for (var entry : apiResponsesByContentType.entrySet()) {
1288+
var contentType = entry.getKey();
1289+
var apiResponses = entry.getValue();
1290+
var requestBody = op.getRequestBody();
1291+
// group by requestBody contentType
1292+
if (groupByRequestAndResponseContentType
1293+
&& requestBody != null
1294+
&& requestBody.getContent() != null
1295+
&& !requestBody.getContent().containsKey(contentType)) {
1296+
continue;
1297+
}
1298+
if (isFirst) {
1299+
op.setResponses(apiResponses);
1300+
isFirst = false;
1301+
continue;
1302+
}
1303+
1304+
additionalOps.add(new Operation()
1305+
.deprecated(op.getDeprecated())
1306+
.callbacks(op.getCallbacks())
1307+
.description(op.getDescription())
1308+
.extensions(op.getExtensions())
1309+
.externalDocs(op.getExternalDocs())
1310+
.operationId(getOrGenerateOperationId(op, path, httpMethod.name()))
1311+
.parameters(op.getParameters())
1312+
.responses(apiResponses)
1313+
.security(op.getSecurity())
1314+
.servers(op.getServers())
1315+
.summary(op.getSummary())
1316+
.tags(op.getTags())
1317+
.requestBody(requestBody)
1318+
);
1319+
}
1320+
}
1321+
10921322
// override with any special handling of the entire OpenAPI spec document
10931323
@Override
10941324
@SuppressWarnings("unused")
@@ -1188,8 +1418,7 @@ public String encodePath(String input) {
11881418
*/
11891419
@Override
11901420
public String escapeUnsafeCharacters(String input) {
1191-
LOGGER.warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape " +
1192-
"unsafe characters");
1421+
LOGGER.warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape unsafe characters");
11931422
// doing nothing by default and code generator should implement
11941423
// the logic to prevent code injection
11951424
// later we'll make this method abstract to make sure
@@ -1205,8 +1434,7 @@ public String escapeUnsafeCharacters(String input) {
12051434
*/
12061435
@Override
12071436
public String escapeQuotationMark(String input) {
1208-
LOGGER.warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape " +
1209-
"single/double quote");
1437+
LOGGER.warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape single/double quote");
12101438
return input.replace("\"", "\\\"");
12111439
}
12121440

@@ -1779,6 +2007,12 @@ public DefaultCodegen() {
17792007
// option to change the order of form/body parameter
17802008
cliOptions.add(CliOption.newBoolean(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS,
17812009
CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS_DESC).defaultValue(Boolean.FALSE.toString()));
2010+
if (supportsDividingOperationsByContentType()) {
2011+
cliOptions.add(CliOption.newBoolean(CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE,
2012+
CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE_DESC).defaultValue(Boolean.TRUE.toString()));
2013+
cliOptions.add(CliOption.newBoolean(CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE,
2014+
CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE_DESC).defaultValue(Boolean.TRUE.toString()));
2015+
}
17822016

17832017
// option to change how we process + set the data in the discriminator mapping
17842018
CliOption legacyDiscriminatorBehaviorOpt = CliOption.newBoolean(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR_DESC).defaultValue(Boolean.TRUE.toString());
@@ -8596,11 +8830,16 @@ public boolean isTypeErasedGenerics() {
85968830
return false;
85978831
}
85988832

8599-
/*
8600-
A function to convert yaml or json ingested strings like property names
8601-
And convert special characters like newline, tab, carriage return
8602-
Into strings that can be rendered in the language that the generator will output to
8603-
*/
8833+
@Override
8834+
public boolean supportsDividingOperationsByContentType() {
8835+
return false;
8836+
}
8837+
8838+
/**
8839+
* A function to convert yaml or json ingested strings like property names
8840+
* And convert special characters like newline, tab, carriage return
8841+
* Into strings that can be rendered in the language that the generator will output to
8842+
*/
86048843
protected String handleSpecialCharacters(String name) {
86058844
return name;
86068845
}

0 commit comments

Comments
 (0)