Skip to content

Commit 0a32fe9

Browse files
committed
Issue #127 Add JsonPathFilterEvaluationTest to prove logical operator robustness on varying inputs
1 parent f3dbe1a commit 0a32fe9

File tree

1 file changed

+164
-0
lines changed

1 file changed

+164
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package json.java21.jsonpath;
2+
3+
import jdk.sandbox.java.util.json.Json;
4+
import jdk.sandbox.java.util.json.JsonValue;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.util.logging.Logger;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
/// Dedicated evaluation tests for logical operators in filters (!, &&, ||, parens).
12+
/// Verifies truth tables and precedence on controlled documents.
13+
class JsonPathFilterEvaluationTest extends JsonPathLoggingConfig {
14+
15+
private static final Logger LOG = Logger.getLogger(JsonPathFilterEvaluationTest.class.getName());
16+
17+
@Test
18+
void testLogicalAnd() {
19+
LOG.info(() -> "TEST: testLogicalAnd (&&)");
20+
// Truth table for AND
21+
// Items:
22+
// 1. T && T -> Match
23+
// 2. T && F -> No
24+
// 3. F && T -> No
25+
// 4. F && F -> No
26+
var json = Json.parse("""
27+
[
28+
{"id": 1, "a": true, "b": true},
29+
{"id": 2, "a": true, "b": false},
30+
{"id": 3, "a": false, "b": true},
31+
{"id": 4, "a": false, "b": false}
32+
]
33+
""");
34+
35+
var results = JsonPath.parse("$[?(@.a == true && @.b == true)]").query(json);
36+
37+
assertThat(results).hasSize(1);
38+
assertThat(asInt(results.getFirst(), "id")).isEqualTo(1);
39+
}
40+
41+
@Test
42+
void testLogicalOr() {
43+
LOG.info(() -> "TEST: testLogicalOr (||)");
44+
// Truth table for OR
45+
// Items:
46+
// 1. T || T -> Match
47+
// 2. T || F -> Match
48+
// 3. F || T -> Match
49+
// 4. F || F -> No
50+
var json = Json.parse("""
51+
[
52+
{"id": 1, "a": true, "b": true},
53+
{"id": 2, "a": true, "b": false},
54+
{"id": 3, "a": false, "b": true},
55+
{"id": 4, "a": false, "b": false}
56+
]
57+
""");
58+
59+
var results = JsonPath.parse("$[?(@.a == true || @.b == true)]").query(json);
60+
61+
assertThat(results).hasSize(3);
62+
assertThat(results.stream().map(v -> asInt(v, "id")).toList())
63+
.containsExactly(1, 2, 3);
64+
}
65+
66+
@Test
67+
void testLogicalNot() {
68+
LOG.info(() -> "TEST: testLogicalNot (!)");
69+
var json = Json.parse("""
70+
[
71+
{"id": 1, "active": true},
72+
{"id": 2, "active": false},
73+
{"id": 3}
74+
]
75+
""");
76+
77+
// !@.active should match where active is false or null/missing (if treated as falsy? strictly missing check is different)
78+
// In this implementation:
79+
// !@.active implies we invert the truthiness of @.active.
80+
// If @.active exists and is true -> false.
81+
// If @.active exists and is false -> true.
82+
// If @.active is missing -> ExistsFilter returns false -> !false -> true.
83+
84+
// However, "ExistsFilter" checks for existence.
85+
// @.active matches id 1 (true) and 2 (false).
86+
// Wait, ExistsFilter checks if the path *exists* and is non-null.
87+
// Let's verify specific behavior for boolean value comparison vs existence.
88+
89+
// Case A: Existence check negation
90+
// [?(!@.active)] -> Match items where "active" does NOT exist.
91+
var missingResults = JsonPath.parse("$[?(!@.active)]").query(json);
92+
assertThat(missingResults).hasSize(1);
93+
assertThat(asInt(missingResults.getFirst(), "id")).isEqualTo(3);
94+
95+
// Case B: Value comparison negation
96+
// [?(!(@.active == true))]
97+
var notTrueResults = JsonPath.parse("$[?(!(@.active == true))]").query(json);
98+
// id 1: active=true -> EQ is true -> !T -> F
99+
// id 2: active=false -> EQ is false -> !F -> T
100+
// id 3: active missing -> EQ is false (null != true) -> !F -> T
101+
assertThat(notTrueResults).hasSize(2);
102+
assertThat(notTrueResults.stream().map(v -> asInt(v, "id")).toList())
103+
.containsExactlyInAnyOrder(2, 3);
104+
}
105+
106+
@Test
107+
void testParenthesesPrecedence() {
108+
LOG.info(() -> "TEST: testParenthesesPrecedence");
109+
// Logic: A && (B || C) vs (A && B) || C
110+
// A=true, B=false, C=true
111+
// A && (B || C) -> T && (F || T) -> T && T -> MATCH
112+
// (A && B) || C -> (T && F) || T -> F || T -> MATCH (Wait, bad example, both match)
113+
114+
// Let's try: A=false, B=true, C=true
115+
// A && (B || C) -> F && T -> NO MATCH
116+
// (A && B) || C -> F || T -> MATCH
117+
118+
var json = Json.parse("""
119+
[
120+
{"id": 1, "A": false, "B": true, "C": true}
121+
]
122+
""");
123+
124+
// Case 1: A && (B || C) -> Expect Empty
125+
var results1 = JsonPath.parse("$[?(@.A == true && (@.B == true || @.C == true))]").query(json);
126+
assertThat(results1).isEmpty();
127+
128+
// Case 2: (A && B) || C -> Expect Match (since C is true)
129+
// Note: The parser must respect precedence. AND usually binds tighter than OR, but we use parens to force order.
130+
// Standard precedence: && before ||.
131+
// So @.A && @.B || @.C means (@.A && @.B) || @.C.
132+
// Let's verify explicit parens first.
133+
var results2 = JsonPath.parse("$[?((@.A == true && @.B == true) || @.C == true)]").query(json);
134+
assertThat(results2).hasSize(1);
135+
}
136+
137+
@Test
138+
void testComplexNestedLogic() {
139+
LOG.info(() -> "TEST: testComplexNestedLogic");
140+
// (Price < 10 OR (Category == 'fiction' AND Not Published))
141+
var json = Json.parse("""
142+
[
143+
{"id": 1, "price": 5, "category": "ref", "published": true},
144+
{"id": 2, "price": 20, "category": "fiction", "published": false},
145+
{"id": 3, "price": 20, "category": "fiction", "published": true},
146+
{"id": 4, "price": 20, "category": "ref", "published": false}
147+
]
148+
""");
149+
150+
var results = JsonPath.parse("$[?(@.price < 10 || (@.category == 'fiction' && !@.published))]").query(json);
151+
152+
assertThat(results).hasSize(2);
153+
assertThat(results.stream().map(v -> asInt(v, "id")).toList())
154+
.containsExactlyInAnyOrder(1, 2);
155+
}
156+
157+
// Helper to extract integer field for assertions
158+
private int asInt(JsonValue v, String key) {
159+
if (v instanceof jdk.sandbox.java.util.json.JsonObject obj) {
160+
return (int) obj.members().get(key).toLong();
161+
}
162+
throw new IllegalArgumentException("Not an object");
163+
}
164+
}

0 commit comments

Comments
 (0)