Skip to content

Commit f39d837

Browse files
committed
Add JsonPath ESM renderer: AST to ES2020 module codegen
Walk the JsonPath AST with exhaustive switch to emit JavaScript that evaluates the expression without interpretation overhead. Generated modules export a query(root) function that returns an array of matched values. Supports all segment types: property access, array index, slice, wildcard, recursive descent, filter, union, script. 18 tests verify structural validity and correct rendering for each segment type. To verify: mvn test -pl json-java21-jsonpath -Dtest=JsonPathEsmRendererTest
1 parent 40dd65c commit f39d837

File tree

2 files changed

+493
-0
lines changed

2 files changed

+493
-0
lines changed
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
package json.java21.jsonpath;
2+
3+
import json.java21.jsonpath.JsonPathAst.*;
4+
5+
import java.util.List;
6+
import java.util.logging.Logger;
7+
8+
/// Renders a JsonPath AST into an ES2020 module that exports a `query(document)` function.
9+
///
10+
/// The generated JavaScript evaluates the JsonPath expression against a JSON document
11+
/// and returns an array of matched values. All dispatch decisions are resolved at
12+
/// render time - no interpretation at runtime.
13+
///
14+
/// Usage:
15+
/// ```java
16+
/// var ast = JsonPath.parse("$.store.book[*].title").ast();
17+
/// String esm = JsonPathEsmRenderer.render(ast, "$.store.book[*].title");
18+
/// // esm contains: export function query(root) { ... }
19+
/// ```
20+
public final class JsonPathEsmRenderer {
21+
22+
private static final Logger LOG = Logger.getLogger(JsonPathEsmRenderer.class.getName());
23+
24+
private JsonPathEsmRenderer() {}
25+
26+
/// Renders a JsonPath AST into an ES2020 module string.
27+
///
28+
/// @param ast the parsed JsonPath AST root
29+
/// @param expression the original expression string (for comments)
30+
/// @return a complete ES2020 module string
31+
public static String render(Root ast, String expression) {
32+
final var sb = new StringBuilder();
33+
34+
sb.append("// Generated JsonPath query: ").append(expression).append('\n');
35+
sb.append("// Do not edit - generated by JsonPathEsmRenderer\n\n");
36+
37+
sb.append("export function query(root) {\n");
38+
sb.append(" const results = [];\n");
39+
40+
emitSegmentChain(sb, ast.segments(), 0, "root", "root", " ");
41+
42+
sb.append(" return results;\n");
43+
sb.append("}\n");
44+
45+
LOG.fine(() -> "Rendered ESM for: " + expression);
46+
return sb.toString();
47+
}
48+
49+
private static void emitSegmentChain(StringBuilder sb, List<Segment> segments,
50+
int segIdx, String currentVar, String rootVar, String indent) {
51+
52+
if (segIdx >= segments.size()) {
53+
sb.append(indent).append("results.push(").append(currentVar).append(");\n");
54+
return;
55+
}
56+
57+
final var segment = segments.get(segIdx);
58+
59+
switch (segment) {
60+
case PropertyAccess prop ->
61+
emitPropertyAccess(sb, prop, segments, segIdx, currentVar, rootVar, indent);
62+
case ArrayIndex arr ->
63+
emitArrayIndex(sb, arr, segments, segIdx, currentVar, rootVar, indent);
64+
case ArraySlice slice ->
65+
emitArraySlice(sb, slice, segments, segIdx, currentVar, rootVar, indent);
66+
case Wildcard ignored ->
67+
emitWildcard(sb, segments, segIdx, currentVar, rootVar, indent);
68+
case RecursiveDescent desc ->
69+
emitRecursiveDescent(sb, desc, segments, segIdx, currentVar, rootVar, indent);
70+
case Filter filter ->
71+
emitFilter(sb, filter, segments, segIdx, currentVar, rootVar, indent);
72+
case Union union ->
73+
emitUnion(sb, union, segments, segIdx, currentVar, rootVar, indent);
74+
case ScriptExpression script ->
75+
emitScript(sb, script, segments, segIdx, currentVar, rootVar, indent);
76+
}
77+
}
78+
79+
private static void emitPropertyAccess(StringBuilder sb, PropertyAccess prop,
80+
List<Segment> segments, int segIdx, String currentVar, String rootVar, String indent) {
81+
final var jsName = jsString(prop.name());
82+
sb.append(indent).append("if (").append(currentVar).append(" != null && typeof ")
83+
.append(currentVar).append(" === 'object' && !Array.isArray(").append(currentVar).append(")) {\n");
84+
final var valVar = "v" + segIdx;
85+
sb.append(indent).append(" const ").append(valVar).append(" = ").append(currentVar)
86+
.append("[").append(jsName).append("];\n");
87+
sb.append(indent).append(" if (").append(valVar).append(" !== undefined) {\n");
88+
89+
emitSegmentChain(sb, segments, segIdx + 1, valVar, rootVar, indent + " ");
90+
91+
sb.append(indent).append(" }\n");
92+
sb.append(indent).append("}\n");
93+
}
94+
95+
private static void emitArrayIndex(StringBuilder sb, ArrayIndex arr,
96+
List<Segment> segments, int segIdx, String currentVar, String rootVar, String indent) {
97+
sb.append(indent).append("if (Array.isArray(").append(currentVar).append(")) {\n");
98+
final var idxExpr = arr.index() < 0
99+
? currentVar + ".length + " + arr.index()
100+
: String.valueOf(arr.index());
101+
final var iVar = "i" + segIdx;
102+
sb.append(indent).append(" const ").append(iVar).append(" = ").append(idxExpr).append(";\n");
103+
sb.append(indent).append(" if (").append(iVar).append(" >= 0 && ").append(iVar)
104+
.append(" < ").append(currentVar).append(".length) {\n");
105+
final var elemVar = "e" + segIdx;
106+
sb.append(indent).append(" const ").append(elemVar).append(" = ").append(currentVar)
107+
.append("[").append(iVar).append("];\n");
108+
109+
emitSegmentChain(sb, segments, segIdx + 1, elemVar, rootVar, indent + " ");
110+
111+
sb.append(indent).append(" }\n");
112+
sb.append(indent).append("}\n");
113+
}
114+
115+
private static void emitArraySlice(StringBuilder sb, ArraySlice slice,
116+
List<Segment> segments, int segIdx, String currentVar, String rootVar, String indent) {
117+
sb.append(indent).append("if (Array.isArray(").append(currentVar).append(")) {\n");
118+
119+
final var step = slice.step() != null ? slice.step() : 1;
120+
final var iVar = "i" + segIdx;
121+
final var lenVar = "len" + segIdx;
122+
sb.append(indent).append(" const ").append(lenVar).append(" = ").append(currentVar).append(".length;\n");
123+
124+
if (step > 0) {
125+
final var startExpr = slice.start() != null
126+
? "Math.max(0, " + normalizeIdx(slice.start(), lenVar) + ")"
127+
: "0";
128+
final var endExpr = slice.end() != null
129+
? "Math.min(" + lenVar + ", " + normalizeIdx(slice.end(), lenVar) + ")"
130+
: lenVar;
131+
sb.append(indent).append(" for (let ").append(iVar).append(" = ").append(startExpr)
132+
.append("; ").append(iVar).append(" < ").append(endExpr)
133+
.append("; ").append(iVar).append(" += ").append(step).append(") {\n");
134+
} else {
135+
final var startExpr = slice.start() != null
136+
? "Math.min(" + lenVar + " - 1, " + normalizeIdx(slice.start(), lenVar) + ")"
137+
: lenVar + " - 1";
138+
final var endExpr = slice.end() != null
139+
? normalizeIdx(slice.end(), lenVar)
140+
: "-1";
141+
sb.append(indent).append(" for (let ").append(iVar).append(" = ").append(startExpr)
142+
.append("; ").append(iVar).append(" > ").append(endExpr)
143+
.append(" && ").append(iVar).append(" >= 0 && ").append(iVar).append(" < ").append(lenVar)
144+
.append("; ").append(iVar).append(" += ").append(step).append(") {\n");
145+
}
146+
147+
final var elemVar = "e" + segIdx;
148+
sb.append(indent).append(" const ").append(elemVar).append(" = ")
149+
.append(currentVar).append("[").append(iVar).append("];\n");
150+
151+
emitSegmentChain(sb, segments, segIdx + 1, elemVar, rootVar, indent + " ");
152+
153+
sb.append(indent).append(" }\n");
154+
sb.append(indent).append("}\n");
155+
}
156+
157+
private static String normalizeIdx(int idx, String lenVar) {
158+
return idx < 0 ? lenVar + " + " + idx : String.valueOf(idx);
159+
}
160+
161+
private static void emitWildcard(StringBuilder sb,
162+
List<Segment> segments, int segIdx, String currentVar, String rootVar, String indent) {
163+
final var valVar = "w" + segIdx;
164+
165+
sb.append(indent).append("if (Array.isArray(").append(currentVar).append(")) {\n");
166+
sb.append(indent).append(" for (const ").append(valVar).append(" of ").append(currentVar).append(") {\n");
167+
emitSegmentChain(sb, segments, segIdx + 1, valVar, rootVar, indent + " ");
168+
sb.append(indent).append(" }\n");
169+
170+
sb.append(indent).append("} else if (").append(currentVar).append(" != null && typeof ")
171+
.append(currentVar).append(" === 'object') {\n");
172+
sb.append(indent).append(" for (const ").append(valVar).append(" of Object.values(")
173+
.append(currentVar).append(")) {\n");
174+
emitSegmentChain(sb, segments, segIdx + 1, valVar, rootVar, indent + " ");
175+
sb.append(indent).append(" }\n");
176+
177+
sb.append(indent).append("}\n");
178+
}
179+
180+
private static void emitRecursiveDescent(StringBuilder sb, RecursiveDescent desc,
181+
List<Segment> segments, int segIdx, String currentVar, String rootVar, String indent) {
182+
final var target = desc.target();
183+
final var fnName = "_descent" + segIdx;
184+
final var nodeVar = "n" + segIdx;
185+
186+
// Emit a local recursive function
187+
sb.append(indent).append("function ").append(fnName).append("(").append(nodeVar).append(") {\n");
188+
189+
// Match target at current level
190+
switch (target) {
191+
case PropertyAccess prop -> {
192+
sb.append(indent).append(" if (").append(nodeVar).append(" != null && typeof ")
193+
.append(nodeVar).append(" === 'object' && !Array.isArray(").append(nodeVar).append(")) {\n");
194+
sb.append(indent).append(" const _m = ").append(nodeVar).append("[")
195+
.append(jsString(prop.name())).append("];\n");
196+
sb.append(indent).append(" if (_m !== undefined) {\n");
197+
emitSegmentChain(sb, segments, segIdx + 1, "_m", rootVar, indent + " ");
198+
sb.append(indent).append(" }\n");
199+
sb.append(indent).append(" }\n");
200+
}
201+
case Wildcard ignored2 -> {
202+
sb.append(indent).append(" if (Array.isArray(").append(nodeVar).append(")) {\n");
203+
sb.append(indent).append(" for (const _m of ").append(nodeVar).append(") {\n");
204+
emitSegmentChain(sb, segments, segIdx + 1, "_m", rootVar, indent + " ");
205+
sb.append(indent).append(" }\n");
206+
sb.append(indent).append(" } else if (").append(nodeVar).append(" != null && typeof ")
207+
.append(nodeVar).append(" === 'object') {\n");
208+
sb.append(indent).append(" for (const _m of Object.values(").append(nodeVar).append(")) {\n");
209+
emitSegmentChain(sb, segments, segIdx + 1, "_m", rootVar, indent + " ");
210+
sb.append(indent).append(" }\n");
211+
sb.append(indent).append(" }\n");
212+
}
213+
default -> {
214+
// Unsupported target in recursive descent
215+
}
216+
}
217+
218+
// Recurse into children
219+
sb.append(indent).append(" if (Array.isArray(").append(nodeVar).append(")) {\n");
220+
sb.append(indent).append(" for (const _c of ").append(nodeVar).append(") ").append(fnName).append("(_c);\n");
221+
sb.append(indent).append(" } else if (").append(nodeVar).append(" != null && typeof ")
222+
.append(nodeVar).append(" === 'object') {\n");
223+
sb.append(indent).append(" for (const _c of Object.values(").append(nodeVar).append(")) ").append(fnName).append("(_c);\n");
224+
sb.append(indent).append(" }\n");
225+
226+
sb.append(indent).append("}\n");
227+
sb.append(indent).append(fnName).append("(").append(currentVar).append(");\n");
228+
}
229+
230+
private static void emitFilter(StringBuilder sb, Filter filter,
231+
List<Segment> segments, int segIdx, String currentVar, String rootVar, String indent) {
232+
sb.append(indent).append("if (Array.isArray(").append(currentVar).append(")) {\n");
233+
final var elemVar = "f" + segIdx;
234+
sb.append(indent).append(" for (const ").append(elemVar).append(" of ").append(currentVar).append(") {\n");
235+
sb.append(indent).append(" if (");
236+
emitFilterExpression(sb, filter.expression(), elemVar);
237+
sb.append(") {\n");
238+
239+
emitSegmentChain(sb, segments, segIdx + 1, elemVar, rootVar, indent + " ");
240+
241+
sb.append(indent).append(" }\n");
242+
sb.append(indent).append(" }\n");
243+
sb.append(indent).append("}\n");
244+
}
245+
246+
private static void emitFilterExpression(StringBuilder sb, FilterExpression expr, String elemVar) {
247+
switch (expr) {
248+
case ExistsFilter exists -> {
249+
emitPropertyPathAccess(sb, exists.path(), elemVar);
250+
sb.append(" !== undefined");
251+
}
252+
case ComparisonFilter comp -> {
253+
sb.append("(");
254+
emitFilterOperand(sb, comp.left(), elemVar);
255+
sb.append(" ").append(jsOp(comp.op())).append(" ");
256+
emitFilterOperand(sb, comp.right(), elemVar);
257+
sb.append(")");
258+
}
259+
case LogicalFilter logical -> {
260+
switch (logical.op()) {
261+
case AND -> {
262+
sb.append("(");
263+
emitFilterExpression(sb, logical.left(), elemVar);
264+
sb.append(" && ");
265+
if (logical.right() != null) {
266+
emitFilterExpression(sb, logical.right(), elemVar);
267+
} else {
268+
sb.append("true");
269+
}
270+
sb.append(")");
271+
}
272+
case OR -> {
273+
sb.append("(");
274+
emitFilterExpression(sb, logical.left(), elemVar);
275+
sb.append(" || ");
276+
if (logical.right() != null) {
277+
emitFilterExpression(sb, logical.right(), elemVar);
278+
} else {
279+
sb.append("false");
280+
}
281+
sb.append(")");
282+
}
283+
case NOT -> {
284+
sb.append("!(");
285+
emitFilterExpression(sb, logical.left(), elemVar);
286+
sb.append(")");
287+
}
288+
}
289+
}
290+
case CurrentNode cn -> sb.append("true");
291+
case PropertyPath path -> {
292+
emitPropertyPathAccess(sb, path, elemVar);
293+
sb.append(" !== undefined");
294+
}
295+
case LiteralValue lv -> sb.append("true");
296+
}
297+
}
298+
299+
private static void emitFilterOperand(StringBuilder sb, FilterExpression expr, String elemVar) {
300+
switch (expr) {
301+
case PropertyPath path -> emitPropertyPathAccess(sb, path, elemVar);
302+
case LiteralValue lit -> {
303+
if (lit.value() == null) {
304+
sb.append("null");
305+
} else if (lit.value() instanceof String s) {
306+
sb.append(jsString(s));
307+
} else if (lit.value() instanceof Number n) {
308+
sb.append(n);
309+
} else if (lit.value() instanceof Boolean b) {
310+
sb.append(b);
311+
} else {
312+
sb.append("null");
313+
}
314+
}
315+
case CurrentNode cn2 -> sb.append(elemVar);
316+
default -> sb.append("null");
317+
}
318+
}
319+
320+
private static void emitPropertyPathAccess(StringBuilder sb, PropertyPath path, String elemVar) {
321+
sb.append(elemVar);
322+
for (final var prop : path.properties()) {
323+
sb.append("?.[").append(jsString(prop)).append("]");
324+
}
325+
}
326+
327+
private static void emitUnion(StringBuilder sb, Union union,
328+
List<Segment> segments, int segIdx, String currentVar, String rootVar, String indent) {
329+
for (final var selector : union.selectors()) {
330+
switch (selector) {
331+
case PropertyAccess prop ->
332+
emitPropertyAccess(sb, prop, segments, segIdx, currentVar, rootVar, indent);
333+
case ArrayIndex arr ->
334+
emitArrayIndex(sb, arr, segments, segIdx, currentVar, rootVar, indent);
335+
default -> {}
336+
}
337+
}
338+
}
339+
340+
private static void emitScript(StringBuilder sb, ScriptExpression script,
341+
List<Segment> segments, int segIdx, String currentVar, String rootVar, String indent) {
342+
if ("@.length-1".equals(script.script())) {
343+
sb.append(indent).append("if (Array.isArray(").append(currentVar)
344+
.append(") && ").append(currentVar).append(".length > 0) {\n");
345+
final var elemVar = "s" + segIdx;
346+
sb.append(indent).append(" const ").append(elemVar).append(" = ")
347+
.append(currentVar).append("[").append(currentVar).append(".length - 1];\n");
348+
349+
emitSegmentChain(sb, segments, segIdx + 1, elemVar, rootVar, indent + " ");
350+
351+
sb.append(indent).append("}\n");
352+
}
353+
}
354+
355+
private static String jsString(String s) {
356+
return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
357+
}
358+
359+
private static String jsOp(ComparisonOp op) {
360+
return switch (op) {
361+
case EQ -> "===";
362+
case NE -> "!==";
363+
case LT -> "<";
364+
case LE -> "<=";
365+
case GT -> ">";
366+
case GE -> ">=";
367+
};
368+
}
369+
}

0 commit comments

Comments
 (0)