Skip to content

Commit ab8f2ad

Browse files
authored
Throw on root node replacement (see #188) (#190)
* Reimplement full test suite with applyInPlace * Support exceptions for InPlaceApplyProcessor + tests
1 parent a446bf5 commit ab8f2ad

File tree

7 files changed

+68
-22
lines changed

7 files changed

+68
-22
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ JsonPatch.applyInPlace(JsonNode patch, JsonNode source);
8686
Given a `patch`, it will apply it to the `source` JSON mutating the instance, opposed to `JsonPatch.apply` which returns
8787
a new instance with the patch applied, leaving the `source` unchanged.
8888

89+
This is an extension to the RFC, and has some additional limitations. Specifically, the source document cannot be fully change in place (the Jackson APIs do not support that level of mutability). This means the following operations are not supported:
90+
* `remove` with an empty or root path;
91+
* `replace` with an empty or root path;
92+
* `move`, `add` or `copy` targeting an empty or root path.
93+
8994
### Tests:
9095
1. 100+ selective hardcoded different input JSONs , with their driver test classes present under /test directory.
9196
2. Apart from selective input, a deterministic random JSON generator is present under ( TestDataGenerator.java ), and its driver test class method is JsonDiffTest.testGeneratedJsonDiff().

src/main/java/com/flipkart/zjsonpatch/CopyingApplyProcessor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@ class CopyingApplyProcessor extends InPlaceApplyProcessor {
1212
CopyingApplyProcessor(JsonNode target, EnumSet<CompatibilityFlags> flags) {
1313
super(target.deepCopy(), flags);
1414
}
15+
16+
@Override
17+
protected boolean allowRootReplacement() {
18+
return true;
19+
}
1520
}

src/main/java/com/flipkart/zjsonpatch/InPlaceApplyProcessor.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public JsonNode result() {
4040
return target;
4141
}
4242

43+
protected boolean allowRootReplacement() {
44+
return false;
45+
}
46+
4347
@Override
4448
public void move(JsonPointer fromPath, JsonPointer toPath) throws JsonPointerEvaluationException {
4549
JsonNode valueNode = fromPath.evaluate(target);
@@ -81,6 +85,8 @@ public void add(JsonPointer path, JsonNode value) throws JsonPointerEvaluationEx
8185
@Override
8286
public void replace(JsonPointer path, JsonNode value) throws JsonPointerEvaluationException {
8387
if (path.isRoot()) {
88+
if (!allowRootReplacement())
89+
throw new JsonPatchApplicationException("Cannot replace root document", Operation.REPLACE, path);
8490
target = value;
8591
return;
8692
}
@@ -132,17 +138,20 @@ else if (parentNode.isArray()) {
132138

133139

134140
private void set(JsonPointer path, JsonNode value, Operation forOp) throws JsonPointerEvaluationException {
135-
if (path.isRoot())
141+
if (path.isRoot()) {
142+
if (!allowRootReplacement())
143+
throw new JsonPatchApplicationException("Cannot replace root document", forOp, path);
136144
target = value;
137-
else {
138-
JsonNode parentNode = path.getParent().evaluate(target);
139-
if (!parentNode.isContainerNode())
140-
throw new JsonPatchApplicationException("Cannot reference past scalar value", forOp, path.getParent());
141-
else if (parentNode.isArray())
142-
addToArray(path, value, parentNode);
143-
else
144-
addToObject(path, parentNode, value);
145+
return;
145146
}
147+
148+
JsonNode parentNode = path.getParent().evaluate(target);
149+
if (!parentNode.isContainerNode())
150+
throw new JsonPatchApplicationException("Cannot reference past scalar value", forOp, path.getParent());
151+
else if (parentNode.isArray())
152+
addToArray(path, value, parentNode);
153+
else
154+
addToObject(path, parentNode, value);
146155
}
147156

148157
private void addToObject(JsonPointer path, JsonNode node, JsonNode value) {

src/test/java/com/flipkart/zjsonpatch/AbstractTest.java

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import java.io.PrintWriter;
2929
import java.io.StringWriter;
3030

31-
import static org.hamcrest.core.IsEqual.equalTo;
3231
import static org.hamcrest.core.IsInstanceOf.instanceOf;
3332
import static org.hamcrest.core.StringContains.containsString;
3433
import static org.junit.Assert.assertEquals;
@@ -46,23 +45,38 @@ protected boolean matchOnErrors() {
4645
}
4746

4847
@Test
49-
public void test() throws Exception {
48+
public void testApply() throws Exception {
5049
if (p.isOperation()) {
51-
testOperation();
50+
testOperation(false);
5251
} else {
53-
testError();
52+
testError(false);
5453
}
5554
}
5655

57-
private void testOperation() throws Exception {
56+
@Test
57+
public void testApplyInPlace() throws Exception {
58+
if (p.isOperation() && p.isApplyInPlaceSupported()) {
59+
testOperation(true);
60+
} else {
61+
testError(true);
62+
}
63+
}
64+
65+
private void testOperation(boolean inPlace) {
5866
JsonNode node = p.getNode();
5967

6068
JsonNode doc = node.get("node");
6169
JsonNode expected = node.get("expected");
6270
JsonNode patch = node.get("op");
6371
String message = node.has("message") ? node.get("message").toString() : "";
6472

65-
JsonNode result = JsonPatch.apply(patch, doc);
73+
JsonNode result;
74+
if (inPlace) {
75+
result = doc.deepCopy();
76+
JsonPatch.applyInPlace(patch, result);
77+
} else {
78+
result = JsonPatch.apply(patch, doc);
79+
}
6680
String failMessage = "The following test failed: \n" +
6781
"message: " + message + '\n' +
6882
"at: " + p.getSourceFile();
@@ -91,7 +105,7 @@ private String errorMessage(String header, Exception e) throws JsonProcessingExc
91105
return res.toString();
92106
}
93107

94-
private void testError() throws JsonProcessingException, ClassNotFoundException {
108+
private void testError(boolean inPlace) throws JsonProcessingException, ClassNotFoundException {
95109
JsonNode node = p.getNode();
96110
JsonNode first = node.get("node");
97111
JsonNode patch = node.get("op");
@@ -100,8 +114,12 @@ private void testError() throws JsonProcessingException, ClassNotFoundException
100114
node.has("type") ? exceptionType(node.get("type").textValue()) : JsonPatchApplicationException.class;
101115

102116
try {
103-
JsonPatch.apply(patch, first);
104-
117+
if (inPlace) {
118+
JsonNode target = first.deepCopy();
119+
JsonPatch.applyInPlace(patch, target);
120+
} else {
121+
JsonPatch.apply(patch, first);
122+
}
105123
fail(errorMessage("Failure expected: " + message));
106124
} catch (Exception e) {
107125
if (matchOnErrors()) {

src/test/java/com/flipkart/zjsonpatch/PatchTestCase.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,9 @@ private static boolean isEnabled(JsonNode node) {
6969
JsonNode disabled = node.get("disabled");
7070
return (disabled == null || !disabled.booleanValue());
7171
}
72+
73+
public boolean isApplyInPlaceSupported() {
74+
JsonNode allowInPlace = node.get("allowInPlace");
75+
return (allowInPlace == null || allowInPlace.booleanValue());
76+
}
7277
}

src/test/resources/testdata/js-libs-samples.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,14 @@
7575
{ "message": "replacing the root of the document is possible with add",
7676
"node": {"foo": "bar"},
7777
"op": [{"op": "add", "path": "", "value": {"baz": "qux"}}],
78-
"expected": {"baz":"qux"}},
78+
"expected": {"baz":"qux"},
79+
"allowInPlace": false },
7980

8081
{ "message": "replacing the root of the document is possible with add",
8182
"node": {"foo": "bar"},
8283
"op": [{"op": "add", "path": "", "value": ["baz", "qux"]}],
83-
"expected": ["baz", "qux"]},
84+
"expected": ["baz", "qux"],
85+
"allowInPlace": false },
8486

8587
{ "message": "empty list, empty docs",
8688
"node": {},
@@ -235,7 +237,8 @@
235237
{ "message": "replace whole document",
236238
"node": {"foo": "bar"},
237239
"op": [{"op": "replace", "path": "", "value": {"baz": "qux"}}],
238-
"expected": {"baz": "qux"} },
240+
"expected": {"baz": "qux"},
241+
"allowInPlace": false },
239242

240243
{ "node": {"foo": null},
241244
"op": [{"op": "replace", "path": "/foo", "value": "truthy"}],

src/test/resources/testdata/replace.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
{
2222
"op": [{ "op": "replace", "path": "", "value": false }],
2323
"node": { "x": { "a": "b", "y": {} } },
24-
"expected": false
24+
"expected": false,
25+
"allowInPlace": false
2526
},
2627
{
2728
"op": [{ "op": "replace", "path": "/x/y", "value": "hello" }],

0 commit comments

Comments
 (0)