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..6453fa072b67 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) { @@ -2424,6 +2424,18 @@ public CodegenModel fromModel(String name, Schema schema) { 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)); + if (ModelUtils.isComposedSchema(schema) + && m.discriminator == null + && schema.getExtensions() != null + && schema.getExtensions().containsKey("x-one-of-name")) { + m.vendorExtensions.put("x-deduction", true); + List deductionModelNames = new ArrayList<>(); + ((ComposedSchema) schema).getOneOf().forEach(model -> { + String modelName = ModelUtils.getSimpleRef(model.get$ref()); + deductionModelNames.add(toModelName(modelName)); + }); + m.vendorExtensions.put("x-deduction-model-names", deductionModelNames); + } if (!this.getLegacyDiscriminatorBehavior()) { m.addDiscriminatorMappedModelsImports(); } @@ -3165,7 +3177,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)) { 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..01e5d28f542c 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; @@ -81,24 +82,9 @@ import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.parser.util.SchemaTypeUtil; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.openapitools.codegen.*; -import org.openapitools.codegen.meta.features.*; -import org.openapitools.codegen.utils.ModelUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.io.File; import java.io.IOException; -import java.time.LocalDate; -import java.time.ZoneId; import java.util.*; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -import static org.openapitools.codegen.utils.StringUtils.*; public abstract class AbstractJavaCodegen extends DefaultCodegen implements CodegenConfig { @@ -1319,6 +1305,16 @@ public Map postProcessModels(Map objs) { } } + // add implements for serializable/parcelable to all models + List> models = (List>) objs.get("models"); + for (Map mo : models) { + CodegenModel cm = (CodegenModel) mo.get("model"); + if (this.serializableModel) { + cm.getVendorExtensions().putIfAbsent("x-implements", new ArrayList()); + ((ArrayList) cm.getVendorExtensions().get("x-implements")).add("Serializable"); + } + } + return postProcessModelsEnum(objs); } @@ -1363,8 +1359,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 +1432,33 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } } + + /** + * Add all OneOf schemas to #/components/schemas and replace them in the original content by ref 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)) { + 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); + } + }); + } + + private static boolean isOneOfSchema(final Schema schema) { + return schema instanceof ComposedSchema + && ((ComposedSchema) schema).getProperties() == null + && Optional.ofNullable(schema.getExtensions()).map(m -> m.containsKey("x-one-of-name")) + .orElse(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..e935c77ef104 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()); @@ -233,7 +235,7 @@ public void processOpts() { } super.processOpts(); - + useOneOfInterfaces = true; // clear model and api doc template as this codegen // does not support auto-generated markdown doc at the moment //TODO: add doc templates @@ -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; } @@ -880,4 +895,25 @@ public void setPerformBeanValidation(boolean performBeanValidation) { public void setUseOptional(boolean useOptional) { this.useOptional = useOptional; } + + @Override + public void postProcessParameter(CodegenParameter p) { + // we use a custom version of this function to remove the l, d, and f suffixes from Long/Double/Float + // defaultValues + // remove the l because our users will use Long.parseLong(String defaultValue) + // remove the d because our users will use Double.parseDouble(String defaultValue) + // remove the f because our users will use Float.parseFloat(String defaultValue) + // NOTE: for CodegenParameters we DO need these suffixes because those defaultValues are used as java value + // literals assigned to Long/Double/Float + if (p.defaultValue == null) { + return; + } + Boolean fixLong = (p.isLong && "l".equals(p.defaultValue.substring(p.defaultValue.length()-1))); + Boolean fixDouble = (p.isDouble && "d".equals(p.defaultValue.substring(p.defaultValue.length()-1))); + Boolean fixFloat = (p.isFloat && "f".equals(p.defaultValue.substring(p.defaultValue.length()-1))); + if (fixLong || fixDouble || fixFloat) { + p.defaultValue = p.defaultValue.substring(0, p.defaultValue.length()-1); + } + } + } diff --git a/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache b/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache index 02deb483d5fd..fe696c971606 100644 --- a/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache +++ b/modules/openapi-generator/src/main/resources/Java/oneof_interface.mustache @@ -1,6 +1,6 @@ {{>additionalModelTypeAnnotations}}{{>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..2aee24b387b2 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}} + {{#vendorExtensions.x-deduction-model-names}} + @JsonSubTypes.Type(value = {{.}}.class, name = "{{.}}"), + {{/vendorExtensions.x-deduction-model-names}} }){{/jackson}} 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..f1484a5ded70 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 @@ -18,23 +18,32 @@ package org.openapitools.codegen.java.spring; import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.oas.models.Components; 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.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; @@ -43,6 +52,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 { @@ -354,6 +364,7 @@ public void testAdditionalPropertiesPutForConfigValues() throws Exception { openAPI.addServersItem(new Server().url("https://api.abcde.xy:8082/v2")); openAPI.setInfo(new Info()); openAPI.getInfo().setTitle("Some test API"); + openAPI.setComponents(new Components().schemas(new HashMap<>())); codegen.preprocessOpenAPI(openAPI); Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP), Boolean.TRUE); @@ -451,6 +462,7 @@ public void testInitialConfigValues() throws Exception { OpenAPI openAPI = new OpenAPI(); openAPI.addServersItem(new Server().url("https://api.abcde.xy:8082/v2")); openAPI.setInfo(new Info()); + openAPI.setComponents(new Components().schemas(new HashMap<>())); codegen.preprocessOpenAPI(openAPI); Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP), Boolean.FALSE); @@ -723,4 +735,137 @@ public void shouldGenerateDefaultValueForEnumRequestParameter() throws IOExcepti "@RequestParam(value = \"testParameter1\", required = false, defaultValue = \"BAR\")", "@RequestParam(value = \"TestParameter2\", required = false, defaultValue = \"BAR\")"); } + + @Test + public void oneOf_5381() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/issue_5381.yaml", null, new ParseOptions()).getOpenAPI(); + + SpringCodegen codegen = new SpringCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true"); + codegen.setUseOneOfInterfaces(true); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + codegen.setHateoas(true); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + //generator.setGeneratorPropertyDefault(CodegenConstants.USE_ONEOF_DISCRIMINATOR_LOOKUP, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "false"); + + codegen.setUseOneOfInterfaces(true); + codegen.setJava8(true); + codegen.setLegacyDiscriminatorBehavior(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(); + + assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/Foo.java"), "public class Foo implements FooRefOrValue"); + assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/FooRef.java"), "public class FooRef implements FooRefOrValue"); + assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/FooRefOrValue.java"), "public interface FooRefOrValue"); + } + + @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" }, + }; + } + + @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)); + }); + }); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_5381.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_5381.yaml new file mode 100644 index 000000000000..192c958f7245 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue_5381.yaml @@ -0,0 +1,134 @@ +openapi: 3.0.1 +info: + title: ByRefOrValue + description: > + This tests for a oneOf interface representation + version: 0.0.1 +servers: + - url: "http://localhost:8080" +tags: + - name: Foo +paths: + /foo: + get: + tags: + - Foo + summary: GET all Foos + operationId: getAllFoos + responses: + '200': + $ref: '#/components/responses/200FooArray' + post: + tags: + - Foo + summary: Create a Foo + operationId: createFoo + requestBody: + $ref: '#/components/requestBodies/Foo' + responses: + '201': + $ref: '#/components/responses/201Foo' + +components: + schemas: + Entity: + type: object + allOf: + - "$ref": "#/components/schemas/Addressable" + - "$ref": "#/components/schemas/Extensible" + + EntityRef: + description: Entity reference schema to be use for all entityRef class. + type: object + properties: + name: + type: string + description: Name of the related entity. + '@referredType': + type: string + description: The actual type of the target instance when needed for disambiguation. + allOf: + - $ref: '#/components/schemas/Addressable' + - "$ref": "#/components/schemas/Extensible" + + + Addressable: + type: object + properties: + href: + type: string + description: Hyperlink reference + id: + type: string + description: unique identifier + description: Base schema for adressable entities + Extensible: + type: object + properties: + "@schemaLocation": + type: string + description: A URI to a JSON-Schema file that defines additional attributes + and relationships + "@baseType": + type: string + description: When sub-classing, this defines the super-class + "@type": + type: string + description: When sub-classing, this defines the sub-class Extensible name + required: + - '@type' + + FooRefOrValue: + type: object + oneOf: + - $ref: "#/components/schemas/Foo" + - $ref: "#/components/schemas/FooRef" + discriminator: + propertyName: "@type" + mapping: + Foo: "#/components/schemas/Foo" + FooRef: "#/components/schemas/FooRef" + + Foo: + type: object + properties: + fooPropA: + type: string + fooPropB: + type: string + allOf: + - $ref: '#/components/schemas/Entity' + + FooRef: + type: object + properties: + foorefPropA: + type: string + allOf: + - $ref: '#/components/schemas/EntityRef' + + requestBodies: + Foo: + description: The Foo to be created + content: + application/json;charset=utf-8: + schema: + $ref: '#/components/schemas/Foo' + responses: + '204': + description: Deleted + content: { } + 201Foo: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/FooRefOrValue' + 200FooArray: + description: Success + content: + application/json;charset=utf-8: + schema: + type: array + items: + $ref: '#/components/schemas/FooRefOrValue' \ No newline at end of file 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_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