Skip to content

fix: equals and hashcode of several classes #1364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion src/main/java/dev/openfeature/sdk/AbstractStructure.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ abstract class AbstractStructure implements Structure {

@Override
public boolean isEmpty() {
return attributes == null || attributes.size() == 0;
return attributes == null || attributes.isEmpty();
}

AbstractStructure() {
Expand Down Expand Up @@ -46,4 +46,46 @@ public Map<String, Object> asObjectMap() {
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
HashMap::putAll);
}

public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof AbstractStructure)) {
return false;
}
final AbstractStructure other = (AbstractStructure) o;
if (other.attributes == attributes) {
return true;
}
if (attributes == null || other.attributes == null) {
return false;
}
if (other.attributes.size() != attributes.size()) {
return false;
}

for (Map.Entry<String, Value> thisEntry : attributes.entrySet()) {
Value thisValue = thisEntry.getValue();
Value otherValue = other.attributes.get(thisEntry.getKey());
if (thisValue == null && otherValue == null) {
continue;
}
if (thisValue == null || otherValue == null) {
return false;
}
if (!thisValue.equals(otherValue)) {
return false;
}
}

return true;
}

public int hashCode() {
if (attributes == null) {
return 0;
}
return attributes.hashCode();
}
}
17 changes: 17 additions & 0 deletions src/main/java/dev/openfeature/sdk/ImmutableContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
return new ImmutableContext(attributes);
}

@Override
public boolean equals(Object object) {
if (object == this) {
return true;
}
if (!(object instanceof ImmutableContext)) {
return false;
}
ImmutableContext other = (ImmutableContext) object;
return this.structure.equals(other.structure);
}

@Override
public int hashCode() {
return structure.hashCode();
}

@SuppressWarnings("all")
private static class DelegateExclusions {
@ExcludeFromGeneratedCoverageReport
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/dev/openfeature/sdk/ImmutableMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,42 @@ public boolean isEmpty() {
return metadata.isEmpty();
}

public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof ImmutableMetadata)) {
return false;
}
final ImmutableMetadata other = (ImmutableMetadata) o;
if (other.metadata == metadata) {
return true;
}
if (other.metadata.size() != metadata.size()) {
return false;
}

for (Map.Entry<String, Object> thisEntry : metadata.entrySet()) {
Object thisValue = thisEntry.getValue();
Object otherValue = other.metadata.get(thisEntry.getKey());
if (thisValue == null && otherValue == null) {
continue;
}
if (thisValue == null || otherValue == null) {
return false;
}
if (!thisValue.equals(otherValue)) {
return false;
}
}

return true;
}

public int hashCode() {
return metadata.hashCode();
}

/**
* Obtain a builder for {@link ImmutableMetadata}.
*/
Expand Down
4 changes: 1 addition & 3 deletions src/main/java/dev/openfeature/sdk/ImmutableStructure.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
Expand All @@ -18,7 +17,6 @@
* not be modified after instantiation. All references are clones.
*/
@ToString
@EqualsAndHashCode
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
public final class ImmutableStructure extends AbstractStructure {

Expand All @@ -38,7 +36,7 @@ public ImmutableStructure(Map<String, Value> attributes) {
super(copyAttributes(attributes, null));
}

protected ImmutableStructure(String targetingKey, Map<String, Value> attributes) {
ImmutableStructure(String targetingKey, Map<String, Value> attributes) {
super(copyAttributes(attributes, targetingKey));
}

Expand Down
19 changes: 17 additions & 2 deletions src/main/java/dev/openfeature/sdk/MutableContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Delegate;

Expand All @@ -17,7 +16,6 @@
* be modified after instantiation.
*/
@ToString
@EqualsAndHashCode
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
public class MutableContext implements EvaluationContext {

Expand Down Expand Up @@ -125,6 +123,23 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
return new MutableContext(attributes);
}

@Override
public boolean equals(Object object) {
if (object == this) {
return true;
}
if (!(object instanceof MutableContext)) {
return false;
}
MutableContext other = (MutableContext) object;
return this.structure.equals(other.structure);
}

@Override
public int hashCode() {
return structure.hashCode();
}

/**
* Hidden class to tell Lombok not to copy these methods over via delegation.
*/
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/dev/openfeature/sdk/MutableStructure.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
Expand All @@ -15,7 +14,6 @@
* be modified after instantiation.
*/
@ToString
@EqualsAndHashCode
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
public class MutableStructure extends AbstractStructure {

Expand Down
29 changes: 27 additions & 2 deletions src/main/java/dev/openfeature/sdk/Value.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.SneakyThrows;
import lombok.ToString;

Expand All @@ -17,7 +16,6 @@
* This intermediate representation provides a good medium of exchange.
*/
@ToString
@EqualsAndHashCode
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType", "checkstyle:NoFinalizer"})
public class Value implements Cloneable {

Expand Down Expand Up @@ -316,4 +314,31 @@ public static Value objectToValue(Object object) {
throw new TypeMismatchError("Flag value " + object + " had unexpected type " + object.getClass() + ".");
}
}

/**
* Returns true iff {@code this} is equal to {@code o}, or if both objects represent the same data.
* @param o the other object
* @return true iff both objects are equal or represent the same data
*/
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Value)) {
return false;
}
final Value other = (Value) o;
return innerObject.equals(other.innerObject);
}

/**
* Returns the `hashCode` of the underlying data, or 0 if {@link Value#isNull()} returns true.
* @return the hash code
*/
public int hashCode() {
if (innerObject == null) {
return 0;
}
return innerObject.hashCode();
}
}
28 changes: 28 additions & 0 deletions src/test/java/dev/openfeature/sdk/ImmutableContextTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static dev.openfeature.sdk.EvaluationContext.TARGETING_KEY;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Collections;
Expand Down Expand Up @@ -133,4 +134,31 @@ void mergeShouldRetainItsSubkeysWhenOverridingContextHasNoTargetingKey() {
Structure value = key1.asStructure();
assertArrayEquals(new Object[] {"key1_1"}, value.keySet().toArray());
}

@DisplayName("Two different MutableContext objects with the different contents are not considered equal")
@Test
void unequalImmutableContextsAreNotEqual() {
final Map<String, Value> attributes = new HashMap<>();
attributes.put("key1", new Value("val1"));
final ImmutableContext ctx = new ImmutableContext(attributes);

final Map<String, Value> attributes2 = new HashMap<>();
final ImmutableContext ctx2 = new ImmutableContext(attributes2);

assertNotEquals(ctx, ctx2);
}

@DisplayName("Two different MutableContext objects with the same content are considered equal")
@Test
void equalImmutableContextsAreEqual() {
final Map<String, Value> attributes = new HashMap<>();
attributes.put("key1", new Value("val1"));
final ImmutableContext ctx = new ImmutableContext(attributes);

final Map<String, Value> attributes2 = new HashMap<>();
attributes2.put("key1", new Value("val1"));
final ImmutableContext ctx2 = new ImmutableContext(attributes2);

assertEquals(ctx, ctx2);
}
}
28 changes: 28 additions & 0 deletions src/test/java/dev/openfeature/sdk/ImmutableMetadataTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.openfeature.sdk;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

import org.junit.jupiter.api.Test;

class ImmutableMetadataTest {
@Test
void unequalImmutableMetadataAreUnequal() {
ImmutableMetadata i1 =
ImmutableMetadata.builder().addString("key1", "value1").build();
ImmutableMetadata i2 =
ImmutableMetadata.builder().addString("key1", "value2").build();

assertNotEquals(i1, i2);
}

@Test
void equalImmutableMetadataAreEqual() {
ImmutableMetadata i1 =
ImmutableMetadata.builder().addString("key1", "value1").build();
ImmutableMetadata i2 =
ImmutableMetadata.builder().addString("key1", "value1").build();

assertEquals(i1, i2);
}
}
Loading
Loading