From e9b7e66e1b6d7b4754bcbd59b5382253b6733a93 Mon Sep 17 00:00:00 2001 From: dpozinen Date: Thu, 18 Jul 2024 21:53:13 +0200 Subject: [PATCH 1/5] feat: recipe for adding a key value pair to a json --- .../org/openrewrite/json/AddKeyValue.java | 144 ++++++++++++++ .../org/openrewrite/json/AddKeyValueTest.java | 179 ++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java create mode 100644 rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java diff --git a/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java b/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java new file mode 100644 index 00000000000..f9200791fc0 --- /dev/null +++ b/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java @@ -0,0 +1,144 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.json; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.json.tree.Json; +import org.openrewrite.json.tree.JsonKey; +import org.openrewrite.json.tree.JsonRightPadded; +import org.openrewrite.json.tree.Space; +import org.openrewrite.marker.Markers; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; +import static org.openrewrite.Tree.randomId; + +@Value +@EqualsAndHashCode(callSuper = false) +public class AddKeyValue extends Recipe { + + @Option(displayName = "Key path", + description = "A JsonPath expression to locate the *parent* JSON entry.", + example = "'$.subjects.*' or '$.' or '$.x[1].y.*' etc.") + String keyPath; + + @Option(displayName = "Key", + description = "The key to create.", + example = "myKey") + String key; + + @Option(displayName = "Value", + description = "The value to add to the array at the specified key. Can be of any type." + + " String values should be quoted to be inserted as Strings.", + example = "\"myValue\" or '{\"a\": 1}' or '[ 123 ]'") + String value; + + @Option(displayName = "Prepend", + required = false, + description = "If set to `true` the value will be added to the beginning of the object") + boolean prepend; + + @Override + public String getDisplayName() { + return "Add value to JSON Object"; + } + + @Override + public String getDescription() { + return "Adds a `value` at the specified `keyPath` with the specified `key`, if the key doesn't already exist."; + } + + + @Override + public TreeVisitor getVisitor() { + JsonPathMatcher pathMatcher = new JsonPathMatcher(keyPath); + + return new JsonIsoVisitor() { + + @Override + public Json.JsonObject visitObject(Json.JsonObject obj, ExecutionContext ctx) { + obj = super.visitObject(obj, ctx); + + if (pathMatcher.matches(getCursor()) && objectDoesNotContainKey(obj, key)) { + + boolean jsonIsEmpty = obj.getMembers().isEmpty() || obj.getMembers().get(0) instanceof Json.Empty; + Space space = jsonIsEmpty ? Space.EMPTY : obj.getMembers().get(0).getPrefix(); + + JsonRightPadded newKey = rightPaddedKey(); + Json.Literal newValue = valueLiteral(); + Json newMember = new Json.Member(randomId(), space, Markers.EMPTY, newKey, newValue); + + List members = jsonIsEmpty ? new ArrayList<>() : obj.getMembers(); + + if (prepend) { + members.add(0, newMember); + } else { + members.add(newMember); + } + + return obj.withMembers(members); + } + return obj; + } + + private Json.Literal valueLiteral() { + return new Json.Literal(randomId(), Space.build(" ", emptyList()), Markers.EMPTY, value, unQuote(value)); + } + + private JsonRightPadded rightPaddedKey() { + return new JsonRightPadded<>( + new Json.Literal(randomId(), Space.EMPTY, Markers.EMPTY, "\"" + key + "\"", key), + Space.EMPTY, Markers.EMPTY + ); + } + + private String unQuote(String value) { + if (value.startsWith("'") || value.startsWith("\"")) { + value = value.substring(1, value.length() - 1); + } + return value; + } + + private boolean objectDoesNotContainKey(Json.JsonObject obj, String key) { + for (Json member : obj.getMembers()) { + if (member instanceof Json.Member) { + if (keyMatches(((Json.Member) member).getKey(), key)) { + return false; + } + } + } + return true; + } + + private boolean keyMatches(JsonKey jsonKey, String key) { + if (jsonKey instanceof Json.Literal) { + return key.equals(((Json.Literal) jsonKey).getValue()); + } else if (jsonKey instanceof Json.Identifier) { + return key.equals(((Json.Identifier) jsonKey).getName()); + } + throw new IllegalStateException("Key is not 'Json.Literal' or 'Json.Identifier': " + jsonKey); + } + + }; + } +} diff --git a/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java b/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java new file mode 100644 index 00000000000..9a288ca43c1 --- /dev/null +++ b/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.json; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.json.Assertions.json; + +class AddKeyValueTest implements RewriteTest { + + @Test + void shouldAppendSimpleValue() { + rewriteRun( + spec -> spec.recipe(new AddKeyValue("$.", "key", "\"val\"", false)), + //language=json + json( + """ + { + "x": "x", + "l": [ + 1, 2 + ] + } + """, + """ + { + "x": "x", + "l": [ + 1, 2 + ] + , + "key": "val"} + """) + + ); + } + + @Test + void shouldAppendToNestedObject() { + rewriteRun( + spec -> spec.recipe(new AddKeyValue("$.x.y.*", "key", "\"val\"", false)), + //language=json + json( + """ + { + "x": { + "y": { } + } + } + """, + """ + { + "x": { + "y": {"key": "val"} + } + } + """) + + ); + } + + @Test + void shouldAppendToNestedObjectInArray() { + rewriteRun( + spec -> spec.recipe(new AddKeyValue("$.x[1].y.*", "key", "\"val\"", false)), + //language=json + json( + """ + { + "x": [ + { }, + { + "y" : { } + } + ] + } + """, + """ + { + "x": [ + { }, + { + "y" : {"key": "val"} + } + ] + } + """) + + ); + } + + @Test + void shouldNotAppendIfExists() { + rewriteRun( + spec -> spec.recipe(new AddKeyValue("$.", "key", "\"val\"", false)), + //language=json + json(""" + { + "key": "x" + }""") + + ); + } + + @Test + void shouldAppendObject() { + rewriteRun( + spec -> spec.recipe(new AddKeyValue( + "$.", "key", """ + { "a": "b" } + """.trim(), false)), + //language=json + json( + """ + { + "x": "x", + "l": [ + 1, 2 + ] + } + """, + """ + { + "x": "x", + "l": [ + 1, 2 + ] + , + "key": { "a": "b" }} + """) + + ); + } + + @Test + void shouldPrependObject() { + rewriteRun( + spec -> spec.recipe(new AddKeyValue( + "$.", "key", """ + { "a": "b" } + """.trim(), true)), + //language=json + json( + """ + { + "x": "x", + "l": [ + 1, 2 + ] + } + """, + """ + { + "key": { "a": "b" }, + "x": "x", + "l": [ + 1, 2 + ] + } + """) + + ); + } + +} \ No newline at end of file From 4f0c4e7f556267d7fad50040f5eb4ef01e815f98 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 30 Jul 2024 23:39:53 +0200 Subject: [PATCH 2/5] Slight polish --- .../org/openrewrite/json/AddKeyValue.java | 50 ++-- .../org/openrewrite/json/AddKeyValueTest.java | 239 +++++++++--------- 2 files changed, 143 insertions(+), 146 deletions(-) diff --git a/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java b/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java index f9200791fc0..702cc04869c 100644 --- a/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java +++ b/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java @@ -21,13 +21,14 @@ import org.openrewrite.Option; import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; import org.openrewrite.json.tree.Json; import org.openrewrite.json.tree.JsonKey; import org.openrewrite.json.tree.JsonRightPadded; import org.openrewrite.json.tree.Space; import org.openrewrite.marker.Markers; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static java.util.Collections.emptyList; @@ -38,24 +39,24 @@ public class AddKeyValue extends Recipe { @Option(displayName = "Key path", - description = "A JsonPath expression to locate the *parent* JSON entry.", - example = "'$.subjects.*' or '$.' or '$.x[1].y.*' etc.") + description = "A JsonPath expression to locate the *parent* JSON entry.", + example = "'$.subjects.*' or '$.' or '$.x[1].y.*' etc.") String keyPath; @Option(displayName = "Key", - description = "The key to create.", - example = "myKey") + description = "The key to create.", + example = "myKey") String key; @Option(displayName = "Value", - description = "The value to add to the array at the specified key. Can be of any type." + - " String values should be quoted to be inserted as Strings.", - example = "\"myValue\" or '{\"a\": 1}' or '[ 123 ]'") + description = "The value to add to the array at the specified key. Can be of any type." + + " String values should be quoted to be inserted as Strings.", + example = "`\"myValue\"` or `{\"a\": 1}` or `[ 123 ]`") String value; @Option(displayName = "Prepend", - required = false, - description = "If set to `true` the value will be added to the beginning of the object") + required = false, + description = "If set to `true` the value will be added to the beginning of the object") boolean prepend; @Override @@ -71,32 +72,27 @@ public String getDescription() { @Override public TreeVisitor getVisitor() { - JsonPathMatcher pathMatcher = new JsonPathMatcher(keyPath); - return new JsonIsoVisitor() { + private final JsonPathMatcher pathMatcher = new JsonPathMatcher(keyPath); @Override public Json.JsonObject visitObject(Json.JsonObject obj, ExecutionContext ctx) { obj = super.visitObject(obj, ctx); if (pathMatcher.matches(getCursor()) && objectDoesNotContainKey(obj, key)) { - - boolean jsonIsEmpty = obj.getMembers().isEmpty() || obj.getMembers().get(0) instanceof Json.Empty; - Space space = jsonIsEmpty ? Space.EMPTY : obj.getMembers().get(0).getPrefix(); + List originalMembers = obj.getMembers(); + boolean jsonIsEmpty = originalMembers.isEmpty() || originalMembers.get(0) instanceof Json.Empty; + Space space = jsonIsEmpty ? Space.EMPTY : originalMembers.get(0).getPrefix(); JsonRightPadded newKey = rightPaddedKey(); Json.Literal newValue = valueLiteral(); Json newMember = new Json.Member(randomId(), space, Markers.EMPTY, newKey, newValue); - List members = jsonIsEmpty ? new ArrayList<>() : obj.getMembers(); - - if (prepend) { - members.add(0, newMember); - } else { - members.add(newMember); - } + List newMembers = jsonIsEmpty ? Collections.singletonList(newMember) : prepend ? + ListUtils.concat(newMember, originalMembers) : + ListUtils.concat(originalMembers, newMember); - return obj.withMembers(members); + return obj.withMembers(newMembers); } return obj; } @@ -107,14 +103,14 @@ private Json.Literal valueLiteral() { private JsonRightPadded rightPaddedKey() { return new JsonRightPadded<>( - new Json.Literal(randomId(), Space.EMPTY, Markers.EMPTY, "\"" + key + "\"", key), - Space.EMPTY, Markers.EMPTY + new Json.Literal(randomId(), Space.EMPTY, Markers.EMPTY, "\"" + key + "\"", key), + Space.EMPTY, Markers.EMPTY ); } private String unQuote(String value) { if (value.startsWith("'") || value.startsWith("\"")) { - value = value.substring(1, value.length() - 1); + return value.substring(1, value.length() - 1); } return value; } @@ -136,7 +132,7 @@ private boolean keyMatches(JsonKey jsonKey, String key) { } else if (jsonKey instanceof Json.Identifier) { return key.equals(((Json.Identifier) jsonKey).getName()); } - throw new IllegalStateException("Key is not 'Json.Literal' or 'Json.Identifier': " + jsonKey); + return false; } }; diff --git a/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java b/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java index 9a288ca43c1..e2d8fdb8f72 100644 --- a/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java +++ b/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java @@ -16,6 +16,7 @@ package org.openrewrite.json; import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; import org.openrewrite.test.RewriteTest; import static org.openrewrite.json.Assertions.json; @@ -23,95 +24,96 @@ class AddKeyValueTest implements RewriteTest { @Test + @DocumentExample void shouldAppendSimpleValue() { rewriteRun( - spec -> spec.recipe(new AddKeyValue("$.", "key", "\"val\"", false)), - //language=json - json( - """ - { - "x": "x", - "l": [ - 1, 2 - ] - } - """, - """ - { - "x": "x", - "l": [ - 1, 2 - ] - , - "key": "val"} - """) - + spec -> spec.recipe(new AddKeyValue("$.", "key", "\"val\"", false)), + //language=json + json( + """ + { + "x": "x", + "l": [ + 1, 2 + ] + } + """, + """ + { + "x": "x", + "l": [ + 1, 2 + ] + , + "key": "val"} + """ + ) ); } @Test void shouldAppendToNestedObject() { rewriteRun( - spec -> spec.recipe(new AddKeyValue("$.x.y.*", "key", "\"val\"", false)), - //language=json - json( - """ - { - "x": { - "y": { } - } - } - """, - """ - { - "x": { - "y": {"key": "val"} - } - } - """) - + spec -> spec.recipe(new AddKeyValue("$.x.y.*", "key", "\"val\"", false)), + //language=json + json( + """ + { + "x": { + "y": { } + } + } + """, + """ + { + "x": { + "y": {"key": "val"} + } + } + """ + ) ); } @Test void shouldAppendToNestedObjectInArray() { rewriteRun( - spec -> spec.recipe(new AddKeyValue("$.x[1].y.*", "key", "\"val\"", false)), - //language=json - json( - """ - { - "x": [ - { }, - { - "y" : { } - } - ] - } - """, - """ - { - "x": [ - { }, - { - "y" : {"key": "val"} - } - ] - } - """) - + spec -> spec.recipe(new AddKeyValue("$.x[1].y.*", "key", "\"val\"", false)), + //language=json + json( + """ + { + "x": [ + { }, + { + "y" : { } + } + ] + } + """, + """ + { + "x": [ + { }, + { + "y" : {"key": "val"} + } + ] + } + """ + ) ); } @Test void shouldNotAppendIfExists() { rewriteRun( - spec -> spec.recipe(new AddKeyValue("$.", "key", "\"val\"", false)), - //language=json - json(""" - { - "key": "x" - }""") + spec -> spec.recipe(new AddKeyValue("$.", "key", "\"val\"", false)), + //language=json + json(""" + { + "key": "x" + }""") ); } @@ -119,61 +121,60 @@ void shouldNotAppendIfExists() { @Test void shouldAppendObject() { rewriteRun( - spec -> spec.recipe(new AddKeyValue( - "$.", "key", """ - { "a": "b" } - """.trim(), false)), - //language=json - json( - """ - { - "x": "x", - "l": [ - 1, 2 - ] - } - """, - """ - { - "x": "x", - "l": [ - 1, 2 - ] - , - "key": { "a": "b" }} - """) - + spec -> spec.recipe(new AddKeyValue( + "$.", "key", """ + { "a": "b" } + """.trim(), false)), + //language=json + json( + """ + { + "x": "x", + "l": [ + 1, 2 + ] + } + """, + """ + { + "x": "x", + "l": [ + 1, 2 + ] + , + "key": { "a": "b" }} + """ + ) ); } @Test void shouldPrependObject() { rewriteRun( - spec -> spec.recipe(new AddKeyValue( - "$.", "key", """ - { "a": "b" } - """.trim(), true)), - //language=json - json( - """ - { - "x": "x", - "l": [ - 1, 2 - ] - } - """, - """ - { - "key": { "a": "b" }, - "x": "x", - "l": [ - 1, 2 - ] - } - """) - + spec -> spec.recipe(new AddKeyValue( + "$.", "key", """ + { "a": "b" } + """.trim(), true)), + //language=json + json( + """ + { + "x": "x", + "l": [ + 1, 2 + ] + } + """, + """ + { + "key": { "a": "b" }, + "x": "x", + "l": [ + 1, 2 + ] + } + """ + ) ); } - -} \ No newline at end of file +} From 97539833b94ee6d68ef956aed9c2adf1d22792da Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 30 Jul 2024 23:44:17 +0200 Subject: [PATCH 3/5] Strive for better formatting after insertion --- .../org/openrewrite/json/AddKeyValueTest.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java b/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java index e2d8fdb8f72..1325e9c2048 100644 --- a/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java +++ b/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java @@ -43,9 +43,9 @@ void shouldAppendSimpleValue() { "x": "x", "l": [ 1, 2 - ] - , - "key": "val"} + ], + "key": "val" + } """ ) ); @@ -113,8 +113,9 @@ void shouldNotAppendIfExists() { json(""" { "key": "x" - }""") - + } + """ + ) ); } @@ -140,9 +141,9 @@ void shouldAppendObject() { "x": "x", "l": [ 1, 2 - ] - , - "key": { "a": "b" }} + ], + "key": { "a": "b" } + } """ ) ); From 4b01a77b9071df1809e05454da32a4809b572172 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 30 Jul 2024 23:53:05 +0200 Subject: [PATCH 4/5] Improve some existing cases already --- .../main/java/org/openrewrite/json/AddKeyValue.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java b/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java index 702cc04869c..8c58c745d30 100644 --- a/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java +++ b/rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java @@ -82,23 +82,26 @@ public Json.JsonObject visitObject(Json.JsonObject obj, ExecutionContext ctx) { if (pathMatcher.matches(getCursor()) && objectDoesNotContainKey(obj, key)) { List originalMembers = obj.getMembers(); boolean jsonIsEmpty = originalMembers.isEmpty() || originalMembers.get(0) instanceof Json.Empty; - Space space = jsonIsEmpty ? Space.EMPTY : originalMembers.get(0).getPrefix(); + Space space = jsonIsEmpty || prepend ? originalMembers.get(0).getPrefix() : Space.SINGLE_SPACE; JsonRightPadded newKey = rightPaddedKey(); Json.Literal newValue = valueLiteral(); Json newMember = new Json.Member(randomId(), space, Markers.EMPTY, newKey, newValue); - List newMembers = jsonIsEmpty ? Collections.singletonList(newMember) : prepend ? + if (jsonIsEmpty) { + return obj.withMembers(Collections.singletonList(newMember)); + } + + List newMembers = prepend ? ListUtils.concat(newMember, originalMembers) : ListUtils.concat(originalMembers, newMember); - return obj.withMembers(newMembers); } return obj; } private Json.Literal valueLiteral() { - return new Json.Literal(randomId(), Space.build(" ", emptyList()), Markers.EMPTY, value, unQuote(value)); + return new Json.Literal(randomId(), Space.SINGLE_SPACE, Markers.EMPTY, value, unQuote(value)); } private JsonRightPadded rightPaddedKey() { @@ -134,7 +137,6 @@ private boolean keyMatches(JsonKey jsonKey, String key) { } return false; } - }; } } From a48433918fc3bfc821a6516e0ba94689dc463dea Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Tue, 30 Jul 2024 23:53:46 +0200 Subject: [PATCH 5/5] Update test as suggested --- .../test/java/org/openrewrite/json/AddKeyValueTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java b/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java index 1325e9c2048..32f20c3ef07 100644 --- a/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java +++ b/rewrite-json/src/test/java/org/openrewrite/json/AddKeyValueTest.java @@ -110,11 +110,12 @@ void shouldNotAppendIfExists() { rewriteRun( spec -> spec.recipe(new AddKeyValue("$.", "key", "\"val\"", false)), //language=json - json(""" - { - "key": "x" - } + json( """ + { + "key": "x" + } + """ ) ); }