diff --git a/docs/generators/groovy.md b/docs/generators/groovy.md index e2f1314c4bb..1e0f68bd537 100644 --- a/docs/generators/groovy.md +++ b/docs/generators/groovy.md @@ -69,6 +69,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false| |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| |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| +|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| +|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| ## SUPPORTED VENDOR EXTENSIONS diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java index 27cd5b4ab0b..1574154e123 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java @@ -366,4 +366,5 @@ public interface CodegenConfig { Set getOpenapiGeneratorIgnoreList(); + boolean supportsDividingOperationsByContentType(); } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java index ae52412b7d2..9f501815c91 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java @@ -454,4 +454,17 @@ public class CodegenConstants { 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."; } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 97e859e80c5..cfc8ca1c316 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -333,6 +333,10 @@ public class DefaultCodegen implements CodegenConfig { // 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() { @@ -392,9 +396,10 @@ public class DefaultCodegen implements CodegenConfig { 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. * @@ -910,7 +915,7 @@ public class DefaultCodegen implements CodegenConfig { * @return the sanitized variable name for enum */ public String toEnumVarName(String value, String datatype) { - if (value.length() == 0) { + if (value.isEmpty()) { return "EMPTY"; } @@ -1011,6 +1016,47 @@ public class DefaultCodegen implements CodegenConfig { @Override @SuppressWarnings("unused") public void preprocessOpenAPI(OpenAPI openAPI) { + + if (supportsDividingOperationsByContentType() && openAPI.getPaths() != null && !openAPI.getPaths().isEmpty()) { + + for (Map.Entry entry : openAPI.getPaths().entrySet()) { + String pathStr = entry.getKey(); + PathItem path = entry.getValue(); + List getOps = divideOperationsByContentType(openAPI, pathStr, PathItem.HttpMethod.GET, path.getGet()); + if (!getOps.isEmpty()) { + path.addExtension("x-get", getOps); + } + List putOps = divideOperationsByContentType(openAPI, pathStr, PathItem.HttpMethod.PUT, path.getPut()); + if (!putOps.isEmpty()) { + path.addExtension("x-put", putOps); + } + List postOps = divideOperationsByContentType(openAPI, pathStr, PathItem.HttpMethod.POST, path.getPost()); + if (!postOps.isEmpty()) { + path.addExtension("x-post", postOps); + } + List deleteOps = divideOperationsByContentType(openAPI, pathStr, PathItem.HttpMethod.DELETE, path.getDelete()); + if (!deleteOps.isEmpty()) { + path.addExtension("x-delete", deleteOps); + } + List optionsOps = divideOperationsByContentType(openAPI, pathStr, PathItem.HttpMethod.OPTIONS, path.getOptions()); + if (!optionsOps.isEmpty()) { + path.addExtension("x-options", optionsOps); + } + List headOps = divideOperationsByContentType(openAPI, pathStr, PathItem.HttpMethod.HEAD, path.getHead()); + if (!headOps.isEmpty()) { + path.addExtension("x-head", headOps); + } + List patchOps = divideOperationsByContentType(openAPI, pathStr, PathItem.HttpMethod.PATCH, path.getPatch()); + if (!patchOps.isEmpty()) { + path.addExtension("x-patch", patchOps); + } + List traceOps = divideOperationsByContentType(openAPI, 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 schemas = new HashMap<>(openAPI.getComponents().getSchemas()); @@ -1092,6 +1138,261 @@ public class DefaultCodegen implements CodegenConfig { } } + private List divideOperationsByContentType(OpenAPI openAPI, String path, PathItem.HttpMethod httpMethod, Operation op) { + + if (op == null) { + return Collections.emptyList(); + } + + var operationIndexes = new HashMap(); + operationIndexes.put(getOrGenerateOperationId(op, path, httpMethod.name()), 0); + + var additionalOps = new ArrayList(); + divideOperationByRequestBody(openAPI, path, httpMethod, op, additionalOps, operationIndexes); + + // Check responses content types and divide operations by them + + var responses = op.getResponses(); + if (responses == null || responses.isEmpty()) { + return additionalOps; + } + var unwrappedResponses = new ApiResponses(); + var allPossibleContentTypes = new ArrayList(); + for (var responseEntry : responses.entrySet()) { + var apiResponse = ModelUtils.getReferencedApiResponse(openAPI, responseEntry.getValue()); + unwrappedResponses.put(responseEntry.getKey(), apiResponse); + if (apiResponse.getContent() == null) { + continue; + } + for (var contentType : apiResponse.getContent().keySet()) { + contentType = contentType.toLowerCase(); + if (!allPossibleContentTypes.contains(contentType)) { + allPossibleContentTypes.add(contentType); + } + } + } + if (allPossibleContentTypes.isEmpty() || allPossibleContentTypes.size() == 1) { + return additionalOps; + } + op.setResponses(unwrappedResponses); + responses = unwrappedResponses; + + var apiResponsesByContentType = new HashMap(); + for (var contentType : allPossibleContentTypes) { + var apiResponses = new ApiResponses(); + for (var responseEntry : responses.entrySet()) { + var code = responseEntry.getKey(); + var response = responseEntry.getValue(); + if (response.getContent() == null) { + continue; + } + 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() != null ? new LinkedHashMap<>(response.getExtensions()) : new LinkedHashMap<>()) + .$ref(response.get$ref()) + .content(new Content() + .addMediaType(contentType, mediaType) + ) + ); + } + apiResponsesByContentType.put(contentType, apiResponses); + } + + var addedContentTypes = new ArrayList(); + var finalAdditionalOps = new ArrayList(); + divideOperationByResponses(path, httpMethod, op, apiResponsesByContentType, finalAdditionalOps, addedContentTypes, operationIndexes); + for (var additionalOp : additionalOps) { + finalAdditionalOps.add(additionalOp); + divideOperationByResponses(path, httpMethod, additionalOp, apiResponsesByContentType, finalAdditionalOps, addedContentTypes, operationIndexes); + } + + // remove correct processed contentTypes + apiResponsesByContentType.entrySet().removeIf(stringMediaTypeEntry -> addedContentTypes.contains(stringMediaTypeEntry.getKey())); + + if (!apiResponsesByContentType.isEmpty()) { + appendCommonResponseMediaTypes(op, apiResponsesByContentType.values()); + for (var additionalOp : additionalOps) { + appendCommonResponseMediaTypes(additionalOp, apiResponsesByContentType.values()); + } + } + + return finalAdditionalOps; + } + + private void divideOperationByRequestBody(OpenAPI openAPI, String path, PathItem.HttpMethod httpMethod, Operation op, + List additionalOps, Map operationIndexes) { + RequestBody body = ModelUtils.getReferencedRequestBody(openAPI, op.getRequestBody()); + if (body == null || body.getContent() == null) { + return; + } + op.setRequestBody(body); + Content content = body.getContent(); + if (content.size() <= 1) { + return; + } + var firstEntry = content.entrySet().iterator().next(); + var mediaTypesToRemove = new ArrayList(); + 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) { + if (firstEntry.getValue().equals(mediaType)) { + if (!groupByRequestAndResponseContentType) { + foundSameOpSignature = true; + } + } else { + for (var additionalOp : additionalOps) { + RequestBody additionalBody = ModelUtils.getReferencedRequestBody(openAPI, 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; + } + } + } + } + + if (groupByResponseContentType && foundSameOpSignature) { + continue; + } + mediaTypesToRemove.add(contentType); + + var apiResponsesCopy = new ApiResponses(); + apiResponsesCopy.putAll(op.getResponses()); + + additionalOps.add(new Operation() + .deprecated(op.getDeprecated()) + .callbacks(op.getCallbacks()) + .description(op.getDescription()) + .extensions(op.getExtensions() != null ? new LinkedHashMap<>(op.getExtensions()) : new LinkedHashMap<>()) + .externalDocs(op.getExternalDocs()) + .operationId(calcOperationId(path, httpMethod, op, operationIndexes)) + .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 String calcOperationId(String path, PathItem.HttpMethod httpMethod, Operation op, Map operationIndexes) { + var operationId = getOrGenerateOperationId(op, path, httpMethod.name()); + var index = operationIndexes.get(operationId); + if (index != null) { + index++; + operationId += "_" + index; + operationIndexes.put(operationId, index); + } else { + operationIndexes.put(operationId, 0); + } + return operationId; + } + + private void divideOperationByResponses( + String path, + PathItem.HttpMethod httpMethod, + Operation op, + Map apiResponsesByContentType, + List additionalOps, + List addedContentTypes, + Map operationIndexes + ) { + + if (!groupByResponseContentType) { + return; + } + + 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; + } + addedContentTypes.add(contentType); + 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(calcOperationId(path, httpMethod, op, operationIndexes)) + .parameters(op.getParameters()) + .responses(apiResponses) + .security(op.getSecurity()) + .servers(op.getServers()) + .summary(op.getSummary()) + .tags(op.getTags()) + .requestBody(requestBody) + ); + } + } + + private void appendCommonResponseMediaTypes(Operation op, Collection commonResponsesList) { + if (commonResponsesList.isEmpty()) { + return; + } + + for (var commonResponses : commonResponsesList) { + var adOpResponses = op.getResponses(); + for (var responseEntry : adOpResponses.entrySet()) { + var content = responseEntry.getValue().getContent(); + var commonResponse = commonResponses.get(responseEntry.getKey()); + if (commonResponse != null && commonResponse.getContent() != null) { + if (content == null) { + content = new Content(); + } + for (var commonResponseEntry : commonResponse.getContent().entrySet()) { + if (!content.containsKey(commonResponseEntry.getKey())) { + content.addMediaType(commonResponseEntry.getKey(), commonResponseEntry.getValue()); + } + } + } + } + } + } + // override with any special handling of the entire OpenAPI spec document @Override @SuppressWarnings("unused") @@ -1191,8 +1492,7 @@ public class DefaultCodegen implements CodegenConfig { */ @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 @@ -1208,8 +1508,7 @@ public class DefaultCodegen implements CodegenConfig { */ @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("\"", "\\\""); } @@ -1782,6 +2081,12 @@ public class DefaultCodegen implements CodegenConfig { // 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()); @@ -8591,11 +8896,16 @@ public class DefaultCodegen implements CodegenConfig { 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; } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index b2e5424c8da..82135398d92 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -606,10 +606,10 @@ public class DefaultGenerator implements Generator { 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()); } } @@ -1002,7 +1002,7 @@ public class DefaultGenerator implements Generator { 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 @@ -1465,6 +1465,9 @@ public class DefaultGenerator implements Generator { if (paths == null) { return ops; } + + var divideOperationsByContentType = config.supportsDividingOperationsByContentType(); + for (Map.Entry pathsEntry : paths.entrySet()) { String resourcePath = pathsEntry.getKey(); PathItem path = pathsEntry.getValue(); @@ -1476,11 +1479,35 @@ public class DefaultGenerator implements Generator { 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; } - public Map> processWebhooks(Map webhooks) { + protected void processAdditionalOperations(String resourcePath, String extName, String httpMethod, Map> ops, PathItem path) { + if (path.getExtensions() == null || !path.getExtensions().containsKey(extName)) { + return; + } + var xOps = (List) path.getExtensions().get(extName); + if (xOps == null) { + return; + } + for (Operation op : xOps) { + processOperation(resourcePath, httpMethod, op, ops, path); + } + } + + public Map> processWebhooks(Map webhooks) { Map> ops = new TreeMap<>(); // when input file is not valid and doesn't contain any paths if (webhooks == null) { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index 8a624b0bbf8..cb5995c51c1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -2461,4 +2461,9 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code throw new RuntimeException(sb.toString()); } } + + @Override + public boolean supportsDividingOperationsByContentType() { + return true; + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java index 35795188b6f..27b874a4dc1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java @@ -27,6 +27,7 @@ import lombok.Setter; 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; @@ -1157,4 +1158,9 @@ public abstract class AbstractKotlinCodegen extends DefaultCodegen implements Co } } } + + @Override + public boolean supportsDividingOperationsByContentType() { + return true; + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 4d1f9438f05..53015e3e140 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -1004,7 +1004,7 @@ public class SpringCodegen extends AbstractJavaCodegen // add Pageable import only if x-spring-paginated explicitly used // this allows to use a custom Pageable schema without importing Spring Pageable. - if (Boolean.TRUE.equals(operation.getExtensions().get("x-spring-paginated"))) { + if (operation.getExtensions() != null && Boolean.TRUE.equals(operation.getExtensions().get("x-spring-paginated"))) { importMapping.put("Pageable", "org.springframework.data.domain.Pageable"); } @@ -1093,7 +1093,7 @@ public class SpringCodegen extends AbstractJavaCodegen private Set reformatProvideArgsParams(Operation operation) { Set provideArgsClassSet = new HashSet<>(); - Object argObj = operation.getExtensions().get("x-spring-provide-args"); + Object argObj = operation.getExtensions() != null ? operation.getExtensions().get("x-spring-provide-args") : null; if (argObj instanceof List) { List provideArgs = (List) argObj; if (!provideArgs.isEmpty()) { @@ -1122,7 +1122,7 @@ public class SpringCodegen extends AbstractJavaCodegen formattedArgs.add(newArg); } } - operation.getExtensions().put("x-spring-provide-args", formattedArgs); + operation.addExtension("x-spring-provide-args", formattedArgs); } } return provideArgsClassSet; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautClientCodegenTest.java index ec5508d74db..798e5080bb9 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautClientCodegenTest.java @@ -20,7 +20,6 @@ import static java.util.stream.Collectors.groupingBy; import static org.openapitools.codegen.TestUtils.newTempFolder; import static org.testng.Assert.assertEquals; - public class JavaMicronautClientCodegenTest extends AbstractMicronautCodegenTest { @Test public void clientOptsUnicity() { @@ -326,13 +325,13 @@ public class JavaMicronautClientCodegenTest extends AbstractMicronautCodegenTest public void shouldGenerateCorrectXmlAnnotations() { // Arrange final CodegenConfigurator config = new CodegenConfigurator() - .addAdditionalProperty(CodegenConstants.WITH_XML, true) - .addGlobalProperty(CodegenConstants.MODELS, "Pet") - .addGlobalProperty(CodegenConstants.MODEL_DOCS, null) - .addGlobalProperty(CodegenConstants.MODEL_TESTS, null) - .setGeneratorName(JavaMicronautClientCodegen.NAME) - .setInputSpec("src/test/resources/3_0/java/xml-annotations-test.yaml") - .setOutputDir(newTempFolder().toString()); + .addAdditionalProperty(CodegenConstants.WITH_XML, true) + .addGlobalProperty(CodegenConstants.MODELS, "Pet") + .addGlobalProperty(CodegenConstants.MODEL_DOCS, null) + .addGlobalProperty(CodegenConstants.MODEL_TESTS, null) + .setGeneratorName(JavaMicronautClientCodegen.NAME) + .setInputSpec("src/test/resources/3_0/java/xml-annotations-test.yaml") + .setOutputDir(newTempFolder().toString()); // Act final List files = new DefaultGenerator().opts(config.toClientOptInput()).generate(); @@ -457,4 +456,320 @@ public class JavaMicronautClientCodegenTest extends AbstractMicronautCodegenTest .hasAnnotation("JacksonXmlProperty", Map.of("localName", "\"item\"")) .hasAnnotation("JacksonXmlElementWrapper", Map.of("localName", "\"activities-array\"")); } + + @Test + public void testMultipleContentTypesToPathTrueTrue() { + + var codegen = new JavaMicronautClientCodegen(); + codegen.setGroupByRequestAndResponseContentType(true); + codegen.setGroupByResponseContentType(true); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/java/multiple-content-types.yaml", CodegenConstants.APIS, CodegenConstants.MODELS); + + assertFileContains(outputPath + "/src/main/java/org/openapitools/api/DefaultApi.java", + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"application/json\"})\n" + + " Mono myOp(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"application/xml\"})\n" + + " Mono myOp_1(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\", \"application/xml\", \"text/json\"})\n" + + " @Produces({\"multipart/form-data\"})\n" + + " Mono myOp_2(\n" + + " @Nullable @Valid Coordinates coordinates, \n" + + " @Nullable File _file\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"application/yaml\", \"text/json\"})\n" + + " Mono myOp_3(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );" + ); + } + + @Test + public void testMultipleContentTypesToPathTrueFalse() { + + var codegen = new JavaMicronautClientCodegen(); + codegen.setGroupByRequestAndResponseContentType(true); + codegen.setGroupByResponseContentType(false); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/java/multiple-content-types.yaml", CodegenConstants.APIS, CodegenConstants.MODELS); + + assertFileContains(outputPath + "/src/main/java/org/openapitools/api/DefaultApi.java", + " @Consumes({\"application/json\"})\n" + + " @Produces({\"application/json\"})\n" + + " Mono myOp(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"application/xml\"})\n" + + " Mono myOp_1(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\", \"application/xml\", \"text/json\"})\n" + + " @Produces({\"multipart/form-data\"})\n" + + " Mono myOp_2(\n" + + " @Nullable @Valid Coordinates coordinates, \n" + + " @Nullable File _file\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\", \"application/xml\", \"text/json\"})\n" + + " @Produces({\"application/yaml\"})\n" + + " Mono myOp_3(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"text/json\"})\n" + + " Mono myOp_4(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );" + ); + } + + @Test + public void testMultipleContentTypesToPathFalseTrue() { + + var codegen = new JavaMicronautClientCodegen(); + codegen.setGroupByRequestAndResponseContentType(false); + codegen.setGroupByResponseContentType(true); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/java/multiple-content-types.yaml", CodegenConstants.APIS, CodegenConstants.MODELS); + + assertFileContains(outputPath + "/src/main/java/org/openapitools/api/DefaultApi.java", + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"application/json\"})\n" + + " Mono myOp(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"application/json\"})\n" + + " Mono myOp_1(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"application/json\"})\n" + + " Mono myOp_2(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"application/yaml\", \"text/json\"})\n" + + " Mono myOp_3(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"application/xml\"})\n" + + " Mono myOp_4(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"application/xml\"})\n" + + " Mono myOp_5(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"application/xml\"})\n" + + " Mono myOp_6(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"multipart/form-data\"})\n" + + " Mono myOp_7(\n" + + " @Nullable @Valid Coordinates coordinates, \n" + + " @Nullable File _file\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"multipart/form-data\"})\n" + + " Mono myOp_8(\n" + + " @Nullable @Valid Coordinates coordinates, \n" + + " @Nullable File _file\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"multipart/form-data\"})\n" + + " Mono myOp_9(\n" + + " @Nullable @Valid Coordinates coordinates, \n" + + " @Nullable File _file\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"application/yaml\", \"text/json\"})\n" + + " Mono myOp_10(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"application/yaml\", \"text/json\"})\n" + + " Mono myOp_11(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );" + ); + } + + @Test + public void testMultipleContentTypesToPathFalseFalse() { + + var codegen = new JavaMicronautClientCodegen(); + codegen.setGroupByRequestAndResponseContentType(false); + codegen.setGroupByResponseContentType(false); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/java/multiple-content-types.yaml", CodegenConstants.APIS, CodegenConstants.MODELS); + + assertFileContains(outputPath + "/src/main/java/org/openapitools/api/DefaultApi.java", + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"application/json\"})\n" + + " Mono myOp(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"application/json\"})\n" + + " Mono myOp_1(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"application/json\"})\n" + + " Mono myOp_2(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"application/yaml\"})\n" + + " Mono myOp_3(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"text/json\"})\n" + + " Mono myOp_4(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"text/json\"})\n" + + " Mono myOp_5(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"text/json\"})\n" + + " Mono myOp_6(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"application/xml\"})\n" + + " Mono myOp_7(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"application/xml\"})\n" + + " Mono myOp_8(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"application/xml\"})\n" + + " Mono myOp_9(\n" + + " @Body @Nullable @Valid Coordinates coordinates\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"multipart/form-data\"})\n" + + " Mono myOp_10(\n" + + " @Nullable @Valid Coordinates coordinates, \n" + + " @Nullable File _file\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"multipart/form-data\"})\n" + + " Mono myOp_11(\n" + + " @Nullable @Valid Coordinates coordinates, \n" + + " @Nullable File _file\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"text/json\"})\n" + + " @Produces({\"multipart/form-data\"})\n" + + " Mono myOp_12(\n" + + " @Nullable @Valid Coordinates coordinates, \n" + + " @Nullable File _file\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/xml\"})\n" + + " @Produces({\"application/yaml\"})\n" + + " Mono myOp_13(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );", + + " @Post(uri=\"/multiplecontentpath\")\n" + + " @Consumes({\"application/json\"})\n" + + " @Produces({\"application/yaml\"})\n" + + " Mono myOp_14(\n" + + " @Body @Nullable @Valid MySchema mySchema\n" + + " );" + ); + } + + @Test + public void testMultipleContentTypesWithRefs() { + + var codegen = new JavaMicronautClientCodegen(); + codegen.setGroupByRequestAndResponseContentType(true); + codegen.setGroupByResponseContentType(true); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/java/multiple-content-types-2.yaml", CodegenConstants.APIS, CodegenConstants.MODELS); + + assertFileContains(outputPath + "/src/main/java/org/openapitools/api/DefaultApi.java", + "" + ); + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautServerCodegenTest.java index 6f7df0f45b7..41098f76677 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautServerCodegenTest.java @@ -487,4 +487,5 @@ public class JavaMicronautServerCodegenTest extends AbstractMicronautCodegenTest .hasAnnotation("JacksonXmlProperty", Map.of("localName", "\"item\"")) .hasAnnotation("JacksonXmlElementWrapper", Map.of("localName", "\"activities-array\"")); } + } diff --git a/modules/openapi-generator/src/test/resources/3_0/java/multiple-content-types-2.yaml b/modules/openapi-generator/src/test/resources/3_0/java/multiple-content-types-2.yaml new file mode 100644 index 00000000000..5b109a3bd69 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/multiple-content-types-2.yaml @@ -0,0 +1,71 @@ +openapi: 3.0.3 +info: + version: "1" + title: Multiple Content Types for same request +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '200': + $ref: "#/components/responses/200" + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' +components: + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/yaml: + schema: + $ref: '#/components/schemas/MySchema' + requestBodies: + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + text/json: + schema: + $ref: '#/components/schemas/MySchema' + description: Pet object that needs to be added to the store + required: true + schemas: + MySchema: + type: object + properties: + id: + type: string + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + name: + type: string + example: doggie diff --git a/modules/openapi-generator/src/test/resources/3_0/java/multiple-content-types.yaml b/modules/openapi-generator/src/test/resources/3_0/java/multiple-content-types.yaml new file mode 100644 index 00000000000..60f2cc10670 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/multiple-content-types.yaml @@ -0,0 +1,67 @@ +openapi: 3.0.3 +info: + version: "1" + title: Multiple Content Types for same request +paths: + /multiplecontentpath: + post: + operationId: myOp + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/coordinates' + application/xml: + schema: + $ref: '#/components/schemas/coordinates' + multipart/form-data: + schema: + type: object + properties: + coordinates: + $ref: '#/components/schemas/coordinates' + file: + type: string + format: binary + application/yaml: + schema: + $ref: '#/components/schemas/MySchema' + text/json: + schema: + $ref: '#/components/schemas/MySchema' + responses: + 201: + description: Successfully created + headers: + Location: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/coordinates' + application/xml: + schema: + $ref: '#/components/schemas/coordinates' + text/json: + schema: + $ref: '#/components/schemas/MySchema' +components: + schemas: + coordinates: + type: object + required: + - lat + - long + properties: + lat: + type: number + long: + type: number + MySchema: + type: object + required: + - lat + properties: + lat: + type: number