Skip to content

Commit 6b3511d

Browse files
cursoragentsimbo1905
andcommitted
Issue #121 Scaffold JsonPath module with initial AST and tests
Co-authored-by: simbo1905 <simbo1905@60hertz.com>
1 parent 4b60358 commit 6b3511d

File tree

13 files changed

+602
-0
lines changed

13 files changed

+602
-0
lines changed

PLAN_121.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
## Issue #121: Add JsonPath module with AST parser and evaluator
2+
3+
### Goal
4+
5+
Add a new Maven module that implements **JsonPath** as described by Stefan Goessner:
6+
`https://goessner.net/articles/JsonPath/index.html`.
7+
8+
This module must:
9+
10+
- Parse a JsonPath string into a **custom AST**
11+
- Evaluate that AST against a JSON document already parsed by this repo’s
12+
`jdk.sandbox.java.util.json` API
13+
- Have **no runtime dependencies** outside `java.base` (and the existing `json-java21` module)
14+
- Target **Java 21** and follow functional / data-oriented style (records + sealed interfaces)
15+
16+
### Non-goals
17+
18+
- Parsing JSON documents from strings (the core `Json.parse(...)` already does that)
19+
- Adding non-trivial external dependencies (no regex engines beyond JDK, no parser generators)
20+
- Supporting every JsonPath dialect ever published; the baseline is the article examples
21+
22+
### Public API shape (module `json-java21-jsonpath`)
23+
24+
Package: `json.java21.jsonpath`
25+
26+
- `JsonPathExpression JsonPath.parse(String path)`
27+
- Parses a JsonPath string into an AST-backed compiled expression.
28+
- `List<JsonValue> JsonPathExpression.select(JsonValue document)`
29+
- Evaluates the expression against a parsed JSON document and returns matched nodes in traversal order.
30+
31+
Notes:
32+
- The return type is a `List<JsonValue>` to avoid introducing a JSON encoding of “result sets”.
33+
Callers can wrap it into `JsonArray.of(...)` if desired.
34+
35+
### AST plan
36+
37+
Use a sealed interface with records (no inheritance trees with stateful objects):
38+
39+
- `sealed interface PathNode permits Root, StepChain`
40+
- `record Root() implements PathNode`
41+
- `record StepChain(PathNode base, Step step) implements PathNode`
42+
43+
Steps are a separate sealed protocol:
44+
45+
- `sealed interface Step permits Child, RecursiveDescent, Wildcard, ArrayIndex, ArraySlice, Union, Filter`
46+
- `record Child(Name name)` where `Name` is either identifier or quoted key
47+
- `record RecursiveDescent(Step selector)` where selector is `Child` or `Wildcard`
48+
- `record Wildcard()`
49+
- `record ArrayIndex(int index)` supports negative indices per examples
50+
- `record ArraySlice(Integer start, Integer end, Integer step)` to cover `[:2]`, `[-1:]`, etc.
51+
- `record Union(List<Step> selectors)` for `[0,1]`, `['a','b']`
52+
- `record Filter(PredicateExpr expr)` for `[?(...)]`
53+
54+
Filter expressions:
55+
56+
- Keep a minimal expression AST that supports the article examples:
57+
- `@.field` access
58+
- `@.length` pseudo-property for array length
59+
- numeric literals
60+
- string literals
61+
- comparison operators: `<`, `<=`, `>`, `>=`, `==`, `!=`
62+
- arithmetic: `+`, `-` (only what’s needed for `(@.length-1)`)
63+
64+
### Parser plan
65+
66+
Hand-rolled scanner + recursive descent parser:
67+
68+
- Lex JsonPath into tokens (`$`, `.`, `..`, `[`, `]`, `*`, `,`, `:`, `?(`, `)`, identifiers, quoted strings, numbers, operators).
69+
- Parse according to the article grammar:
70+
- Root `$` must appear first
71+
- Dot-notation steps: `.name`, `.*`, `..name`, `..*`
72+
- Bracket steps:
73+
- `['name']`
74+
- `[0]`, `[-1]`
75+
- `[0,1]`, `['a','b']`
76+
- `[:2]`, `[2:]`, `[-1:]`
77+
- `[?(...)]`
78+
- `[(...)]` for script expressions used as array index in examples (limited support)
79+
80+
### Evaluator plan
81+
82+
Evaluator is a pure function over immutable inputs, implemented as static methods:
83+
84+
- Maintain a worklist of “current nodes” (starting with the document root).
85+
- For each step:
86+
- **Child**: for objects, pick member by key; for arrays, apply to each element if they’re objects (per example behavior).
87+
- **RecursiveDescent**: walk the subtree of each current node (object members + array elements) and apply the selector to every node encountered.
88+
- **Wildcard**: for objects select all member values; for arrays select all elements.
89+
- **ArrayIndex / Slice / Union**: apply only when the current node is an array.
90+
- **Filter**: apply only when current node is an array; keep elements where predicate is true.
91+
92+
Ordering:
93+
- Preserve traversal order implied by iterating object members (`JsonObject.members()` is order-preserving) and array elements order.
94+
95+
### Tests (TDD baseline)
96+
97+
Add tests that correspond 1:1 with every example on the article page:
98+
99+
- Use the article’s sample document (embedded as a Java text block) and parse with `Json.parse(...)`.
100+
- Assertions check matched values by rendering to JSON (`JsonValue.toString()`) and comparing to expected fragments.
101+
- Every test method logs an INFO banner at start (common base class).
102+
103+
### Verification
104+
105+
Run focused module tests with logging:
106+
107+
```bash
108+
$(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-jsonpath test -Djava.util.logging.ConsoleHandler.level=FINE
109+
```
110+
111+
Run full suite once stable:
112+
113+
```bash
114+
$(command -v mvnd || command -v mvn || command -v ./mvnw) test -Djava.util.logging.ConsoleHandler.level=INFO
115+
```
116+

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,29 @@ This repo contains an incubating JTD validator that has the core JSON API as its
328328

329329
A complete JSON Type Definition validator is included (module: json-java21-jtd).
330330

331+
## JsonPath (AST + evaluator over `java.util.json`)
332+
333+
This repo also includes (in progress) a **JsonPath** module based on Stefan Goessner’s article:
334+
`https://goessner.net/articles/JsonPath/index.html`.
335+
336+
Design goals:
337+
- **No runtime deps beyond `java.base`** (and the core `json-java21` module)
338+
- **Parse JsonPath strings to a custom AST**
339+
- **Evaluate against already-parsed JSON** (`JsonValue`), not against JSON text
340+
- Pure Java 21, functional/data-oriented style (records + sealed interfaces)
341+
- Unit tests mirror the article examples
342+
343+
Planned public API (module: `json-java21-jsonpath`):
344+
345+
```java
346+
import jdk.sandbox.java.util.json.JsonValue;
347+
import json.java21.jsonpath.JsonPath;
348+
349+
JsonValue doc = /* Json.parse(...) from json-java21 */;
350+
var expr = JsonPath.parse("$.store.book[*].author");
351+
var matches = expr.select(doc); // List<JsonValue>
352+
```
353+
331354
### Empty Schema `{}` Semantics (RFC 8927)
332355

333356
Per **RFC 8927 (JSON Typedef)**, the empty schema `{}` is the **empty form** and

json-java21-jsonpath/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# JsonPath (Goessner) for `java.util.json` (Java 21)
2+
3+
This module implements **JsonPath** as described by Stefan Goessner:
4+
`https://goessner.net/articles/JsonPath/index.html`.
5+
6+
Design constraints:
7+
- Evaluates over already-parsed JSON (`jdk.sandbox.java.util.json.JsonValue`)
8+
- Parses JsonPath strings into a custom AST
9+
- No runtime dependencies outside `java.base` (and the core `java.util.json` backport)
10+
11+
## Usage (planned API)
12+
13+
```java
14+
import jdk.sandbox.java.util.json.Json;
15+
import jdk.sandbox.java.util.json.JsonValue;
16+
import json.java21.jsonpath.JsonPath;
17+
18+
JsonValue doc = Json.parse("{\"a\": {\"b\": [1,2,3]}}");
19+
var expr = JsonPath.parse("$.a.b[0]");
20+
var matches = expr.select(doc);
21+
```
22+

json-java21-jsonpath/pom.xml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
5+
http://maven.apache.org/xsd/maven-4.0.0.xsd">
6+
<modelVersion>4.0.0</modelVersion>
7+
8+
<parent>
9+
<groupId>io.github.simbo1905.json</groupId>
10+
<artifactId>parent</artifactId>
11+
<version>0.1.9</version>
12+
</parent>
13+
14+
<artifactId>java.util.json.jsonpath</artifactId>
15+
<packaging>jar</packaging>
16+
<name>java.util.json Java21 Backport JsonPath</name>
17+
<url>https://simbo1905.github.io/java.util.json.Java21/</url>
18+
<scm>
19+
<connection>scm:git:https://github.com/simbo1905/java.util.json.Java21.git</connection>
20+
<developerConnection>scm:git:git@github.com:simbo1905/java.util.json.Java21.git</developerConnection>
21+
<url>https://github.com/simbo1905/java.util.json.Java21</url>
22+
<tag>HEAD</tag>
23+
</scm>
24+
<description>Experimental JsonPath (Goessner) parser to AST and evaluator over the java.util.json Java 21 backport.</description>
25+
26+
<properties>
27+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
28+
<maven.compiler.release>21</maven.compiler.release>
29+
</properties>
30+
31+
<dependencies>
32+
<dependency>
33+
<groupId>io.github.simbo1905.json</groupId>
34+
<artifactId>java.util.json</artifactId>
35+
<version>${project.version}</version>
36+
</dependency>
37+
38+
<!-- Test dependencies -->
39+
<dependency>
40+
<groupId>org.junit.jupiter</groupId>
41+
<artifactId>junit-jupiter-api</artifactId>
42+
<scope>test</scope>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.junit.jupiter</groupId>
46+
<artifactId>junit-jupiter-engine</artifactId>
47+
<scope>test</scope>
48+
</dependency>
49+
<dependency>
50+
<groupId>org.junit.jupiter</groupId>
51+
<artifactId>junit-jupiter-params</artifactId>
52+
<scope>test</scope>
53+
</dependency>
54+
<dependency>
55+
<groupId>org.assertj</groupId>
56+
<artifactId>assertj-core</artifactId>
57+
<scope>test</scope>
58+
</dependency>
59+
</dependencies>
60+
61+
<build>
62+
<plugins>
63+
<!-- Treat all warnings as errors, enable all lint warnings -->
64+
<plugin>
65+
<groupId>org.apache.maven.plugins</groupId>
66+
<artifactId>maven-compiler-plugin</artifactId>
67+
<version>3.11.0</version>
68+
<configuration>
69+
<release>21</release>
70+
<compilerArgs>
71+
<arg>-Xlint:all</arg>
72+
<arg>-Werror</arg>
73+
<arg>-Xdiags:verbose</arg>
74+
</compilerArgs>
75+
</configuration>
76+
</plugin>
77+
</plugins>
78+
</build>
79+
</project>
80+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package json.java21.jsonpath;
2+
3+
import java.util.Objects;
4+
5+
/// JsonPath entry point: parse to an AST-backed expression.
6+
public final class JsonPath {
7+
8+
public static JsonPathExpression parse(String path) {
9+
Objects.requireNonNull(path, "path must not be null");
10+
return new JsonPathExpression(JsonPathParser.parse(path));
11+
}
12+
13+
private JsonPath() {}
14+
}
15+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package json.java21.jsonpath;
2+
3+
import java.util.List;
4+
import java.util.Objects;
5+
6+
record JsonPathAst(List<Segment> segments) {
7+
JsonPathAst {
8+
Objects.requireNonNull(segments, "segments must not be null");
9+
segments.forEach(s -> Objects.requireNonNull(s, "segment must not be null"));
10+
}
11+
12+
sealed interface Segment permits Segment.Child, Segment.Wildcard {
13+
record Child(String name) implements Segment {
14+
Child {
15+
Objects.requireNonNull(name, "name must not be null");
16+
}
17+
}
18+
19+
record Wildcard() implements Segment {}
20+
}
21+
}
22+
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package json.java21.jsonpath;
2+
3+
import jdk.sandbox.java.util.json.JsonArray;
4+
import jdk.sandbox.java.util.json.JsonObject;
5+
import jdk.sandbox.java.util.json.JsonValue;
6+
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
import static json.java21.jsonpath.JsonPathAst.Segment;
11+
12+
sealed interface JsonPathEvaluator permits JsonPathEvaluator.Impl {
13+
14+
static List<JsonValue> select(JsonPathAst ast, JsonValue document) {
15+
return Impl.select(ast, document);
16+
}
17+
18+
final class Impl implements JsonPathEvaluator {
19+
static List<JsonValue> select(JsonPathAst ast, JsonValue document) {
20+
var current = List.of(document);
21+
for (final var seg : ast.segments()) {
22+
current = apply(seg, current);
23+
}
24+
return current;
25+
}
26+
27+
private static List<JsonValue> apply(Segment seg, List<JsonValue> current) {
28+
return switch (seg) {
29+
case Segment.Child child -> selectChild(current, child.name());
30+
case Segment.Wildcard ignored -> selectWildcard(current);
31+
};
32+
}
33+
34+
private static List<JsonValue> selectChild(List<JsonValue> current, String name) {
35+
final var out = new ArrayList<JsonValue>();
36+
for (final var node : current) {
37+
switch (node) {
38+
case JsonObject obj -> {
39+
final var val = obj.members().get(name);
40+
if (val != null) out.add(val);
41+
}
42+
case JsonArray arr -> arr.elements().forEach(v -> {
43+
if (v instanceof JsonObject o) {
44+
final var val = o.members().get(name);
45+
if (val != null) out.add(val);
46+
}
47+
});
48+
default -> {
49+
}
50+
}
51+
}
52+
return List.copyOf(out);
53+
}
54+
55+
private static List<JsonValue> selectWildcard(List<JsonValue> current) {
56+
final var out = new ArrayList<JsonValue>();
57+
for (final var node : current) {
58+
switch (node) {
59+
case JsonObject obj -> out.addAll(obj.members().values());
60+
case JsonArray arr -> out.addAll(arr.elements());
61+
default -> {
62+
}
63+
}
64+
}
65+
return List.copyOf(out);
66+
}
67+
}
68+
}
69+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package json.java21.jsonpath;
2+
3+
import jdk.sandbox.java.util.json.JsonValue;
4+
5+
import java.util.List;
6+
import java.util.Objects;
7+
8+
/// A compiled JsonPath expression (AST + evaluator).
9+
public record JsonPathExpression(JsonPathAst ast) {
10+
11+
public JsonPathExpression {
12+
Objects.requireNonNull(ast, "ast must not be null");
13+
}
14+
15+
public List<JsonValue> select(JsonValue document) {
16+
Objects.requireNonNull(document, "document must not be null");
17+
return JsonPathEvaluator.select(ast, document);
18+
}
19+
}
20+

0 commit comments

Comments
 (0)