Skip to content

Commit

Permalink
Divide operations by request and response content types
Browse files Browse the repository at this point in the history
Fixed #17877
  • Loading branch information
altro3 committed Oct 27, 2024
1 parent 30ff0d7 commit 4a9d594
Show file tree
Hide file tree
Showing 9 changed files with 673 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -366,4 +366,5 @@ public interface CodegenConfig {

Set<String> getOpenapiGeneratorIgnoreList();

boolean supportsDividingOperationsByContentType();
}
Original file line number Diff line number Diff line change
Expand Up @@ -453,4 +453,17 @@ 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 GROUP_BY_RESPONSE_CONTENT_TYPE = "groupByResponseContentType";
public static final String GROUP_BY_RESPONSE_CONTENT_TYPE_DESC =
"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.";

public static final String GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE = "groupByRequestAndResponseContentType";
public static final String GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE_DESC =
"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.";
}
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,10 @@ apiTemplateFiles are for API outputs only (controllers/handlers).

// Whether to automatically hardcode params that are considered Constants by OpenAPI Spec
@Setter protected boolean autosetConstants = false;
@Setter
protected boolean groupByRequestAndResponseContentType = true;
@Setter
protected boolean groupByResponseContentType = true;

@Override
public boolean getAddSuffixToDuplicateOperationNicknames() {
Expand Down Expand Up @@ -392,8 +396,9 @@ 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);
}

convertPropertyToBooleanAndWriteBack(CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE, this::setGroupByRequestAndResponseContentType);
convertPropertyToBooleanAndWriteBack(CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE, this::setGroupByResponseContentType);
}

/***
* Preset map builder with commonly used Mustache lambdas.
Expand Down Expand Up @@ -898,7 +903,7 @@ public String toEnumValue(String value, String datatype) {
* @return the sanitized variable name for enum
*/
public String toEnumVarName(String value, String datatype) {
if (value.length() == 0) {
if (value.isEmpty()) {
return "EMPTY";
}

Expand Down Expand Up @@ -999,6 +1004,47 @@ public void postProcessParameter(CodegenParameter parameter) {
@Override
@SuppressWarnings("unused")
public void preprocessOpenAPI(OpenAPI openAPI) {

if (supportsDividingOperationsByContentType() && 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 @@ -1080,6 +1126,183 @@ 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>();
divideOperationByRequestBody(path, httpMethod, op, additionalOps);

// Check responses content types and divide operations by them

var responses = op.getResponses();
if (responses == null || responses.isEmpty()) {
return additionalOps;
}
var allPossibleContentTypes = new ArrayList<String>();
for (var responseEntry : responses.entrySet()) {
for (var contentType : responseEntry.getValue().getContent().keySet()) {
contentType = contentType.toLowerCase();
if (!allPossibleContentTypes.contains(contentType)) {
allPossibleContentTypes.add(contentType);
}
}
}
if (allPossibleContentTypes.isEmpty() || allPossibleContentTypes.size() == 1) {
return additionalOps;
}

var apiResponsesByContentType = new HashMap<String, ApiResponses>();
for (var contentType : allPossibleContentTypes) {
var apiResponses = new ApiResponses();
for (var responseEntry : responses.entrySet()) {
var code = responseEntry.getKey();
var response = responseEntry.getValue();
var mediaType = response.getContent().get(contentType);
if (mediaType == null) {
continue;
}
apiResponses.addApiResponse(code, new ApiResponse()
.description(response.getDescription())
.headers(response.getHeaders())
.links(response.getLinks())
.extensions(response.getExtensions())
.$ref(response.get$ref())
.content(new Content()
.addMediaType(contentType, mediaType)
)
);
}
apiResponsesByContentType.put(contentType, apiResponses);
}

var finalAdditionalOps = new ArrayList<Operation>();
divideOperationByResponses(path, httpMethod, op, apiResponsesByContentType, finalAdditionalOps);
for (var additionalOp : additionalOps) {
finalAdditionalOps.add(additionalOp);
divideOperationByResponses(path, httpMethod, additionalOp, apiResponsesByContentType, finalAdditionalOps);
}

return finalAdditionalOps;
}

private void divideOperationByRequestBody(String path, PathItem.HttpMethod httpMethod, Operation op, List<Operation> additionalOps) {
RequestBody body = op.getRequestBody();
if (body == null || body.getContent() == null) {
return;
}
Content content = body.getContent();
if (content.size() <= 1) {
return;
}
var firstEntry = content.entrySet().iterator().next();
var mediaTypesToRemove = new ArrayList<String>();
for (var entry : content.entrySet()) {
var contentType = entry.getKey();
MediaType mediaType = entry.getValue();
if (mediaTypesToRemove.contains(contentType) || contentType.equals(firstEntry.getKey())) {
continue;
}
var foundSameOpSignature = false;
// group by response content type
if (groupByResponseContentType) {
for (var additionalOp : additionalOps) {
RequestBody additionalBody = additionalOp.getRequestBody();
if (additionalBody == null || additionalBody.getContent() == null) {
return;
}
for (var addContentEntry : additionalBody.getContent().entrySet()) {
if (addContentEntry.getValue().equals(mediaType)) {
foundSameOpSignature = true;
break;
}
}
if (foundSameOpSignature) {
additionalBody.getContent().put(contentType, mediaType);
break;
}
}
}

mediaTypesToRemove.add(contentType);
if (groupByResponseContentType && foundSameOpSignature) {
continue;
}

var apiResponsesCopy = new ApiResponses();
apiResponsesCopy.putAll(op.getResponses());

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(apiResponsesCopy)
.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(contentType, mediaType))
)
);
}
if (!mediaTypesToRemove.isEmpty()) {
content.entrySet().removeIf(stringMediaTypeEntry -> mediaTypesToRemove.contains(stringMediaTypeEntry.getKey()));
}
}

private void divideOperationByResponses(
String path,
PathItem.HttpMethod httpMethod,
Operation op,
Map<String, ApiResponses> apiResponsesByContentType,
List<Operation> additionalOps
) {
var isFirst = true;
for (var entry : apiResponsesByContentType.entrySet()) {
var contentType = entry.getKey();
var apiResponses = entry.getValue();
var requestBody = op.getRequestBody();
// group by requestBody contentType
if (groupByRequestAndResponseContentType
&& requestBody != null
&& requestBody.getContent() != null
&& !requestBody.getContent().containsKey(contentType)) {
continue;
}
if (isFirst) {
op.setResponses(apiResponses);
isFirst = false;
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(apiResponses)
.security(op.getSecurity())
.servers(op.getServers())
.summary(op.getSummary())
.tags(op.getTags())
.requestBody(requestBody)
);
}
}

// override with any special handling of the entire OpenAPI spec document
@Override
@SuppressWarnings("unused")
Expand Down Expand Up @@ -1164,8 +1387,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 @@ -1181,8 +1403,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 Expand Up @@ -1755,6 +1976,12 @@ public DefaultCodegen() {
// option to change the order of form/body parameter
cliOptions.add(CliOption.newBoolean(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS,
CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS_DESC).defaultValue(Boolean.FALSE.toString()));
if (supportsDividingOperationsByContentType()) {
cliOptions.add(CliOption.newBoolean(CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE,
CodegenConstants.GROUP_BY_RESPONSE_CONTENT_TYPE_DESC).defaultValue(Boolean.TRUE.toString()));
cliOptions.add(CliOption.newBoolean(CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE,
CodegenConstants.GROUP_BY_REQUEST_AND_RESPONSE_CONTENT_TYPE_DESC).defaultValue(Boolean.TRUE.toString()));
}

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

/*
A function to convert yaml or json ingested strings like property names
And convert special characters like newline, tab, carriage return
Into strings that can be rendered in the language that the generator will output to
*/
@Override
public boolean supportsDividingOperationsByContentType() {
return false;
}

/**
* A function to convert yaml or json ingested strings like property names
* And convert special characters like newline, tab, carriage return
* Into strings that can be rendered in the language that the generator will output to
*/
protected String handleSpecialCharacters(String name) {
return name;
}
Expand Down
Loading

0 comments on commit 4a9d594

Please sign in to comment.