diff --git a/src/main/java/io/github/jamsesso/jsonlogic/evaluator/JsonLogicEvaluator.java b/src/main/java/io/github/jamsesso/jsonlogic/evaluator/JsonLogicEvaluator.java index d78892b..e15fbb0 100644 --- a/src/main/java/io/github/jamsesso/jsonlogic/evaluator/JsonLogicEvaluator.java +++ b/src/main/java/io/github/jamsesso/jsonlogic/evaluator/JsonLogicEvaluator.java @@ -6,6 +6,10 @@ import java.util.*; public class JsonLogicEvaluator { + + /** Sentinel object to represent a missing value (for internal use only). */ + private static final Object MISSING = new Object(); + private final Map expressions; public JsonLogicEvaluator(Collection expressions) { @@ -82,8 +86,10 @@ public Object evaluate(JsonLogicVariable variable, Object data, String jsonPath) for (String partial : keys) { result = evaluatePartialVariable(partial, result, jsonPath + "[0]"); - if (result == null) { + if (result == MISSING) { return defaultValue; + } else if (result == null) { + return null; } } @@ -106,14 +112,19 @@ private Object evaluatePartialVariable(String key, Object data, String jsonPath) } if (index < 0 || index >= list.size()) { - return null; + return MISSING; } return transform(list.get(index)); } if (data instanceof Map) { - return transform(((Map) data).get(key)); + Map map = (Map) data; + if (map.containsKey(key)) { + return transform(map.get(key)); + } else { + return MISSING; + } } return null; diff --git a/src/test/java/io/github/jamsesso/jsonlogic/VariableTests.java b/src/test/java/io/github/jamsesso/jsonlogic/VariableTests.java index 9a4e98f..980fc58 100644 --- a/src/test/java/io/github/jamsesso/jsonlogic/VariableTests.java +++ b/src/test/java/io/github/jamsesso/jsonlogic/VariableTests.java @@ -3,12 +3,15 @@ import org.junit.Test; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; public class VariableTests { private static final JsonLogic jsonLogic = new JsonLogic(); @@ -99,4 +102,115 @@ public void testComplexAccess() throws JsonLogicException { assertEquals("Jane", jsonLogic.apply("{\"var\": \"users.1.name\"}", data)); assertEquals(2048.0, jsonLogic.apply("{\"var\": \"users.1.followers\"}", data)); } + + @Test + public void missingNestedMapKey_returnsDefault() throws JsonLogicException { + // data.a.b is missing -> should use default + String rule = "{\"var\": [\"a.b.c\", \"fallback\"]}"; + Map data = map("a", map("b", new HashMap<>())); + + Object result = jsonLogic.apply(rule, data); + + assertEquals("fallback", result); + } + + @Test + public void presentNullLeaf_returnsNull_notDefault() throws JsonLogicException { + // data.user.age present with value null -> should return null (no default) + String rule = "{\"var\": [\"user.age\", 42]}"; + Map user = new HashMap<>(); + user.put("age", null); + Map data = map("user", user); + + Object result = jsonLogic.apply(rule, data); + + assertNull(result); + } + + @Test + public void intermediateNull_returnsNull_notDefault() throws JsonLogicException { + // data.a.b is null before finishing path -> should return null (no default) + String rule = "{\"var\": [\"a.b.c\", \"fallback\"]}"; + Map data = map("a", map("b", null)); + + Object result = jsonLogic.apply(rule, data); + + assertNull(result); + } + + @Test + public void nonTraversableIntermediate_returnsNull_notDefault() throws JsonLogicException { + // data.a is a number; trying to access a.b -> should return null (no default) + String rule = "{\"var\": [\"a.b\", \"fallback\"]}"; + Map data = map("a", 5); + + Object result = jsonLogic.apply(rule, data); + + assertNull(result); + } + + @Test + public void arrayIndexWithinBounds_returnsElement_asDoubleForNumbers() throws JsonLogicException { + // items.1 exists -> should return 20 (as a double per evaluator.transform) + String rule = "{\"var\": [\"items.1\", 999]}"; + Map data = map("items", Arrays.asList(10, 20)); + + Object result = jsonLogic.apply(rule, data); + + assertTrue(result instanceof Number); + assertEquals(20.0, ((Number) result).doubleValue(), 0.0); + } + + @Test + public void arrayIndexOutOfBounds_returnsDefault() throws JsonLogicException { + // items.2 missing -> use default + String rule = "{\"var\": [\"items.2\", \"missing\"]}"; + Map data = map("items", Arrays.asList(10, 20)); + + Object result = jsonLogic.apply(rule, data); + + assertEquals("missing", result); + } + + @Test + public void arrayElementPresentButNull_returnsNull_notDefault() throws JsonLogicException { + // items.0 exists and is null -> should return null (no default) + String rule = "{\"var\": [\"items.0\", \"missing\"]}"; + Map data = map("items", Collections.singletonList(null)); + + Object result = jsonLogic.apply(rule, data); + + assertNull(result); + } + + @Test + public void topLevelNumericIndex_overList_works() throws JsonLogicException { + // {"var": [1, "missing"]} over a top-level list -> "banana" + String rule = "{\"var\": [1, \"missing\"]}"; + List data = Arrays.asList("apple", "banana", "carrot"); + + Object result = jsonLogic.apply(rule, data); + + assertEquals("banana", result); + } + + @Test + public void emptyVarKey_returnsWholeDataObject() throws JsonLogicException { + // {"var": ""} should return the entire data object (same instance) + String rule = "{\"var\": \"\"}"; + Map data = map("x", 1); + + Object result = jsonLogic.apply(rule, data); + + assertSame("Should return the same data instance", data, result); + } + + /** Helper to make small maps concisely. */ + private static Map map(Object... kv) { + Map m = new HashMap<>(); + for (int i = 0; i < kv.length; i += 2) { + m.put((String) kv[i], kv[i + 1]); + } + return m; + } }