Skip to content

Commit 40dd65c

Browse files
committed
Add JDT AST sealed interface hierarchy and parser
Create JdtAst with three sealed node types: - DirectiveNode: contains @jdt.rename/remove/merge/replace + children - MergeNode: default object-to-object deep merge - ReplacementNode: direct value replacement JdtAstParser builds the AST from a JsonValue transform document. Jdt.parseToAst() public API for codegen modules to access the AST. 4 new AST parser tests (22 total JDT unit tests). To verify: mvn test -pl json-java21-jdt -am
1 parent c0452aa commit 40dd65c

File tree

4 files changed

+226
-0
lines changed

4 files changed

+226
-0
lines changed

json-java21-jdt/src/main/java/json/java21/jdt/Jdt.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ private Jdt() {
5959
// Static utility class
6060
}
6161

62+
/// Parses a transform specification into a JDT AST for codegen or analysis.
63+
///
64+
/// @param transform the transform specification document
65+
/// @return the root AST node
66+
public static JdtAst.JdtNode parseToAst(JsonValue transform) {
67+
Objects.requireNonNull(transform, "transform must not be null");
68+
return JdtAstParser.parse(transform);
69+
}
70+
6271
/// Default path resolver using the interpreter-based JsonPath.
6372
private static final Function<String, Function<JsonValue, List<JsonValue>>> DEFAULT_RESOLVER =
6473
expr -> JsonPath.parse(expr)::query;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package json.java21.jdt;
2+
3+
import jdk.sandbox.java.util.json.JsonValue;
4+
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Objects;
8+
9+
/// AST representation for JSON Document Transform specifications.
10+
///
11+
/// A JDT transform document is parsed into an immutable tree of [JdtNode] records.
12+
/// Each node represents either a directive-bearing transform, a default merge,
13+
/// or a direct replacement. The AST can be walked with exhaustive switch expressions
14+
/// for bytecode codegen, ESM codegen, or interpretation.
15+
///
16+
/// ## Node Types
17+
/// - [DirectiveNode]: Contains `@jdt.*` directives (rename, remove, merge, replace)
18+
/// plus child transforms for non-directive keys
19+
/// - [MergeNode]: Default object-to-object deep merge (no directives)
20+
/// - [ReplacementNode]: Direct value replacement (primitive or array)
21+
///
22+
/// ## Parse Entry Point
23+
/// Use [JdtAstParser.parse] to convert a `JsonValue` transform into a [JdtNode].
24+
public interface JdtAst {
25+
26+
/// A node in the JDT transform AST.
27+
sealed interface JdtNode permits DirectiveNode, MergeNode, ReplacementNode {}
28+
29+
/// A transform node containing `@jdt.*` directives.
30+
///
31+
/// Directives execute in order: Rename -> Remove -> Merge -> Replace.
32+
/// After directives, child transforms are applied as recursive merges.
33+
///
34+
/// @param rename the rename directive spec (null if absent)
35+
/// @param remove the remove directive spec (null if absent)
36+
/// @param merge the merge directive spec (null if absent)
37+
/// @param replace the replace directive spec (null if absent)
38+
/// @param children non-directive key transforms to apply after directives
39+
record DirectiveNode(
40+
JsonValue rename,
41+
JsonValue remove,
42+
JsonValue merge,
43+
JsonValue replace,
44+
Map<String, JdtNode> children
45+
) implements JdtNode {
46+
public DirectiveNode {
47+
Objects.requireNonNull(children, "children must not be null");
48+
children = Map.copyOf(children);
49+
}
50+
}
51+
52+
/// Default object merge: recursively merge transform keys into the source object.
53+
///
54+
/// @param children the key-to-transform mapping for recursive merge
55+
record MergeNode(Map<String, JdtNode> children) implements JdtNode {
56+
public MergeNode {
57+
Objects.requireNonNull(children, "children must not be null");
58+
children = Map.copyOf(children);
59+
}
60+
}
61+
62+
/// Direct replacement: the transform value replaces the source wholesale.
63+
///
64+
/// @param value the replacement value (primitive, array, or object without directives)
65+
record ReplacementNode(JsonValue value) implements JdtNode {
66+
public ReplacementNode {
67+
Objects.requireNonNull(value, "value must not be null");
68+
}
69+
}
70+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package json.java21.jdt;
2+
3+
import jdk.sandbox.java.util.json.*;
4+
5+
import java.util.LinkedHashMap;
6+
import java.util.logging.Logger;
7+
8+
import json.java21.jdt.JdtAst.*;
9+
10+
/// Parses a JSON transform document into a [JdtAst.JdtNode] tree.
11+
///
12+
/// The parser examines each object in the transform for `@jdt.*` directive
13+
/// keys and builds the appropriate AST node type.
14+
final class JdtAstParser {
15+
16+
private static final Logger LOG = Logger.getLogger(JdtAstParser.class.getName());
17+
18+
private JdtAstParser() {}
19+
20+
/// Parses a transform JsonValue into an AST node.
21+
///
22+
/// @param transform the transform specification value
23+
/// @return the root AST node
24+
static JdtNode parse(JsonValue transform) {
25+
if (!(transform instanceof JsonObject transformObj)) {
26+
// Non-object transforms are direct replacements
27+
return new ReplacementNode(transform);
28+
}
29+
30+
final var members = transformObj.members();
31+
32+
// Check if this object has any JDT directives
33+
final var hasDirectives = members.keySet().stream()
34+
.anyMatch(k -> k.startsWith(Jdt.JDT_PREFIX));
35+
36+
if (hasDirectives) {
37+
return parseDirectiveNode(transformObj);
38+
}
39+
40+
// No directives: this is a default merge node
41+
return parseMergeNode(transformObj);
42+
}
43+
44+
private static DirectiveNode parseDirectiveNode(JsonObject transformObj) {
45+
final var members = transformObj.members();
46+
47+
final var rename = members.get(Jdt.JDT_RENAME);
48+
final var remove = members.get(Jdt.JDT_REMOVE);
49+
final var merge = members.get(Jdt.JDT_MERGE);
50+
final var replace = members.get(Jdt.JDT_REPLACE);
51+
52+
// Parse non-directive keys as child transforms
53+
final var children = new LinkedHashMap<String, JdtNode>();
54+
for (final var entry : members.entrySet()) {
55+
if (!entry.getKey().startsWith(Jdt.JDT_PREFIX)) {
56+
children.put(entry.getKey(), parse(entry.getValue()));
57+
}
58+
}
59+
60+
LOG.finer(() -> "Parsed DirectiveNode: rename=" + (rename != null) +
61+
" remove=" + (remove != null) + " merge=" + (merge != null) +
62+
" replace=" + (replace != null) + " children=" + children.size());
63+
64+
return new DirectiveNode(rename, remove, merge, replace, children);
65+
}
66+
67+
private static MergeNode parseMergeNode(JsonObject transformObj) {
68+
final var children = new LinkedHashMap<String, JdtNode>();
69+
for (final var entry : transformObj.members().entrySet()) {
70+
children.put(entry.getKey(), parse(entry.getValue()));
71+
}
72+
73+
LOG.finer(() -> "Parsed MergeNode with " + children.size() + " children");
74+
75+
return new MergeNode(children);
76+
}
77+
}

json-java21-jdt/src/test/java/json/java21/jdt/JdtTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,4 +393,74 @@ void primitiveTransform_replacesSource() {
393393

394394
assertThat(result).isEqualTo(transform);
395395
}
396+
397+
// ========== AST Parser Tests ==========
398+
399+
@Test
400+
void ast_primitiveTransformParsesToReplacement() {
401+
LOG.info(() -> "TEST: ast_primitiveTransformParsesToReplacement");
402+
403+
final var transform = Json.parse("42");
404+
final var ast = Jdt.parseToAst(transform);
405+
406+
assertThat(ast).isInstanceOf(JdtAst.ReplacementNode.class);
407+
assertThat(((JdtAst.ReplacementNode) ast).value()).isEqualTo(transform);
408+
}
409+
410+
@Test
411+
void ast_objectWithoutDirectivesParsesToMergeNode() {
412+
LOG.info(() -> "TEST: ast_objectWithoutDirectivesParsesToMergeNode");
413+
414+
final var transform = Json.parse("""
415+
{"A": 1, "B": {"C": 2}}
416+
""");
417+
final var ast = Jdt.parseToAst(transform);
418+
419+
assertThat(ast).isInstanceOf(JdtAst.MergeNode.class);
420+
final var merge = (JdtAst.MergeNode) ast;
421+
assertThat(merge.children()).containsKeys("A", "B");
422+
assertThat(merge.children().get("A")).isInstanceOf(JdtAst.ReplacementNode.class);
423+
assertThat(merge.children().get("B")).isInstanceOf(JdtAst.MergeNode.class);
424+
}
425+
426+
@Test
427+
void ast_objectWithDirectivesParsesToDirectiveNode() {
428+
LOG.info(() -> "TEST: ast_objectWithDirectivesParsesToDirectiveNode");
429+
430+
final var transform = Json.parse("""
431+
{
432+
"@jdt.rename": {"old": "new"},
433+
"@jdt.remove": "B",
434+
"C": 3
435+
}
436+
""");
437+
final var ast = Jdt.parseToAst(transform);
438+
439+
assertThat(ast).isInstanceOf(JdtAst.DirectiveNode.class);
440+
final var directive = (JdtAst.DirectiveNode) ast;
441+
assertThat(directive.rename()).isNotNull();
442+
assertThat(directive.remove()).isNotNull();
443+
assertThat(directive.merge()).isNull();
444+
assertThat(directive.replace()).isNull();
445+
assertThat(directive.children()).containsKey("C");
446+
}
447+
448+
@Test
449+
void ast_nestedDirectivesParseCorrectly() {
450+
LOG.info(() -> "TEST: ast_nestedDirectivesParseCorrectly");
451+
452+
final var transform = Json.parse("""
453+
{
454+
"Settings": {
455+
"@jdt.merge": {"newKey": "value"},
456+
"existing": "updated"
457+
}
458+
}
459+
""");
460+
final var ast = Jdt.parseToAst(transform);
461+
462+
assertThat(ast).isInstanceOf(JdtAst.MergeNode.class);
463+
final var root = (JdtAst.MergeNode) ast;
464+
assertThat(root.children().get("Settings")).isInstanceOf(JdtAst.DirectiveNode.class);
465+
}
396466
}

0 commit comments

Comments
 (0)