Skip to content

Commit 6ddd97b

Browse files
committed
Divide operations by request and response content types
Fixed #17877
1 parent 30ff0d7 commit 6ddd97b

File tree

8 files changed

+418
-15
lines changed

8 files changed

+418
-15
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,4 +453,19 @@ 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";
458+
459+
public static final String GROUP_BY_RESPONSE_CONTENT_TYPE = "groupByResponseContentType";
460+
public static final String GROUP_BY_RESPONSE_CONTENT_TYPE_DESC =
461+
"Group server or client methods by response content types. "
462+
+ "For example, when openapi operation produces one of \"application/json\" and \"application/xml\" content types "
463+
+ "will be generated only one method for both content types. Otherwise for each content type will be generated different method.";
464+
465+
public static final String GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE = "groupByRequestAndResponseContentType";
466+
public static final String GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE_DESC =
467+
"Group server or client methods by request body and response content types. "
468+
+ "For example, when openapi operation consumes \"application/json\" and \"application/xml\" content type and also api response "
469+
+ "has content with the same content types, 2 different methods will be generated. The content of the request and response types will match. "
470+
+ "Otherwise, will be generated 4 methods - for each combination of request body content type and response content type.";
456471
}

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

Lines changed: 236 additions & 7 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;
@@ -332,6 +333,10 @@ apiTemplateFiles are for API outputs only (controllers/handlers).
332333

333334
// Whether to automatically hardcode params that are considered Constants by OpenAPI Spec
334335
@Setter protected boolean autosetConstants = false;
336+
@Setter
337+
protected boolean groupByRequestAndResponseContentType = true;
338+
@Setter
339+
protected boolean groupByResponseContentType = true;
335340

336341
@Override
337342
public boolean getAddSuffixToDuplicateOperationNicknames() {
@@ -392,8 +397,9 @@ public void processOpts() {
392397
convertPropertyToBooleanAndWriteBack(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, this::setDisallowAdditionalPropertiesIfNotPresent);
393398
convertPropertyToBooleanAndWriteBack(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, this::setEnumUnknownDefaultCase);
394399
convertPropertyToBooleanAndWriteBack(CodegenConstants.AUTOSET_CONSTANTS, this::setAutosetConstants);
395-
}
396-
400+
convertPropertyToBooleanAndWriteBack(CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE, this::setGroupByRequestAndResponseContentType);
401+
convertPropertyToBooleanAndWriteBack(CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE, this::setGroupByResponseContentType);
402+
}
397403

398404
/***
399405
* Preset map builder with commonly used Mustache lambdas.
@@ -898,7 +904,7 @@ public String toEnumValue(String value, String datatype) {
898904
* @return the sanitized variable name for enum
899905
*/
900906
public String toEnumVarName(String value, String datatype) {
901-
if (value.length() == 0) {
907+
if (value.isEmpty()) {
902908
return "EMPTY";
903909
}
904910

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

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

@@ -1755,6 +1980,10 @@ public DefaultCodegen() {
17551980
// option to change the order of form/body parameter
17561981
cliOptions.add(CliOption.newBoolean(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS,
17571982
CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS_DESC).defaultValue(Boolean.FALSE.toString()));
1983+
cliOptions.add(CliOption.newBoolean(CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE,
1984+
CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE_DESC).defaultValue(Boolean.TRUE.toString()));
1985+
cliOptions.add(CliOption.newBoolean(CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE,
1986+
CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE_DESC).defaultValue(Boolean.TRUE.toString()));
17581987

17591988
// option to change how we process + set the data in the discriminator mapping
17601989
CliOption legacyDiscriminatorBehaviorOpt = CliOption.newBoolean(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR_DESC).defaultValue(Boolean.TRUE.toString());

0 commit comments

Comments
 (0)