From aa6325223d5c7b437aee4860872061fa58ca6bab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:09:25 +0000 Subject: [PATCH 1/6] Issue #130 Add json-transforms module docs and Maven wiring Co-authored-by: simbo1905 --- README.md | 17 ++++++++ json-transforms/README.md | 53 ++++++++++++++++++++++ json-transforms/pom.xml | 92 +++++++++++++++++++++++++++++++++++++++ pom.xml | 1 + 4 files changed, 163 insertions(+) create mode 100644 json-transforms/README.md create mode 100644 json-transforms/pom.xml diff --git a/README.md b/README.md index 6fe065a..e21f30b 100644 --- a/README.md +++ b/README.md @@ -440,6 +440,23 @@ System.out.println("Avg price: " + priceStats.getAverage()); See `json-java21-jsonpath/README.md` for JsonPath operators and more examples. +## json-transforms + +This repo includes **json-transforms** (module `json-transforms`): a Java 21 implementation of Microsoft’s *json-document-transforms* specification, which defines JSON-to-JSON transforms using `@jdt.*` verbs (`remove`, `replace`, `merge`, `rename`) plus JSONPath selectors. + +```java +import jdk.sandbox.java.util.json.Json; +import json.java21.transforms.JsonTransform; + +var source = Json.parse("{\"A\": 1, \"B\": 2}"); +var transform = Json.parse("{\"@jdt.remove\": \"A\"}"); + +var program = JsonTransform.parse(transform); // parse/compile once +var result = program.run(source); // run many times +``` + +See `json-transforms/README.md` for the supported syntax and more examples. + ## Contributing If you use an AI assistant while contributing, ensure it follows the contributor/agent workflow rules in `AGENTS.md`. diff --git a/json-transforms/README.md b/json-transforms/README.md new file mode 100644 index 0000000..3c3b4ad --- /dev/null +++ b/json-transforms/README.md @@ -0,0 +1,53 @@ +# json-transforms + +This module implements **json-transforms**: JSON-to-JSON transformations defined by a JSON “transform” document applied to a JSON “source” document. + +This is based on Microsoft’s *json-document-transforms* specification: +- Wiki/spec: `https://github.com/Microsoft/json-document-transforms/wiki` +- Reference implementation (C#): `https://github.com/microsoft/json-document-transforms` + +Important naming note: +- We refer to this technology as **json-transforms** in this repository (to avoid ambiguity with other unrelated acronyms). +- The specification’s on-the-wire syntax uses the literal keys `@jdt.*` (for example `@jdt.remove`), and this implementation follows that syntax for compatibility. + +## Quick Start + +This library has a **parse/run split** so transform parsing (including JSONPath compilation) can be reused across documents: + +```java +import jdk.sandbox.java.util.json.Json; +import json.java21.transforms.JsonTransform; + +var source = Json.parse(""" + { "Settings": { "A": 1, "B": 2 }, "Keep": true } + """); + +var transform = Json.parse(""" + { + "Settings": { "@jdt.remove": "A" } + } + """); + +var program = JsonTransform.parse(transform); // parse/compile once (reusable + thread-safe) +var result = program.run(source); // run (immutable result) +``` + +## Supported Syntax (Spec) + +This module follows the Microsoft wiki spec: +- **Default transform**: merge transform object into source object +- **Verbs** (object keys, case-sensitive): + - `@jdt.remove` + - `@jdt.replace` + - `@jdt.merge` + - `@jdt.rename` +- **Attributes** (inside a verb object, case-sensitive): + - `@jdt.path` (JSONPath selector) + - `@jdt.value` (the value to apply) + +See the wiki pages for details and examples: +- `Default Transformation` +- `Transform Attributes` +- `Transform Verbs` (Remove/Replace/Merge/Rename) +- `Order of Execution` + diff --git a/json-transforms/pom.xml b/json-transforms/pom.xml new file mode 100644 index 0000000..cbf4377 --- /dev/null +++ b/json-transforms/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + + io.github.simbo1905.json + parent + 0.1.9 + + + java.util.json.transforms + jar + java.util.json Java21 Backport JSON Transforms + https://simbo1905.github.io/java.util.json.Java21/ + + scm:git:https://github.com/simbo1905/java.util.json.Java21.git + scm:git:git@github.com:simbo1905/java.util.json.Java21.git + https://github.com/simbo1905/java.util.json.Java21 + HEAD + + json-transforms implementation for the java.util.json Java 21 backport; parses transform JSON to an immutable program and applies it to JSON documents using JSONPath selectors. + + + UTF-8 + 21 + + + + + io.github.simbo1905.json + java.util.json + ${project.version} + + + + io.github.simbo1905.json + java.util.json.jsonpath + ${project.version} + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + + -Xlint:all + -Werror + -Xdiags:verbose + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -ea + + + + + + diff --git a/pom.xml b/pom.xml index 16d8439..7acb437 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ json-compatibility-suite json-java21-jtd json-java21-jsonpath + json-transforms From 2c0067876f50a829f8a6d1c17e9f1e53c70d20c2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:19:23 +0000 Subject: [PATCH 2/6] Issue #130 Implement json-transforms parse/run core and JsonPath match locations Co-authored-by: simbo1905 --- .../java/json/java21/jsonpath/JsonPath.java | 309 +++++++++++++++ .../java21/jsonpath/JsonPathLocationStep.java | 20 + .../json/java21/jsonpath/JsonPathMatch.java | 19 + .../json/java21/transforms/JsonTransform.java | 47 +++ .../transforms/JsonTransformException.java | 13 + .../json/java21/transforms/TransformAst.java | 119 ++++++ .../java21/transforms/TransformCompiler.java | 281 ++++++++++++++ .../java21/transforms/TransformPatch.java | 167 +++++++++ .../java21/transforms/TransformRunner.java | 354 ++++++++++++++++++ .../java21/transforms/TransformSyntax.java | 36 ++ 10 files changed, 1365 insertions(+) create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathLocationStep.java create mode 100644 json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathMatch.java create mode 100644 json-transforms/src/main/java/json/java21/transforms/JsonTransform.java create mode 100644 json-transforms/src/main/java/json/java21/transforms/JsonTransformException.java create mode 100644 json-transforms/src/main/java/json/java21/transforms/TransformAst.java create mode 100644 json-transforms/src/main/java/json/java21/transforms/TransformCompiler.java create mode 100644 json-transforms/src/main/java/json/java21/transforms/TransformPatch.java create mode 100644 json-transforms/src/main/java/json/java21/transforms/TransformRunner.java create mode 100644 json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java index a18225a..baffee6 100644 --- a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPath.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import java.util.logging.Logger; /// JsonPath query evaluator for JSON documents. @@ -58,6 +59,19 @@ public List query(JsonValue json) { return evaluate(ast, json); } + /// Queries matching values from a JSON document, including their locations. + /// + /// This is intended for transform engines that must remove/replace/rename nodes, where the match location matters. + /// + /// @param json the JSON document to query + /// @return a list of matches including match location and value (maybe empty) + /// @throws NullPointerException if JSON is null + public List queryMatches(JsonValue json) { + Objects.requireNonNull(json, "json must not be null"); + LOG.fine(() -> "Querying document with path (matches): " + this); + return evaluateMatches(ast, json); + } + /// Reconstructs the JsonPath expression from the AST. @Override public String toString() { @@ -74,6 +88,16 @@ public static List query(JsonPath path, JsonValue json) { return path.query(json); } + /// Evaluates a compiled JsonPath against a JSON document, returning match locations. + /// @param path a compiled JsonPath (typically cached) + /// @param json the JSON document to query + /// @return a list of matches including match location and value (maybe empty) + /// @throws NullPointerException if path or JSON is null + public static List queryMatches(JsonPath path, JsonValue json) { + Objects.requireNonNull(path, "path must not be null"); + return path.queryMatches(json); + } + /// Evaluates a JsonPath expression against a JSON document. /// /// Intended for one-off usage; for hot paths, prefer caching the compiled `JsonPath` via `parse(String)`. @@ -102,6 +126,16 @@ static List evaluate(JsonPathAst.Root ast, JsonValue json) { return results; } + static List evaluateMatches(JsonPathAst.Root ast, JsonValue json) { + Objects.requireNonNull(ast, "ast must not be null"); + Objects.requireNonNull(json, "json must not be null"); + + final var results = new ArrayList(); + final var location = new ArrayList(); + evaluateSegmentsMatches(ast.segments(), 0, json, json, location, results::add); + return results; + } + private static void evaluateSegments( List segments, int index, @@ -130,6 +164,42 @@ private static void evaluateSegments( } } + private static void evaluateSegmentsMatches( + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + if (index >= segments.size()) { + sink.accept(new JsonPathMatch(location, current)); + return; + } + + final var segment = segments.get(index); + LOG.finer(() -> "Evaluating segment (matches) " + index + ": " + segment); + + switch (segment) { + case JsonPathAst.PropertyAccess prop -> + evaluatePropertyAccessMatches(prop, segments, index, current, root, location, sink); + case JsonPathAst.ArrayIndex arr -> + evaluateArrayIndexMatches(arr, segments, index, current, root, location, sink); + case JsonPathAst.ArraySlice slice -> + evaluateArraySliceMatches(slice, segments, index, current, root, location, sink); + case JsonPathAst.Wildcard ignored -> + evaluateWildcardMatches(segments, index, current, root, location, sink); + case JsonPathAst.RecursiveDescent desc -> + evaluateRecursiveDescentMatches(desc, segments, index, current, root, location, sink); + case JsonPathAst.Filter filter -> + evaluateFilterMatches(filter, segments, index, current, root, location, sink); + case JsonPathAst.Union union -> + evaluateUnionMatches(union, segments, index, current, root, location, sink); + case JsonPathAst.ScriptExpression script -> + evaluateScriptExpressionMatches(script, segments, index, current, root, location, sink); + } + } + private static void evaluatePropertyAccess( JsonPathAst.PropertyAccess prop, List segments, @@ -146,6 +216,25 @@ private static void evaluatePropertyAccess( } } + private static void evaluatePropertyAccessMatches( + JsonPathAst.PropertyAccess prop, + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + if (current instanceof JsonObject obj) { + final var value = obj.members().get(prop.name()); + if (value != null) { + location.add(new JsonPathLocationStep.Property(prop.name())); + evaluateSegmentsMatches(segments, index + 1, value, root, location, sink); + location.removeLast(); + } + } + } + private static void evaluateArrayIndex( JsonPathAst.ArrayIndex arr, List segments, @@ -169,6 +258,31 @@ private static void evaluateArrayIndex( } } + private static void evaluateArrayIndexMatches( + JsonPathAst.ArrayIndex arr, + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + if (current instanceof JsonArray array) { + final var elements = array.elements(); + int idx = arr.index(); + + if (idx < 0) { + idx = elements.size() + idx; + } + + if (idx >= 0 && idx < elements.size()) { + location.add(new JsonPathLocationStep.Index(idx)); + evaluateSegmentsMatches(segments, index + 1, elements.get(idx), root, location, sink); + location.removeLast(); + } + } + } + private static void evaluateArraySlice( JsonPathAst.ArraySlice slice, List segments, @@ -210,6 +324,49 @@ private static void evaluateArraySlice( } } + private static void evaluateArraySliceMatches( + JsonPathAst.ArraySlice slice, + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + if (current instanceof JsonArray array) { + final var elements = array.elements(); + final int size = elements.size(); + + final int step = slice.step() != null ? slice.step() : 1; + if (step == 0) return; + + if (step > 0) { + int start = slice.start() != null ? normalizeIndex(slice.start(), size) : 0; + int end = slice.end() != null ? normalizeIndex(slice.end(), size) : size; + + start = Math.max(0, Math.min(start, size)); + end = Math.max(0, Math.min(end, size)); + + for (int i = start; i < end; i += step) { + location.add(new JsonPathLocationStep.Index(i)); + evaluateSegmentsMatches(segments, index + 1, elements.get(i), root, location, sink); + location.removeLast(); + } + } else { + int start = slice.start() != null ? normalizeIndex(slice.start(), size) : size - 1; + final int end = slice.end() != null ? normalizeIndex(slice.end(), size) : -1; + + start = Math.max(0, Math.min(start, size - 1)); + + for (int i = start; i > end; i += step) { + location.add(new JsonPathLocationStep.Index(i)); + evaluateSegmentsMatches(segments, index + 1, elements.get(i), root, location, sink); + location.removeLast(); + } + } + } + } + private static int normalizeIndex(int index, int size) { if (index < 0) { return size + index; @@ -235,6 +392,30 @@ private static void evaluateWildcard( } } + private static void evaluateWildcardMatches( + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + if (current instanceof JsonObject obj) { + for (final var entry : obj.members().entrySet()) { + location.add(new JsonPathLocationStep.Property(entry.getKey())); + evaluateSegmentsMatches(segments, index + 1, entry.getValue(), root, location, sink); + location.removeLast(); + } + } else if (current instanceof JsonArray array) { + final var elements = array.elements(); + for (int i = 0; i < elements.size(); i++) { + location.add(new JsonPathLocationStep.Index(i)); + evaluateSegmentsMatches(segments, index + 1, elements.get(i), root, location, sink); + location.removeLast(); + } + } + } + private static void evaluateRecursiveDescent( JsonPathAst.RecursiveDescent desc, List segments, @@ -258,6 +439,70 @@ private static void evaluateRecursiveDescent( } } + private static void evaluateRecursiveDescentMatches( + JsonPathAst.RecursiveDescent desc, + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + evaluateTargetSegmentMatches(desc.target(), segments, index, current, root, location, sink); + + if (current instanceof JsonObject obj) { + for (final var entry : obj.members().entrySet()) { + location.add(new JsonPathLocationStep.Property(entry.getKey())); + evaluateRecursiveDescentMatches(desc, segments, index, entry.getValue(), root, location, sink); + location.removeLast(); + } + } else if (current instanceof JsonArray array) { + final var elements = array.elements(); + for (int i = 0; i < elements.size(); i++) { + location.add(new JsonPathLocationStep.Index(i)); + evaluateRecursiveDescentMatches(desc, segments, index, elements.get(i), root, location, sink); + location.removeLast(); + } + } + } + + private static void evaluateTargetSegmentMatches( + JsonPathAst.Segment target, + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + switch (target) { + case JsonPathAst.PropertyAccess prop -> { + if (current instanceof JsonObject obj) { + final var value = obj.members().get(prop.name()); + if (value != null) { + location.add(new JsonPathLocationStep.Property(prop.name())); + evaluateSegmentsMatches(segments, index + 1, value, root, location, sink); + location.removeLast(); + } + } + } + case JsonPathAst.Wildcard ignored -> evaluateWildcardMatches(segments, index, current, root, location, sink); + case JsonPathAst.ArrayIndex arr -> { + if (current instanceof JsonArray array) { + final var elements = array.elements(); + int idx = arr.index(); + if (idx < 0) idx = elements.size() + idx; + if (idx >= 0 && idx < elements.size()) { + location.add(new JsonPathLocationStep.Index(idx)); + evaluateSegmentsMatches(segments, index + 1, elements.get(idx), root, location, sink); + location.removeLast(); + } + } + } + default -> LOG.finer(() -> "Unsupported target in recursive descent (matches): " + target); + } + } + private static void evaluateTargetSegment( JsonPathAst.Segment target, List segments, @@ -318,6 +563,28 @@ private static void evaluateFilter( } } + private static void evaluateFilterMatches( + JsonPathAst.Filter filter, + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + if (current instanceof JsonArray array) { + final var elements = array.elements(); + for (int i = 0; i < elements.size(); i++) { + final var element = elements.get(i); + if (matchesFilter(filter.expression(), element)) { + location.add(new JsonPathLocationStep.Index(i)); + evaluateSegmentsMatches(segments, index + 1, element, root, location, sink); + location.removeLast(); + } + } + } + } + private static boolean matchesFilter(JsonPathAst.FilterExpression expr, JsonValue current) { return switch (expr) { case JsonPathAst.ExistsFilter exists -> { @@ -458,6 +725,24 @@ private static void evaluateUnion( } } + private static void evaluateUnionMatches( + JsonPathAst.Union union, + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + for (final var selector : union.selectors()) { + switch (selector) { + case JsonPathAst.ArrayIndex arr -> evaluateArrayIndexMatches(arr, segments, index, current, root, location, sink); + case JsonPathAst.PropertyAccess prop -> evaluatePropertyAccessMatches(prop, segments, index, current, root, location, sink); + default -> LOG.finer(() -> "Unsupported selector in union (matches): " + selector); + } + } + } + private static void evaluateScriptExpression( JsonPathAst.ScriptExpression script, List segments, @@ -480,6 +765,30 @@ private static void evaluateScriptExpression( } } + private static void evaluateScriptExpressionMatches( + JsonPathAst.ScriptExpression script, + List segments, + int index, + JsonValue current, + JsonValue root, + ArrayList location, + Consumer sink) { + + if (current instanceof JsonArray array) { + final var scriptText = script.script().trim(); + if (scriptText.equals("@.length-1")) { + final int lastIndex = array.elements().size() - 1; + if (lastIndex >= 0) { + location.add(new JsonPathLocationStep.Index(lastIndex)); + evaluateSegmentsMatches(segments, index + 1, array.elements().get(lastIndex), root, location, sink); + location.removeLast(); + } + } else { + LOG.warning(() -> "Unsupported script expression: " + scriptText); + } + } + } + private static String reconstruct(JsonPathAst.Root root) { final var sb = new StringBuilder("$"); for (final var segment : root.segments()) { diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathLocationStep.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathLocationStep.java new file mode 100644 index 0000000..07a3cb4 --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathLocationStep.java @@ -0,0 +1,20 @@ +package json.java21.jsonpath; + +import java.util.Objects; + +/// A single step in a JsonPath match location. +/// +/// Locations are expressed relative to the JSON value passed to `JsonPath.queryMatches(...)`. +public sealed interface JsonPathLocationStep permits JsonPathLocationStep.Property, JsonPathLocationStep.Index { + + /// Object member step (by property name). + record Property(String name) implements JsonPathLocationStep { + public Property { + Objects.requireNonNull(name, "name must not be null"); + } + } + + /// Array element step (by index). + record Index(int index) implements JsonPathLocationStep {} +} + diff --git a/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathMatch.java b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathMatch.java new file mode 100644 index 0000000..407805b --- /dev/null +++ b/json-java21-jsonpath/src/main/java/json/java21/jsonpath/JsonPathMatch.java @@ -0,0 +1,19 @@ +package json.java21.jsonpath; + +import jdk.sandbox.java.util.json.JsonValue; + +import java.util.List; +import java.util.Objects; + +/// A single JsonPath match: the matched value plus its location. +/// +/// The `location` is a list of steps from the queried root value to the match. +/// The empty list indicates the root value itself. +public record JsonPathMatch(List location, JsonValue value) { + public JsonPathMatch { + Objects.requireNonNull(location, "location must not be null"); + Objects.requireNonNull(value, "value must not be null"); + location = List.copyOf(location); + } +} + diff --git a/json-transforms/src/main/java/json/java21/transforms/JsonTransform.java b/json-transforms/src/main/java/json/java21/transforms/JsonTransform.java new file mode 100644 index 0000000..e482606 --- /dev/null +++ b/json-transforms/src/main/java/json/java21/transforms/JsonTransform.java @@ -0,0 +1,47 @@ +package json.java21.transforms; + +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonValue; + +import java.util.Objects; + +/// A compiled json-transforms program. +/// +/// This implementation follows the Microsoft json-document-transforms specification (wiki): +/// `https://github.com/Microsoft/json-document-transforms/wiki` +/// +/// Parse/compile once via `parse(...)`, then apply to many source documents via `run(...)`. +public final class JsonTransform { + + private final TransformAst.ObjectTransform root; + + private JsonTransform(TransformAst.ObjectTransform root) { + this.root = root; + } + + /// Parses (compiles) a transform JSON value into a reusable program. + /// @param transform the transform JSON (must be an object) + /// @return a compiled, reusable transform program + public static JsonTransform parse(JsonValue transform) { + Objects.requireNonNull(transform, "transform must not be null"); + if (!(transform instanceof JsonObject obj)) { + throw new JsonTransformException("transform must be a JSON object, got: " + transform.getClass().getSimpleName()); + } + return new JsonTransform(TransformCompiler.compileObject(obj)); + } + + /// Applies this transform to a source JSON value. + /// + /// The source must be a JSON object, matching the reference implementation expectation. + /// + /// @param source the source JSON value (must be an object) + /// @return the transformed JSON value + public JsonValue run(JsonValue source) { + Objects.requireNonNull(source, "source must not be null"); + if (!(source instanceof JsonObject obj)) { + throw new JsonTransformException("source must be a JSON object, got: " + source.getClass().getSimpleName()); + } + return TransformRunner.applyAtDocumentRoot(obj, root); + } +} + diff --git a/json-transforms/src/main/java/json/java21/transforms/JsonTransformException.java b/json-transforms/src/main/java/json/java21/transforms/JsonTransformException.java new file mode 100644 index 0000000..094bff9 --- /dev/null +++ b/json-transforms/src/main/java/json/java21/transforms/JsonTransformException.java @@ -0,0 +1,13 @@ +package json.java21.transforms; + +/// Exception thrown for invalid transform syntax or runtime failures applying a transform. +public final class JsonTransformException extends RuntimeException { + public JsonTransformException(String message) { + super(message); + } + + public JsonTransformException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformAst.java b/json-transforms/src/main/java/json/java21/transforms/TransformAst.java new file mode 100644 index 0000000..39aa2b7 --- /dev/null +++ b/json-transforms/src/main/java/json/java21/transforms/TransformAst.java @@ -0,0 +1,119 @@ +package json.java21.transforms; + +import json.java21.jsonpath.JsonPath; +import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonValue; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +sealed interface TransformAst { + + record ObjectTransform( + Map nonVerbMembers, + Map childObjects, + List removes, + List replaces, + List merges, + List renames + ) implements TransformAst { + ObjectTransform { + Objects.requireNonNull(nonVerbMembers, "nonVerbMembers must not be null"); + Objects.requireNonNull(childObjects, "childObjects must not be null"); + Objects.requireNonNull(removes, "removes must not be null"); + Objects.requireNonNull(replaces, "replaces must not be null"); + Objects.requireNonNull(merges, "merges must not be null"); + Objects.requireNonNull(renames, "renames must not be null"); + nonVerbMembers = Map.copyOf(nonVerbMembers); + childObjects = Map.copyOf(childObjects); + removes = List.copyOf(removes); + replaces = List.copyOf(replaces); + merges = List.copyOf(merges); + renames = List.copyOf(renames); + } + } + + sealed interface RemoveOp extends TransformAst permits RemoveOp.ByName, RemoveOp.RemoveThis, RemoveOp.ByPath { + record ByName(String name) implements RemoveOp { + ByName { + Objects.requireNonNull(name, "name must not be null"); + } + } + + record RemoveThis() implements RemoveOp {} + + record ByPath(String rawPath, JsonPath path) implements RemoveOp { + ByPath { + Objects.requireNonNull(rawPath, "rawPath must not be null"); + Objects.requireNonNull(path, "path must not be null"); + } + } + } + + sealed interface ReplaceOp extends TransformAst permits ReplaceOp.ReplaceThis, ReplaceOp.ByPath { + record ReplaceThis(JsonValue value) implements ReplaceOp { + ReplaceThis { + Objects.requireNonNull(value, "value must not be null"); + } + } + + record ByPath(String rawPath, JsonPath path, JsonValue value) implements ReplaceOp { + ByPath { + Objects.requireNonNull(rawPath, "rawPath must not be null"); + Objects.requireNonNull(path, "path must not be null"); + Objects.requireNonNull(value, "value must not be null"); + } + } + } + + sealed interface MergeOp extends TransformAst permits MergeOp.MergeThis, MergeOp.ByPath { + + record MergeThis(Value value) implements MergeOp { + MergeThis { + Objects.requireNonNull(value, "value must not be null"); + } + } + + record ByPath(String rawPath, JsonPath path, Value value) implements MergeOp { + ByPath { + Objects.requireNonNull(rawPath, "rawPath must not be null"); + Objects.requireNonNull(path, "path must not be null"); + Objects.requireNonNull(value, "value must not be null"); + } + } + + sealed interface Value permits Value.Raw, Value.TransformObjectValue { + record Raw(JsonValue value) implements Value { + Raw { + Objects.requireNonNull(value, "value must not be null"); + } + } + + record TransformObjectValue(JsonObject rawObject, ObjectTransform compiled) implements Value { + TransformObjectValue { + Objects.requireNonNull(rawObject, "rawObject must not be null"); + Objects.requireNonNull(compiled, "compiled must not be null"); + } + } + } + } + + sealed interface RenameOp extends TransformAst permits RenameOp.Mapping, RenameOp.ByPath { + record Mapping(Map renames) implements RenameOp { + Mapping { + Objects.requireNonNull(renames, "renames must not be null"); + renames = Map.copyOf(renames); + } + } + + record ByPath(String rawPath, JsonPath path, String newName) implements RenameOp { + ByPath { + Objects.requireNonNull(rawPath, "rawPath must not be null"); + Objects.requireNonNull(path, "path must not be null"); + Objects.requireNonNull(newName, "newName must not be null"); + } + } + } +} + diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformCompiler.java b/json-transforms/src/main/java/json/java21/transforms/TransformCompiler.java new file mode 100644 index 0000000..4787179 --- /dev/null +++ b/json-transforms/src/main/java/json/java21/transforms/TransformCompiler.java @@ -0,0 +1,281 @@ +package json.java21.transforms; + +import json.java21.jsonpath.JsonPath; +import jdk.sandbox.java.util.json.*; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import static json.java21.transforms.TransformSyntax.*; + +sealed interface TransformCompiler permits TransformCompiler.Nothing { + enum Nothing implements TransformCompiler { INSTANCE } + + static TransformAst.ObjectTransform compileObject(JsonObject transformObject) { + Objects.requireNonNull(transformObject, "transformObject must not be null"); + + validateVerbsAtObjectLevel(transformObject); + + final var nonVerbMembers = new LinkedHashMap(); + final var childObjects = new LinkedHashMap(); + + final var removes = new ArrayList(); + final var replaces = new ArrayList(); + final var merges = new ArrayList(); + final var renames = new ArrayList(); + + for (final var entry : transformObject.members().entrySet()) { + final var key = entry.getKey(); + final var value = entry.getValue(); + + if (VERB_REMOVE.equals(key)) { + removes.addAll(compileRemove(value)); + continue; + } + if (VERB_REPLACE.equals(key)) { + replaces.addAll(compileReplace(value)); + continue; + } + if (VERB_MERGE.equals(key)) { + merges.addAll(compileMerge(value)); + continue; + } + if (VERB_RENAME.equals(key)) { + renames.addAll(compileRename(value)); + continue; + } + + if (isSyntaxKey(key)) { + // Unknown @jdt.* key at object level: invalid verb + throw new JsonTransformException("invalid transform verb: '" + key + "'"); + } + + nonVerbMembers.put(key, value); + if (value instanceof JsonObject childObj) { + childObjects.put(key, compileObject(childObj)); + } + } + + return new TransformAst.ObjectTransform(nonVerbMembers, childObjects, removes, replaces, merges, renames); + } + + private static void validateVerbsAtObjectLevel(JsonObject transformObject) { + for (final var key : transformObject.members().keySet()) { + if (!isSyntaxKey(key)) continue; + if (VERB_REMOVE.equals(key) || VERB_REPLACE.equals(key) || VERB_MERGE.equals(key) || VERB_RENAME.equals(key)) { + continue; + } + throw new JsonTransformException("invalid transform verb: '" + key + "'"); + } + } + + private static List compileRemove(JsonValue value) { + return switch (value) { + case JsonArray arr -> { + final var ops = new ArrayList(); + for (final var element : arr.elements()) { + if (element instanceof JsonArray) { + throw new JsonTransformException("invalid @jdt.remove value: array"); + } + ops.addAll(compileRemove(element)); + } + yield ops; + } + case JsonString s -> List.of(new TransformAst.RemoveOp.ByName(s.string())); + case JsonBoolean b -> b.bool() ? List.of(new TransformAst.RemoveOp.RemoveThis()) : List.of(); + case JsonObject obj -> List.of(compileRemoveWithAttributes(obj)); + default -> throw new JsonTransformException("invalid @jdt.remove value: " + value.getClass().getSimpleName()); + }; + } + + private static TransformAst.RemoveOp compileRemoveWithAttributes(JsonObject obj) { + final var attrs = parseAttributeObject(obj, Set.of(ATTR_PATH)); + final var pathVal = attrs.attributes().get(ATTR_PATH); + if (!(pathVal instanceof JsonString s)) { + throw new JsonTransformException("@jdt.path must be a string"); + } + final var rawPath = s.string(); + final var normalized = normalizePathString(rawPath); + final var path = JsonPath.parse(normalized); + return new TransformAst.RemoveOp.ByPath(rawPath, path); + } + + private static List compileReplace(JsonValue value) { + if (value instanceof JsonArray arr) { + final var ops = new ArrayList(); + for (final var element : arr.elements()) { + ops.add(compileReplaceElement(element)); + } + return ops; + } + return List.of(compileReplaceElement(value)); + } + + private static TransformAst.ReplaceOp compileReplaceElement(JsonValue value) { + if (value instanceof JsonObject obj) { + final var attrs = parseAttributeObjectOrEmpty(obj, Set.of(ATTR_PATH, ATTR_VALUE)); + if (attrs.isPresent()) { + final var pathVal = attrs.attributes().get(ATTR_PATH); + final var valueVal = attrs.attributes().get(ATTR_VALUE); + if (!(pathVal instanceof JsonString s)) { + throw new JsonTransformException("@jdt.path must be a string"); + } + final var rawPath = s.string(); + final var normalized = normalizePathString(rawPath); + final var path = JsonPath.parse(normalized); + return new TransformAst.ReplaceOp.ByPath(rawPath, path, valueVal); + } + } + return new TransformAst.ReplaceOp.ReplaceThis(value); + } + + private static List compileMerge(JsonValue value) { + if (value instanceof JsonArray arr) { + final var ops = new ArrayList(); + for (final var element : arr.elements()) { + ops.add(compileMergeElement(element)); + } + return ops; + } + return List.of(compileMergeElement(value)); + } + + private static TransformAst.MergeOp compileMergeElement(JsonValue value) { + if (value instanceof JsonObject obj) { + final var attrs = parseAttributeObjectOrEmpty(obj, Set.of(ATTR_PATH, ATTR_VALUE)); + if (attrs.isPresent()) { + final var pathVal = attrs.attributes().get(ATTR_PATH); + final var valueVal = attrs.attributes().get(ATTR_VALUE); + if (!(pathVal instanceof JsonString s)) { + throw new JsonTransformException("@jdt.path must be a string"); + } + final var rawPath = s.string(); + final var normalized = normalizePathString(rawPath); + final var path = JsonPath.parse(normalized); + return new TransformAst.MergeOp.ByPath(rawPath, path, compileMergeValue(valueVal)); + } + return new TransformAst.MergeOp.MergeThis(new TransformAst.MergeOp.Value.TransformObjectValue(obj, compileObject(obj))); + } + return new TransformAst.MergeOp.MergeThis(new TransformAst.MergeOp.Value.Raw(value)); + } + + private static TransformAst.MergeOp.Value compileMergeValue(JsonValue value) { + if (value instanceof JsonObject obj) { + return new TransformAst.MergeOp.Value.TransformObjectValue(obj, compileObject(obj)); + } + return new TransformAst.MergeOp.Value.Raw(value); + } + + private static List compileRename(JsonValue value) { + if (value instanceof JsonArray arr) { + final var ops = new ArrayList(); + for (final var element : arr.elements()) { + if (element instanceof JsonArray) { + throw new JsonTransformException("invalid @jdt.rename value: array"); + } + ops.addAll(compileRename(element)); + } + return ops; + } + return List.of(compileRenameElement(value)); + } + + private static TransformAst.RenameOp compileRenameElement(JsonValue value) { + if (!(value instanceof JsonObject obj)) { + throw new JsonTransformException("invalid @jdt.rename value: " + value.getClass().getSimpleName()); + } + + final var attrs = parseAttributeObjectOrEmpty(obj, Set.of(ATTR_PATH, ATTR_VALUE)); + if (attrs.isPresent()) { + final var pathVal = attrs.attributes().get(ATTR_PATH); + final var valueVal = attrs.attributes().get(ATTR_VALUE); + if (!(pathVal instanceof JsonString s)) { + throw new JsonTransformException("@jdt.path must be a string"); + } + if (!(valueVal instanceof JsonString v)) { + throw new JsonTransformException("@jdt.value must be a string for @jdt.rename"); + } + final var rawPath = s.string(); + final var normalized = normalizePathString(rawPath); + final var path = JsonPath.parse(normalized); + return new TransformAst.RenameOp.ByPath(rawPath, path, v.string()); + } + + // Direct mapping: { "Old": "New", ... } + final var mapping = new LinkedHashMap(); + for (final var entry : obj.members().entrySet()) { + if (!(entry.getValue() instanceof JsonString s)) { + throw new JsonTransformException("rename mapping values must be strings"); + } + mapping.put(entry.getKey(), s.string()); + } + return new TransformAst.RenameOp.Mapping(mapping); + } + + private record AttrParseResult(Map attributes, boolean isPresent) {} + + private static AttrParseResult parseAttributeObjectOrEmpty(JsonObject obj, Set allowedAttributes) { + // Detect whether this object is using attribute syntax at all. + boolean hasAnyAttr = obj.members().containsKey(ATTR_PATH) || obj.members().containsKey(ATTR_VALUE); + if (!hasAnyAttr) { + // Still need to reject unknown @jdt.* keys (invalid attribute) to match the reference tests. + for (final var key : obj.members().keySet()) { + if (isSyntaxKey(key)) { + throw new JsonTransformException("invalid attribute: '" + key + "'"); + } + } + return new AttrParseResult(Map.of(), false); + } + final var parsed = parseAttributeObject(obj, allowedAttributes); + return new AttrParseResult(parsed.attributes(), true); + } + + private record ParsedAttributes(Map attributes) {} + + private static ParsedAttributes parseAttributeObject(JsonObject obj, Set allowedAttributes) { + Objects.requireNonNull(obj, "obj must not be null"); + Objects.requireNonNull(allowedAttributes, "allowedAttributes must not be null"); + + boolean hasNonAttr = false; + final var attrs = new LinkedHashMap(); + + for (final var entry : obj.members().entrySet()) { + final var key = entry.getKey(); + final var value = entry.getValue(); + + if (isSyntaxKey(key)) { + if (!allowedAttributes.contains(key)) { + throw new JsonTransformException("invalid attribute: '" + key + "'"); + } + attrs.put(key, value); + } else { + hasNonAttr = true; + } + } + + if (hasNonAttr) { + throw new JsonTransformException("attribute objects must contain only @jdt.* attributes"); + } + + if (allowedAttributes.contains(ATTR_PATH) && !attrs.containsKey(ATTR_PATH)) { + // For all current uses, path is required when attributes are in play. + throw new JsonTransformException("missing required attribute: @jdt.path"); + } + + if (allowedAttributes.contains(ATTR_VALUE) && !attrs.containsKey(ATTR_VALUE)) { + throw new JsonTransformException("missing required attribute: @jdt.value"); + } + + // Some verbs allow only a subset; enforce here. + if (allowedAttributes.equals(Set.of(ATTR_PATH)) && attrs.size() != 1) { + throw new JsonTransformException("@jdt.remove only supports @jdt.path"); + } + + return new ParsedAttributes(attrs); + } +} + diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformPatch.java b/json-transforms/src/main/java/json/java21/transforms/TransformPatch.java new file mode 100644 index 0000000..eb1946e --- /dev/null +++ b/json-transforms/src/main/java/json/java21/transforms/TransformPatch.java @@ -0,0 +1,167 @@ +package json.java21.transforms; + +import json.java21.jsonpath.JsonPathLocationStep; +import jdk.sandbox.java.util.json.*; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; + +sealed interface TransformPatch permits TransformPatch.Nothing { + enum Nothing implements TransformPatch { INSTANCE } + + static JsonValue removeAt(JsonValue root, List location) { + Objects.requireNonNull(root, "root must not be null"); + Objects.requireNonNull(location, "location must not be null"); + if (location.isEmpty()) { + throw new IllegalArgumentException("removeAt cannot remove the root; handle at caller"); + } + return removeAt0(root, location, 0); + } + + static JsonValue replaceAt(JsonValue root, List location, JsonValue newValue) { + Objects.requireNonNull(root, "root must not be null"); + Objects.requireNonNull(location, "location must not be null"); + Objects.requireNonNull(newValue, "newValue must not be null"); + if (location.isEmpty()) { + return newValue; + } + return replaceAt0(root, location, 0, newValue); + } + + static JsonArray append(JsonArray original, JsonArray toAppend) { + Objects.requireNonNull(original, "original must not be null"); + Objects.requireNonNull(toAppend, "toAppend must not be null"); + if (toAppend.elements().isEmpty()) return original; + final var out = new ArrayList(original.elements().size() + toAppend.elements().size()); + out.addAll(original.elements()); + out.addAll(toAppend.elements()); + return JsonArray.of(out); + } + + static JsonObject renameKey(JsonObject obj, String oldName, String newName) { + Objects.requireNonNull(obj, "obj must not be null"); + Objects.requireNonNull(oldName, "oldName must not be null"); + Objects.requireNonNull(newName, "newName must not be null"); + + if (!obj.members().containsKey(oldName)) { + return obj; + } + + final var out = new LinkedHashMap(obj.members().size()); + for (final var entry : obj.members().entrySet()) { + if (entry.getKey().equals(oldName)) { + out.put(newName, entry.getValue()); + } else { + out.put(entry.getKey(), entry.getValue()); + } + } + return JsonObject.of(out); + } + + private static JsonValue removeAt0(JsonValue current, List location, int index) { + final var step = location.get(index); + final boolean isLast = index == location.size() - 1; + + if (isLast) { + return switch (step) { + case JsonPathLocationStep.Property p -> { + if (!(current instanceof JsonObject obj)) yield current; + if (!obj.members().containsKey(p.name())) yield current; + final var out = new LinkedHashMap(obj.members().size()); + for (final var entry : obj.members().entrySet()) { + if (!entry.getKey().equals(p.name())) { + out.put(entry.getKey(), entry.getValue()); + } + } + yield JsonObject.of(out); + } + case JsonPathLocationStep.Index idx -> { + if (!(current instanceof JsonArray arr)) yield current; + final int i = idx.index(); + if (i < 0 || i >= arr.elements().size()) yield current; + final var out = new ArrayList(Math.max(0, arr.elements().size() - 1)); + for (int j = 0; j < arr.elements().size(); j++) { + if (j != i) out.add(arr.elements().get(j)); + } + yield JsonArray.of(out); + } + }; + } + + return switch (step) { + case JsonPathLocationStep.Property p -> { + if (!(current instanceof JsonObject obj)) yield current; + final var child = obj.members().get(p.name()); + if (child == null) yield current; + final var newChild = removeAt0(child, location, index + 1); + if (newChild == child) yield current; + final var out = new LinkedHashMap(obj.members()); + out.put(p.name(), newChild); + yield JsonObject.of(out); + } + case JsonPathLocationStep.Index idx -> { + if (!(current instanceof JsonArray arr)) yield current; + final int i = idx.index(); + if (i < 0 || i >= arr.elements().size()) yield current; + final var child = arr.elements().get(i); + final var newChild = removeAt0(child, location, index + 1); + if (newChild == child) yield current; + final var out = new ArrayList(arr.elements()); + out.set(i, newChild); + yield JsonArray.of(out); + } + }; + } + + private static JsonValue replaceAt0(JsonValue current, List location, int index, JsonValue newValue) { + final var step = location.get(index); + final boolean isLast = index == location.size() - 1; + + if (isLast) { + return switch (step) { + case JsonPathLocationStep.Property p -> { + if (!(current instanceof JsonObject obj)) yield current; + if (!obj.members().containsKey(p.name())) yield current; + final var out = new LinkedHashMap(obj.members()); + out.put(p.name(), newValue); + yield JsonObject.of(out); + } + case JsonPathLocationStep.Index idx -> { + if (!(current instanceof JsonArray arr)) yield current; + final int i = idx.index(); + if (i < 0 || i >= arr.elements().size()) yield current; + final var out = new ArrayList(arr.elements()); + out.set(i, newValue); + yield JsonArray.of(out); + } + }; + } + + return switch (step) { + case JsonPathLocationStep.Property p -> { + if (!(current instanceof JsonObject obj)) yield current; + final var child = obj.members().get(p.name()); + if (child == null) yield current; + final var newChild = replaceAt0(child, location, index + 1, newValue); + if (newChild == child) yield current; + final var out = new LinkedHashMap(obj.members()); + out.put(p.name(), newChild); + yield JsonObject.of(out); + } + case JsonPathLocationStep.Index idx -> { + if (!(current instanceof JsonArray arr)) yield current; + final int i = idx.index(); + if (i < 0 || i >= arr.elements().size()) yield current; + final var child = arr.elements().get(i); + final var newChild = replaceAt0(child, location, index + 1, newValue); + if (newChild == child) yield current; + final var out = new ArrayList(arr.elements()); + out.set(i, newChild); + yield JsonArray.of(out); + } + }; + } +} + diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java b/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java new file mode 100644 index 0000000..abcfb30 --- /dev/null +++ b/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java @@ -0,0 +1,354 @@ +package json.java21.transforms; + +import json.java21.jsonpath.JsonPathMatch; +import json.java21.jsonpath.JsonPathLocationStep; +import jdk.sandbox.java.util.json.*; + +import java.util.*; +import java.util.logging.Logger; + +import static json.java21.transforms.TransformPatch.*; + +sealed interface TransformRunner permits TransformRunner.Nothing { + enum Nothing implements TransformRunner { INSTANCE } + + Logger LOG = Logger.getLogger(TransformRunner.class.getName()); + + static JsonValue applyAtDocumentRoot(JsonObject source, TransformAst.ObjectTransform transform) { + Objects.requireNonNull(source, "source must not be null"); + Objects.requireNonNull(transform, "transform must not be null"); + final var applied = processTransform(source, transform, true); + return applied.value(); + } + + private record Applied(JsonValue value, boolean halt) { + Applied { + Objects.requireNonNull(value, "value must not be null"); + } + } + + private static Applied processTransform(JsonObject source, TransformAst.ObjectTransform transform, boolean isDocumentRoot) { + // Step 1: Recurse into non-verb objects that exist in both source + transform + final var recursedKeys = new HashSet(); + JsonObject current = source; + + for (final var entry : transform.childObjects().entrySet()) { + final var key = entry.getKey(); + final var childTransform = entry.getValue(); + + final var sourceChild = current.members().get(key); + if (sourceChild instanceof JsonObject childObj) { + final var appliedChild = processTransform(childObj, childTransform, false); + current = (JsonObject) replaceAt(current, List.of(new JsonPathLocationStep.Property(key)), appliedChild.value()); + recursedKeys.add(key); + } + } + + // Step 2: Apply verbs in priority order: Remove > Replace > Merge > Default > Rename + final var removed = applyRemoves(current, transform.removes(), isDocumentRoot); + if (removed.halt()) return removed; + current = (JsonObject) removed.value(); + + final var replaced = applyReplaces(current, transform.replaces(), isDocumentRoot); + if (replaced.halt()) return replaced; + current = (JsonObject) replaced.value(); + + final var merged = applyMerges(current, transform.merges(), isDocumentRoot); + if (merged.halt()) return merged; + if (!(merged.value() instanceof JsonObject mergedObj)) { + // If merge replaced this node with a non-object, no further object transforms are meaningful. + return merged; + } + current = mergedObj; + + current = applyDefault(current, transform.nonVerbMembers(), recursedKeys); + + current = applyRenames(current, transform.renames(), isDocumentRoot); + + return new Applied(current, false); + } + + private static Applied applyRemoves(JsonObject source, List removes, boolean isDocumentRoot) { + JsonValue current = source; + + for (final var op : removes) { + switch (op) { + case TransformAst.RemoveOp.ByName byName -> { + if (current instanceof JsonObject obj) { + if (!obj.members().containsKey(byName.name())) { + LOG.fine(() -> "Remove by name had no effect; missing key: " + byName.name()); + } else { + final var out = new LinkedHashMap(obj.members().size()); + for (final var entry : obj.members().entrySet()) { + if (!entry.getKey().equals(byName.name())) out.put(entry.getKey(), entry.getValue()); + } + current = JsonObject.of(out); + } + } + } + case TransformAst.RemoveOp.RemoveThis ignored -> { + if (isDocumentRoot) { + throw new JsonTransformException("cannot remove the document root"); + } + return new Applied(JsonNull.of(), true); + } + case TransformAst.RemoveOp.ByPath byPath -> { + if (!(current instanceof JsonValue root)) { + break; + } + final var matches = byPath.path().queryMatches(root); + if (matches.isEmpty()) { + LOG.fine(() -> "Remove path produced no results: " + byPath.rawPath()); + break; + } + // Apply removals; remove from arrays in descending index order for stability. + final var sorted = sortForRemoval(matches); + for (final var match : sorted) { + final var location = match.location(); + if (location.isEmpty()) { + if (isDocumentRoot) { + throw new JsonTransformException("cannot remove the document root"); + } + return new Applied(JsonNull.of(), true); + } + current = removeAt(current, location); + } + } + } + } + + return new Applied(current, false); + } + + private static Applied applyReplaces(JsonObject source, List replaces, boolean isDocumentRoot) { + JsonValue current = source; + + for (final var op : replaces) { + switch (op) { + case TransformAst.ReplaceOp.ReplaceThis rt -> { + final var value = rt.value(); + if (isDocumentRoot && !(value instanceof JsonObject)) { + throw new JsonTransformException("cannot replace the document root with a non-object"); + } + return new Applied(value, true); + } + case TransformAst.ReplaceOp.ByPath byPath -> { + final var matches = byPath.path().queryMatches(current); + if (matches.isEmpty()) { + LOG.fine(() -> "Replace path produced no results: " + byPath.rawPath()); + break; + } + for (final var match : matches) { + final var location = match.location(); + if (location.isEmpty() && isDocumentRoot && !(byPath.value() instanceof JsonObject)) { + throw new JsonTransformException("cannot replace the document root with a non-object"); + } + current = replaceAt(current, location, byPath.value()); + if (location.isEmpty()) { + return new Applied(current, true); + } + } + } + } + } + + return new Applied(current, false); + } + + private static Applied applyMerges(JsonObject source, List merges, boolean isDocumentRoot) { + JsonValue current = source; + + for (final var op : merges) { + switch (op) { + case TransformAst.MergeOp.MergeThis mt -> { + current = applyMergeValueToNode(current, mt.value(), isDocumentRoot); + if (!(current instanceof JsonObject)) { + return new Applied(current, true); + } + } + case TransformAst.MergeOp.ByPath byPath -> { + final var matches = byPath.path().queryMatches(current); + if (matches.isEmpty()) { + LOG.fine(() -> "Merge path produced no results: " + byPath.rawPath()); + break; + } + + for (final var match : matches) { + final var location = match.location(); + final var merged = applyMergeValueToMatched(match.value(), byPath.value(), isDocumentRoot && location.isEmpty()); + current = replaceAt(current, location, merged); + if (location.isEmpty() && !(current instanceof JsonObject)) { + return new Applied(current, true); + } + } + } + } + } + + return new Applied(current, false); + } + + private static JsonValue applyMergeValueToNode(JsonValue currentNode, TransformAst.MergeOp.Value mergeValue, boolean isDocumentRoot) { + return switch (mergeValue) { + case TransformAst.MergeOp.Value.Raw raw -> { + final var v = raw.value(); + if (isDocumentRoot && !(v instanceof JsonObject)) { + throw new JsonTransformException("cannot replace the document root with a non-object"); + } + yield v; + } + case TransformAst.MergeOp.Value.TransformObjectValue tov -> { + // Apply nested transforms (ProcessTransform) at this node. + if (!(currentNode instanceof JsonObject obj)) { + // Reference behavior is unclear; keep current unchanged. + yield currentNode; + } + final var applied = processTransform(obj, tov.compiled(), isDocumentRoot); + yield applied.value(); + } + }; + } + + private static JsonValue applyMergeValueToMatched(JsonValue matched, TransformAst.MergeOp.Value mergeValue, boolean isDocumentRootForMatch) { + return switch (mergeValue) { + case TransformAst.MergeOp.Value.Raw raw -> { + final var v = raw.value(); + if (matched instanceof JsonArray a && v instanceof JsonArray b) { + yield append(a, b); + } + if (isDocumentRootForMatch && !(v instanceof JsonObject) && matched instanceof JsonObject) { + // Mirror root restriction when replacing the true root object. + throw new JsonTransformException("cannot replace the document root with a non-object"); + } + yield v; + } + case TransformAst.MergeOp.Value.TransformObjectValue tov -> { + if (matched instanceof JsonObject obj) { + final var applied = processTransform(obj, tov.compiled(), isDocumentRootForMatch); + yield applied.value(); + } + yield matched; + } + }; + } + + private static JsonObject applyDefault(JsonObject source, Map nonVerbMembers, Set recursedKeys) { + JsonObject current = source; + + for (final var entry : nonVerbMembers.entrySet()) { + final var key = entry.getKey(); + final var tVal = entry.getValue(); + + if (recursedKeys.contains(key) && current.members().get(key) instanceof JsonObject && tVal instanceof JsonObject) { + continue; + } + + final var sVal = current.members().get(key); + if (sVal == null) { + final var out = new LinkedHashMap(current.members()); + out.put(key, tVal); + current = JsonObject.of(out); + continue; + } + + if (sVal instanceof JsonArray sArr && tVal instanceof JsonArray tArr) { + final var out = new LinkedHashMap(current.members()); + out.put(key, append(sArr, tArr)); + current = JsonObject.of(out); + continue; + } + + if (!(sVal instanceof JsonObject) || !(tVal instanceof JsonObject)) { + final var out = new LinkedHashMap(current.members()); + out.put(key, tVal); + current = JsonObject.of(out); + } + } + + return current; + } + + private static JsonObject applyRenames(JsonObject source, List renames, boolean isDocumentRoot) { + JsonObject current = source; + + for (final var op : renames) { + switch (op) { + case TransformAst.RenameOp.Mapping mapping -> { + for (final var entry : mapping.renames().entrySet()) { + final var oldName = entry.getKey(); + final var newName = entry.getValue(); + if (!current.members().containsKey(oldName)) { + LOG.fine(() -> "Rename mapping skipped; missing key: " + oldName); + continue; + } + current = renameKey(current, oldName, newName); + } + } + case TransformAst.RenameOp.ByPath byPath -> { + final var matches = byPath.path().queryMatches(current); + if (matches.isEmpty()) { + LOG.fine(() -> "Rename path produced no results: " + byPath.rawPath()); + break; + } + for (final var match : matches) { + final var loc = match.location(); + if (loc.isEmpty()) { + throw new JsonTransformException("cannot rename the root node"); + } + final var last = loc.getLast(); + if (!(last instanceof JsonPathLocationStep.Property p)) { + throw new JsonTransformException("cannot rename array elements"); + } + final var parentLoc = loc.subList(0, loc.size() - 1); + final var parent = resolve(current, parentLoc); + if (!(parent instanceof JsonObject parentObj)) { + throw new JsonTransformException("rename target is not an object member"); + } + final var updatedParent = renameKey(parentObj, p.name(), byPath.newName()); + current = (JsonObject) replaceAt(current, parentLoc, updatedParent); + } + } + } + } + + return current; + } + + private static JsonValue resolve(JsonValue root, List location) { + JsonValue cur = root; + for (final var step : location) { + cur = switch (step) { + case JsonPathLocationStep.Property p -> cur instanceof JsonObject obj ? obj.members().get(p.name()) : null; + case JsonPathLocationStep.Index idx -> cur instanceof JsonArray arr ? arr.elements().get(idx.index()) : null; + }; + if (cur == null) { + return null; + } + } + return cur; + } + + private static List sortForRemoval(List matches) { + final var sorted = new ArrayList(matches); + sorted.sort((a, b) -> compareRemovalLocations(b.location(), a.location())); // descending + return sorted; + } + + private static int compareRemovalLocations(List a, List b) { + final int min = Math.min(a.size(), b.size()); + for (int i = 0; i < min; i++) { + final var sa = a.get(i); + final var sb = b.get(i); + if (!sa.equals(sb)) { + // Only attempt special ordering for array indices at same depth + if (sa instanceof JsonPathLocationStep.Index ia && sb instanceof JsonPathLocationStep.Index ib) { + return Integer.compare(ia.index(), ib.index()); + } + // Otherwise keep stable, but reversed comparator already applied at caller + return 0; + } + } + return Integer.compare(a.size(), b.size()); + } +} + diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java b/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java new file mode 100644 index 0000000..2e46e4b --- /dev/null +++ b/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java @@ -0,0 +1,36 @@ +package json.java21.transforms; + +import java.util.Objects; + +sealed interface TransformSyntax permits TransformSyntax.Nothing { + enum Nothing implements TransformSyntax { INSTANCE } + + String SYNTAX_PREFIX = "@jdt."; + + String VERB_REMOVE = "@jdt.remove"; + String VERB_REPLACE = "@jdt.replace"; + String VERB_MERGE = "@jdt.merge"; + String VERB_RENAME = "@jdt.rename"; + + String ATTR_PATH = "@jdt.path"; + String ATTR_VALUE = "@jdt.value"; + + static boolean isSyntaxKey(String key) { + return key != null && key.startsWith(SYNTAX_PREFIX); + } + + static String syntaxSuffixOrNull(String key) { + if (!isSyntaxKey(key)) return null; + return key.substring(SYNTAX_PREFIX.length()); + } + + static String normalizePathString(String path) { + Objects.requireNonNull(path, "path must not be null"); + final var trimmed = path.trim(); + if (trimmed.startsWith("@")) { + return "$" + trimmed.substring(1); + } + return trimmed; + } +} + From 32f60d901fc1b5039167c3892017afd2f14146bd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:22:12 +0000 Subject: [PATCH 3/6] Issue #130 Fix json-transforms compilation visibility and warnings Co-authored-by: simbo1905 --- .../transforms/JsonTransformException.java | 2 ++ .../json/java21/transforms/TransformAst.java | 22 +++++++++---------- .../java21/transforms/TransformCompiler.java | 5 +++-- .../java21/transforms/TransformPatch.java | 5 +++-- .../java21/transforms/TransformRunner.java | 7 +++--- .../java21/transforms/TransformSyntax.java | 19 ++++++++-------- 6 files changed, 33 insertions(+), 27 deletions(-) diff --git a/json-transforms/src/main/java/json/java21/transforms/JsonTransformException.java b/json-transforms/src/main/java/json/java21/transforms/JsonTransformException.java index 094bff9..46b2dc0 100644 --- a/json-transforms/src/main/java/json/java21/transforms/JsonTransformException.java +++ b/json-transforms/src/main/java/json/java21/transforms/JsonTransformException.java @@ -2,6 +2,8 @@ /// Exception thrown for invalid transform syntax or runtime failures applying a transform. public final class JsonTransformException extends RuntimeException { + private static final long serialVersionUID = 1L; + public JsonTransformException(String message) { super(message); } diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformAst.java b/json-transforms/src/main/java/json/java21/transforms/TransformAst.java index 39aa2b7..2ac9966 100644 --- a/json-transforms/src/main/java/json/java21/transforms/TransformAst.java +++ b/json-transforms/src/main/java/json/java21/transforms/TransformAst.java @@ -18,7 +18,7 @@ record ObjectTransform( List merges, List renames ) implements TransformAst { - ObjectTransform { + public ObjectTransform { Objects.requireNonNull(nonVerbMembers, "nonVerbMembers must not be null"); Objects.requireNonNull(childObjects, "childObjects must not be null"); Objects.requireNonNull(removes, "removes must not be null"); @@ -36,7 +36,7 @@ record ObjectTransform( sealed interface RemoveOp extends TransformAst permits RemoveOp.ByName, RemoveOp.RemoveThis, RemoveOp.ByPath { record ByName(String name) implements RemoveOp { - ByName { + public ByName { Objects.requireNonNull(name, "name must not be null"); } } @@ -44,7 +44,7 @@ record ByName(String name) implements RemoveOp { record RemoveThis() implements RemoveOp {} record ByPath(String rawPath, JsonPath path) implements RemoveOp { - ByPath { + public ByPath { Objects.requireNonNull(rawPath, "rawPath must not be null"); Objects.requireNonNull(path, "path must not be null"); } @@ -53,13 +53,13 @@ record ByPath(String rawPath, JsonPath path) implements RemoveOp { sealed interface ReplaceOp extends TransformAst permits ReplaceOp.ReplaceThis, ReplaceOp.ByPath { record ReplaceThis(JsonValue value) implements ReplaceOp { - ReplaceThis { + public ReplaceThis { Objects.requireNonNull(value, "value must not be null"); } } record ByPath(String rawPath, JsonPath path, JsonValue value) implements ReplaceOp { - ByPath { + public ByPath { Objects.requireNonNull(rawPath, "rawPath must not be null"); Objects.requireNonNull(path, "path must not be null"); Objects.requireNonNull(value, "value must not be null"); @@ -70,13 +70,13 @@ record ByPath(String rawPath, JsonPath path, JsonValue value) implements Replace sealed interface MergeOp extends TransformAst permits MergeOp.MergeThis, MergeOp.ByPath { record MergeThis(Value value) implements MergeOp { - MergeThis { + public MergeThis { Objects.requireNonNull(value, "value must not be null"); } } record ByPath(String rawPath, JsonPath path, Value value) implements MergeOp { - ByPath { + public ByPath { Objects.requireNonNull(rawPath, "rawPath must not be null"); Objects.requireNonNull(path, "path must not be null"); Objects.requireNonNull(value, "value must not be null"); @@ -85,13 +85,13 @@ record ByPath(String rawPath, JsonPath path, Value value) implements MergeOp { sealed interface Value permits Value.Raw, Value.TransformObjectValue { record Raw(JsonValue value) implements Value { - Raw { + public Raw { Objects.requireNonNull(value, "value must not be null"); } } record TransformObjectValue(JsonObject rawObject, ObjectTransform compiled) implements Value { - TransformObjectValue { + public TransformObjectValue { Objects.requireNonNull(rawObject, "rawObject must not be null"); Objects.requireNonNull(compiled, "compiled must not be null"); } @@ -101,14 +101,14 @@ record TransformObjectValue(JsonObject rawObject, ObjectTransform compiled) impl sealed interface RenameOp extends TransformAst permits RenameOp.Mapping, RenameOp.ByPath { record Mapping(Map renames) implements RenameOp { - Mapping { + public Mapping { Objects.requireNonNull(renames, "renames must not be null"); renames = Map.copyOf(renames); } } record ByPath(String rawPath, JsonPath path, String newName) implements RenameOp { - ByPath { + public ByPath { Objects.requireNonNull(rawPath, "rawPath must not be null"); Objects.requireNonNull(path, "path must not be null"); Objects.requireNonNull(newName, "newName must not be null"); diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformCompiler.java b/json-transforms/src/main/java/json/java21/transforms/TransformCompiler.java index 4787179..040e8ed 100644 --- a/json-transforms/src/main/java/json/java21/transforms/TransformCompiler.java +++ b/json-transforms/src/main/java/json/java21/transforms/TransformCompiler.java @@ -12,8 +12,9 @@ import static json.java21.transforms.TransformSyntax.*; -sealed interface TransformCompiler permits TransformCompiler.Nothing { - enum Nothing implements TransformCompiler { INSTANCE } +final class TransformCompiler { + + private TransformCompiler() {} static TransformAst.ObjectTransform compileObject(JsonObject transformObject) { Objects.requireNonNull(transformObject, "transformObject must not be null"); diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformPatch.java b/json-transforms/src/main/java/json/java21/transforms/TransformPatch.java index eb1946e..f851d2f 100644 --- a/json-transforms/src/main/java/json/java21/transforms/TransformPatch.java +++ b/json-transforms/src/main/java/json/java21/transforms/TransformPatch.java @@ -8,8 +8,9 @@ import java.util.List; import java.util.Objects; -sealed interface TransformPatch permits TransformPatch.Nothing { - enum Nothing implements TransformPatch { INSTANCE } +final class TransformPatch { + + private TransformPatch() {} static JsonValue removeAt(JsonValue root, List location) { Objects.requireNonNull(root, "root must not be null"); diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java b/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java index abcfb30..5bf5355 100644 --- a/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java +++ b/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java @@ -9,10 +9,11 @@ import static json.java21.transforms.TransformPatch.*; -sealed interface TransformRunner permits TransformRunner.Nothing { - enum Nothing implements TransformRunner { INSTANCE } +final class TransformRunner { - Logger LOG = Logger.getLogger(TransformRunner.class.getName()); + private static final Logger LOG = Logger.getLogger(TransformRunner.class.getName()); + + private TransformRunner() {} static JsonValue applyAtDocumentRoot(JsonObject source, TransformAst.ObjectTransform transform) { Objects.requireNonNull(source, "source must not be null"); diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java b/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java index 2e46e4b..a65225d 100644 --- a/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java +++ b/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java @@ -2,18 +2,19 @@ import java.util.Objects; -sealed interface TransformSyntax permits TransformSyntax.Nothing { - enum Nothing implements TransformSyntax { INSTANCE } +final class TransformSyntax { - String SYNTAX_PREFIX = "@jdt."; + private TransformSyntax() {} - String VERB_REMOVE = "@jdt.remove"; - String VERB_REPLACE = "@jdt.replace"; - String VERB_MERGE = "@jdt.merge"; - String VERB_RENAME = "@jdt.rename"; + static final String SYNTAX_PREFIX = "@jdt."; - String ATTR_PATH = "@jdt.path"; - String ATTR_VALUE = "@jdt.value"; + static final String VERB_REMOVE = "@jdt.remove"; + static final String VERB_REPLACE = "@jdt.replace"; + static final String VERB_MERGE = "@jdt.merge"; + static final String VERB_RENAME = "@jdt.rename"; + + static final String ATTR_PATH = "@jdt.path"; + static final String ATTR_VALUE = "@jdt.value"; static boolean isSyntaxKey(String key) { return key != null && key.startsWith(SYNTAX_PREFIX); From f475894ac9f924181388f294631d98db92d3cbc3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:25:34 +0000 Subject: [PATCH 4/6] Issue #130 Add json-transforms golden tests and ported fixtures Co-authored-by: simbo1905 --- .../JsonTransformGoldenFilesTest.java | 78 ++++++++++ .../JsonTransformValidationTest.java | 136 ++++++++++++++++++ .../JsonTransformsLoggingConfig.java | 53 +++++++ .../Inputs/Default/Array.Merge.Expected.json | 31 ++++ .../Inputs/Default/Array.Merge.Transform.json | 21 +++ .../Default/Array.Replace.Expected.json | 15 ++ .../Default/Array.Replace.Transform.json | 9 ++ .../Inputs/Default/Array.Source.json | 17 +++ .../Inputs/Default/Object.Merge.Expected.json | 24 ++++ .../Default/Object.Merge.Transform.json | 21 +++ .../Default/Object.Replace.Expected.json | 8 ++ .../Default/Object.Replace.Transform.json | 7 + .../Inputs/Default/Object.Source.json | 14 ++ .../Default/Primitive.AddArray.Expected.json | 20 +++ .../Default/Primitive.AddArray.Transform.json | 16 +++ .../Default/Primitive.AddObject.Expected.json | 22 +++ .../Primitive.AddObject.Transform.json | 18 +++ .../Primitive.AddPrimitive.Expected.json | 10 ++ .../Primitive.AddPrimitive.Transform.json | 6 + .../Default/Primitive.Replace.Expected.json | 6 + .../Default/Primitive.Replace.Transform.json | 5 + .../Inputs/Default/Primitive.Source.json | 6 + .../Merge/Object.MergeObjects.Expected.json | 15 ++ .../Merge/Object.MergeObjects.Transform.json | 26 ++++ .../resources/Inputs/Merge/Object.Source.json | 13 ++ .../Object.UsingDirectPath.Expected.json | 17 +++ .../Object.UsingDirectPath.Transform.json | 18 +++ .../Primitive.MergeObjects.Expected.json | 11 ++ .../Primitive.MergeObjects.Transform.json | 11 ++ .../Primitive.MergePrimitives.Expected.json | 8 ++ .../Primitive.MergePrimitives.Transform.json | 7 + .../Inputs/Merge/Primitive.Source.json | 6 + .../Primitive.UsingSimplePath.Expected.json | 6 + .../Primitive.UsingSimplePath.Transform.json | 6 + .../Remove/Array.DirectPath.Expected.json | 13 ++ .../Remove/Array.DirectPath.Transform.json | 5 + .../Remove/Array.ScriptPath.Expected.json | 12 ++ .../Remove/Array.ScriptPath.Transform.json | 5 + .../resources/Inputs/Remove/Array.Source.json | 14 ++ .../Remove/Object.DirectPath.Expected.json | 10 ++ .../Remove/Object.DirectPath.Transform.json | 10 ++ .../Remove/Object.InObject.Expected.json | 8 ++ .../Remove/Object.InObject.Transform.json | 12 ++ .../Remove/Object.PathToItself.Expected.json | 13 ++ .../Remove/Object.PathToItself.Transform.json | 7 + .../Inputs/Remove/Object.Source.json | 14 ++ .../Remove/Object.WithBool.Expected.json | 11 ++ .../Remove/Object.WithBool.Transform.json | 14 ++ .../Remove/Primitive.FromRoot.Expected.json | 5 + .../Remove/Primitive.FromRoot.Transform.json | 3 + .../Inputs/Remove/Primitive.Source.json | 6 + .../Primitive.UsingPathArray.Expected.json | 4 + .../Primitive.UsingPathArray.Transform.json | 10 ++ .../Primitive.UsingSimpleArray.Expected.json | 4 + .../Primitive.UsingSimpleArray.Transform.json | 3 + .../Primitive.UsingSimplePath.Expected.json | 5 + .../Primitive.UsingSimplePath.Transform.json | 5 + .../Rename/Array.DirectPath.Expected.json | 14 ++ .../Rename/Array.DirectPath.Transform.json | 6 + .../Rename/Array.ScriptPath.Expected.json | 14 ++ .../resources/Inputs/Rename/Array.Source.json | 14 ++ .../Rename/Object.InObject.Expected.json | 14 ++ .../Rename/Object.InObject.Transform.json | 13 ++ .../Inputs/Rename/Object.Source.json | 14 ++ .../Object.UsingSimplePath.Expected.json | 14 ++ .../Object.UsingSimplePath.Transform.json | 6 + .../Object.WithChangingNames.Expected.json | 14 ++ .../Object.WithChangingNames.Transform.json | 12 ++ .../Rename/Primitive.FromRoot.Expected.json | 6 + .../Rename/Primitive.FromRoot.Transform.json | 6 + .../Inputs/Rename/Primitive.Source.json | 6 + .../Primitive.UsingSimpleArray.Expected.json | 6 + .../Primitive.UsingSimpleArray.Transform.json | 10 ++ .../Primitive.UsingSimplePath.Expected.json | 6 + .../Primitive.UsingSimplePath.Transform.json | 6 + .../Replace/Array.DirectPath.Expected.json | 14 ++ .../Replace/Array.DirectPath.Transform.json | 15 ++ .../Replace/Array.ScriptPath.Expected.json | 14 ++ .../Replace/Array.ScriptPath.Transform.json | 6 + .../Inputs/Replace/Array.Source.json | 14 ++ .../Inputs/Replace/Object.Source.json | 14 ++ .../Replace/Object.WithArray.Expected.json | 11 ++ .../Replace/Object.WithArray.Transform.json | 16 +++ .../Replace/Object.WithObject.Expected.json | 16 +++ .../Replace/Object.WithObject.Transform.json | 22 +++ .../Object.WithPrimitive.Expected.json | 8 ++ .../Object.WithPrimitive.Transform.json | 15 ++ 87 files changed, 1226 insertions(+) create mode 100644 json-transforms/src/test/java/json/java21/transforms/JsonTransformGoldenFilesTest.java create mode 100644 json-transforms/src/test/java/json/java21/transforms/JsonTransformValidationTest.java create mode 100644 json-transforms/src/test/java/json/java21/transforms/JsonTransformsLoggingConfig.java create mode 100644 json-transforms/src/test/resources/Inputs/Default/Array.Merge.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Array.Merge.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Array.Replace.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Array.Replace.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Array.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Object.Merge.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Object.Merge.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Object.Replace.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Object.Replace.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Object.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Primitive.AddArray.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Primitive.AddArray.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Primitive.AddObject.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Primitive.AddObject.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Primitive.AddPrimitive.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Primitive.AddPrimitive.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Primitive.Replace.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Primitive.Replace.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Default/Primitive.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Object.MergeObjects.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Object.MergeObjects.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Object.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Object.UsingDirectPath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Object.UsingDirectPath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Primitive.MergeObjects.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Primitive.MergeObjects.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Primitive.MergePrimitives.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Primitive.MergePrimitives.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Primitive.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Primitive.UsingSimplePath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Merge/Primitive.UsingSimplePath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Array.DirectPath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Array.DirectPath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Array.ScriptPath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Array.ScriptPath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Array.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Object.DirectPath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Object.DirectPath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Object.InObject.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Object.InObject.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Object.PathToItself.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Object.PathToItself.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Object.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Object.WithBool.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Object.WithBool.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Primitive.FromRoot.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Primitive.FromRoot.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Primitive.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingPathArray.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingPathArray.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimpleArray.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimpleArray.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimplePath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimplePath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Array.DirectPath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Array.DirectPath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Array.ScriptPath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Array.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Object.InObject.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Object.InObject.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Object.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Object.UsingSimplePath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Object.UsingSimplePath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Object.WithChangingNames.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Object.WithChangingNames.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Primitive.FromRoot.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Primitive.FromRoot.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Primitive.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimpleArray.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimpleArray.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimplePath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimplePath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Array.DirectPath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Array.DirectPath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Array.ScriptPath.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Array.ScriptPath.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Array.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Object.Source.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Object.WithArray.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Object.WithArray.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Object.WithObject.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Object.WithObject.Transform.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Object.WithPrimitive.Expected.json create mode 100644 json-transforms/src/test/resources/Inputs/Replace/Object.WithPrimitive.Transform.json diff --git a/json-transforms/src/test/java/json/java21/transforms/JsonTransformGoldenFilesTest.java b/json-transforms/src/test/java/json/java21/transforms/JsonTransformGoldenFilesTest.java new file mode 100644 index 0000000..ddec4cf --- /dev/null +++ b/json-transforms/src/test/java/json/java21/transforms/JsonTransformGoldenFilesTest.java @@ -0,0 +1,78 @@ +package json.java21.transforms; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonValue; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class JsonTransformGoldenFilesTest extends JsonTransformsLoggingConfig { + + private static final Logger LOG = Logger.getLogger(JsonTransformGoldenFilesTest.class.getName()); + + @ParameterizedTest(name = "{0} - {1}") + @MethodSource("inputs") + void goldenFiles(String category, String testName) throws IOException { + LOG.info(() -> "TEST: goldenFiles category=" + category + " testName=" + testName); + + final var base = Path.of(System.getProperty("jsontransforms.test.resources")); + final var dir = base.resolve("Inputs").resolve(category); + + final var sourceName = testName.substring(0, testName.lastIndexOf('.')); + final var sourcePath = dir.resolve(sourceName + ".Source.json"); + final var transformPath = dir.resolve(testName + ".Transform.json"); + final var expectedPath = dir.resolve(testName + ".Expected.json"); + + final JsonValue source = parseFile(sourcePath); + final JsonValue transform = parseFile(transformPath); + final JsonValue expected = parseFile(expectedPath); + + final var program = JsonTransform.parse(transform); + final var actual = program.run(source); + + assertThat(actual).isEqualTo(expected); + } + + static Stream inputs() throws IOException { + final var base = Path.of(System.getProperty("jsontransforms.test.resources")); + final var inputsBase = base.resolve("Inputs"); + final List categories = List.of("Default", "Remove", "Rename", "Replace", "Merge"); + + Stream all = Stream.empty(); + for (final var category : categories) { + final var dir = inputsBase.resolve(category); + try (var stream = Files.list(dir)) { + final var args = stream + .filter(p -> p.getFileName().toString().endsWith(".Transform.json")) + .map(p -> p.getFileName().toString()) + .map(name -> name.substring(0, name.length() - ".Transform.json".length())) + .sorted() + .map(testName -> Arguments.of(category, testName)); + all = Stream.concat(all, args); + } + } + return all; + } + + private static JsonValue parseFile(Path path) throws IOException { + final var text = Files.readString(path); + return Json.parse(stripBom(text)); + } + + private static String stripBom(String s) { + if (s != null && !s.isEmpty() && s.charAt(0) == '\uFEFF') { + return s.substring(1); + } + return s; + } +} + diff --git a/json-transforms/src/test/java/json/java21/transforms/JsonTransformValidationTest.java b/json-transforms/src/test/java/json/java21/transforms/JsonTransformValidationTest.java new file mode 100644 index 0000000..5297016 --- /dev/null +++ b/json-transforms/src/test/java/json/java21/transforms/JsonTransformValidationTest.java @@ -0,0 +1,136 @@ +package json.java21.transforms; + +import jdk.sandbox.java.util.json.Json; +import org.junit.jupiter.api.Test; + +import java.util.logging.Logger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public final class JsonTransformValidationTest extends JsonTransformsLoggingConfig { + + private static final Logger LOG = Logger.getLogger(JsonTransformValidationTest.class.getName()); + + @Test + void invalidVerb() { + LOG.info(() -> "TEST: invalidVerb"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.invalid\":false}"); + + assertThatThrownBy(() -> JsonTransform.parse(transform).run(source)) + .isInstanceOf(JsonTransformException.class); + } + + @Test + void invalidVerbValue() { + LOG.info(() -> "TEST: invalidVerbValue"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.remove\":10}"); + + assertThatThrownBy(() -> JsonTransform.parse(transform).run(source)) + .isInstanceOf(JsonTransformException.class); + } + + @Test + void invalidAttribute() { + LOG.info(() -> "TEST: invalidAttribute"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.replace\": {\"@jdt.invalid\": false}}"); + + assertThatThrownBy(() -> JsonTransform.parse(transform).run(source)) + .isInstanceOf(JsonTransformException.class); + } + + @Test + void missingAttribute() { + LOG.info(() -> "TEST: missingAttribute"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.rename\": {\"@jdt.path\": \"$.A\"}}"); + + assertThatThrownBy(() -> JsonTransform.parse(transform).run(source)) + .isInstanceOf(JsonTransformException.class); + } + + @Test + void mixedAttributes() { + LOG.info(() -> "TEST: mixedAttributes"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.rename\": {\"@jdt.path\": \"$.A\", \"@jdt.value\": \"Astar\", \"NotAttribute\": true}}"); + + assertThatThrownBy(() -> JsonTransform.parse(transform).run(source)) + .isInstanceOf(JsonTransformException.class); + } + + @Test + void wrongAttributeValueType() { + LOG.info(() -> "TEST: wrongAttributeValueType"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.remove\": {\"@jdt.path\": false}}"); + + assertThatThrownBy(() -> JsonTransform.parse(transform).run(source)) + .isInstanceOf(JsonTransformException.class); + } + + @Test + void removeNonExistentNodeByPathIsNoOp() { + LOG.info(() -> "TEST: removeNonExistentNodeByPathIsNoOp"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.remove\": {\"@jdt.path\": \"$.B\"}}"); + + var result = JsonTransform.parse(transform).run(source); + assertThat(result).isEqualTo(source); + } + + @Test + void removeRootThrows() { + LOG.info(() -> "TEST: removeRootThrows"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.remove\": true}"); + + assertThatThrownBy(() -> JsonTransform.parse(transform).run(source)) + .isInstanceOf(JsonTransformException.class); + } + + @Test + void invalidRenameMappingValue() { + LOG.info(() -> "TEST: invalidRenameMappingValue"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.rename\": {\"A\": 10}}"); + + assertThatThrownBy(() -> JsonTransform.parse(transform).run(source)) + .isInstanceOf(JsonTransformException.class); + } + + @Test + void renameNonExistentNodeIsNoOp() { + LOG.info(() -> "TEST: renameNonExistentNodeIsNoOp"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.rename\": {\"B\": \"Bstar\"}}"); + + var result = JsonTransform.parse(transform).run(source); + assertThat(result).isEqualTo(source); + } + + @Test + void replaceRootWithPrimitiveThrows() { + LOG.info(() -> "TEST: replaceRootWithPrimitiveThrows"); + + var source = Json.parse("{\"A\":1}"); + var transform = Json.parse("{\"@jdt.replace\": 10}"); + + assertThatThrownBy(() -> JsonTransform.parse(transform).run(source)) + .isInstanceOf(JsonTransformException.class); + } +} + diff --git a/json-transforms/src/test/java/json/java21/transforms/JsonTransformsLoggingConfig.java b/json-transforms/src/test/java/json/java21/transforms/JsonTransformsLoggingConfig.java new file mode 100644 index 0000000..e4a5df0 --- /dev/null +++ b/json-transforms/src/test/java/json/java21/transforms/JsonTransformsLoggingConfig.java @@ -0,0 +1,53 @@ +package json.java21.transforms; + +import org.junit.jupiter.api.BeforeAll; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +/// Base class for json-transforms tests that configures JUL logging from system properties. +/// All test classes should extend this class to enable consistent logging behavior. +public class JsonTransformsLoggingConfig { + + @BeforeAll + static void enableJulDebug() { + final var log = Logger.getLogger(JsonTransformsLoggingConfig.class.getName()); + final var root = Logger.getLogger(""); + final var levelProp = System.getProperty("java.util.logging.ConsoleHandler.level"); + + Level targetLevel = Level.INFO; + if (levelProp != null) { + try { + targetLevel = Level.parse(levelProp.trim()); + } catch (IllegalArgumentException ex) { + try { + targetLevel = Level.parse(levelProp.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + log.warning(() -> "Unrecognized logging level from 'java.util.logging.ConsoleHandler.level': " + levelProp); + } + } + } + + if (root.getLevel() == null || root.getLevel().intValue() > targetLevel.intValue()) { + root.setLevel(targetLevel); + } + for (Handler handler : root.getHandlers()) { + final var handlerLevel = handler.getLevel(); + if (handlerLevel == null || handlerLevel.intValue() > targetLevel.intValue()) { + handler.setLevel(targetLevel); + } + } + + final var prop = System.getProperty("jsontransforms.test.resources"); + if (prop == null || prop.isBlank()) { + Path base = Paths.get("src", "test", "resources").toAbsolutePath(); + System.setProperty("jsontransforms.test.resources", base.toString()); + log.config(() -> "jsontransforms.test.resources set to " + base); + } + } +} + diff --git a/json-transforms/src/test/resources/Inputs/Default/Array.Merge.Expected.json b/json-transforms/src/test/resources/Inputs/Default/Array.Merge.Expected.json new file mode 100644 index 0000000..2fa7296 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Array.Merge.Expected.json @@ -0,0 +1,31 @@ +{ + "A": [ + { "Added": true } + ], + "B": [ + { + "Name": "B1", + "Value": 1 + }, + { + "Name": "B2", + "Value": 2 + }, + { + "Name": "B2", + "Value": 3 + } + ], + "C": [ + {}, + { "Empty": false }, + { "Array": [ { "Inception": true } ] }, + { "Empty": false } + ], + "D": { + "D1": [ + { "Value": 1 }, + { "Value": 0 } + ] + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Array.Merge.Transform.json b/json-transforms/src/test/resources/Inputs/Default/Array.Merge.Transform.json new file mode 100644 index 0000000..410c63d --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Array.Merge.Transform.json @@ -0,0 +1,21 @@ +{ + "A": [ + { "Added": true } + ], + "B": [ + { + "Name": "B2", + "Value": 2 + }, + { + "Name": "B2", + "Value": 3 + } + ], + "C": [ + { "Empty": false } + ], + "D": { + "D1": [ { "Value": 0 } ] + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Array.Replace.Expected.json b/json-transforms/src/test/resources/Inputs/Default/Array.Replace.Expected.json new file mode 100644 index 0000000..ca01b5f --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Array.Replace.Expected.json @@ -0,0 +1,15 @@ +{ + "A": { + "Replaced": true + }, + "B": [ + { + "Name": "B1", + "Value": 1 + } + ], + "C": "Replaced", + "D": { + "D1": null + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Array.Replace.Transform.json b/json-transforms/src/test/resources/Inputs/Default/Array.Replace.Transform.json new file mode 100644 index 0000000..1e94018 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Array.Replace.Transform.json @@ -0,0 +1,9 @@ +{ + "A": { + "Replaced": true + }, + "C": "Replaced", + "D": { + "D1": null + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Array.Source.json b/json-transforms/src/test/resources/Inputs/Default/Array.Source.json new file mode 100644 index 0000000..209b6a2 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Array.Source.json @@ -0,0 +1,17 @@ +{ + "A": [], + "B": [ + { + "Name": "B1", + "Value": 1 + } + ], + "C": [ + {}, + { "Empty": false }, + { "Array": [ { "Inception": true } ] } + ], + "D": { + "D1": [ { "Value": 1 } ] + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Object.Merge.Expected.json b/json-transforms/src/test/resources/Inputs/Default/Object.Merge.Expected.json new file mode 100644 index 0000000..bf7128a --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Object.Merge.Expected.json @@ -0,0 +1,24 @@ +{ + "A": { + "A1": "New" + }, + "B": { + "B1": 10, + "B2": 2, + "B3": 30 + }, + "C": { + "C1": { + "C11": true, + "C12": false, + "C13": [ + { "Name": "C131" }, + { "Name": "C132" } + ] + }, + "C2": 2, + "C3": { + "Added": true + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Object.Merge.Transform.json b/json-transforms/src/test/resources/Inputs/Default/Object.Merge.Transform.json new file mode 100644 index 0000000..9608428 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Object.Merge.Transform.json @@ -0,0 +1,21 @@ +{ + "A": { + "A1": "New" + }, + "B": { + "B1": 10, + "B3": 30 + }, + "C": { + "C1": { + "C12": false, + "C13": [ + { "Name": "C131" }, + { "Name": "C132" } + ] + }, + "C3": { + "Added": true + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Object.Replace.Expected.json b/json-transforms/src/test/resources/Inputs/Default/Object.Replace.Expected.json new file mode 100644 index 0000000..b7342f8 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Object.Replace.Expected.json @@ -0,0 +1,8 @@ +{ + "A": null, + "B": "Replaced", + "C": { + "C1": [ { "C11": true } ], + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Object.Replace.Transform.json b/json-transforms/src/test/resources/Inputs/Default/Object.Replace.Transform.json new file mode 100644 index 0000000..6c17b1a --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Object.Replace.Transform.json @@ -0,0 +1,7 @@ +{ + "A": null, + "B": "Replaced", + "C": { + "C1": [ {"C11": true} ] + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Object.Source.json b/json-transforms/src/test/resources/Inputs/Default/Object.Source.json new file mode 100644 index 0000000..a24f2bc --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Object.Source.json @@ -0,0 +1,14 @@ +{ + "A": { + }, + "B": { + "B1": 1, + "B2": 2 + }, + "C": { + "C1": { + "C11": true + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Primitive.AddArray.Expected.json b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddArray.Expected.json new file mode 100644 index 0000000..265ad62 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddArray.Expected.json @@ -0,0 +1,20 @@ +{ + "A": 1, + "B": true, + "C": null, + "D": "4", + "E": [ + { + "String": "string", + "Primitive": null + }, + { "Object": { "Value": 10 } }, + { "Array": [ { "Inception": true } ] }, + {} + ], + "F": [ + { "Repeat": true }, + { "Repeat": true } + ], + "G": [] +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Primitive.AddArray.Transform.json b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddArray.Transform.json new file mode 100644 index 0000000..1be59bd --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddArray.Transform.json @@ -0,0 +1,16 @@ +{ + "E": [ + { + "String": "string", + "Primitive": null + }, + { "Object": { "Value": 10 } }, + { "Array": [ {"Inception": true} ] }, + {} + ], + "F": [ + { "Repeat": true }, + { "Repeat": true } + ], + "G": [] +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Primitive.AddObject.Expected.json b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddObject.Expected.json new file mode 100644 index 0000000..349d6a2 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddObject.Expected.json @@ -0,0 +1,22 @@ +{ + "A": 1, + "B": true, + "C": null, + "D": "4", + "E": { + }, + "F": { + "F1": 1, + "F2": true, + "F3": "F3" + }, + "G": { + "G1": { + }, + "G2": null, + "G3": { + "G31": 10, + "G32": null + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Primitive.AddObject.Transform.json b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddObject.Transform.json new file mode 100644 index 0000000..f28b434 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddObject.Transform.json @@ -0,0 +1,18 @@ +{ + "E": { + }, + "F": { + "F1": 1, + "F2": true, + "F3": "F3" + }, + "G": { + "G1": { + }, + "G2": null, + "G3": { + "G31": 10, + "G32": null + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Primitive.AddPrimitive.Expected.json b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddPrimitive.Expected.json new file mode 100644 index 0000000..940c2e1 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddPrimitive.Expected.json @@ -0,0 +1,10 @@ +{ + "A": 1, + "B": true, + "C": null, + "D": "4", + "E": true, + "F": 6, + "G": null, + "H": "8" +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Primitive.AddPrimitive.Transform.json b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddPrimitive.Transform.json new file mode 100644 index 0000000..7054c33 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Primitive.AddPrimitive.Transform.json @@ -0,0 +1,6 @@ +{ + "E": true, + "F": 6, + "G": null, + "H": "8" +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Primitive.Replace.Expected.json b/json-transforms/src/test/resources/Inputs/Default/Primitive.Replace.Expected.json new file mode 100644 index 0000000..0200f99 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Primitive.Replace.Expected.json @@ -0,0 +1,6 @@ +{ + "A": 10, + "B": null, + "C": "Replaced", + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Primitive.Replace.Transform.json b/json-transforms/src/test/resources/Inputs/Default/Primitive.Replace.Transform.json new file mode 100644 index 0000000..b5d8f8d --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Primitive.Replace.Transform.json @@ -0,0 +1,5 @@ +{ + "A": 10, + "B": null, + "C": "Replaced" +} diff --git a/json-transforms/src/test/resources/Inputs/Default/Primitive.Source.json b/json-transforms/src/test/resources/Inputs/Default/Primitive.Source.json new file mode 100644 index 0000000..051ecde --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Default/Primitive.Source.json @@ -0,0 +1,6 @@ +{ + "A": 1, + "B": true, + "C": null, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Object.MergeObjects.Expected.json b/json-transforms/src/test/resources/Inputs/Merge/Object.MergeObjects.Expected.json new file mode 100644 index 0000000..c398d2c --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Object.MergeObjects.Expected.json @@ -0,0 +1,15 @@ +{ + "A": 1, + "B": { + "B1": 1, + "B2": 20, + "B3": 3 + }, + "C": { + "C1": { + "C11": 1, + "C12": 2 + }, + "C2": null + } +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Object.MergeObjects.Transform.json b/json-transforms/src/test/resources/Inputs/Merge/Object.MergeObjects.Transform.json new file mode 100644 index 0000000..c5a1d6c --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Object.MergeObjects.Transform.json @@ -0,0 +1,26 @@ +{ + "A": { + "@jdt.merge": 1 + }, + "B": { + "@jdt.merge": { + "B2": 20, + "B3": 3 + } + }, + "C": { + "@jdt.merge": { + "C1": { + "C12": 2, + "C11": 1 + }, + "C2": null + }, + "C1": { + "@jdt.merge": { + "C12": false + }, + "C11": true + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Object.Source.json b/json-transforms/src/test/resources/Inputs/Merge/Object.Source.json new file mode 100644 index 0000000..0882672 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Object.Source.json @@ -0,0 +1,13 @@ +{ + "A": {}, + "B": { + "B1": 1, + "B2": 2 + }, + "C": { + "C1": { + "C11": true + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Object.UsingDirectPath.Expected.json b/json-transforms/src/test/resources/Inputs/Merge/Object.UsingDirectPath.Expected.json new file mode 100644 index 0000000..43dfafe --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Object.UsingDirectPath.Expected.json @@ -0,0 +1,17 @@ +{ + "A": {}, + "B": { + "B1": 1, + "B2": { + "B21": 1, + "B22": 2 + } + }, + "C": { + "C1": { + "C11": 1, + "C12": 2 + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Object.UsingDirectPath.Transform.json b/json-transforms/src/test/resources/Inputs/Merge/Object.UsingDirectPath.Transform.json new file mode 100644 index 0000000..512b43f --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Object.UsingDirectPath.Transform.json @@ -0,0 +1,18 @@ +{ + "@jdt.merge": { + "@jdt.path": "$.B.B2", + "@jdt.value": { + "B21": 1, + "B22": 2 + } + }, + "C": { + "@jdt.merge": { + "@jdt.path": "$.C1", + "@jdt.value": { + "C11": 1, + "C12": 2 + } + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergeObjects.Expected.json b/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergeObjects.Expected.json new file mode 100644 index 0000000..e9a1549 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergeObjects.Expected.json @@ -0,0 +1,11 @@ +{ + "A": { + "A1": 1 + }, + "B": true, + "C": {}, + "D": "4", + "E": { + "Added": true + } +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergeObjects.Transform.json b/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergeObjects.Transform.json new file mode 100644 index 0000000..2ab118c --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergeObjects.Transform.json @@ -0,0 +1,11 @@ +{ + "@jdt.merge": { + "A": { + "A1": 1 + }, + "C": {}, + "E": { + "Added": true + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergePrimitives.Expected.json b/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergePrimitives.Expected.json new file mode 100644 index 0000000..8930c38 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergePrimitives.Expected.json @@ -0,0 +1,8 @@ +{ + "A": 1, + "B": false, + "C": null, + "D": "4", + "E": "new", + "F": 10 +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergePrimitives.Transform.json b/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergePrimitives.Transform.json new file mode 100644 index 0000000..99cc948 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Primitive.MergePrimitives.Transform.json @@ -0,0 +1,7 @@ +{ + "@jdt.merge": { + "B": false, + "E": "new", + "F": 10 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Primitive.Source.json b/json-transforms/src/test/resources/Inputs/Merge/Primitive.Source.json new file mode 100644 index 0000000..051ecde --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Primitive.Source.json @@ -0,0 +1,6 @@ +{ + "A": 1, + "B": true, + "C": null, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Primitive.UsingSimplePath.Expected.json b/json-transforms/src/test/resources/Inputs/Merge/Primitive.UsingSimplePath.Expected.json new file mode 100644 index 0000000..1c74337 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Primitive.UsingSimplePath.Expected.json @@ -0,0 +1,6 @@ +{ + "A": "One", + "B": true, + "C": null, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Merge/Primitive.UsingSimplePath.Transform.json b/json-transforms/src/test/resources/Inputs/Merge/Primitive.UsingSimplePath.Transform.json new file mode 100644 index 0000000..0345af5 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Merge/Primitive.UsingSimplePath.Transform.json @@ -0,0 +1,6 @@ +{ + "@jdt.merge": { + "@jdt.path": "$.A", + "@jdt.value": "One" + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Array.DirectPath.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Array.DirectPath.Expected.json new file mode 100644 index 0000000..ff3364e --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Array.DirectPath.Expected.json @@ -0,0 +1,13 @@ +{ + "A": [ + { "Index": 1 }, + { "Index": 3 } + ], + "B": [ + { "Remove": true }, + { "Remove": false }, + { "Remove": "WrongValue" }, + { "HasRemove": false }, + { "Remove": true } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Array.DirectPath.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Array.DirectPath.Transform.json new file mode 100644 index 0000000..3204ae6 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Array.DirectPath.Transform.json @@ -0,0 +1,5 @@ +{ + "@jdt.remove": { + "@jdt.path": "$.A[1]" + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Array.ScriptPath.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Array.ScriptPath.Expected.json new file mode 100644 index 0000000..38f19a7 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Array.ScriptPath.Expected.json @@ -0,0 +1,12 @@ +{ + "A": [ + { "Index": 1 }, + { "Index": 2 }, + { "Index": 3 } + ], + "B": [ + { "Remove": false }, + { "Remove": "WrongValue" }, + { "HasRemove": false } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Array.ScriptPath.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Array.ScriptPath.Transform.json new file mode 100644 index 0000000..15a7068 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Array.ScriptPath.Transform.json @@ -0,0 +1,5 @@ +{ + "@jdt.remove": { + "@jdt.path": "$.B[?(@.Remove == true)]" + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Array.Source.json b/json-transforms/src/test/resources/Inputs/Remove/Array.Source.json new file mode 100644 index 0000000..32a0f33 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Array.Source.json @@ -0,0 +1,14 @@ +{ + "A": [ + { "Index": 1 }, + { "Index": 2 }, + { "Index": 3 } + ], + "B": [ + { "Remove": true }, + { "Remove": false }, + { "Remove": "WrongValue" }, + { "HasRemove": false }, + { "Remove": true } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Object.DirectPath.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Object.DirectPath.Expected.json new file mode 100644 index 0000000..f7a9b94 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Object.DirectPath.Expected.json @@ -0,0 +1,10 @@ +{ + "A": { + }, + "B": { + "B1": 1 + }, + "C": { + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Object.DirectPath.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Object.DirectPath.Transform.json new file mode 100644 index 0000000..d87d0f7 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Object.DirectPath.Transform.json @@ -0,0 +1,10 @@ +{ + "@jdt.remove": { + "@jdt.path": "$.B.B2" + }, + "C": { + "@jdt.remove": { + "@jdt.path": "$.C1" + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Object.InObject.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Object.InObject.Expected.json new file mode 100644 index 0000000..92da521 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Object.InObject.Expected.json @@ -0,0 +1,8 @@ +{ + "B": { + "B2": 2 + }, + "C": { + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Object.InObject.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Object.InObject.Transform.json new file mode 100644 index 0000000..62b813f --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Object.InObject.Transform.json @@ -0,0 +1,12 @@ +{ + "@jdt.remove": "A", + "B": { + "@jdt.remove": "B1" + }, + "C": { + "@jdt.remove": "C1", + "C1": { + "@jdt.remove": "C11" + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Object.PathToItself.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Object.PathToItself.Expected.json new file mode 100644 index 0000000..d524182 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Object.PathToItself.Expected.json @@ -0,0 +1,13 @@ +{ + "A": null, + "B": { + "B1": 1, + "B2": 2 + }, + "C": { + "C1": { + "C11": true + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Object.PathToItself.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Object.PathToItself.Transform.json new file mode 100644 index 0000000..276c605 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Object.PathToItself.Transform.json @@ -0,0 +1,7 @@ +{ + "A": { + "@jdt.remove": { + "@jdt.path": "$" + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Object.Source.json b/json-transforms/src/test/resources/Inputs/Remove/Object.Source.json new file mode 100644 index 0000000..a24f2bc --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Object.Source.json @@ -0,0 +1,14 @@ +{ + "A": { + }, + "B": { + "B1": 1, + "B2": 2 + }, + "C": { + "C1": { + "C11": true + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Object.WithBool.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Object.WithBool.Expected.json new file mode 100644 index 0000000..0e07ab1 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Object.WithBool.Expected.json @@ -0,0 +1,11 @@ +{ + "A": null, + "B": { + "B1": 1, + "B2": 2 + }, + "C": { + "C1": null, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Object.WithBool.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Object.WithBool.Transform.json new file mode 100644 index 0000000..25752b0 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Object.WithBool.Transform.json @@ -0,0 +1,14 @@ +{ + "A": { + "@jdt.remove": true + }, + "B": { + "@jdt.remove": false + }, + "C": { + "C1": { + "@jdt.remove": true, + "C12": 3 + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Primitive.FromRoot.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Primitive.FromRoot.Expected.json new file mode 100644 index 0000000..d65042c --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Primitive.FromRoot.Expected.json @@ -0,0 +1,5 @@ +{ + "B": true, + "C": null, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Primitive.FromRoot.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Primitive.FromRoot.Transform.json new file mode 100644 index 0000000..d275318 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Primitive.FromRoot.Transform.json @@ -0,0 +1,3 @@ +{ + "@jdt.remove": "A" +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Primitive.Source.json b/json-transforms/src/test/resources/Inputs/Remove/Primitive.Source.json new file mode 100644 index 0000000..051ecde --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Primitive.Source.json @@ -0,0 +1,6 @@ +{ + "A": 1, + "B": true, + "C": null, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingPathArray.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingPathArray.Expected.json new file mode 100644 index 0000000..11558b5 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingPathArray.Expected.json @@ -0,0 +1,4 @@ +{ + "A": 1, + "C": null +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingPathArray.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingPathArray.Transform.json new file mode 100644 index 0000000..037ae17 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingPathArray.Transform.json @@ -0,0 +1,10 @@ +{ + "@jdt.remove": [ + { + "@jdt.path": "$.D" + }, + { + "@jdt.path": "B" + } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimpleArray.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimpleArray.Expected.json new file mode 100644 index 0000000..c359407 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimpleArray.Expected.json @@ -0,0 +1,4 @@ +{ + "C": null, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimpleArray.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimpleArray.Transform.json new file mode 100644 index 0000000..68a0edc --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimpleArray.Transform.json @@ -0,0 +1,3 @@ +{ + "@jdt.remove": ["A", "B"] +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimplePath.Expected.json b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimplePath.Expected.json new file mode 100644 index 0000000..bcc9540 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimplePath.Expected.json @@ -0,0 +1,5 @@ +{ + "A": 1, + "B": true, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimplePath.Transform.json b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimplePath.Transform.json new file mode 100644 index 0000000..ee1766d --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Remove/Primitive.UsingSimplePath.Transform.json @@ -0,0 +1,5 @@ +{ + "@jdt.remove": { + "@jdt.path": "C" + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Array.DirectPath.Expected.json b/json-transforms/src/test/resources/Inputs/Rename/Array.DirectPath.Expected.json new file mode 100644 index 0000000..1412ea4 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Array.DirectPath.Expected.json @@ -0,0 +1,14 @@ +{ + "A": [ + { "Ind": 1 }, + { "Index": 2 }, + { "Ind": 3 } + ], + "B": [ + { "Rename": true, "ToChange": 1 }, + { "Rename": false, "ToChange": 1 }, + { "Rename": "WrongValue", "ToChange": 1 }, + { "HasRename": false, "ToChange": 1 }, + { "Rename": true, "ToChange": 1 } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Array.DirectPath.Transform.json b/json-transforms/src/test/resources/Inputs/Rename/Array.DirectPath.Transform.json new file mode 100644 index 0000000..7ee646c --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Array.DirectPath.Transform.json @@ -0,0 +1,6 @@ +{ + "@jdt.rename": { + "@jdt.path": "$.A[0,2].Index", + "@jdt.value": "Ind" + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Array.ScriptPath.Expected.json b/json-transforms/src/test/resources/Inputs/Rename/Array.ScriptPath.Expected.json new file mode 100644 index 0000000..7ed8700 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Array.ScriptPath.Expected.json @@ -0,0 +1,14 @@ +{ + "A": [ + { "Index": 1 }, + { "Index": 2 }, + { "Index": 3 } + ], + "B": [ + { "Rename": true, "Changed": 1 }, + { "Rename": false, "ToChange": 2 }, + { "Rename": "WrongValue", "ToChange": 3 }, + { "HasRename": false, "ToChange": 4 }, + { "Rename": true, "Changed": 5 } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Array.Source.json b/json-transforms/src/test/resources/Inputs/Rename/Array.Source.json new file mode 100644 index 0000000..abf4a94 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Array.Source.json @@ -0,0 +1,14 @@ +{ + "A": [ + { "Index": 1 }, + { "Index": 2 }, + { "Index": 3 } + ], + "B": [ + { "Rename": true, "ToChange": 1 }, + { "Rename": false, "ToChange": 1 }, + { "Rename": "WrongValue", "ToChange": 1 }, + { "HasRename": false, "ToChange": 1 }, + { "Rename": true, "ToChange": 1 } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Object.InObject.Expected.json b/json-transforms/src/test/resources/Inputs/Rename/Object.InObject.Expected.json new file mode 100644 index 0000000..2627090 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Object.InObject.Expected.json @@ -0,0 +1,14 @@ +{ + "A": { + }, + "Bee": { + "B1": 1, + "B2": 2 + }, + "Cstar": { + "C01": { + "C11": true + }, + "C02": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Object.InObject.Transform.json b/json-transforms/src/test/resources/Inputs/Rename/Object.InObject.Transform.json new file mode 100644 index 0000000..ef0f7bc --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Object.InObject.Transform.json @@ -0,0 +1,13 @@ +{ + "@jdt.rename": { + "C": "Cstar", + "B": "Bee" + }, + + "C": { + "@jdt.rename": { + "C1": "C01", + "C2": "C02" + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Object.Source.json b/json-transforms/src/test/resources/Inputs/Rename/Object.Source.json new file mode 100644 index 0000000..a24f2bc --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Object.Source.json @@ -0,0 +1,14 @@ +{ + "A": { + }, + "B": { + "B1": 1, + "B2": 2 + }, + "C": { + "C1": { + "C11": true + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Object.UsingSimplePath.Expected.json b/json-transforms/src/test/resources/Inputs/Rename/Object.UsingSimplePath.Expected.json new file mode 100644 index 0000000..432769a --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Object.UsingSimplePath.Expected.json @@ -0,0 +1,14 @@ +{ + "A": { + }, + "B": { + "B1": 1, + "B2": 2 + }, + "C": { + "C01": { + "C11": true + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Object.UsingSimplePath.Transform.json b/json-transforms/src/test/resources/Inputs/Rename/Object.UsingSimplePath.Transform.json new file mode 100644 index 0000000..a2522ed --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Object.UsingSimplePath.Transform.json @@ -0,0 +1,6 @@ +{ + "@jdt.rename": { + "@jdt.path": "$.C.C1", + "@jdt.value": "C01" + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Object.WithChangingNames.Expected.json b/json-transforms/src/test/resources/Inputs/Rename/Object.WithChangingNames.Expected.json new file mode 100644 index 0000000..4202aeb --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Object.WithChangingNames.Expected.json @@ -0,0 +1,14 @@ +{ + "A": { + }, + "B": { + "B1": 1, + "B2": 2 + }, + "C": { + "C01": { + "Cee11": true + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Object.WithChangingNames.Transform.json b/json-transforms/src/test/resources/Inputs/Rename/Object.WithChangingNames.Transform.json new file mode 100644 index 0000000..b96f6dc --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Object.WithChangingNames.Transform.json @@ -0,0 +1,12 @@ +{ + "@jdt.rename": { + "@jdt.path": "$.C.C1", + "@jdt.value": "C01" + }, + "C": { + "@jdt.rename": { + "@jdt.path": "$.C1.C11", + "@jdt.value": "Cee11" + } + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Primitive.FromRoot.Expected.json b/json-transforms/src/test/resources/Inputs/Rename/Primitive.FromRoot.Expected.json new file mode 100644 index 0000000..d351dba --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Primitive.FromRoot.Expected.json @@ -0,0 +1,6 @@ +{ + "Astar": 1, + "B": true, + "NewC": null, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Primitive.FromRoot.Transform.json b/json-transforms/src/test/resources/Inputs/Rename/Primitive.FromRoot.Transform.json new file mode 100644 index 0000000..d5c7815 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Primitive.FromRoot.Transform.json @@ -0,0 +1,6 @@ +{ + "@jdt.rename": { + "A": "Astar", + "C": "NewC" + } +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Primitive.Source.json b/json-transforms/src/test/resources/Inputs/Rename/Primitive.Source.json new file mode 100644 index 0000000..051ecde --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Primitive.Source.json @@ -0,0 +1,6 @@ +{ + "A": 1, + "B": true, + "C": null, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimpleArray.Expected.json b/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimpleArray.Expected.json new file mode 100644 index 0000000..be95910 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimpleArray.Expected.json @@ -0,0 +1,6 @@ +{ + "Astar": 1, + "B": true, + "C": null, + "Dee": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimpleArray.Transform.json b/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimpleArray.Transform.json new file mode 100644 index 0000000..cbd1bcd --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimpleArray.Transform.json @@ -0,0 +1,10 @@ +{ + "@jdt.rename": [ + { + "A": "Astar" + }, + { + "D" : "Dee" + } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimplePath.Expected.json b/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimplePath.Expected.json new file mode 100644 index 0000000..35abb61 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimplePath.Expected.json @@ -0,0 +1,6 @@ +{ + "Eh": 1, + "B": true, + "C": null, + "D": "4" +} diff --git a/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimplePath.Transform.json b/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimplePath.Transform.json new file mode 100644 index 0000000..b924970 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Rename/Primitive.UsingSimplePath.Transform.json @@ -0,0 +1,6 @@ +{ + "@jdt.rename": { + "@jdt.path": "$.A", + "@jdt.value": "Eh" + } +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Array.DirectPath.Expected.json b/json-transforms/src/test/resources/Inputs/Replace/Array.DirectPath.Expected.json new file mode 100644 index 0000000..1aa97f2 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Array.DirectPath.Expected.json @@ -0,0 +1,14 @@ +{ + "A": [ + 1, + { "Index": 2 }, + { "StringIndex": "03", "Replaced" : true } + ], + "B": [ + { "Replace": true }, + { "Replace": false }, + { "Replace": "WrongValue" }, + { "HasReplace": true }, + { "Replace": true } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Array.DirectPath.Transform.json b/json-transforms/src/test/resources/Inputs/Replace/Array.DirectPath.Transform.json new file mode 100644 index 0000000..a0715dd --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Array.DirectPath.Transform.json @@ -0,0 +1,15 @@ +{ + "@jdt.replace": [ + { + "@jdt.path": "$.A[0]", + "@jdt.value": 1 + }, + { + "@jdt.path": "$.A[2]", + "@jdt.value": { + "StringIndex": "03", + "Replaced" : true + } + } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Array.ScriptPath.Expected.json b/json-transforms/src/test/resources/Inputs/Replace/Array.ScriptPath.Expected.json new file mode 100644 index 0000000..28b6752 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Array.ScriptPath.Expected.json @@ -0,0 +1,14 @@ +{ + "A": [ + { "Index": 1 }, + { "Index": 2 }, + { "Index": 3 } + ], + "B": [ + { "Replaced": "Yes" }, + { "Replace": false }, + { "Replace": "WrongValue" }, + { "HasReplace": true }, + { "Replaced": "Yes" } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Array.ScriptPath.Transform.json b/json-transforms/src/test/resources/Inputs/Replace/Array.ScriptPath.Transform.json new file mode 100644 index 0000000..d25e96c --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Array.ScriptPath.Transform.json @@ -0,0 +1,6 @@ +{ + "@jdt.replace": { + "@jdt.path": "$.B[?(@.Replace == true)]", + "@jdt.value": {"Replaced": "Yes"} + } +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Array.Source.json b/json-transforms/src/test/resources/Inputs/Replace/Array.Source.json new file mode 100644 index 0000000..426d297 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Array.Source.json @@ -0,0 +1,14 @@ +{ + "A": [ + { "Index": 1 }, + { "Index": 2 }, + { "Index": 3 } + ], + "B": [ + { "Replace": true }, + { "Replace": false }, + { "Replace": "WrongValue" }, + { "HasReplace": true }, + { "Replace": true } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Object.Source.json b/json-transforms/src/test/resources/Inputs/Replace/Object.Source.json new file mode 100644 index 0000000..a24f2bc --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Object.Source.json @@ -0,0 +1,14 @@ +{ + "A": { + }, + "B": { + "B1": 1, + "B2": 2 + }, + "C": { + "C1": { + "C11": true + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Object.WithArray.Expected.json b/json-transforms/src/test/resources/Inputs/Replace/Object.WithArray.Expected.json new file mode 100644 index 0000000..273cc4b --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Object.WithArray.Expected.json @@ -0,0 +1,11 @@ +{ + "A": 1, + "B": { + "B1": 1, + "B2": 2 + }, + "C": [ + { "C1": { "C11": 1 } }, + { "C2": 2 } + ] +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Object.WithArray.Transform.json b/json-transforms/src/test/resources/Inputs/Replace/Object.WithArray.Transform.json new file mode 100644 index 0000000..935c2c2 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Object.WithArray.Transform.json @@ -0,0 +1,16 @@ +{ + "A": { + "@jdt.replace": [ + 1, + "Skipped" + ] + }, + "C": { + "@jdt.replace": [ + [ + { "C1": { "C11": 1 } }, + { "C2": 2 } + ] + ] + } +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Object.WithObject.Expected.json b/json-transforms/src/test/resources/Inputs/Replace/Object.WithObject.Expected.json new file mode 100644 index 0000000..312d039 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Object.WithObject.Expected.json @@ -0,0 +1,16 @@ +{ + "A": { + "A1": 1, + "A2": 2 + }, + "B": { + "1B": 10, + "2B": 22 + }, + "C": { + "C1": { + "Value": 1 + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Object.WithObject.Transform.json b/json-transforms/src/test/resources/Inputs/Replace/Object.WithObject.Transform.json new file mode 100644 index 0000000..5b7c412 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Object.WithObject.Transform.json @@ -0,0 +1,22 @@ +{ + "A": { + "@jdt.replace": { + "A1": 1, + "A2": 2 + } + }, + "B": { + "@jdt.replace": { + "1B": 10, + "2B": 22 + } + }, + "C": { + "C1": { + "@jdt.replace": { + "Value": 1 + } + }, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Object.WithPrimitive.Expected.json b/json-transforms/src/test/resources/Inputs/Replace/Object.WithPrimitive.Expected.json new file mode 100644 index 0000000..361d9ea --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Object.WithPrimitive.Expected.json @@ -0,0 +1,8 @@ +{ + "A": true, + "B": 20, + "C": { + "C1": 1, + "C2": 2 + } +} diff --git a/json-transforms/src/test/resources/Inputs/Replace/Object.WithPrimitive.Transform.json b/json-transforms/src/test/resources/Inputs/Replace/Object.WithPrimitive.Transform.json new file mode 100644 index 0000000..4f7ef84 --- /dev/null +++ b/json-transforms/src/test/resources/Inputs/Replace/Object.WithPrimitive.Transform.json @@ -0,0 +1,15 @@ +{ + "A": { + "@jdt.replace": true + }, + "B": { + "@jdt.replace": 20 + }, + "C": { + "C1": { + "@jdt.replace": 1, + "C11": "Skipped" + }, + "C2": 2 + } +} From f2f09768f9e9c674206c26e32165d09a0d6b04f5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:26:17 +0000 Subject: [PATCH 5/6] Issue #130 Fix golden test argument discovery Co-authored-by: simbo1905 --- .../java21/transforms/JsonTransformGoldenFilesTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/json-transforms/src/test/java/json/java21/transforms/JsonTransformGoldenFilesTest.java b/json-transforms/src/test/java/json/java21/transforms/JsonTransformGoldenFilesTest.java index ddec4cf..eaedf32 100644 --- a/json-transforms/src/test/java/json/java21/transforms/JsonTransformGoldenFilesTest.java +++ b/json-transforms/src/test/java/json/java21/transforms/JsonTransformGoldenFilesTest.java @@ -51,13 +51,13 @@ static Stream inputs() throws IOException { for (final var category : categories) { final var dir = inputsBase.resolve(category); try (var stream = Files.list(dir)) { - final var args = stream + final var testNames = stream .filter(p -> p.getFileName().toString().endsWith(".Transform.json")) .map(p -> p.getFileName().toString()) .map(name -> name.substring(0, name.length() - ".Transform.json".length())) .sorted() - .map(testName -> Arguments.of(category, testName)); - all = Stream.concat(all, args); + .toList(); + all = Stream.concat(all, testNames.stream().map(testName -> Arguments.of(category, testName))); } } return all; From d9e59644b2b3ce6b834bc672adce6d7d989e1d7a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 2 Feb 2026 00:31:43 +0000 Subject: [PATCH 6/6] Issue #130 Fix default recursion skip, merge replacement, and relative path normalization Co-authored-by: simbo1905 --- .../java/json/java21/transforms/TransformRunner.java | 7 +++++-- .../java/json/java21/transforms/TransformSyntax.java | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java b/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java index 5bf5355..0918f03 100644 --- a/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java +++ b/json-transforms/src/main/java/json/java21/transforms/TransformRunner.java @@ -228,7 +228,8 @@ private static JsonValue applyMergeValueToMatched(JsonValue matched, TransformAs final var applied = processTransform(obj, tov.compiled(), isDocumentRootForMatch); yield applied.value(); } - yield matched; + // Merge-with-attributes replaces mismatched types (including primitive -> object). + yield tov.rawObject(); } }; } @@ -240,7 +241,9 @@ private static JsonObject applyDefault(JsonObject source, Map final var key = entry.getKey(); final var tVal = entry.getValue(); - if (recursedKeys.contains(key) && current.members().get(key) instanceof JsonObject && tVal instanceof JsonObject) { + // Mirror the reference implementation: nodes already recursed into are removed from the transform + // before default processing, so default must never re-process them. + if (recursedKeys.contains(key)) { continue; } diff --git a/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java b/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java index a65225d..1a6b68b 100644 --- a/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java +++ b/json-transforms/src/main/java/json/java21/transforms/TransformSyntax.java @@ -31,7 +31,14 @@ static String normalizePathString(String path) { if (trimmed.startsWith("@")) { return "$" + trimmed.substring(1); } - return trimmed; + if (trimmed.startsWith("$")) { + return trimmed; + } + if (trimmed.startsWith(".") || trimmed.startsWith("[")) { + return "$" + trimmed; + } + // Json.NET SelectTokens accepts bare property names; treat them as relative paths. + return "$." + trimmed; } }