-
Notifications
You must be signed in to change notification settings - Fork 0
JsonPath AST implementation #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6b3511d
c0fa453
c5fca6b
59d909b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| ## Issue #121: Add JsonPath module with AST parser and evaluator | ||
|
|
||
| ### Goal | ||
|
|
||
| Add a new Maven module that implements **JsonPath** as described by Stefan Goessner: | ||
| `https://goessner.net/articles/JsonPath/index.html`. | ||
|
|
||
| This module must: | ||
|
|
||
| - Parse a JsonPath string into a **custom AST** | ||
| - Evaluate that AST against a JSON document already parsed by this repo’s | ||
| `jdk.sandbox.java.util.json` API | ||
| - Have **no runtime dependencies** outside `java.base` (and the existing `json-java21` module) | ||
| - Target **Java 21** and follow functional / data-oriented style (records + sealed interfaces) | ||
|
|
||
| ### Non-goals | ||
|
|
||
| - Parsing JSON documents from strings (the core `Json.parse(...)` already does that) | ||
| - Adding non-trivial external dependencies (no regex engines beyond JDK, no parser generators) | ||
| - Supporting every JsonPath dialect ever published; the baseline is the article examples | ||
|
|
||
| ### Public API shape (module `json-java21-jsonpath`) | ||
|
|
||
| Package: `json.java21.jsonpath` | ||
|
|
||
| - `JsonPathExpression JsonPath.parse(String path)` | ||
| - Parses a JsonPath string into an AST-backed compiled expression. | ||
| - `List<JsonValue> JsonPathExpression.select(JsonValue document)` | ||
| - Evaluates the expression against a parsed JSON document and returns matched nodes in traversal order. | ||
|
|
||
| Notes: | ||
| - The return type is a `List<JsonValue>` to avoid introducing a JSON encoding of “result sets”. | ||
| Callers can wrap it into `JsonArray.of(...)` if desired. | ||
|
|
||
| ### AST plan | ||
|
|
||
| Use a sealed interface with records (no inheritance trees with stateful objects): | ||
|
|
||
| - `sealed interface PathNode permits Root, StepChain` | ||
| - `record Root() implements PathNode` | ||
| - `record StepChain(PathNode base, Step step) implements PathNode` | ||
|
|
||
| Steps are a separate sealed protocol: | ||
|
|
||
| - `sealed interface Step permits Child, RecursiveDescent, Wildcard, ArrayIndex, ArraySlice, Union, Filter` | ||
| - `record Child(Name name)` where `Name` is either identifier or quoted key | ||
| - `record RecursiveDescent(Step selector)` where selector is `Child` or `Wildcard` | ||
| - `record Wildcard()` | ||
| - `record ArrayIndex(int index)` supports negative indices per examples | ||
| - `record ArraySlice(Integer start, Integer end, Integer step)` to cover `[:2]`, `[-1:]`, etc. | ||
| - `record Union(List<Step> selectors)` for `[0,1]`, `['a','b']` | ||
| - `record Filter(PredicateExpr expr)` for `[?(...)]` | ||
|
|
||
| Filter expressions: | ||
|
|
||
| - Keep a minimal expression AST that supports the article examples: | ||
| - `@.field` access | ||
| - `@.length` pseudo-property for array length | ||
| - numeric literals | ||
| - string literals | ||
| - comparison operators: `<`, `<=`, `>`, `>=`, `==`, `!=` | ||
| - arithmetic: `+`, `-` (only what’s needed for `(@.length-1)`) | ||
|
|
||
| ### Parser plan | ||
|
|
||
| Hand-rolled scanner + recursive descent parser: | ||
|
|
||
| - Lex JsonPath into tokens (`$`, `.`, `..`, `[`, `]`, `*`, `,`, `:`, `?(`, `)`, identifiers, quoted strings, numbers, operators). | ||
| - Parse according to the article grammar: | ||
| - Root `$` must appear first | ||
| - Dot-notation steps: `.name`, `.*`, `..name`, `..*` | ||
| - Bracket steps: | ||
| - `['name']` | ||
| - `[0]`, `[-1]` | ||
| - `[0,1]`, `['a','b']` | ||
| - `[:2]`, `[2:]`, `[-1:]` | ||
| - `[?(...)]` | ||
| - `[(...)]` for script expressions used as array index in examples (limited support) | ||
|
|
||
| ### Evaluator plan | ||
|
|
||
| Evaluator is a pure function over immutable inputs, implemented as static methods: | ||
|
|
||
| - Maintain a worklist of “current nodes” (starting with the document root). | ||
| - For each step: | ||
| - **Child**: for objects, pick member by key; for arrays, apply to each element if they’re objects (per example behavior). | ||
| - **RecursiveDescent**: walk the subtree of each current node (object members + array elements) and apply the selector to every node encountered. | ||
| - **Wildcard**: for objects select all member values; for arrays select all elements. | ||
| - **ArrayIndex / Slice / Union**: apply only when the current node is an array. | ||
| - **Filter**: apply only when current node is an array; keep elements where predicate is true. | ||
|
|
||
| Ordering: | ||
| - Preserve traversal order implied by iterating object members (`JsonObject.members()` is order-preserving) and array elements order. | ||
|
|
||
| ### Tests (TDD baseline) | ||
|
|
||
| Add tests that correspond 1:1 with every example on the article page: | ||
|
|
||
| - Use the article’s sample document (embedded as a Java text block) and parse with `Json.parse(...)`. | ||
| - Assertions check matched values by rendering to JSON (`JsonValue.toString()`) and comparing to expected fragments. | ||
| - Every test method logs an INFO banner at start (common base class). | ||
|
|
||
| ### Verification | ||
|
|
||
| Run focused module tests with logging: | ||
|
|
||
| ```bash | ||
| $(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-jsonpath test -Djava.util.logging.ConsoleHandler.level=FINE | ||
| ``` | ||
|
|
||
| Run full suite once stable: | ||
|
|
||
| ```bash | ||
| $(command -v mvnd || command -v mvn || command -v ./mvnw) test -Djava.util.logging.ConsoleHandler.level=INFO | ||
| ``` | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # JsonPath (Goessner) for `java.util.json` (Java 21) | ||
|
|
||
| This module implements **JsonPath** as described by Stefan Goessner: | ||
| `https://goessner.net/articles/JsonPath/index.html`. | ||
|
|
||
| Design constraints: | ||
| - Evaluates over already-parsed JSON (`jdk.sandbox.java.util.json.JsonValue`) | ||
| - Parses JsonPath strings into a custom AST | ||
| - No runtime dependencies outside `java.base` (and the core `java.util.json` backport) | ||
|
|
||
| ## Usage (planned API) | ||
|
|
||
| ```java | ||
| import jdk.sandbox.java.util.json.Json; | ||
| import jdk.sandbox.java.util.json.JsonValue; | ||
| import json.java21.jsonpath.JsonPath; | ||
|
|
||
| JsonValue doc = Json.parse("{\"a\": {\"b\": [1,2,3]}}"); | ||
| var expr = JsonPath.parse("$.a.b[0]"); | ||
| var matches = expr.select(doc); | ||
| ``` | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 | ||
| http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
|
|
||
| <parent> | ||
| <groupId>io.github.simbo1905.json</groupId> | ||
| <artifactId>parent</artifactId> | ||
| <version>0.1.9</version> | ||
| </parent> | ||
|
|
||
| <artifactId>java.util.json.jsonpath</artifactId> | ||
| <packaging>jar</packaging> | ||
| <name>java.util.json Java21 Backport JsonPath</name> | ||
| <url>https://simbo1905.github.io/java.util.json.Java21/</url> | ||
| <scm> | ||
| <connection>scm:git:https://github.com/simbo1905/java.util.json.Java21.git</connection> | ||
| <developerConnection>scm:git:git@github.com:simbo1905/java.util.json.Java21.git</developerConnection> | ||
| <url>https://github.com/simbo1905/java.util.json.Java21</url> | ||
| <tag>HEAD</tag> | ||
| </scm> | ||
| <description>Experimental JsonPath (Goessner) parser to AST and evaluator over the java.util.json Java 21 backport.</description> | ||
|
|
||
| <properties> | ||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
| <maven.compiler.release>21</maven.compiler.release> | ||
| </properties> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>io.github.simbo1905.json</groupId> | ||
| <artifactId>java.util.json</artifactId> | ||
| <version>${project.version}</version> | ||
| </dependency> | ||
|
|
||
| <!-- Test dependencies --> | ||
| <dependency> | ||
| <groupId>org.junit.jupiter</groupId> | ||
| <artifactId>junit-jupiter-api</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.junit.jupiter</groupId> | ||
| <artifactId>junit-jupiter-engine</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.junit.jupiter</groupId> | ||
| <artifactId>junit-jupiter-params</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.assertj</groupId> | ||
| <artifactId>assertj-core</artifactId> | ||
| <scope>test</scope> | ||
| </dependency> | ||
| </dependencies> | ||
|
|
||
| <build> | ||
| <plugins> | ||
| <!-- Treat all warnings as errors, enable all lint warnings --> | ||
| <plugin> | ||
| <groupId>org.apache.maven.plugins</groupId> | ||
| <artifactId>maven-compiler-plugin</artifactId> | ||
| <version>3.11.0</version> | ||
| <configuration> | ||
| <release>21</release> | ||
| <compilerArgs> | ||
| <arg>-Xlint:all</arg> | ||
| <arg>-Werror</arg> | ||
| <arg>-Xdiags:verbose</arg> | ||
| </compilerArgs> | ||
| </configuration> | ||
| </plugin> | ||
| </plugins> | ||
| </build> | ||
| </project> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package json.java21.jsonpath; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| sealed interface BoolExpr permits BoolExpr.Exists, BoolExpr.Compare { | ||
| record Exists(ValueExpr.Path path) implements BoolExpr { | ||
| public Exists { | ||
| Objects.requireNonNull(path, "path must not be null"); | ||
| } | ||
| } | ||
|
|
||
| enum CmpOp { | ||
| LT, LE, GT, GE, EQ, NE | ||
| } | ||
|
|
||
| record Compare(CmpOp op, ValueExpr left, ValueExpr right) implements BoolExpr { | ||
| public Compare { | ||
| Objects.requireNonNull(op, "op must not be null"); | ||
| Objects.requireNonNull(left, "left must not be null"); | ||
| Objects.requireNonNull(right, "right must not be null"); | ||
| } | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package json.java21.jsonpath; | ||
|
|
||
| import java.util.Objects; | ||
|
|
||
| /// JsonPath entry point: parse to an AST-backed expression. | ||
| public final class JsonPath { | ||
|
|
||
| public static JsonPathExpression parse(String path) { | ||
| Objects.requireNonNull(path, "path must not be null"); | ||
| return new JsonPathExpression(JsonPathParser.parse(path)); | ||
| } | ||
|
|
||
| private JsonPath() {} | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,79 @@ | ||||||
| package json.java21.jsonpath; | ||||||
|
|
||||||
| import java.util.List; | ||||||
| import java.util.Objects; | ||||||
|
|
||||||
| record JsonPathAst(List<Step> steps) { | ||||||
| JsonPathAst { | ||||||
| Objects.requireNonNull(steps, "steps must not be null"); | ||||||
| steps.forEach(s -> Objects.requireNonNull(s, "step must not be null")); | ||||||
| } | ||||||
|
|
||||||
| sealed interface Step permits | ||||||
| Step.Child, | ||||||
| Step.Wildcard, | ||||||
| Step.RecursiveDescent, | ||||||
| Step.ArrayIndex, | ||||||
| Step.ArraySlice, | ||||||
| Step.Union, | ||||||
| Step.Filter, | ||||||
| Step.ScriptIndex { | ||||||
|
|
||||||
| record Child(String name) implements Step { | ||||||
| public Child { | ||||||
| Objects.requireNonNull(name, "name must not be null"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| record Wildcard() implements Step {} | ||||||
|
|
||||||
| record RecursiveDescent(RecursiveSelector selector) implements Step { | ||||||
| public RecursiveDescent { | ||||||
| Objects.requireNonNull(selector, "selector must not be null"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| sealed interface RecursiveSelector permits RecursiveSelector.Name, RecursiveSelector.Wildcard { | ||||||
| record Name(String name) implements RecursiveSelector { | ||||||
| public Name { | ||||||
| Objects.requireNonNull(name, "name must not be null"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| record Wildcard() implements RecursiveSelector {} | ||||||
| } | ||||||
|
|
||||||
| record ArrayIndex(int index) implements Step {} | ||||||
|
|
||||||
| record ArraySlice(Integer startInclusive, Integer endExclusive) implements Step {} | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
|
|
||||||
| record Union(List<Selector> selectors) implements Step { | ||||||
| public Union { | ||||||
| Objects.requireNonNull(selectors, "selectors must not be null"); | ||||||
| selectors.forEach(s -> Objects.requireNonNull(s, "selector must not be null")); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| sealed interface Selector permits Selector.Name, Selector.Index { | ||||||
| record Name(String name) implements Selector { | ||||||
| public Name { | ||||||
| Objects.requireNonNull(name, "name must not be null"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| record Index(int index) implements Selector {} | ||||||
| } | ||||||
|
|
||||||
| record Filter(BoolExpr predicate) implements Step { | ||||||
| public Filter { | ||||||
| Objects.requireNonNull(predicate, "predicate must not be null"); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| record ScriptIndex(ValueExpr expr) implements Step { | ||||||
| public ScriptIndex { | ||||||
| Objects.requireNonNull(expr, "expr must not be null"); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The artifactId
java.util.json.jsonpathis unconventional and inconsistent with the module's directory name (json-java21-jsonpath). While this seems to follow a pattern from thejtdmodule, it can be confusing. Maven convention generally suggests that the artifactId matches the directory name for multi-module projects. Consider renaming the artifactId tojson-java21-jsonpathfor better clarity and consistency with standard Maven practices.