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 b7d31d9fce53..f791d39762f2 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 @@ -818,7 +818,7 @@ public void postProcessParameter(CodegenParameter parameter) { @Override @SuppressWarnings("unused") public void preprocessOpenAPI(OpenAPI openAPI) { - if (useOneOfInterfaces) { + 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()); if (schemas == null) { @@ -2418,9 +2418,6 @@ public CodegenModel fromModel(String name, Schema schema) { m.classFilename = toModelFilename(name); m.modelJson = Json.pretty(schema); m.externalDocumentation = schema.getExternalDocs(); - if (schema.getExtensions() != null && !schema.getExtensions().isEmpty()) { - m.getVendorExtensions().putAll(schema.getExtensions()); - } m.isAlias = (typeAliases.containsKey(name) || isAliasOfSimpleTypes(schema)); // check if the unaliased schema is an alias of simple OAS types m.setDiscriminator(createDiscriminator(name, schema, this.openAPI)); @@ -2428,6 +2425,28 @@ public CodegenModel fromModel(String name, Schema schema) { m.addDiscriminatorMappedModelsImports(); } + if (schema.getExtensions() != null && !schema.getExtensions().isEmpty()) { + m.getVendorExtensions().putAll(schema.getExtensions()); + if (ModelUtils.isComposedSchema(schema) + && m.discriminator == null + && schema.getExtensions().containsKey("x-one-of-name")) { + boolean isDeductionCase = true; + List deductionModelNames = new ArrayList<>(); + for (Schema modelSchema : ((ComposedSchema) schema).getOneOf()) { + if (modelSchema.get$ref() == null) { + isDeductionCase = false; + break; + } + String modelName = ModelUtils.getSimpleRef(modelSchema.get$ref()); + deductionModelNames.add(toModelName(modelName)); + } + if (isDeductionCase) { + m.vendorExtensions.put("x-deduction", true); + m.vendorExtensions.put("x-deduction-model-names", deductionModelNames); + } + } + } + if (schema.getDeprecated() != null) { m.isDeprecated = schema.getDeprecated(); } @@ -3165,7 +3184,7 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch } } // if there are composed oneOf/anyOf schemas, add them to this discriminator - if (ModelUtils.isComposedSchema(schema) && !this.getLegacyDiscriminatorBehavior()) { + if (ModelUtils.isComposedSchema(schema)) { List otherDescendants = getOneOfAnyOfDescendants(schemaName, discPropName, (ComposedSchema) schema, openAPI); for (MappedModel otherDescendant : otherDescendants) { if (!uniqueDescendants.contains(otherDescendant)) { @@ -4177,6 +4196,11 @@ public CodegenResponse fromResponse(String responseCode, ApiResponse response) { } else { // no model/alias defined responseSchema = ModelUtils.getSchemaFromResponse(response); } + + if (!ModelUtils.isSchemaOneOfConsistsOfCustomTypes(this.openAPI, responseSchema)) { + responseSchema = new Schema(); + } + r.schema = responseSchema; if (responseSchema != null) { ModelUtils.syncValidationProperties(responseSchema, r); 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 748e30815490..a6007654bc84 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 @@ -43,6 +43,7 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import io.swagger.v3.oas.models.responses.ApiResponse; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; @@ -1363,8 +1364,23 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } for (Operation operation : path.readOperations()) { LOGGER.info("Processing operation {}", operation.getOperationId()); - if (hasBodyParameter(openAPI, operation) || hasFormParameter(openAPI, operation)) { - String defaultContentType = hasFormParameter(openAPI, operation) ? "application/x-www-form-urlencoded" : "application/json"; + boolean hasBodyParameter = hasBodyParameter(openAPI, operation); + boolean hasFormParameter = hasFormParameter(openAPI, operation); + + // OpenAPI parser do not add Inline One Of models in Operations to Components/Schemas + if (hasBodyParameter) { + Optional.ofNullable(operation.getRequestBody()) + .map(RequestBody::getContent) + .ifPresent(this::repairInlineOneOf); + } + if (operation.getResponses() != null) { + operation.getResponses().values().stream().map(ApiResponse::getContent) + .filter(Objects::nonNull) + .forEach(this::repairInlineOneOf); + } + + if (hasBodyParameter || hasFormParameter) { + String defaultContentType = hasFormParameter ? "application/x-www-form-urlencoded" : "application/json"; List consumes = new ArrayList<>(getConsumesInfo(openAPI, operation)); String contentType = consumes == null || consumes.isEmpty() ? defaultContentType : consumes.get(0); operation.addExtension("x-contentType", contentType); @@ -1421,6 +1437,39 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } } + /** + * Add all OneOf schemas to #/components/schemas and replace them in the original content by ref schema + * Replace OneOf with unmodifiable types with an empty Schema + * + * OpenAPI Parser does not add inline OneOf schemas to models to generate + * + * @param content a 'content' section in the OAS specification. + */ + private void repairInlineOneOf(final Content content) { + content.values().forEach(mediaType -> { + final Schema replacingSchema = mediaType.getSchema(); + if (isOneOfSchema(replacingSchema)) { + if (ModelUtils.isSchemaOneOfConsistsOfCustomTypes(openAPI, replacingSchema)) { + final String oneOfModelName = (String) replacingSchema.getExtensions().get("x-one-of-name"); + final Schema newRefSchema = new Schema<>().$ref("#/components/schemas/" + oneOfModelName); + mediaType.setSchema(newRefSchema); + ModelUtils.getSchemas(openAPI).put(oneOfModelName, replacingSchema); + } else { + mediaType.setSchema(new Schema()); + } + } + }); + } + + private static boolean isOneOfSchema(final Schema schema) { + if (schema instanceof ComposedSchema) { + ComposedSchema composedSchema = (ComposedSchema) schema; + return Optional.ofNullable(composedSchema.getProperties()).map(Map::isEmpty).orElse(true) + && Optional.ofNullable(schema.getExtensions()).map(m -> m.containsKey("x-one-of-name")).orElse(false); + } + return false; + } + private static String getAccept(OpenAPI openAPI, Operation operation) { String accepts = null; String defaultContentType = "application/json"; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index aa43075765d0..3555f170faec 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -129,6 +129,8 @@ public JavaClientCodegen() { artifactId = "openapi-java-client"; apiPackage = "org.openapitools.client.api"; modelPackage = "org.openapitools.client.model"; + useOneOfInterfaces = true; + addOneOfInterfaceImports = true; // cliOptions default redefinition need to be updated updateOption(CodegenConstants.INVOKER_PACKAGE, this.getInvokerPackage()); @@ -1027,11 +1029,13 @@ public String toApiVarName(String name) { @Override public void addImportsToOneOfInterface(List> imports) { - for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) { - Map oneImport = new HashMap<>(); - oneImport.put("import", importMapping.get(i)); - if (!imports.contains(oneImport)) { - imports.add(oneImport); + if (additionalProperties.containsKey(JACKSON)) { + for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) { + Map oneImport = new HashMap<>(); + oneImport.put("import", importMapping.get(i)); + if (!imports.contains(oneImport)) { + imports.add(oneImport); + } } } } 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 ba5a7f99ef57..83a802c33a25 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 @@ -135,6 +135,8 @@ public SpringCodegen() { modelPackage = "org.openapitools.model"; invokerPackage = "org.openapitools.api"; artifactId = "openapi-spring"; + useOneOfInterfaces = true; + addOneOfInterfaceImports = true; // clioOptions default redefinition need to be updated updateOption(CodegenConstants.INVOKER_PACKAGE, this.getInvokerPackage()); @@ -868,6 +870,19 @@ public Map postProcessModelsEnum(Map objs) { return objs; } + @Override + public void addImportsToOneOfInterface(List> imports) { + if (additionalProperties.containsKey(JACKSON)) { + for (String i : Arrays.asList("JsonSubTypes", "JsonTypeInfo")) { + Map oneImport = new HashMap<>(); + oneImport.put("import", importMapping.get(i)); + if (!imports.contains(oneImport)) { + imports.add(oneImport); + } + } + } + } + public void setUseBeanValidation(boolean useBeanValidation) { this.useBeanValidation = useBeanValidation; } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index a4bc0147c655..f4943b9b1f67 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -1652,4 +1652,25 @@ public static SemVer getOpenApiVersion(OpenAPI openAPI, String location, ListadditionalModelTypeAnnotations}}{{>generatedAnnotation}}{{>typeInfoAnnotation}}{{>xmlAnnotation}} public interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { - {{#discriminator}} + {{^vendorExtensions.x-deduction}}{{#discriminator}} public {{propertyType}} {{propertyGetter}}(); - {{/discriminator}} + {{/discriminator}}{{/vendorExtensions.x-deduction}} } diff --git a/modules/openapi-generator/src/main/resources/Java/typeInfoAnnotation.mustache b/modules/openapi-generator/src/main/resources/Java/typeInfoAnnotation.mustache index 63eb42ea5001..2eda38e95cfb 100644 --- a/modules/openapi-generator/src/main/resources/Java/typeInfoAnnotation.mustache +++ b/modules/openapi-generator/src/main/resources/Java/typeInfoAnnotation.mustache @@ -1,6 +1,15 @@ {{#jackson}} +{{^vendorExtensions.x-deduction}} @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) +{{/vendorExtensions.x-deduction}}{{#vendorExtensions.x-deduction}} +@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) +@JsonSubTypes({ +{{#vendorExtensions.x-deduction-model-names}} + @JsonSubTypes.Type(value = {{.}}.class, name = "{{.}}"), +{{/vendorExtensions.x-deduction-model-names}} +}) +{{/vendorExtensions.x-deduction}} {{#discriminator.mappedModels}} {{#-first}} @JsonSubTypes({ @@ -13,4 +22,4 @@ {{#isClassnameSanitized}} @JsonTypeName("{{name}}") {{/isClassnameSanitized}} -{{/jackson}} +{{/jackson}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/oneof_interface.mustache new file mode 100644 index 000000000000..fe696c971606 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/oneof_interface.mustache @@ -0,0 +1,6 @@ +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{>typeInfoAnnotation}}{{>xmlAnnotation}} +public interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { + {{^vendorExtensions.x-deduction}}{{#discriminator}} + public {{propertyType}} {{propertyGetter}}(); + {{/discriminator}}{{/vendorExtensions.x-deduction}} +} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/model.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/model.mustache index 78c3c9ae19d4..8b39ebd13c30 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/model.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/model.mustache @@ -37,7 +37,7 @@ import org.springframework.hateoas.RepresentationModel; {{>enumOuterClass}} {{/isEnum}} {{^isEnum}} -{{>pojo}} +{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{>pojo}}{{/vendorExtensions.x-is-one-of-interface}} {{/isEnum}} {{/model}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache new file mode 100644 index 000000000000..fe696c971606 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/JavaSpring/oneof_interface.mustache @@ -0,0 +1,6 @@ +{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{>typeInfoAnnotation}}{{>xmlAnnotation}} +public interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { + {{^vendorExtensions.x-deduction}}{{#discriminator}} + public {{propertyType}} {{propertyGetter}}(); + {{/discriminator}}{{/vendorExtensions.x-deduction}} +} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache index 4226edb1170c..3122414c5982 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache @@ -3,7 +3,7 @@ */{{#description}} @ApiModel(description = "{{{.}}}"){{/description}} {{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}}{{>additionalModelTypeAnnotations}} -public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}}{{^parent}}{{#hateoas}}extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}} {{#serializableModel}}implements Serializable{{/serializableModel}} { +public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{^parent}}{{#hateoas}}extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{ {{#serializableModel}} private static final long serialVersionUID = 1L; diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/typeInfoAnnotation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/typeInfoAnnotation.mustache index 81c2ba05f903..206f1f878898 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/typeInfoAnnotation.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/typeInfoAnnotation.mustache @@ -1,8 +1,14 @@ {{#jackson}} +{{^vendorExtensions.x-deduction}} @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) +{{/vendorExtensions.x-deduction}}{{#vendorExtensions.x-deduction}} +@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION){{/vendorExtensions.x-deduction}} @JsonSubTypes({ {{#discriminator.mappedModels}} @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"), {{/discriminator.mappedModels}} -}){{/jackson}} + {{#vendorExtensions.x-deduction-model-names}} + @JsonSubTypes.Type(value = {{.}}.class, name = "{{.}}"), + {{/vendorExtensions.x-deduction-model-names}} +}){{/jackson}} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 6d74693c21a3..c8ce597df0d4 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -1137,10 +1137,15 @@ public void testComposedSchemaAllOfDiscriminatorMapLegacy() { Assert.assertNull(reptile.discriminator); // the MyPets discriminator contains Cat and Lizard + List myPetsModelNames = Arrays.asList("Cat", "Lizard"); CodegenDiscriminator myPetDisc = new CodegenDiscriminator(); myPetDisc.setPropertyName(propertyName); myPetDisc.setPropertyBaseName(propertyBaseName); hs.clear(); + for (String myPetsModelName: myPetsModelNames) { + hs.add(new CodegenDiscriminator.MappedModel(myPetsModelName, codegen.toModelName(myPetsModelName))); + } + myPetDisc.setMappedModels(hs); modelName = "MyPets"; sc = openAPI.getComponents().getSchemas().get(modelName); CodegenModel myPets = codegen.fromModel(modelName, sc); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index f034760ea969..aaa1c0530990 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -17,8 +17,10 @@ package org.openapitools.codegen.java; +import static org.openapitools.codegen.TestUtils.assertFileContains; import static org.openapitools.codegen.TestUtils.validateJavaSourceFiles; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; import java.io.File; @@ -43,6 +45,7 @@ import java.util.stream.Collectors; import org.openapitools.codegen.ClientOptInput; +import org.openapitools.codegen.CodegenConfig; import org.openapitools.codegen.CodegenConstants; import org.openapitools.codegen.CodegenModel; import org.openapitools.codegen.CodegenOperation; @@ -55,7 +58,9 @@ import org.openapitools.codegen.config.CodegenConfigurator; import org.openapitools.codegen.languages.AbstractJavaCodegen; import org.openapitools.codegen.languages.JavaClientCodegen; +import org.openapitools.codegen.utils.ModelUtils; import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.google.common.collect.ImmutableMap; @@ -1222,4 +1227,130 @@ public void testWebClientWithFreeFormInQueryParameters() throws IOException { final Path defaultApi = Paths.get(output + "/src/main/java/xyz/abcdef/ApiClient.java"); TestUtils.assertFileContains(defaultApi, "value instanceof Map"); } + + @Test(dataProvider = "librariesToSpecificationsCartesianProduct") + public void oneOfModelsGeneration(String library, String specificationFile) throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/" + specificationFile); + final CodegenConfig codegen = new JavaClientCodegen(); + ((JavaClientCodegen)codegen).setSerializationLibrary(JavaClientCodegen.SERIALIZATION_LIBRARY_JACKSON); + codegen.setLibrary(library); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + + generator.opts(input).generate(); + + final String responseCode = "200"; + final String extension = "x-one-of-name"; + final Operation operation = openAPI.getPaths().get("/addFruits").getPost(); + List oneOfModels = new ArrayList<>(); + + // If response body contains oneOf definition - add OneOf model + final ApiResponse apiResponse = operation.getResponses().get(responseCode); + final Schema responseSchema = ModelUtils.getSchemaFromResponse(apiResponse); + if (ModelUtils.isArraySchema(responseSchema)) { + Schema responseItems = ((ArraySchema) responseSchema).getItems(); + if (responseItems.get$ref() == null) { + oneOfModels.add((String) (responseItems.getExtensions().get(extension))); + } + } + + // If request body contains oneOf definition - add OneOf model + final RequestBody requestBody = operation.getRequestBody(); + final Schema requestSchema = ModelUtils.getSchemaFromRequestBody(requestBody); + if (ModelUtils.isArraySchema(requestSchema)) { + Schema requestItems = ((ArraySchema) requestSchema).getItems(); + if (requestItems.get$ref() == null) { + oneOfModels.add((String) (requestItems.getExtensions().get(extension))); + } + } + + List models = new ArrayList<>(); + // If model contains discriminator - add to OneOf models, otherwise - generic models + openAPI.getComponents().getSchemas().forEach((modelName, modelSchema) -> { + if (modelSchema.getDiscriminator() != null || (modelSchema.getExtensions() != null && modelSchema.getExtensions().containsKey("x-one-of-name"))) { + oneOfModels.add(modelName); + } else { + // exclude allOf models + if (!modelName.contains("_")) { + models.add(modelName); + } + } + }); + assertFalse(oneOfModels.isEmpty()); + + final String pathFormat = "%s/%s/%s.java"; + final String relativePath = "/src/main/java/org/openapitools/client/model"; + final String jacksonSubTypeFormat = "@JsonSubTypes.Type(value = %s.class, name = \"%s\"),"; + + models.forEach(modelName -> { + final String modelPath = String.format(Locale.ROOT, pathFormat, outputPath, relativePath, modelName); + + oneOfModels.forEach(oneOfModelName -> { + // Models should implement all linked OneOf interfaces + assertFileContains(Paths.get(modelPath), oneOfModelName); + + // OneOf model should contain relevant jackson annotations + final String oneOfPath = String.format(Locale.ROOT, pathFormat, outputPath, relativePath, oneOfModelName); + assertFileContains(Paths.get(oneOfPath), String.format(Locale.ROOT, jacksonSubTypeFormat, modelName, modelName)); + }); + }); + } + + // Currently, not for NATIVE, JERSEY2, OKHTTP_GSON, MICROPROFILE or RETROFIT2 + public static Object[][] librariesProviderMethod() { + return new Object[][] { + { JavaClientCodegen.FEIGN }, + { JavaClientCodegen.GOOGLE_API_CLIENT }, + { JavaClientCodegen.JERSEY1 }, + { JavaClientCodegen.RESTEASY }, + { JavaClientCodegen.RESTTEMPLATE }, + { JavaClientCodegen.WEBCLIENT }, + { JavaClientCodegen.REST_ASSURED }, + { JavaClientCodegen.VERTX }, + { JavaClientCodegen.APACHE }, + }; + } + + public static Object[][] specificationsProviderMethod() { + return new Object[][] { + { "oneOf_inherited_class.yaml" }, + { "oneOf_inherited_class_array.yaml" }, + { "oneOf_interface.yaml" }, + { "oneOf_interface_array.yaml" }, + { "oneOf_with_allOf_inherited_class.yaml" }, + { "oneOf_with_allOf_inherited_class_array.yaml" }, + }; + } + + // Combines each library to each specification case + @DataProvider(name = "librariesToSpecificationsCartesianProduct") + public static Object[][] librariesToSpecificationsCartesianProduct() { + Object[][] librariesProvider = librariesProviderMethod(); + Object[][] specProvider = specificationsProviderMethod(); + Object[][] cartesianProduct = new Object[librariesProvider.length * specProvider.length][2]; + int i = 0; + for (Object[] library : librariesProvider) { + for (Object[] spec : specProvider) { + cartesianProduct[i][0] = library[0]; + cartesianProduct[i++][1] = spec[0]; + } + } + return cartesianProduct; + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 66817e5dbc77..56cffd2d1088 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -21,20 +21,27 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.ArraySchema; import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.parser.core.models.ParseOptions; import org.openapitools.codegen.*; import org.openapitools.codegen.languages.SpringCodegen; import org.openapitools.codegen.languages.features.CXFServerFeatures; +import org.openapitools.codegen.utils.ModelUtils; import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; @@ -43,6 +50,7 @@ import static org.openapitools.codegen.TestUtils.assertFileNotContains; import static org.openapitools.codegen.languages.SpringCodegen.RESPONSE_WRAPPER; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; public class SpringCodegenTest { @@ -723,4 +731,139 @@ public void shouldGenerateDefaultValueForEnumRequestParameter() throws IOExcepti "@RequestParam(value = \"testParameter1\", required = false, defaultValue = \"BAR\")", "@RequestParam(value = \"TestParameter2\", required = false, defaultValue = \"BAR\")"); } + + @Test(dataProvider = "specifications") + public void oneOfModelsGeneration(String specificationFile) throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/" + specificationFile); + final CodegenConfig codegen = new SpringCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + + generator.opts(input).generate(); + + final String responseCode = "200"; + final String extension = "x-one-of-name"; + final Operation operation = openAPI.getPaths().get("/addFruits").getPost(); + List oneOfModels = new ArrayList<>(); + + // If response body contains oneOf definition - add OneOf model + final ApiResponse apiResponse = operation.getResponses().get(responseCode); + final Schema responseSchema = ModelUtils.getSchemaFromResponse(apiResponse); + if (ModelUtils.isArraySchema(responseSchema)) { + Schema responseItems = ((ArraySchema) responseSchema).getItems(); + if (responseItems.get$ref() == null) { + oneOfModels.add((String) (responseItems.getExtensions().get(extension))); + } + } + + // If request body contains oneOf definition - add OneOf model + final RequestBody requestBody = operation.getRequestBody(); + final Schema requestSchema = ModelUtils.getSchemaFromRequestBody(requestBody); + if (ModelUtils.isArraySchema(requestSchema)) { + Schema requestItems = ((ArraySchema) requestSchema).getItems(); + if (requestItems.get$ref() == null) { + oneOfModels.add((String) (requestItems.getExtensions().get(extension))); + } + } + + List models = new ArrayList<>(); + // If model contains discriminator - add to OneOf models, otherwise - generic models + openAPI.getComponents().getSchemas().forEach((modelName, modelSchema) -> { + if (modelSchema.getDiscriminator() != null || (modelSchema.getExtensions() != null && modelSchema.getExtensions().containsKey("x-one-of-name"))) { + oneOfModels.add(modelName); + } else { + // exclude allOf models + if (!modelName.contains("_")) { + models.add(modelName); + } + } + }); + assertFalse(oneOfModels.isEmpty()); + + final String pathFormat = "%s/%s/%s.java"; + final String relativePath = "/src/main/java/org/openapitools/model"; + final String jacksonSubTypeFormat = "@JsonSubTypes.Type(value = %s.class, name = \"%s\"),"; + + models.forEach(modelName -> { + final String modelPath = String.format(Locale.ROOT, pathFormat, outputPath, relativePath, modelName); + + oneOfModels.forEach(oneOfModelName -> { + // Models should implement all linked OneOf interfaces + assertFileContains(Paths.get(modelPath), oneOfModelName); + + // OneOf model should contain relevant jackson annotations + final String oneOfPath = String.format(Locale.ROOT, pathFormat, outputPath, relativePath, oneOfModelName); + assertFileContains(Paths.get(oneOfPath), String.format(Locale.ROOT, jacksonSubTypeFormat, modelName, modelName)); + }); + }); + } + + @Test(dataProvider = "baseClassSpecifications") + public void oneOfShouldBeObject(String specificationFile) throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/" + specificationFile); + final CodegenConfig codegen = new SpringCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + + generator.opts(input).generate(); + + final String pathFormat = "%s/%s/%s.java"; + final String relativePath = "/src/main/java/org/openapitools/api"; + + // OneOf model should be replaced with object + final String oneOfPath = String.format(Locale.ROOT, pathFormat, outputPath, relativePath, "AddFruitsApi"); + assertFileContains(Paths.get(oneOfPath), "ResponseEntity", "Object body"); + } + + @DataProvider(name = "specifications") + public static Object[][] specificationsProviderMethod() { + return new Object[][] { + { "oneOf_inherited_class.yaml" }, + { "oneOf_inherited_class_array.yaml" }, + { "oneOf_interface.yaml" }, + { "oneOf_interface_array.yaml" }, + { "oneOf_with_allOf_inherited_class.yaml" }, + { "oneOf_with_allOf_inherited_class_array.yaml" }, + }; + } + + @DataProvider(name = "baseClassSpecifications") + public static Object[][] baseClassSpecificationsProviderMethod() { + return new Object[][] { + { "oneOf_interface_base_classes.yaml" }, + { "oneOf_interface_base_classes_combined.yaml" }, + }; + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_inherited_class.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_inherited_class.yaml new file mode 100644 index 000000000000..e9a4a9360024 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_inherited_class.yaml @@ -0,0 +1,46 @@ +openapi: 3.0.0 +info: + version: "1.0.0" + title: "API should have import for OneOf Class, models should extend OneOf Class" +paths: + /addFruits: + post: + responses: + '200': + description: Returns a list of fruits + content: + application/json: + schema: + $ref: '#/components/schemas/Fruit' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Fruit' +components: + schemas: + Fruit: + type: object + oneOf: + - $ref: '#/components/schemas/Apple' + - $ref: '#/components/schemas/Orange' + discriminator: + propertyName: kind + Apple: + type: object + properties: + kind: + type: string + identifier: + type: string + required: + - kind + Orange: + type: object + properties: + kind: + type: string + identifier2: + type: string + required: + - kind diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_inherited_class_array.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_inherited_class_array.yaml new file mode 100644 index 000000000000..6f56d61f530a --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_inherited_class_array.yaml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + version: "1.0.0" + title: "API should have import for OneOf Class, models should extend OneOf Class" +paths: + /addFruits: + post: + responses: + '200': + description: Returns a list of fruits + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Fruit' + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Fruit' +components: + schemas: + Fruit: + type: object + oneOf: + - $ref: '#/components/schemas/Apple' + - $ref: '#/components/schemas/Orange' + discriminator: + propertyName: kind + Apple: + type: object + properties: + kind: + type: string + identifier: + type: string + required: + - kind + Orange: + type: object + properties: + kind: + type: string + identifier2: + type: string + required: + - kind diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_interface.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_interface.yaml new file mode 100644 index 000000000000..93d67198c866 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_interface.yaml @@ -0,0 +1,52 @@ +openapi: 3.0.0 +info: + version: "1.0.0" + title: "OneOf interface should be generated, API has link to OneOf, models implement OneOf interface" +paths: + /addFruits: + post: + responses: + '200': + description: Returns a list of fruits + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Apple' + - $ref: '#/components/schemas/Orange' + discriminator: + propertyName: kind + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Apple' + - $ref: '#/components/schemas/Orange' +components: + schemas: + Apple: + type: object + properties: + kind: + type: string + identifier: + type: string + required: + - kind + Orange: + type: object + properties: + kind: + type: string + identifier2: + type: string + required: + - kind + Fruit: + type: object + oneOf: + - $ref: '#/components/schemas/Apple' + - $ref: '#/components/schemas/Orange' + discriminator: + propertyName: kind diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_interface_array.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_interface_array.yaml new file mode 100644 index 000000000000..77122d4b576d --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_interface_array.yaml @@ -0,0 +1,51 @@ +openapi: 3.0.0 +info: + version: "1.0.0" + title: "OneOf interface should be generated, API has link to OneOf, models implement OneOf interface" +paths: + /addFruits: + post: + responses: + '200': + description: Returns a list of fruits + content: + application/json: + schema: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Apple' + - $ref: '#/components/schemas/Orange' + discriminator: + propertyName: kind + requestBody: + content: + application/json: + schema: + type: array + items: + oneOf: + - $ref: '#/components/schemas/Apple' + - $ref: '#/components/schemas/Orange' + discriminator: + propertyName: kind +components: + schemas: + Apple: + type: object + properties: + kind: + type: string + identifier: + type: string + required: + - kind + Orange: + type: object + properties: + kind: + type: string + identifier2: + type: string + required: + - kind diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_interface_base_classes.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_interface_base_classes.yaml new file mode 100644 index 000000000000..3b85c08b91d0 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_interface_base_classes.yaml @@ -0,0 +1,27 @@ +openapi: 3.0.0 +info: + version: "1.0.0" + title: "OneOf interface should be generated, API has link to OneOf, models implement OneOf interface" +paths: + /addFruits: + post: + responses: + '200': + description: Returns a list of fruits + content: + application/json: + schema: + oneOf: + - type: string + format: date + - type: string + format: date-time + requestBody: + content: + application/json: + schema: + oneOf: + - type: string + format: date + - type: string + format: date-time diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_interface_base_classes_combined.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_interface_base_classes_combined.yaml new file mode 100644 index 000000000000..cae0db1458e4 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_interface_base_classes_combined.yaml @@ -0,0 +1,36 @@ +openapi: 3.0.0 +info: + version: "1.0.0" + title: "OneOf interface should be generated, API has link to OneOf, models implement OneOf interface" +paths: + /addFruits: + post: + responses: + '200': + description: Returns a list of fruits + content: + application/json: + schema: + oneOf: + - type: string + format: date + - $ref: '#/components/schemas/Apple' + requestBody: + content: + application/json: + schema: + oneOf: + - type: string + format: date + - $ref: '#/components/schemas/Apple' +components: + schemas: + Apple: + type: object + properties: + kind: + type: string + identifier: + type: string + required: + - kind diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_with_allOf_inherited_class.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_with_allOf_inherited_class.yaml new file mode 100644 index 000000000000..76134cd5dd73 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_with_allOf_inherited_class.yaml @@ -0,0 +1,46 @@ +openapi: 3.0.0 +info: + version: "1.0.0" + title: "API should have import for OneOf Class, models should extend OneOf Class" +paths: + /addFruits: + post: + responses: + '200': + description: Returns a list of fruits + content: + application/json: + schema: + $ref: '#/components/schemas/Fruit' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Fruit' +components: + schemas: + Apple: + type: object + allOf: + - $ref: '#/components/schemas/Fruit' + - type: object + properties: + identifier: + type: string + Orange: + type: object + allOf: + - $ref: '#/components/schemas/Fruit' + - type: object + properties: + identifier2: + type: string + Fruit: + type: object + properties: + kind: + type: string + required: + - kind + discriminator: + propertyName: kind diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_with_allOf_inherited_class_array.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_with_allOf_inherited_class_array.yaml new file mode 100644 index 000000000000..c067a483714d --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_with_allOf_inherited_class_array.yaml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + version: "1.0.0" + title: "API should have import for OneOf Class, models should extend OneOf Class" +paths: + /addFruits: + post: + responses: + '200': + description: Returns a list of fruits + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Fruit' + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Fruit' +components: + schemas: + Apple: + type: object + allOf: + - $ref: '#/components/schemas/Fruit' + - type: object + properties: + identifier: + type: string + Orange: + type: object + allOf: + - $ref: '#/components/schemas/Fruit' + - type: object + properties: + identifier2: + type: string + Fruit: + type: object + properties: + kind: + type: string + required: + - kind + discriminator: + propertyName: kind