Skip to content

Commit 5a00a57

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

File tree

10 files changed

+682
-20
lines changed

10 files changed

+682
-20
lines changed

docs/generators/groovy.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
6868
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
6969
|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|
7070
|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|
71+
|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|
72+
|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|
7173

7274
## SUPPORTED VENDOR EXTENSIONS
7375

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

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

Lines changed: 251 additions & 12 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() {
@@ -392,8 +396,9 @@ public void processOpts() {
392396
convertPropertyToBooleanAndWriteBack(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, this::setDisallowAdditionalPropertiesIfNotPresent);
393397
convertPropertyToBooleanAndWriteBack(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, this::setEnumUnknownDefaultCase);
394398
convertPropertyToBooleanAndWriteBack(CodegenConstants.AUTOSET_CONSTANTS, this::setAutosetConstants);
395-
}
396-
399+
convertPropertyToBooleanAndWriteBack(CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE, this::setGroupByRequestAndResponseContentType);
400+
convertPropertyToBooleanAndWriteBack(CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE, this::setGroupByResponseContentType);
401+
}
397402

398403
/***
399404
* Preset map builder with commonly used Mustache lambdas.
@@ -898,7 +903,7 @@ public String toEnumValue(String value, String datatype) {
898903
* @return the sanitized variable name for enum
899904
*/
900905
public String toEnumVarName(String value, String datatype) {
901-
if (value.length() == 0) {
906+
if (value.isEmpty()) {
902907
return "EMPTY";
903908
}
904909

@@ -999,6 +1004,47 @@ public void postProcessParameter(CodegenParameter parameter) {
9991004
@Override
10001005
@SuppressWarnings("unused")
10011006
public void preprocessOpenAPI(OpenAPI openAPI) {
1007+
1008+
if (supportsDividingOperationsByContentType() && openAPI.getPaths() != null && !openAPI.getPaths().isEmpty()) {
1009+
1010+
for (Map.Entry<String, PathItem> entry : openAPI.getPaths().entrySet()) {
1011+
String pathStr = entry.getKey();
1012+
PathItem path = entry.getValue();
1013+
List<Operation> getOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.GET, path.getGet());
1014+
if (!getOps.isEmpty()) {
1015+
path.addExtension("x-get", getOps);
1016+
}
1017+
List<Operation> putOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.PUT, path.getPut());
1018+
if (!putOps.isEmpty()) {
1019+
path.addExtension("x-put", putOps);
1020+
}
1021+
List<Operation> postOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.POST, path.getPost());
1022+
if (!postOps.isEmpty()) {
1023+
path.addExtension("x-post", postOps);
1024+
}
1025+
List<Operation> deleteOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.DELETE, path.getDelete());
1026+
if (!deleteOps.isEmpty()) {
1027+
path.addExtension("x-delete", deleteOps);
1028+
}
1029+
List<Operation> optionsOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.OPTIONS, path.getOptions());
1030+
if (!optionsOps.isEmpty()) {
1031+
path.addExtension("x-options", optionsOps);
1032+
}
1033+
List<Operation> headOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.HEAD, path.getHead());
1034+
if (!headOps.isEmpty()) {
1035+
path.addExtension("x-head", headOps);
1036+
}
1037+
List<Operation> patchOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.PATCH, path.getPatch());
1038+
if (!patchOps.isEmpty()) {
1039+
path.addExtension("x-patch", patchOps);
1040+
}
1041+
List<Operation> traceOps = divideOperationsByContentType(pathStr, PathItem.HttpMethod.TRACE, path.getTrace());
1042+
if (!traceOps.isEmpty()) {
1043+
path.addExtension("x-trace", traceOps);
1044+
}
1045+
}
1046+
}
1047+
10021048
if (useOneOfInterfaces && openAPI.getComponents() != null) {
10031049
// we process the openapi schema here to find oneOf schemas and create interface models for them
10041050
Map<String, Schema> schemas = new HashMap<>(openAPI.getComponents().getSchemas());
@@ -1080,6 +1126,190 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
10801126
}
10811127
}
10821128

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

@@ -1755,6 +1983,12 @@ public DefaultCodegen() {
17551983
// option to change the order of form/body parameter
17561984
cliOptions.add(CliOption.newBoolean(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS,
17571985
CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS_DESC).defaultValue(Boolean.FALSE.toString()));
1986+
if (supportsDividingOperationsByContentType()) {
1987+
cliOptions.add(CliOption.newBoolean(CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE,
1988+
CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE_DESC).defaultValue(Boolean.TRUE.toString()));
1989+
cliOptions.add(CliOption.newBoolean(CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE,
1990+
CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE_DESC).defaultValue(Boolean.TRUE.toString()));
1991+
}
17581992

17591993
// option to change how we process + set the data in the discriminator mapping
17601994
CliOption legacyDiscriminatorBehaviorOpt = CliOption.newBoolean(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR_DESC).defaultValue(Boolean.TRUE.toString());
@@ -8514,11 +8748,16 @@ public boolean isTypeErasedGenerics() {
85148748
return false;
85158749
}
85168750

8517-
/*
8518-
A function to convert yaml or json ingested strings like property names
8519-
And convert special characters like newline, tab, carriage return
8520-
Into strings that can be rendered in the language that the generator will output to
8521-
*/
8751+
@Override
8752+
public boolean supportsDividingOperationsByContentType() {
8753+
return false;
8754+
}
8755+
8756+
/**
8757+
* A function to convert yaml or json ingested strings like property names
8758+
* And convert special characters like newline, tab, carriage return
8759+
* Into strings that can be rendered in the language that the generator will output to
8760+
*/
85228761
protected String handleSpecialCharacters(String name) {
85238762
return name;
85248763
}

0 commit comments

Comments
 (0)