Skip to content

Commit

Permalink
Divide operations by content type
Browse files Browse the repository at this point in the history
Fixed #17877
  • Loading branch information
altro3 committed Oct 23, 2024
1 parent e9c3c63 commit 127a9ae
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -453,4 +453,6 @@ 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 DIVIDE_OPERATIONS_BY_CONTENT_TYPE = "divideOperationsByContentType";
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.openapitools.codegen.CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE;
import static org.openapitools.codegen.CodegenConstants.UNSUPPORTED_V310_SPEC_MSG;
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
import static org.openapitools.codegen.utils.OnceLogger.once;
Expand Down Expand Up @@ -392,7 +393,7 @@ 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);
}
}


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

var divideOperationsByContentType = Boolean.parseBoolean(GlobalSettings.getProperty(DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "false"));

if (divideOperationsByContentType && 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 +1124,77 @@ 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>();

RequestBody body = op.getRequestBody();
if (body == null || body.getContent() == null) {
return Collections.emptyList();
}
Content content = body.getContent();
if (content.size() <= 1) {
return Collections.emptyList();
}
var firstEntry = content.entrySet().iterator().next();
var mediaTypesToRemove = new ArrayList<String>();
for (var entry : content.entrySet()) {
if (mediaTypesToRemove.contains(entry.getKey()) || entry.getKey().equals(firstEntry.getKey()) || entry.getValue().equals(firstEntry.getValue())) {
continue;
}
var foundSameOpSignature = false;
for (var additionalOp : additionalOps) {
RequestBody additionalBody = additionalOp.getRequestBody();
if (additionalBody == null || additionalBody.getContent() == null) {
return Collections.emptyList();
}
for (var addContentEntry : additionalBody.getContent().entrySet()) {
if (addContentEntry.getValue().equals(entry.getValue())) {
foundSameOpSignature = true;
break;
}
}
if (foundSameOpSignature) {
additionalBody.getContent().put(entry.getKey(), entry.getValue());
break;
}
}
mediaTypesToRemove.add(entry.getKey());
if (foundSameOpSignature) {
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(op.getResponses())
.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(entry.getKey(), entry.getValue()))
)
);
}
if (!mediaTypesToRemove.isEmpty()) {
content.entrySet().removeIf(stringMediaTypeEntry -> mediaTypesToRemove.contains(stringMediaTypeEntry.getKey()));
}

return additionalOps;
}

// override with any special handling of the entire OpenAPI spec document
@Override
@SuppressWarnings("unused")
Expand Down Expand Up @@ -1164,8 +1279,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 +1295,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
Original file line number Diff line number Diff line change
Expand Up @@ -616,10 +616,10 @@ private void generateModelsForVariable(List<File> files, List<ModelMap> allModel
if (!processedModels.contains(key) && allSchemas.containsKey(key)) {
generateModels(files, allModels, unusedModels, aliasModels, processedModels, () -> Set.of(key));
} else {
LOGGER.info("Type " + variable.getComplexType()+" of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getComplexType(), variable.getName());
}
} else {
LOGGER.info("Type " + variable.getOpenApiType()+" of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
LOGGER.info("Type {} of variable {} could not be resolve because it is not declared as a model.", variable.getComplexType(), variable.getName());
}
}

Expand Down Expand Up @@ -1012,7 +1012,7 @@ private void generateOpenapiGeneratorIgnoreFile() {
File ignoreFile = new File(ignoreFileNameTarget);
// use the entries provided by the users to pre-populate .openapi-generator-ignore
try {
LOGGER.info("Writing file " + ignoreFileNameTarget + " (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)");
LOGGER.info("Writing file {} (which is always overwritten when the option `openapiGeneratorIgnoreFile` is enabled.)", ignoreFileNameTarget);
new File(config.outputFolder()).mkdirs();
if (!ignoreFile.createNewFile()) {
// file may already exist, do nothing
Expand Down Expand Up @@ -1475,6 +1475,9 @@ public Map<String, List<CodegenOperation>> processPaths(Paths paths) {
if (paths == null) {
return ops;
}

var divideOperationsByContentType = Boolean.parseBoolean(GlobalSettings.getProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "false"));

for (Map.Entry<String, PathItem> pathsEntry : paths.entrySet()) {
String resourcePath = pathsEntry.getKey();
PathItem path = pathsEntry.getValue();
Expand All @@ -1486,10 +1489,34 @@ public Map<String, List<CodegenOperation>> processPaths(Paths paths) {
processOperation(resourcePath, "patch", path.getPatch(), ops, path);
processOperation(resourcePath, "options", path.getOptions(), ops, path);
processOperation(resourcePath, "trace", path.getTrace(), ops, path);

if (divideOperationsByContentType) {
processAdditionalOperations(resourcePath, "x-get", "get", ops, path);
processAdditionalOperations(resourcePath, "x-head", "head", ops, path);
processAdditionalOperations(resourcePath, "x-put", "put", ops, path);
processAdditionalOperations(resourcePath, "x-post", "post", ops, path);
processAdditionalOperations(resourcePath, "x-delete", "delete", ops, path);
processAdditionalOperations(resourcePath, "x-patch", "patch", ops, path);
processAdditionalOperations(resourcePath, "x-options", "options", ops, path);
processAdditionalOperations(resourcePath, "x-trace", "trace", ops, path);
}
}
return ops;
}

protected void processAdditionalOperations(String resourcePath, String extName, String httpMethod, Map<String, List<CodegenOperation>> ops, PathItem path) {
if (path.getExtensions() == null || !path.getExtensions().containsKey(extName)) {
return;
}
var xOps = (List<Operation>) path.getExtensions().get(extName);
if (xOps == null) {
return;
}
for (Operation op : xOps) {
processOperation(resourcePath, httpMethod, op, ops, path);
}
}

public Map<String, List<CodegenOperation>> processWebhooks(Map<String, PathItem> webhooks) {
Map<String, List<CodegenOperation>> ops = new TreeMap<>();
// when input file is not valid and doesn't contain any paths
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures;
import org.openapitools.codegen.meta.features.*;
Expand Down Expand Up @@ -193,6 +194,8 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
public AbstractJavaCodegen() {
super();

GlobalSettings.setProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "true");

modifyFeatureSet(features -> features
.includeDocumentationFeatures(DocumentationFeature.Readme)
.wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.templating.mustache.EscapeChar;
Expand Down Expand Up @@ -88,6 +89,7 @@ public abstract class AbstractKotlinCodegen extends DefaultCodegen implements Co
public AbstractKotlinCodegen() {
super();

GlobalSettings.setProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "true");
supportsInheritance = true;
setSortModelPropertiesByRequiredFlag(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.config.GlobalSettings;
import org.openapitools.codegen.java.assertions.JavaFileAssert;
import org.openapitools.codegen.languages.JavaMicronautClientCodegen;
import org.openapitools.codegen.testutils.ConfigAssert;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.io.File;
Expand All @@ -21,7 +21,6 @@
import static org.openapitools.codegen.TestUtils.newTempFolder;
import static org.testng.Assert.assertEquals;


public class JavaMicronautClientCodegenTest extends AbstractMicronautCodegenTest {
@Test
public void clientOptsUnicity() {
Expand Down Expand Up @@ -323,7 +322,8 @@ public void testConfigurePathSeparator() {
* Includes regression tests for:
* - <a href="https://github.com/OpenAPITools/openapi-generator/issues/2417">Correct Jackson annotation when `wrapped: false`</a>
*/
@Test public void shouldGenerateCorrectXmlAnnotations() {
@Test
public void shouldGenerateCorrectXmlAnnotations() {
// Arrange
final CodegenConfigurator config = new CodegenConfigurator()
.addAdditionalProperty(CodegenConstants.WITH_XML, true)
Expand All @@ -333,7 +333,7 @@ public void testConfigurePathSeparator() {
.setGeneratorName(JavaMicronautClientCodegen.NAME)
.setInputSpec("src/test/resources/3_0/java/xml-annotations-test.yaml")
.setOutputDir(newTempFolder().toString());

// Act
final List<File> files = new DefaultGenerator().opts(config.toClientOptInput()).generate();

Expand Down Expand Up @@ -457,4 +457,33 @@ public void testConfigurePathSeparator() {
.hasAnnotation("JacksonXmlProperty", Map.of("localName", "\"item\""))
.hasAnnotation("JacksonXmlElementWrapper", Map.of("localName", "\"activities-array\""));
}

@Test
public void testMultipleContentTypesToPath() {

GlobalSettings.setProperty(CodegenConstants.DIVIDE_OPERATIONS_BY_CONTENT_TYPE, "true");

var codegen = new JavaMicronautClientCodegen();
String outputPath = generateFiles(codegen, "src/test/resources/3_0/java/multiple-content-types.yaml", CodegenConstants.APIS, CodegenConstants.MODELS);

// Micronaut declarative http client should use the provided path separator
assertFileContains(outputPath + "/src/main/java/org/openapitools/api/DefaultApi.java",
" @Post(uri=\"/multiplecontentpath\")\n" +
" @Produces({\"application/json\", \"application/xml\"})\n" +
" Mono<Void> myOp(\n" +
" @Body @Nullable @Valid Coordinates coordinates\n" +
" );\n",
" @Post(uri=\"/multiplecontentpath\")\n" +
" @Produces({\"multipart/form-data\"})\n" +
" Mono<Void> myOp_1(\n" +
" @Nullable @Valid Coordinates coordinates, \n" +
" @Nullable File _file\n" +
" );",
" @Post(uri=\"/multiplecontentpath\")\n" +
" @Produces({\"application/yaml\", \"text/json\"})\n" +
" Mono<Void> myOp_2(\n" +
" @Body @Nullable @Valid MySchema mySchema\n" +
" );"
);
}
}
Loading

0 comments on commit 127a9ae

Please sign in to comment.