Skip to content

Commit 49995a7

Browse files
fix: distinguish missing vs null in var resolution using MISSING (#59)
(cherry picked from commit 0e14b3b)
1 parent 96f0582 commit 49995a7

File tree

2 files changed

+128
-3
lines changed

2 files changed

+128
-3
lines changed

src/main/java/io/github/jamsesso/jsonlogic/evaluator/JsonLogicEvaluator.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
import java.util.*;
77

88
public class JsonLogicEvaluator {
9+
10+
/** Sentinel object to represent a missing value (for internal use only). */
11+
private static final Object MISSING = new Object();
12+
913
private final Map<String, JsonLogicExpression> expressions;
1014

1115
public JsonLogicEvaluator(Collection<JsonLogicExpression> expressions) {
@@ -82,8 +86,10 @@ public Object evaluate(JsonLogicVariable variable, Object data, String jsonPath)
8286
for (String partial : keys) {
8387
result = evaluatePartialVariable(partial, result, jsonPath + "[0]");
8488

85-
if (result == null) {
89+
if (result == MISSING) {
8690
return defaultValue;
91+
} else if (result == null) {
92+
return null;
8793
}
8894
}
8995

@@ -106,14 +112,19 @@ private Object evaluatePartialVariable(String key, Object data, String jsonPath)
106112
}
107113

108114
if (index < 0 || index >= list.size()) {
109-
return null;
115+
return MISSING;
110116
}
111117

112118
return transform(list.get(index));
113119
}
114120

115121
if (data instanceof Map) {
116-
return transform(((Map) data).get(key));
122+
Map<?, ?> map = (Map<?, ?>) data;
123+
if (map.containsKey(key)) {
124+
return transform(map.get(key));
125+
} else {
126+
return MISSING;
127+
}
117128
}
118129

119130
return null;

src/test/java/io/github/jamsesso/jsonlogic/VariableTests.java

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import org.junit.Test;
44

55
import java.util.Arrays;
6+
import java.util.Collections;
67
import java.util.HashMap;
78
import java.util.List;
89
import java.util.Map;
910

1011
import static org.junit.Assert.assertEquals;
1112
import static org.junit.Assert.assertNull;
13+
import static org.junit.Assert.assertSame;
14+
import static org.junit.Assert.assertTrue;
1215

1316
public class VariableTests {
1417
private static final JsonLogic jsonLogic = new JsonLogic();
@@ -99,4 +102,115 @@ public void testComplexAccess() throws JsonLogicException {
99102
assertEquals("Jane", jsonLogic.apply("{\"var\": \"users.1.name\"}", data));
100103
assertEquals(2048.0, jsonLogic.apply("{\"var\": \"users.1.followers\"}", data));
101104
}
105+
106+
@Test
107+
public void missingNestedMapKey_returnsDefault() throws JsonLogicException {
108+
// data.a.b is missing -> should use default
109+
String rule = "{\"var\": [\"a.b.c\", \"fallback\"]}";
110+
Map<String, Object> data = map("a", map("b", new HashMap<>()));
111+
112+
Object result = jsonLogic.apply(rule, data);
113+
114+
assertEquals("fallback", result);
115+
}
116+
117+
@Test
118+
public void presentNullLeaf_returnsNull_notDefault() throws JsonLogicException {
119+
// data.user.age present with value null -> should return null (no default)
120+
String rule = "{\"var\": [\"user.age\", 42]}";
121+
Map<String, Object> user = new HashMap<>();
122+
user.put("age", null);
123+
Map<String, Object> data = map("user", user);
124+
125+
Object result = jsonLogic.apply(rule, data);
126+
127+
assertNull(result);
128+
}
129+
130+
@Test
131+
public void intermediateNull_returnsNull_notDefault() throws JsonLogicException {
132+
// data.a.b is null before finishing path -> should return null (no default)
133+
String rule = "{\"var\": [\"a.b.c\", \"fallback\"]}";
134+
Map<String, Object> data = map("a", map("b", null));
135+
136+
Object result = jsonLogic.apply(rule, data);
137+
138+
assertNull(result);
139+
}
140+
141+
@Test
142+
public void nonTraversableIntermediate_returnsNull_notDefault() throws JsonLogicException {
143+
// data.a is a number; trying to access a.b -> should return null (no default)
144+
String rule = "{\"var\": [\"a.b\", \"fallback\"]}";
145+
Map<String, Object> data = map("a", 5);
146+
147+
Object result = jsonLogic.apply(rule, data);
148+
149+
assertNull(result);
150+
}
151+
152+
@Test
153+
public void arrayIndexWithinBounds_returnsElement_asDoubleForNumbers() throws JsonLogicException {
154+
// items.1 exists -> should return 20 (as a double per evaluator.transform)
155+
String rule = "{\"var\": [\"items.1\", 999]}";
156+
Map<String, Object> data = map("items", Arrays.asList(10, 20));
157+
158+
Object result = jsonLogic.apply(rule, data);
159+
160+
assertTrue(result instanceof Number);
161+
assertEquals(20.0, ((Number) result).doubleValue(), 0.0);
162+
}
163+
164+
@Test
165+
public void arrayIndexOutOfBounds_returnsDefault() throws JsonLogicException {
166+
// items.2 missing -> use default
167+
String rule = "{\"var\": [\"items.2\", \"missing\"]}";
168+
Map<String, Object> data = map("items", Arrays.asList(10, 20));
169+
170+
Object result = jsonLogic.apply(rule, data);
171+
172+
assertEquals("missing", result);
173+
}
174+
175+
@Test
176+
public void arrayElementPresentButNull_returnsNull_notDefault() throws JsonLogicException {
177+
// items.0 exists and is null -> should return null (no default)
178+
String rule = "{\"var\": [\"items.0\", \"missing\"]}";
179+
Map<String, Object> data = map("items", Collections.singletonList(null));
180+
181+
Object result = jsonLogic.apply(rule, data);
182+
183+
assertNull(result);
184+
}
185+
186+
@Test
187+
public void topLevelNumericIndex_overList_works() throws JsonLogicException {
188+
// {"var": [1, "missing"]} over a top-level list -> "banana"
189+
String rule = "{\"var\": [1, \"missing\"]}";
190+
List<String> data = Arrays.asList("apple", "banana", "carrot");
191+
192+
Object result = jsonLogic.apply(rule, data);
193+
194+
assertEquals("banana", result);
195+
}
196+
197+
@Test
198+
public void emptyVarKey_returnsWholeDataObject() throws JsonLogicException {
199+
// {"var": ""} should return the entire data object (same instance)
200+
String rule = "{\"var\": \"\"}";
201+
Map<String, Object> data = map("x", 1);
202+
203+
Object result = jsonLogic.apply(rule, data);
204+
205+
assertSame("Should return the same data instance", data, result);
206+
}
207+
208+
/** Helper to make small maps concisely. */
209+
private static Map<String, Object> map(Object... kv) {
210+
Map<String, Object> m = new HashMap<>();
211+
for (int i = 0; i < kv.length; i += 2) {
212+
m.put((String) kv[i], kv[i + 1]);
213+
}
214+
return m;
215+
}
102216
}

0 commit comments

Comments
 (0)