Skip to content

Commit 0472a1b

Browse files
committed
fix equals and hashcode
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
1 parent 4b8bec8 commit 0472a1b

File tree

7 files changed

+214
-38
lines changed

7 files changed

+214
-38
lines changed

src/main/java/dev/openfeature/sdk/AbstractStructure.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import java.util.Collections;
44
import java.util.HashMap;
55
import java.util.Map;
6-
import lombok.EqualsAndHashCode;
6+
import java.util.Objects;
77

88
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
9-
@EqualsAndHashCode
109
abstract class AbstractStructure implements Structure {
1110

1211
protected final Map<String, Value> attributes;
@@ -48,4 +47,17 @@ public Map<String, Object> asObjectMap() {
4847
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
4948
HashMap::putAll);
5049
}
50+
51+
@Override
52+
public boolean equals(Object object) {
53+
if (!(object instanceof AbstractStructure))
54+
return false;
55+
AbstractStructure that = (AbstractStructure) object;
56+
return Objects.equals(attributes, that.attributes);
57+
}
58+
59+
@Override
60+
public int hashCode() {
61+
return Objects.hashCode(attributes);
62+
}
5163
}

src/main/java/dev/openfeature/sdk/EvaluationContext.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ public interface EvaluationContext extends Structure {
2424
*/
2525
EvaluationContext merge(EvaluationContext overridingContext);
2626

27+
default boolean isEqualTo(Object other) {
28+
if (other == null) {
29+
return false;
30+
}
31+
if (other == this) {
32+
return true;
33+
}
34+
if (!(other instanceof EvaluationContext)) {
35+
return false;
36+
}
37+
var otherContext = (EvaluationContext) other;
38+
return asUnmodifiableMap().equals(otherContext.asUnmodifiableMap());
39+
}
40+
2741
/**
2842
* Recursively merges the overriding map into the base Value map.
2943
* The base map is mutated, the overriding map is not.

src/main/java/dev/openfeature/sdk/ImmutableContext.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,7 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
107107
*/
108108
@Override
109109
public boolean equals(Object o) {
110-
if (this == o) {
111-
return true;
112-
}
113-
if (!(o instanceof EvaluationContext)) {
114-
return false;
115-
}
116-
EvaluationContext that = (EvaluationContext) o;
117-
return this.asUnmodifiableMap().equals(that.asUnmodifiableMap());
110+
return isEqualTo(o);
118111
}
119112

120113
/**
@@ -130,7 +123,7 @@ public int hashCode() {
130123
synchronized (this) {
131124
result = cachedHashCode;
132125
if (result == null) {
133-
result = asUnmodifiableMap().hashCode();
126+
result = structure.hashCode();
134127
cachedHashCode = result;
135128
}
136129
}

src/main/java/dev/openfeature/sdk/MutableContext.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
* be modified after instantiation.
1818
*/
1919
@ToString
20-
@EqualsAndHashCode
2120
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
2221
public class MutableContext implements EvaluationContext {
2322

@@ -125,6 +124,24 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
125124
return new MutableContext(attributes);
126125
}
127126

127+
/**
128+
* Equality for EvaluationContext implementations is defined in terms of their resolved
129+
* attribute maps. Two contexts are considered equal if their {@link #asMap()} representations
130+
* contain the same key/value pairs, regardless of how the context was constructed or layered.
131+
*
132+
* @param o the object to compare with this context
133+
* @return true if the other object is an EvaluationContext whose resolved attributes match
134+
*/
135+
@Override
136+
public boolean equals(Object o) {
137+
return isEqualTo(o);
138+
}
139+
140+
@Override
141+
public int hashCode() {
142+
return structure.hashCode();
143+
}
144+
128145
/**
129146
* Hidden class to tell Lombok not to copy these methods over via delegation.
130147
*/

src/test/java/dev/openfeature/sdk/ImmutableContextTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.util.HashSet;
1414
import java.util.Map;
1515
import org.junit.jupiter.api.DisplayName;
16+
import org.junit.jupiter.api.Nested;
1617
import org.junit.jupiter.api.Test;
1718

1819
class ImmutableContextTest {
@@ -192,4 +193,45 @@ void immutableContextHashCodeIsStable() {
192193
int second = ctx.hashCode();
193194
assertEquals(first, second);
194195
}
196+
197+
@Nested
198+
class Equals {
199+
ImmutableContext ctx = new ImmutableContext("c", Map.of("a", new Value("b")));
200+
201+
@Test
202+
void equalsItself() {
203+
assertEquals(ctx, ctx);
204+
}
205+
206+
@Test
207+
void equalsLayeredEvalCtxIfSameValues() {
208+
var layeredContext = new LayeredEvaluationContext(ctx, null, null, null);
209+
assertEquals(layeredContext, ctx);
210+
assertEquals(ctx, layeredContext);
211+
}
212+
213+
@Test
214+
void equalsDifferentMutableEvalCtxIfSameValues() {
215+
var mutable = new MutableContext("c", Map.of("a", new Value("b")));
216+
assertEquals(mutable, ctx);
217+
assertEquals(ctx, mutable);
218+
}
219+
}
220+
221+
@Nested
222+
class HashCode {
223+
ImmutableContext ctx = new ImmutableContext("c", Map.of("a", new Value("b")));
224+
225+
@Test
226+
void hashCodeEqualsLayeredEvalCtxIfSameValues() {
227+
var layeredContext = new LayeredEvaluationContext(ctx, null, null, null);
228+
assertEquals(layeredContext.hashCode(), ctx.hashCode());
229+
}
230+
231+
@Test
232+
void hashCodeEqualsDifferentMutableEvalCtxIfSameValues() {
233+
var mutable = new MutableContext("c", Map.of("a", new Value("b")));
234+
assertEquals(mutable.hashCode(), ctx.hashCode());
235+
}
236+
}
195237
}

src/test/java/dev/openfeature/sdk/LayeredEvaluationContextTest.java

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ void keySetIsGeneratedCorrectly() {
156156
"api",
157157
"override",
158158
"targetingKey" // expected, even though not explicitly set
159-
);
159+
);
160160

161161
assertEquals(expectedKeys, layeredContext.keySet());
162162
// cached key set
@@ -197,7 +197,7 @@ void mapIsGeneratedCorrectly() {
197197
"api", new Value("api"),
198198
"override", new Value("hook"),
199199
"targetingKey", new Value("hook-level") // expected, even though not explicitly set
200-
);
200+
);
201201

202202
assertEquals(expectedKeys, layeredContext.asMap());
203203
assertEquals(expectedKeys, layeredContext.asUnmodifiableMap());
@@ -244,7 +244,7 @@ void mapIsGeneratedCorrectly() {
244244
"api", "api",
245245
"override", "hook",
246246
"targetingKey", "hook-level" // expected, even though not explicitly set in map
247-
);
247+
);
248248

249249
assertEquals(expectedKeys, layeredContext.asObjectMap());
250250
}
@@ -504,4 +504,60 @@ void testMixedContextEquality() {
504504
assertEquals(base.asMap().hashCode(), layered.hashCode());
505505
}
506506
}
507+
508+
@Nested
509+
class Equals {
510+
@Test
511+
void equalsItself() {
512+
var layeredContext =
513+
new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext);
514+
layeredContext.putHookContext(hookContext);
515+
assertEquals(layeredContext, layeredContext);
516+
}
517+
518+
@Test
519+
void equalsDifferentLayeredEvalCtxIfSameValues() {
520+
var layeredContext1 = new LayeredEvaluationContext(apiContext, null, null, null);
521+
var layeredContext2 = new LayeredEvaluationContext(null, apiContext, null, null);
522+
assertEquals(layeredContext1, layeredContext2);
523+
}
524+
525+
@Test
526+
void equalsDifferentImmutableEvalCtxIfSameValues() {
527+
var immutable = new ImmutableContext("key", Map.of("prop", new Value("erty")));
528+
var layeredContext = new LayeredEvaluationContext(immutable, null, null, null);
529+
assertEquals(immutable, layeredContext);
530+
assertEquals(layeredContext, immutable);
531+
}
532+
533+
@Test
534+
void equalsDifferentMutableEvalCtxIfSameValues() {
535+
var mutable = new MutableContext("key", Map.of("prop", new Value("erty")));
536+
var layeredContext = new LayeredEvaluationContext(mutable, null, null, null);
537+
assertEquals(mutable, layeredContext);
538+
assertEquals(layeredContext, mutable);
539+
}
540+
}
541+
@Nested
542+
class HashCode {
543+
ImmutableContext immutable = new ImmutableContext("c", Map.of("a", new Value("b")));
544+
LayeredEvaluationContext layeredContext = new LayeredEvaluationContext(immutable, null, null, null);
545+
546+
@Test
547+
void hashCodeEqualsItself() {
548+
var layeredContext2 = new LayeredEvaluationContext(null, null, immutable, null);
549+
assertEquals(layeredContext.hashCode(), layeredContext2.hashCode());
550+
}
551+
552+
@Test
553+
void hasSameHashCodeAsImmutableEvalCtxIfSameValues() {
554+
assertEquals(immutable.hashCode(), layeredContext.hashCode());
555+
}
556+
557+
@Test
558+
void hashCodeEqualsDifferentMutableEvalCtxIfSameValues() {
559+
MutableContext ctx = new MutableContext("c", Map.of("a", new Value("b")));
560+
assertEquals(immutable.hashCode(), ctx.hashCode());
561+
}
562+
}
507563
}

src/test/java/dev/openfeature/sdk/MutableContextTest.java

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.util.HashMap;
1111
import java.util.Map;
1212
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Nested;
1314
import org.junit.jupiter.api.Test;
1415

1516
class MutableContextTest {
@@ -139,30 +140,71 @@ void shouldAllowChainingOfMutations() {
139140
assertEquals(3.0, context.getValue("key3").asDouble());
140141
}
141142

142-
@DisplayName("Two different MutableContext objects with the different contents are not considered equal")
143-
@Test
144-
void unequalMutableContextsAreNotEqual() {
145-
final Map<String, Value> attributes = new HashMap<>();
146-
attributes.put("key1", new Value("val1"));
147-
final MutableContext ctx = new MutableContext(attributes);
148-
149-
final Map<String, Value> attributes2 = new HashMap<>();
150-
final MutableContext ctx2 = new MutableContext(attributes2);
151-
152-
assertNotEquals(ctx, ctx2);
143+
@Nested
144+
class Equals {
145+
MutableContext ctx = new MutableContext("c", Map.of("a", new Value("b")));
146+
147+
@Test
148+
void equalsItself() {
149+
assertEquals(ctx, ctx);
150+
}
151+
152+
@Test
153+
void equalsLayeredEvalCtxIfSameValues() {
154+
var layeredContext = new LayeredEvaluationContext(ctx, null, null, null);
155+
assertEquals(layeredContext, ctx);
156+
assertEquals(ctx, layeredContext);
157+
}
158+
159+
@Test
160+
void equalsDifferentMutableEvalCtxIfSameValues() {
161+
var immutable = new ImmutableContext("c", Map.of("a", new Value("b")));
162+
assertEquals(immutable, ctx);
163+
assertEquals(ctx, immutable);
164+
}
165+
166+
@DisplayName("Two different MutableContext objects with the different contents are not considered equal")
167+
@Test
168+
void unequalMutableContextsAreNotEqual() {
169+
final Map<String, Value> attributes = new HashMap<>();
170+
attributes.put("key1", new Value("val1"));
171+
final MutableContext ctx = new MutableContext(attributes);
172+
173+
final Map<String, Value> attributes2 = new HashMap<>();
174+
final MutableContext ctx2 = new MutableContext(attributes2);
175+
176+
assertNotEquals(ctx, ctx2);
177+
}
178+
179+
@DisplayName("Two different MutableContext objects with the same content are considered equal")
180+
@Test
181+
void equalMutableContextsAreEqual() {
182+
final Map<String, Value> attributes = new HashMap<>();
183+
attributes.put("key1", new Value("val1"));
184+
final MutableContext ctx = new MutableContext(attributes);
185+
186+
final Map<String, Value> attributes2 = new HashMap<>();
187+
attributes2.put("key1", new Value("val1"));
188+
final MutableContext ctx2 = new MutableContext(attributes2);
189+
190+
assertEquals(ctx, ctx2);
191+
}
153192
}
154193

155-
@DisplayName("Two different MutableContext objects with the same content are considered equal")
156-
@Test
157-
void equalMutableContextsAreEqual() {
158-
final Map<String, Value> attributes = new HashMap<>();
159-
attributes.put("key1", new Value("val1"));
160-
final MutableContext ctx = new MutableContext(attributes);
161-
162-
final Map<String, Value> attributes2 = new HashMap<>();
163-
attributes2.put("key1", new Value("val1"));
164-
final MutableContext ctx2 = new MutableContext(attributes2);
165-
166-
assertEquals(ctx, ctx2);
194+
@Nested
195+
class HashCode {
196+
MutableContext ctx = new MutableContext("c", Map.of("a", new Value("b")));
197+
198+
@Test
199+
void hashCodeEqualsLayeredEvalCtxIfSameValues() {
200+
var layeredContext = new LayeredEvaluationContext(ctx, null, null, null);
201+
assertEquals(layeredContext.hashCode(), ctx.hashCode());
202+
}
203+
204+
@Test
205+
void hashCodeEqualsDifferentMutableEvalCtxIfSameValues() {
206+
var immutable = new ImmutableContext("c", Map.of("a", new Value("b")));
207+
assertEquals(immutable.hashCode(), ctx.hashCode());
208+
}
167209
}
168210
}

0 commit comments

Comments
 (0)