diff --git a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodegenConfig.java b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodegenConfig.java index aa707bc7e..02192c6a6 100644 --- a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodegenConfig.java +++ b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodegenConfig.java @@ -74,7 +74,8 @@ public enum ConfigName { GENERATE_APIS("generate-apis"), GENERATE_MODELS("generate-models"), BEAN_VALIDATION("use-bean-validation"), - SERIALIZABLE_MODEL("serializable-model"); + SERIALIZABLE_MODEL("serializable-model"), + EQUALS_HASHCODE("equals-hashcode"); private final String name; diff --git a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CommonItemConfig.java b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CommonItemConfig.java index 926ea95ef..6d65cf4b8 100644 --- a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CommonItemConfig.java +++ b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CommonItemConfig.java @@ -172,4 +172,11 @@ public class CommonItemConfig { */ @ConfigItem(name = "generate-models") public Optional generateModels; + + /** + * Enable the generation of equals and hashcode in models. If you set this to {@code false}, the models + * will not have equals and hashcode. + */ + @ConfigItem(name = "equals-hashcode") + public Optional equalsHashcode; } diff --git a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java index c4f69013b..619611011 100644 --- a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java +++ b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java @@ -320,6 +320,9 @@ protected void generate(OpenApiGeneratorOptions options) { getValues(smallRyeConfig, openApiFilePath, CodegenConfig.ConfigName.SERIALIZABLE_MODEL, Boolean.class) .ifPresent(generator::withSerialiableModel); + getValues(smallRyeConfig, openApiFilePath, CodegenConfig.ConfigName.EQUALS_HASHCODE, Boolean.class) + .ifPresent(generator::withEqualsHashcode); + getValues(smallRyeConfig, openApiFilePath, CodegenConfig.ConfigName.NORMALIZER, String.class, String.class) .ifPresent(generator::withOpenApiNormalizer); diff --git a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java index 12147ece2..4f06bc514 100644 --- a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java +++ b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java @@ -112,6 +112,7 @@ private void setDefaults() { this.configurator.addAdditionalProperty("use-field-name-in-part-filename", FALSE); this.configurator.addAdditionalProperty("verbose", FALSE); this.configurator.addAdditionalProperty(CodegenConstants.SERIALIZABLE_MODEL, FALSE); + this.configurator.addAdditionalProperty("equals-hashcode", TRUE); } /** @@ -202,6 +203,11 @@ public OpenApiClientGeneratorWrapper withSerialiableModel(final Boolean serialia return this; } + public OpenApiClientGeneratorWrapper withEqualsHashcode(final Boolean equalsHashcode) { + this.configurator.addAdditionalProperty("equals-hashcode", equalsHashcode); + return this; + } + /** * Sets the global 'additionalModelTypeAnnotations' setting. If not set this setting will default to empty. * diff --git a/client/deployment/src/main/resources/templates/libraries/microprofile/pojo.qute b/client/deployment/src/main/resources/templates/libraries/microprofile/pojo.qute index ae5542c1e..8a0c4ec3a 100644 --- a/client/deployment/src/main/resources/templates/libraries/microprofile/pojo.qute +++ b/client/deployment/src/main/resources/templates/libraries/microprofile/pojo.qute @@ -135,6 +135,64 @@ public class {m.classname} {#if m.parent}extends {m.parent}{/if}{#if serializabl return sb.toString(); } + {#if equals-hashcode} + {#if m.vars.size > 0} + /** + * Compares this object to the specified object. The result is + * \{@code true\} if and only if the argument is not + * \{@code null\} and is a \{@code {m.classname}\} object that + * contains the same values as this object. + * + * @param obj the object to compare with. + * @return \{@code true\} if the objects are the same; + * \{@code false\} otherwise. + **/ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + {m.classname} model = ({m.classname}) obj; + + {#if m.vars.size == 1} + return java.util.Objects.equals({m.vars.0.name}, model.{m.vars.0.name}); + {#else} + {#for v in m.vars} + {#if v_isFirst} + return java.util.Objects.equals({v.name}, model.{v.name}) && + {#else if v_isLast} + java.util.Objects.equals({v.name}, model.{v.name}); + {#else} + java.util.Objects.equals({v.name}, model.{v.name}) && + {/if} + {/for} + {/if} + } + + /** + * Returns a hash code for a \{@code {m.classname}\}. + * + * @return a hash code value for a \{@code {m.classname}\}. + **/ + @Override + public int hashCode() { + {#if m.vars.size == 1} + return java.util.Objects.hash({m.vars.0.name}); + {#else} + {#for v in m.vars} + {#if v_isFirst} + return java.util.Objects.hash({v.name}, + {#else if v_isLast} + {v.name}); + {#else} + {v.name}, + {/if} + {/for} + {/if} + } + {/if} + {/if} + /** * Convert the given object to string with each line indented by 4 spaces * (except the first line). diff --git a/client/integration-tests/equals-hashcode/pom.xml b/client/integration-tests/equals-hashcode/pom.xml new file mode 100644 index 000000000..5e8d8b7b4 --- /dev/null +++ b/client/integration-tests/equals-hashcode/pom.xml @@ -0,0 +1,93 @@ + + + + quarkus-openapi-generator-integration-tests + io.quarkiverse.openapi.generator + 3.0.0-lts-SNAPSHOT + + 4.0.0 + + quarkus-openapi-generator-it-equals-hashcode + Quarkus - Openapi Generator - Integration Tests - Client - Equals hashcode + Example project for general usage + + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator + + + org.assertj + assertj-core + test + + + io.quarkus + quarkus-junit5 + test + + + + + + io.quarkus + quarkus-maven-plugin + true + + + + build + generate-code + generate-code-tests + + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + org.jboss.logmanager.LogManager + + ${maven.home} + + + + + + + + + native + + + + + diff --git a/client/integration-tests/equals-hashcode/src/main/openapi/quarkus-equals-hashcode-openapi.yaml b/client/integration-tests/equals-hashcode/src/main/openapi/quarkus-equals-hashcode-openapi.yaml new file mode 100644 index 000000000..ec431be5e --- /dev/null +++ b/client/integration-tests/equals-hashcode/src/main/openapi/quarkus-equals-hashcode-openapi.yaml @@ -0,0 +1,125 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Animals - OpenAPI 3.0", + "version": "1.0.5" + }, + "servers": [ + { + "url": "/api/v3" + } + ], + "tags": [ + { + "name": "primate", + "description": "Everything about Primates" + } + ], + "paths": { + "/primate/{id}": { + "get": { + "tags": [ + "primate" + ], + "summary": "Find primate by ID", + "description": "Returns a single primate", + "operationId": "getPrimateById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of primate to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Primate" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Primate not found" + } + } + } + } + }, + "components": { + "schemas": { + "Animal": { + "type": "object", + "properties": { + "born": { + "type": "string", + "description": "Dated Base extension.", + "format": "date-time" + }, + "deceased": { + "type": "string", + "description": "Dated Base extension.", + "format": "date-time" + } + }, + "xml": { + "name": "animal" + } + }, + "Mammal": { + "type": "object", + "allOf": [ { + "$ref": "#/components/schemas/Animal" + } ], + "properties": { + "gender": { + "type": "string", + "enum": [ + "female", + "male" + ] + } + }, + "xml": { + "name": "mammal" + } + }, + "Primate": { + "required": [ + "name" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Mammal" + } + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "name": { + "type": "string", + "example": "jane doe" + } + }, + "xml": { + "name": "primate" + } + } + } + } +} diff --git a/client/integration-tests/equals-hashcode/src/main/openapi/quarkus-non-equals-hashcode-openapi.yaml b/client/integration-tests/equals-hashcode/src/main/openapi/quarkus-non-equals-hashcode-openapi.yaml new file mode 100644 index 000000000..ec431be5e --- /dev/null +++ b/client/integration-tests/equals-hashcode/src/main/openapi/quarkus-non-equals-hashcode-openapi.yaml @@ -0,0 +1,125 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Animals - OpenAPI 3.0", + "version": "1.0.5" + }, + "servers": [ + { + "url": "/api/v3" + } + ], + "tags": [ + { + "name": "primate", + "description": "Everything about Primates" + } + ], + "paths": { + "/primate/{id}": { + "get": { + "tags": [ + "primate" + ], + "summary": "Find primate by ID", + "description": "Returns a single primate", + "operationId": "getPrimateById", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of primate to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Primate" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Primate not found" + } + } + } + } + }, + "components": { + "schemas": { + "Animal": { + "type": "object", + "properties": { + "born": { + "type": "string", + "description": "Dated Base extension.", + "format": "date-time" + }, + "deceased": { + "type": "string", + "description": "Dated Base extension.", + "format": "date-time" + } + }, + "xml": { + "name": "animal" + } + }, + "Mammal": { + "type": "object", + "allOf": [ { + "$ref": "#/components/schemas/Animal" + } ], + "properties": { + "gender": { + "type": "string", + "enum": [ + "female", + "male" + ] + } + }, + "xml": { + "name": "mammal" + } + }, + "Primate": { + "required": [ + "name" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Mammal" + } + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "name": { + "type": "string", + "example": "jane doe" + } + }, + "xml": { + "name": "primate" + } + } + } + } +} diff --git a/client/integration-tests/equals-hashcode/src/main/resources/application.properties b/client/integration-tests/equals-hashcode/src/main/resources/application.properties new file mode 100644 index 000000000..929c6beeb --- /dev/null +++ b/client/integration-tests/equals-hashcode/src/main/resources/application.properties @@ -0,0 +1,6 @@ +quarkus.openapi-generator.codegen.spec.quarkus_equals_hashcode_openapi_yaml.base-package=org.acme.equals.hashcode + +quarkus.openapi-generator.codegen.spec.quarkus_non_equals_hashcode_openapi_yaml.base-package=org.acme.non.equals.hashcode +quarkus.openapi-generator.codegen.spec.quarkus_non_equals_hashcode_openapi_yaml.equals-hashcode=false + +quarkus.keycloak.devservices.enabled=false \ No newline at end of file diff --git a/client/integration-tests/equals-hashcode/src/test/java/io/quarkiverse/openapi/generator/it/EqualsHashcodeTest.java b/client/integration-tests/equals-hashcode/src/test/java/io/quarkiverse/openapi/generator/it/EqualsHashcodeTest.java new file mode 100644 index 000000000..c9bf43842 --- /dev/null +++ b/client/integration-tests/equals-hashcode/src/test/java/io/quarkiverse/openapi/generator/it/EqualsHashcodeTest.java @@ -0,0 +1,52 @@ +package io.quarkiverse.openapi.generator.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.time.OffsetDateTime; + +import org.acme.equals.hashcode.model.Animal; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class EqualsHashcodeTest { + + @Test + void verifyModelNotEquals() { + var object1 = new Animal(); + object1.setDeceased(OffsetDateTime.now().minusHours(2)); + + var object2 = new Animal(); + object2.setBorn(OffsetDateTime.now().minusYears(1)); + + assertNotEquals(object1, object2); + } + + @Test + void verifyModelEquals() { + var offset = OffsetDateTime.now().minusHours(2); + + var object1 = new Animal(); + object1.setDeceased(offset); + + var object2 = new Animal(); + object2.setDeceased(offset); + + assertEquals(object1, object2); + } + + @Test + void verifyModelHasHashCode() { + var offset = OffsetDateTime.now().minusHours(2); + + var object1 = new Animal(); + object1.setDeceased(offset); + + var object2 = new Animal(); + object2.setDeceased(offset); + + assertEquals(object1.hashCode(), object2.hashCode()); + } +} diff --git a/client/integration-tests/equals-hashcode/src/test/java/io/quarkiverse/openapi/generator/it/VerifyGenerationTest.java b/client/integration-tests/equals-hashcode/src/test/java/io/quarkiverse/openapi/generator/it/VerifyGenerationTest.java new file mode 100644 index 000000000..30224b75a --- /dev/null +++ b/client/integration-tests/equals-hashcode/src/test/java/io/quarkiverse/openapi/generator/it/VerifyGenerationTest.java @@ -0,0 +1,42 @@ +package io.quarkiverse.openapi.generator.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class VerifyGenerationTest { + + @Test + void verifyThatGeneratedModelDoesHaveMethods() { + var equalsMethod = getMethod(org.acme.equals.hashcode.model.Animal.class, "equals"); + var hashCodeMethod = getMethod(org.acme.equals.hashcode.model.Animal.class, "hashCode"); + + assertEquals(equalsMethod.getDeclaringClass(), org.acme.equals.hashcode.model.Animal.class); + assertEquals(hashCodeMethod.getDeclaringClass(), org.acme.equals.hashcode.model.Animal.class); + } + + @Test + void verifyThatGeneratedModelDoesntHaveMethods() { + var equalsMethod = getMethod(org.acme.non.equals.hashcode.model.Animal.class, "equals"); + var hashCodeMethod = getMethod(org.acme.non.equals.hashcode.model.Animal.class, "hashCode"); + + assertNotEquals(equalsMethod.getDeclaringClass(), org.acme.equals.hashcode.model.Animal.class); + assertNotEquals(hashCodeMethod.getDeclaringClass(), org.acme.equals.hashcode.model.Animal.class); + } + + private static Method getMethod(Class clazz, String methodName) { + var methods = clazz.getMethods(); + + return Arrays.stream(methods) + .filter(method -> method.getName().equals(methodName)) + .findAny() + .orElseThrow(); + } +} diff --git a/client/integration-tests/pom.xml b/client/integration-tests/pom.xml index 01fec6671..fef96b073 100644 --- a/client/integration-tests/pom.xml +++ b/client/integration-tests/pom.xml @@ -45,6 +45,7 @@ github without-oidc serializable-model + equals-hashcode diff --git a/docs/modules/ROOT/pages/client.adoc b/docs/modules/ROOT/pages/client.adoc index 7ea4386b0..aa6bf61c2 100644 --- a/docs/modules/ROOT/pages/client.adoc +++ b/docs/modules/ROOT/pages/client.adoc @@ -194,6 +194,11 @@ to true: quarkus.openapi-generator.codegen.spec.my_openapi_yaml.serializable-model=true ---- +[[equals-hashcode]] +== Equals and hashcode + +include::./includes/equals-hashcode.adoc[leveloffset=+1, opts=optional] + == Known Limitations === Supported Arguments diff --git a/docs/modules/ROOT/pages/includes/equals-hashcode.adoc b/docs/modules/ROOT/pages/includes/equals-hashcode.adoc new file mode 100644 index 000000000..b916c4c8f --- /dev/null +++ b/docs/modules/ROOT/pages/includes/equals-hashcode.adoc @@ -0,0 +1,8 @@ +By default, `hashcode` and `equals` methods are automatically generated for all models. These methods are based on all the variables in the model and can not be customized. + +To disable set `equals-hashcode` to false: + +[source,properties] +---- +quarkus.openapi-generator.codegen.spec.my_openapi_yaml.equals-hashcode=false +---- \ No newline at end of file