Skip to content

Commit 19b7c2d

Browse files
authored
Add JsonPatchStep (#1753)
2 parents b6e30f8 + 1eb2a4f commit 19b7c2d

File tree

18 files changed

+400
-5
lines changed

18 files changed

+400
-5
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,6 @@ nb-configuration.xml
124124

125125
# MacOS jenv
126126
.java-version
127+
128+
# VS Code
129+
.vscode/

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
### Added
14+
* Add a `jsonPatch` step to `json` formatter configurations. This allows patching of JSON documents using [JSON Patches](https://jsonpatch.com). ([#1753](https://github.com/diffplug/spotless/pull/1753))
1315
### Fixed
1416
* Use latest versions of popular style guides for `eslint` tests to fix failing `useEslintXoStandardRules` test. ([#1761](https://github.com/diffplug/spotless/pull/1761), [#1756](https://github.com/diffplug/spotless/issues/1756))
1517
* Add support for `prettier` version `3.0.0` and newer. ([#1760]https://github.com/diffplug/spotless/pull/1760), [#1751](https://github.com/diffplug/spotless/issues/1751))

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ lib('java.CleanthatJavaStep') +'{{yes}} | {{yes}}
8989
lib('json.gson.GsonStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
9090
lib('json.JacksonJsonStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
9191
lib('json.JsonSimpleStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
92+
lib('json.JsonPatchStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
9293
lib('kotlin.KtLintStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
9394
lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
9495
lib('kotlin.DiktatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
@@ -140,6 +141,7 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}}
140141
| [`json.gson.GsonStep`](lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
141142
| [`json.JacksonJsonStep`](lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
142143
| [`json.JsonSimpleStep`](lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
144+
| [`json.JsonPatchStep`](lib/src/main/java/com/diffplug/spotless/json/JsonPatchStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
143145
| [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
144146
| [`kotlin.KtfmtStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
145147
| [`kotlin.DiktatStep`](lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |

lib/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ def NEEDS_GLUE = [
2020
'ktlint',
2121
'palantirJavaFormat',
2222
'scalafmt',
23-
'sortPom'
23+
'sortPom',
24+
'zjsonPatch',
2425
]
2526
for (glue in NEEDS_GLUE) {
2627
sourceSets.register(glue) {
@@ -114,6 +115,8 @@ dependencies {
114115
// sortPom
115116
sortPomCompileOnly 'com.github.ekryd.sortpom:sortpom-sorter:3.2.1'
116117
sortPomCompileOnly 'org.slf4j:slf4j-api:2.0.0'
118+
// zjsonPatch
119+
zjsonPatchCompileOnly 'com.flipkart.zjsonpatch:zjsonpatch:0.4.14'
117120
}
118121

119122
// we'll hold the core lib to a high standard
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright 2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.json;
17+
18+
import java.io.IOException;
19+
import java.io.Serializable;
20+
import java.lang.reflect.Constructor;
21+
import java.lang.reflect.InvocationTargetException;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Objects;
25+
26+
import com.diffplug.spotless.FormatterFunc;
27+
import com.diffplug.spotless.FormatterStep;
28+
import com.diffplug.spotless.JarState;
29+
import com.diffplug.spotless.Provisioner;
30+
31+
public class JsonPatchStep {
32+
// https://mvnrepository.com/artifact/com.flipkart.zjsonpatch/zjsonpatch
33+
static final String MAVEN_COORDINATE = "com.flipkart.zjsonpatch:zjsonpatch";
34+
static final String DEFAULT_VERSION = "0.4.14";
35+
36+
private JsonPatchStep() {}
37+
38+
public static FormatterStep create(String patchString, Provisioner provisioner) {
39+
return create(DEFAULT_VERSION, patchString, provisioner);
40+
}
41+
42+
public static FormatterStep create(String zjsonPatchVersion, String patchString, Provisioner provisioner) {
43+
Objects.requireNonNull(zjsonPatchVersion, "zjsonPatchVersion cannot be null");
44+
Objects.requireNonNull(patchString, "patchString cannot be null");
45+
Objects.requireNonNull(provisioner, "provisioner cannot be null");
46+
return FormatterStep.createLazy("apply-json-patch", () -> new State(zjsonPatchVersion, patchString, provisioner), State::toFormatter);
47+
}
48+
49+
public static FormatterStep create(List<Map<String, Object>> patch, Provisioner provisioner) {
50+
return create(DEFAULT_VERSION, patch, provisioner);
51+
}
52+
53+
public static FormatterStep create(String zjsonPatchVersion, List<Map<String, Object>> patch, Provisioner provisioner) {
54+
Objects.requireNonNull(zjsonPatchVersion, "zjsonPatchVersion cannot be null");
55+
Objects.requireNonNull(patch, "patch cannot be null");
56+
Objects.requireNonNull(provisioner, "provisioner cannot be null");
57+
return FormatterStep.createLazy("apply-json-patch", () -> new State(zjsonPatchVersion, patch, provisioner), State::toFormatter);
58+
}
59+
60+
static final class State implements Serializable {
61+
private static final long serialVersionUID = 1L;
62+
63+
private final JarState jarState;
64+
private final List<Map<String, Object>> patch;
65+
private final String patchString;
66+
67+
private State(String zjsonPatchVersion, List<Map<String, Object>> patch, Provisioner provisioner) throws IOException {
68+
this.jarState = JarState.from(MAVEN_COORDINATE + ":" + zjsonPatchVersion, provisioner);
69+
this.patch = patch;
70+
this.patchString = null;
71+
}
72+
73+
private State(String zjsonPatchVersion, String patchString, Provisioner provisioner) throws IOException {
74+
this.jarState = JarState.from(MAVEN_COORDINATE + ":" + zjsonPatchVersion, provisioner);
75+
this.patch = null;
76+
this.patchString = patchString;
77+
}
78+
79+
FormatterFunc toFormatter() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
80+
Class<?> formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.json.JsonPatchFormatterFunc");
81+
if (this.patch != null) {
82+
Constructor<?> constructor = formatterFunc.getConstructor(List.class);
83+
return (FormatterFunc) constructor.newInstance(patch);
84+
} else {
85+
Constructor<?> constructor = formatterFunc.getConstructor(String.class);
86+
return (FormatterFunc) constructor.newInstance(patchString);
87+
}
88+
}
89+
}
90+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.glue.json;
17+
18+
import java.util.List;
19+
import java.util.Map;
20+
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.flipkart.zjsonpatch.JsonPatch;
23+
24+
import com.diffplug.spotless.FormatterFunc;
25+
26+
public class JsonPatchFormatterFunc implements FormatterFunc {
27+
private final ObjectMapper objectMapper;
28+
private final List<Map<String, Object>> patch;
29+
private final String patchString;
30+
31+
public JsonPatchFormatterFunc(String patchString) {
32+
this.objectMapper = new ObjectMapper();
33+
this.patch = null;
34+
this.patchString = patchString;
35+
}
36+
37+
public JsonPatchFormatterFunc(List<Map<String, Object>> patch) {
38+
this.objectMapper = new ObjectMapper();
39+
this.patch = patch;
40+
this.patchString = null;
41+
}
42+
43+
@Override
44+
public String apply(String input) throws Exception {
45+
var patchNode = this.patch == null
46+
? objectMapper.readTree(patchString)
47+
: objectMapper.valueToTree(patch);
48+
49+
var inputNode = objectMapper.readTree(input);
50+
51+
var patchedNode = JsonPatch.apply(patchNode, inputNode);
52+
53+
return objectMapper.writeValueAsString(patchedNode);
54+
}
55+
}

plugin-gradle/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
44

55
## [Unreleased]
6+
### Added
7+
* Add a `jsonPatch` step to `json` formatter configurations. This allows patching of JSON documents using [JSON Patches](https://jsonpatch.com). ([#1753](https://github.com/diffplug/spotless/pull/1753))
68
### Fixed
79
* Add support for `prettier` version `3.0.0` and newer. ([#1760]https://github.com/diffplug/spotless/pull/1760), [#1751](https://github.com/diffplug/spotless/issues/1751))
810
* Fix npm install calls when npm cache is not up-to-date. ([#1760]https://github.com/diffplug/spotless/pull/1760), [#1750](https://github.com/diffplug/spotless/issues/1750))

plugin-gradle/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ spotless {
811811
gson() // has its own section below
812812
jackson() // has its own section below
813813
rome() // has its own section below
814+
jsonPatch([]) // has its own section below
814815
}
815816
}
816817
```
@@ -872,6 +873,49 @@ spotless {
872873
}
873874
```
874875
876+
### jsonPatch
877+
878+
Uses [zjsonpatch](https://github.com/flipkart-incubator/zjsonpatch) to apply [JSON Patches](https://jsonpatch.com/) as per [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902/) to JSON documents.
879+
880+
This enables you to add, replace or remove properties at locations in the JSON document that you specify using [JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901/).
881+
882+
In Spotless Gradle, these JSON patches are represented as a `List<Map<String, Object>>`, or a list of patch operations.
883+
884+
Each patch operation must be a map with the following properties:
885+
886+
* `"op"` - the operation to apply, one of `"replace"`, `"add"` or `"remove"`.
887+
* `"path"` - a JSON Pointer string, for example `"/foo"`
888+
* `"value"` - the value to `"add"` or `"replace"` at the specified path. Not needed for `"remove"` operations.
889+
890+
For example, to apply the patch from the [JSON Patch homepage](https://jsonpatch.com/#the-patch):
891+
892+
```gradle
893+
spotless {
894+
json {
895+
target 'src/**/*.json'
896+
jsonPatch([
897+
[op: 'replace', path: '/baz', value: 'boo'],
898+
[op: 'add', path: '/hello', value: ['world']],
899+
[op: 'remove', path: '/foo']
900+
])
901+
}
902+
}
903+
```
904+
905+
Or using the Kotlin DSL:
906+
907+
```kotlin
908+
spotless {
909+
json {
910+
target("src/**/*.json")
911+
jsonPatch(listOf(
912+
mapOf("op" to "replace", "path" to "/baz", "value" to "boo"),
913+
mapOf("op" to "add", "path" to "/hello", "value" to listOf("world")),
914+
mapOf("op" to "remove", "path" to "/foo")
915+
))
916+
}
917+
}
918+
```
875919
876920
## YAML
877921

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,22 @@
1616
package com.diffplug.gradle.spotless;
1717

1818
import java.util.Collections;
19+
import java.util.List;
20+
import java.util.Map;
1921

2022
import javax.inject.Inject;
2123

2224
import com.diffplug.spotless.FormatterStep;
2325
import com.diffplug.spotless.json.JacksonJsonConfig;
2426
import com.diffplug.spotless.json.JacksonJsonStep;
27+
import com.diffplug.spotless.json.JsonPatchStep;
2528
import com.diffplug.spotless.json.JsonSimpleStep;
2629
import com.diffplug.spotless.json.gson.GsonStep;
2730

2831
public class JsonExtension extends FormatExtension {
2932
private static final int DEFAULT_INDENTATION = 4;
3033
private static final String DEFAULT_GSON_VERSION = "2.10.1";
34+
private static final String DEFAULT_ZJSONPATCH_VERSION = "0.4.14";
3135
static final String NAME = "json";
3236

3337
@Inject
@@ -71,6 +75,14 @@ public RomeJson rome(String version) {
7175
return romeConfig;
7276
}
7377

78+
public JsonPatchConfig jsonPatch(List<Map<String, Object>> patch) {
79+
return new JsonPatchConfig(patch);
80+
}
81+
82+
public JsonPatchConfig jsonPatch(String zjsonPatchVersion, List<Map<String, Object>> patch) {
83+
return new JsonPatchConfig(zjsonPatchVersion, patch);
84+
}
85+
7486
public class SimpleConfig {
7587
private int indent;
7688

@@ -191,4 +203,29 @@ protected RomeJson getThis() {
191203
return this;
192204
}
193205
}
206+
207+
public class JsonPatchConfig {
208+
private String zjsonPatchVersion;
209+
private List<Map<String, Object>> patch;
210+
211+
public JsonPatchConfig(List<Map<String, Object>> patch) {
212+
this(DEFAULT_ZJSONPATCH_VERSION, patch);
213+
}
214+
215+
public JsonPatchConfig(String zjsonPatchVersion, List<Map<String, Object>> patch) {
216+
this.zjsonPatchVersion = zjsonPatchVersion;
217+
this.patch = patch;
218+
addStep(createStep());
219+
}
220+
221+
public JsonPatchConfig version(String zjsonPatchVersion) {
222+
this.zjsonPatchVersion = zjsonPatchVersion;
223+
replaceStep(createStep());
224+
return this;
225+
}
226+
227+
private FormatterStep createStep() {
228+
return JsonPatchStep.create(zjsonPatchVersion, patch, provisioner());
229+
}
230+
}
194231
}

plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,44 @@ void jacksonFormattingWithSortingByKeys() throws IOException {
156156
gradleRunner().withArguments("spotlessApply").build();
157157
assertFile("src/main/resources/example.json").sameAsResource("json/sortByKeysAfter_Jackson.json");
158158
}
159+
160+
@Test
161+
void jsonPatchReplaceString() throws IOException {
162+
setFile("build.gradle").toLines(
163+
"plugins {",
164+
" id 'java'",
165+
" id 'com.diffplug.spotless'",
166+
"}",
167+
"repositories { mavenCentral() }",
168+
"spotless {",
169+
" json {",
170+
" target 'src/**/*.json'",
171+
" jsonPatch([[op: 'replace', path: '/abc', value: 'ghi']])",
172+
" gson()",
173+
" }",
174+
"}");
175+
setFile("src/main/resources/example.json").toResource("json/patchObjectBefore.json");
176+
gradleRunner().withArguments("spotlessApply").build();
177+
assertFile("src/main/resources/example.json").sameAsResource("json/patchObjectAfterReplaceString.json");
178+
}
179+
180+
@Test
181+
void jsonPatchReplaceWithObject() throws IOException {
182+
setFile("build.gradle").toLines(
183+
"plugins {",
184+
" id 'java'",
185+
" id 'com.diffplug.spotless'",
186+
"}",
187+
"repositories { mavenCentral() }",
188+
"spotless {",
189+
" json {",
190+
" target 'src/**/*.json'",
191+
" jsonPatch([[op: 'replace', path: '/abc', value: [def: 'ghi']]])",
192+
" gson()",
193+
" }",
194+
"}");
195+
setFile("src/main/resources/example.json").toResource("json/patchObjectBefore.json");
196+
gradleRunner().withArguments("spotlessApply").build();
197+
assertFile("src/main/resources/example.json").sameAsResource("json/patchObjectAfterReplaceWithObject.json");
198+
}
159199
}

0 commit comments

Comments
 (0)