Skip to content

Commit bd4e12e

Browse files
javicvthiyagu06toddbaertbeeme1mr
authored
fix: MutableContext and ImmutableContext merge are made recursive (#280)
MutableContext and ImmutableContext merge are made recursive Signed-off-by: Javier Collado <[email protected]> Co-authored-by: Thiyagu GK <[email protected]> Co-authored-by: Todd Baert <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
1 parent fe8698f commit bd4e12e

File tree

4 files changed

+99
-10
lines changed

4 files changed

+99
-10
lines changed

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

+4-3
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,11 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
8686
if (overridingContext.getTargetingKey() != null && !overridingContext.getTargetingKey().trim().equals("")) {
8787
newTargetingKey = overridingContext.getTargetingKey();
8888
}
89-
Map<String, Value> merged = new HashMap<>();
9089

91-
merged.putAll(this.asMap());
92-
merged.putAll(overridingContext.asMap());
90+
Map<String, Value> merged = this.merge(m -> new ImmutableStructure(m),
91+
this.asMap(),
92+
overridingContext.asMap());
93+
9394
return new ImmutableContext(newTargetingKey, merged);
9495
}
9596
}

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

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package dev.openfeature.sdk;
22

33
import java.time.Instant;
4-
import java.util.HashMap;
54
import java.util.List;
65
import java.util.Map;
76

@@ -92,18 +91,25 @@ public EvaluationContext merge(EvaluationContext overridingContext) {
9291
return new MutableContext(this.asMap());
9392
}
9493

95-
Map<String, Value> merged = new HashMap<String, Value>();
94+
Map<String, Value> merged = this.merge(map -> new MutableStructure(map),
95+
this.asMap(),
96+
overridingContext.asMap());
9697

97-
merged.putAll(this.asMap());
98-
merged.putAll(overridingContext.asMap());
99-
EvaluationContext ec = new MutableContext(merged);
98+
String newTargetingKey = "";
10099

101100
if (this.getTargetingKey() != null && !this.getTargetingKey().trim().equals("")) {
102-
ec.setTargetingKey(this.getTargetingKey());
101+
newTargetingKey = this.getTargetingKey();
103102
}
104103

105104
if (overridingContext.getTargetingKey() != null && !overridingContext.getTargetingKey().trim().equals("")) {
106-
ec.setTargetingKey(overridingContext.getTargetingKey());
105+
newTargetingKey = overridingContext.getTargetingKey();
106+
}
107+
108+
EvaluationContext ec = null;
109+
if (newTargetingKey != null && !newTargetingKey.trim().equals("")) {
110+
ec = new MutableContext(newTargetingKey, merged);
111+
} else {
112+
ec = new MutableContext(merged);
107113
}
108114

109115
return ec;

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

+32
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
44

5+
import java.util.HashMap;
56
import java.util.Map;
67
import java.util.Set;
8+
import java.util.Map.Entry;
9+
import java.util.function.Function;
710
import java.util.stream.Collectors;
811

912
/**
@@ -90,4 +93,33 @@ default Object convertValue(Value value) {
9093
}
9194
throw new ValueNotConvertableError();
9295
}
96+
97+
/**
98+
* Recursively merges the base map with the overriding map.
99+
*
100+
* @param <T> Structure type
101+
* @param newStructure function to create the right structure
102+
* @param base base map to merge
103+
* @param overriding overriding map to merge
104+
* @return resulting merged map
105+
*/
106+
default <T extends Structure> Map<String, Value> merge(Function<Map<String, Value>, Structure> newStructure,
107+
Map<String, Value> base,
108+
Map<String, Value> overriding) {
109+
Map<String, Value> merged = new HashMap<>();
110+
111+
merged.putAll(base);
112+
for (Entry<String, Value> overridingEntry : overriding.entrySet()) {
113+
String key = overridingEntry.getKey();
114+
if (overridingEntry.getValue().isStructure() && merged.containsKey(key) && merged.get(key).isStructure()) {
115+
Structure mergedValue = merged.get(key).asStructure();
116+
Structure overridingValue = overridingEntry.getValue().asStructure();
117+
Map<String, Value> newMap = this.merge(newStructure, mergedValue.asMap(), overridingValue.asMap());
118+
merged.put(key, new Value(newStructure.apply(newMap)));
119+
} else {
120+
merged.put(key, overridingEntry.getValue());
121+
}
122+
}
123+
return merged;
124+
}
93125
}

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

+50
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
99
import static org.junit.jupiter.api.Assertions.assertEquals;
1010
import static org.junit.jupiter.api.Assertions.assertThrows;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
1112

1213
class ImmutableContextTest {
1314

@@ -64,4 +65,53 @@ void mergeShouldReturnAllTheValuesFromTheContextWhenOverridingContextIsNull() {
6465
assertEquals("targeting_key", merge.getTargetingKey());
6566
assertArrayEquals(new Object[]{"key1", "key2"}, merge.keySet().toArray());
6667
}
68+
69+
@DisplayName("Merge should retain subkeys from the existing context when the overriding context has the same targeting key")
70+
@Test
71+
void mergeShouldRetainItsSubkeysWhenOverridingContextHasTheSameKey() {
72+
HashMap<String, Value> attributes = new HashMap<>();
73+
HashMap<String, Value> overridingAttributes = new HashMap<>();
74+
HashMap<String, Value> key1Attributes = new HashMap<>();
75+
HashMap<String, Value> ovKey1Attributes = new HashMap<>();
76+
77+
key1Attributes.put("key1_1", new Value("val1_1"));
78+
attributes.put("key1", new Value(new ImmutableStructure(key1Attributes)));
79+
attributes.put("key2", new Value("val2"));
80+
ovKey1Attributes.put("overriding_key1_1", new Value("overriding_val_1_1"));
81+
overridingAttributes.put("key1", new Value(new ImmutableStructure(ovKey1Attributes)));
82+
83+
EvaluationContext ctx = new ImmutableContext("targeting_key", attributes);
84+
EvaluationContext overriding = new ImmutableContext("targeting_key", overridingAttributes);
85+
EvaluationContext merge = ctx.merge(overriding);
86+
assertEquals("targeting_key", merge.getTargetingKey());
87+
assertArrayEquals(new Object[]{"key1", "key2"}, merge.keySet().toArray());
88+
89+
Value key1 = merge.getValue("key1");
90+
assertTrue(key1.isStructure());
91+
92+
Structure value = key1.asStructure();
93+
assertArrayEquals(new Object[]{"key1_1","overriding_key1_1"}, value.keySet().toArray());
94+
}
95+
96+
@DisplayName("Merge should retain subkeys from the existing context when the overriding context doesn't have targeting key")
97+
@Test
98+
void mergeShouldRetainItsSubkeysWhenOverridingContextHasNoTargetingKey() {
99+
HashMap<String, Value> attributes = new HashMap<>();
100+
HashMap<String, Value> key1Attributes = new HashMap<>();
101+
102+
key1Attributes.put("key1_1", new Value("val1_1"));
103+
attributes.put("key1", new Value(new ImmutableStructure(key1Attributes)));
104+
attributes.put("key2", new Value("val2"));
105+
106+
EvaluationContext ctx = new ImmutableContext(attributes);
107+
EvaluationContext overriding = new ImmutableContext();
108+
EvaluationContext merge = ctx.merge(overriding);
109+
assertArrayEquals(new Object[]{"key1", "key2"}, merge.keySet().toArray());
110+
111+
Value key1 = merge.getValue("key1");
112+
assertTrue(key1.isStructure());
113+
114+
Structure value = key1.asStructure();
115+
assertArrayEquals(new Object[]{"key1_1"}, value.keySet().toArray());
116+
}
67117
}

0 commit comments

Comments
 (0)