From ec21842f572d4db090831966074bec320bb962a0 Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Mon, 2 Jun 2025 21:50:34 -0500 Subject: [PATCH] patch: allow for adding values to arrays --- README.md | 9 +++++++ .../itzg/helpers/patch/PatchSetProcessor.java | 8 ++++++ .../patch/model/PatchAddOperation.java | 25 +++++++++++++++++ .../helpers/patch/model/PatchOperation.java | 3 ++- .../helpers/patch/PatchSetProcessorTest.java | 27 +++++++++++++++++++ .../resources/patch/testing-with-array.json | 5 ++++ 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/main/java/me/itzg/helpers/patch/model/PatchAddOperation.java create mode 100644 src/test/resources/patch/testing-with-array.json diff --git a/README.md b/README.md index ade4bbf9..b5513450 100644 --- a/README.md +++ b/README.md @@ -1225,6 +1225,15 @@ Example: } ``` +#### `$add` + +The `$add` operation allows for adding a value to an array. + +- `$add` + - `path` : The [JSON path](https://github.com/json-path/JsonPath#readme) to the array + - `value` : The value to add. If the given value is a string, variable placeholders of the form `${...}` will be replaced from the environment variables and the resulting string can be converted by setting value-type. + - `value-type` : **optional** [see below](#valuetype) + ### ValueType One of the following identifiers or can be prefixed with `list of ` to indicate a list of the identified type: diff --git a/src/main/java/me/itzg/helpers/patch/PatchSetProcessor.java b/src/main/java/me/itzg/helpers/patch/PatchSetProcessor.java index 2cc906c9..b8c6e35c 100644 --- a/src/main/java/me/itzg/helpers/patch/PatchSetProcessor.java +++ b/src/main/java/me/itzg/helpers/patch/PatchSetProcessor.java @@ -17,6 +17,7 @@ import me.itzg.helpers.env.Interpolator; import me.itzg.helpers.env.Interpolator.Result; import me.itzg.helpers.env.MissingVariablesException; +import me.itzg.helpers.patch.model.PatchAddOperation; import me.itzg.helpers.patch.model.PatchDefinition; import me.itzg.helpers.patch.model.PatchOperation; import me.itzg.helpers.patch.model.PatchPutOperation; @@ -151,6 +152,13 @@ private void applyOps(Map data, List ops) throws "put", putOp + " at " + putOp.getPath(), obj -> doc.put(putOp.getPath(), putOp.getKey(), obj) ); + } else if (op instanceof PatchAddOperation) { + final PatchAddOperation addOp = (PatchAddOperation) op; + processValueType( + addOp.getValue(), addOp.getValueType(), + "add", addOp + " at " + addOp.getPath(), + obj -> doc.add(addOp.getPath(), obj) + ); } } } diff --git a/src/main/java/me/itzg/helpers/patch/model/PatchAddOperation.java b/src/main/java/me/itzg/helpers/patch/model/PatchAddOperation.java new file mode 100644 index 00000000..e88e75b3 --- /dev/null +++ b/src/main/java/me/itzg/helpers/patch/model/PatchAddOperation.java @@ -0,0 +1,25 @@ +package me.itzg.helpers.patch.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class PatchAddOperation extends PatchOperation { + @JsonProperty(required = true) + @JsonPropertyDescription("The JSON path to the array to add to.") + String path; + + @JsonProperty(required = true) + @JsonPropertyDescription(VALUE_DESCRIPTION) + JsonNode value; + + @JsonProperty("value-type") + @JsonPropertyDescription(VALUE_TYPE_DESCRIPTION) + @JsonInclude(JsonInclude.Include.NON_NULL) + String valueType; +} diff --git a/src/main/java/me/itzg/helpers/patch/model/PatchOperation.java b/src/main/java/me/itzg/helpers/patch/model/PatchOperation.java index 56b507ed..e26fbadf 100644 --- a/src/main/java/me/itzg/helpers/patch/model/PatchOperation.java +++ b/src/main/java/me/itzg/helpers/patch/model/PatchOperation.java @@ -6,7 +6,8 @@ @JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME) @JsonSubTypes({ @JsonSubTypes.Type(value = PatchSetOperation.class, name = "$set"), - @JsonSubTypes.Type(value = PatchPutOperation.class, name = "$put") + @JsonSubTypes.Type(value = PatchPutOperation.class, name = "$put"), + @JsonSubTypes.Type(value = PatchAddOperation.class, name = "$add"), }) public abstract class PatchOperation { protected static final String VALUE_DESCRIPTION = "The value to set." + diff --git a/src/test/java/me/itzg/helpers/patch/PatchSetProcessorTest.java b/src/test/java/me/itzg/helpers/patch/PatchSetProcessorTest.java index 063652d2..9f13ffa6 100644 --- a/src/test/java/me/itzg/helpers/patch/PatchSetProcessorTest.java +++ b/src/test/java/me/itzg/helpers/patch/PatchSetProcessorTest.java @@ -3,6 +3,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; +import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; import static uk.org.webcompere.modelassert.json.JsonAssertions.assertYaml; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -19,6 +20,7 @@ import java.util.stream.Stream; import me.itzg.helpers.env.EnvironmentVariablesProvider; import me.itzg.helpers.env.Interpolator; +import me.itzg.helpers.patch.model.PatchAddOperation; import me.itzg.helpers.patch.model.PatchDefinition; import me.itzg.helpers.patch.model.PatchPutOperation; import me.itzg.helpers.patch.model.PatchSet; @@ -74,6 +76,31 @@ void setInJson(String file, boolean allowComments, @TempDir Path tempDir) throws ); } + @Test + void addToArray(@TempDir Path tempDir) throws IOException { + final Path src = tempDir.resolve("testing.json"); + Files.copy(Paths.get("src/test/resources/patch/testing-with-array.json"), src); + + final PatchSetProcessor processor = new PatchSetProcessor( + new Interpolator(environmentVariablesProvider, "CFG_") + ); + + processor.process(new PatchSet() + .setPatches(singletonList( + new PatchDefinition() + .setFile(src.toString()) + .setOps(singletonList( + new PatchAddOperation() + .setPath("$.outer.array") + .setValue(new TextNode("new value")) + )) + )) + ); + + assertJson(src) + .at("/outer/array/0").hasValue("new value"); + } + @Test void setInJson5(@TempDir Path tempDir) throws IOException { final Path src = tempDir.resolve("testing.json5"); diff --git a/src/test/resources/patch/testing-with-array.json b/src/test/resources/patch/testing-with-array.json new file mode 100644 index 00000000..c4888527 --- /dev/null +++ b/src/test/resources/patch/testing-with-array.json @@ -0,0 +1,5 @@ +{ + "outer": { + "array": [] + } +} \ No newline at end of file