diff --git a/src/net/sourceforge/plantuml/tim/TContext.java b/src/net/sourceforge/plantuml/tim/TContext.java index c3edbd42e..0f1c37ae8 100644 --- a/src/net/sourceforge/plantuml/tim/TContext.java +++ b/src/net/sourceforge/plantuml/tim/TContext.java @@ -112,6 +112,7 @@ import net.sourceforge.plantuml.tim.stdlib.IsDark; import net.sourceforge.plantuml.tim.stdlib.IsLight; import net.sourceforge.plantuml.tim.stdlib.JsonAdd; import net.sourceforge.plantuml.tim.stdlib.JsonKeyExists; +import net.sourceforge.plantuml.tim.stdlib.JsonMerge; import net.sourceforge.plantuml.tim.stdlib.JsonRemove; import net.sourceforge.plantuml.tim.stdlib.JsonSet; import net.sourceforge.plantuml.tim.stdlib.Lighten; @@ -224,6 +225,7 @@ public class TContext { functionsSet.addFunction(new JsonAdd()); functionsSet.addFunction(new JsonRemove()); functionsSet.addFunction(new JsonSet()); + functionsSet.addFunction(new JsonMerge()); // %standard_exists_function // %str_replace // !exit diff --git a/src/net/sourceforge/plantuml/tim/stdlib/JsonMerge.java b/src/net/sourceforge/plantuml/tim/stdlib/JsonMerge.java new file mode 100644 index 000000000..f7d1cf74d --- /dev/null +++ b/src/net/sourceforge/plantuml/tim/stdlib/JsonMerge.java @@ -0,0 +1,92 @@ +/* ======================================================================== + * PlantUML : a free UML diagram generator + * ======================================================================== + * + * (C) Copyright 2009-2024, Arnaud Roques + * + * Project Info: https://plantuml.com + * + * If you like this project or if you find it useful, you can support us at: + * + * https://plantuml.com/patreon (only 1$ per month!) + * https://plantuml.com/paypal + * + * This file is part of PlantUML. + * + * PlantUML is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PlantUML distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public + * License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * + * Original Author: Arnaud Roques + * Contribution: The-Lum + * + */ +package net.sourceforge.plantuml.tim.stdlib; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.sourceforge.plantuml.json.JsonArray; +import net.sourceforge.plantuml.json.JsonObject; +import net.sourceforge.plantuml.json.JsonValue; +import net.sourceforge.plantuml.text.StringLocated; +import net.sourceforge.plantuml.tim.EaterException; +import net.sourceforge.plantuml.tim.TContext; +import net.sourceforge.plantuml.tim.TFunctionSignature; +import net.sourceforge.plantuml.tim.TMemory; +import net.sourceforge.plantuml.tim.expression.TValue; + +public class JsonMerge extends SimpleReturnFunction { + + public TFunctionSignature getSignature() { + return new TFunctionSignature("%json_merge", 2); + } + + @Override + public boolean canCover(int nbArg, Set namedArgument) { + return nbArg == 2; + } + + @Override + public TValue executeReturnFunction(TContext context, TMemory memory, StringLocated location, List values, + Map named) throws EaterException { + final TValue data0 = values.get(0); + if (!data0.isJson()) + throw new EaterException("Not JSON data", location); + final TValue data1 = values.get(1); + if (!data1.isJson()) + throw new EaterException("Not JSON data", location); + + final JsonValue json0 = data0.toJson(); + final JsonValue json1 = data1.toJson(); + + if ((!json0.isArray() && !json0.isObject() && !json1.isArray() && !json1.isObject()) + || ((json0.isArray() && json1.isObject()) || (json0.isObject() && json1.isArray()))) + return data0; + + if (json0.isArray() && json1.isArray()) { + for (JsonValue j1 : json1.asArray()) { + json0.asArray().add(j1); + } + return TValue.fromJson(json0); + } + if (json0.isObject() && json1.isObject()) { + json0.asObject().merge(json1.asObject()); + return TValue.fromJson(json0); + } + throw new EaterException("Bad JSON type", location); + } +} diff --git a/test/net/sourceforge/plantuml/tim/stdlib/JsonMergeTest.java b/test/net/sourceforge/plantuml/tim/stdlib/JsonMergeTest.java new file mode 100644 index 000000000..0cda83d8e --- /dev/null +++ b/test/net/sourceforge/plantuml/tim/stdlib/JsonMergeTest.java @@ -0,0 +1,93 @@ +package net.sourceforge.plantuml.tim.stdlib; + +import static net.sourceforge.plantuml.tim.TimTestUtils.assertTimExpectedOutputFromInput; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.IndicativeSentencesGeneration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.provider.CsvSource; + +import net.sourceforge.plantuml.json.JsonValue; +import net.sourceforge.plantuml.tim.EaterException; +import net.sourceforge.plantuml.tim.TFunction; +import test.utils.JunitUtils.StringJsonConverter; + +/** + * Tests the builtin function. + */ +@IndicativeSentencesGeneration(separator = ": ", generator = ReplaceUnderscores.class) + +class JsonMergeTest { + TFunction cut = new JsonMerge(); + final String cutName = "json_merge"; + final String paramTestName = "[{index}] " + cutName + "({0}, {1}) = {2}"; + + @ParameterizedTest(name = paramTestName) + @CsvSource(value = { + " [], [1], [1]", + " [0], [\"a\"], '[0,\"a\"]' ", + " [0], [{\"a\": 123}], '[0,{\"a\":123}]' ", + " [0], [1], '[0,1]' ", + " '[{\"a\":[1, 2]}]', [1], '[{\"a\":[1,2]},1]' ", + + }) + void Test_with_Array_Json(@ConvertWith(StringJsonConverter.class) JsonValue input1, @ConvertWith(StringJsonConverter.class) JsonValue input2, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, expected); + } + + @ParameterizedTest(name = "[{index}] " + cutName + "({0}, {1}, {2}) = {3}") + @CsvSource(value = { + " {}, {\"a\":1}, {\"a\":1}", + " {}, '{\"a\":[1,2,3]}', '{\"a\":[1,2,3]}'", + " {}, '{\"a\":{\"b\":123}}', '{\"a\":{\"b\":123}}'", + " {}, '{\"a\":{\"b\":\"abc\"}}', '{\"a\":{\"b\":\"abc\"}}'", + " {\"z\":0}, '{\"a\":1}', '{\"z\":0,\"a\":1}'", + " {\"z\":0}, '{\"a\":[1,2,3]}', '{\"z\":0,\"a\":[1,2,3]}'", + " {\"z\":0}, '{\"a\":{\"b\":123}}', '{\"z\":0,\"a\":{\"b\":123}}'", + " {\"z\":0}, '{\"a\":{\"b\":\"abc\"}}', '{\"z\":0,\"a\":{\"b\":\"abc\"}}'", + " '{\"a\": 1, \"b\": \"two\"}', {\"c\":3}, '{\"a\":1,\"b\":\"two\",\"c\":3}'", + " '{\"a\": 1, \"b\": \"two\"}', '{\"d\":{\"da\": 1, \"db\": \"two\"}}', '{\"a\":1,\"b\":\"two\",\"d\":{\"da\":1,\"db\":\"two\"}}'", + " {\"z\":0}, '{\"z\":1}', '{\"z\":1}'", + " {\"z\":0}, '{\"z\":[1,2,3]}', '{\"z\":[1,2,3]}'", + " {\"z\":0}, '{\"z\":{\"b\":123}}', '{\"z\":{\"b\":123}}'", + " {\"z\":0}, '{\"z\":{\"b\":\"abc\"}}', '{\"z\":{\"b\":\"abc\"}}'", + " '{\"a\": 1, \"b\": \"two\"}', {\"b\":3}, '{\"a\":1,\"b\":3}'", + + }) + void Test_with_Object_Json(@ConvertWith(StringJsonConverter.class) JsonValue input1, @ConvertWith(StringJsonConverter.class) JsonValue input2, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, expected); + } + + @Nested + class Not_Nominal_Test { + @ParameterizedTest(name = paramTestName) + @CsvSource(value = { + " [], {\"a\":1}, []", + " [0], {\"a\":1}, '[0]' ", + " [0], {\"a\":1}, '[0]' ", + " [0], {\"a\":1}, '[0]' ", + " '[{\"a\":[1, 2]}]', {\"a\":1}, '[{\"a\":[1,2]}]' ", + + }) + void Test_with_Array_Object_Json(@ConvertWith(StringJsonConverter.class) JsonValue input1, @ConvertWith(StringJsonConverter.class) JsonValue input2, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, expected); + } + + @ParameterizedTest(name = paramTestName) + @CsvSource(value = { + " {}, [], {} ", + " {\"a\":1}, [], {\"a\":1} ", + " '{\"a\":[1,2,3]}', [0], '{\"a\":[1,2,3]}' ", + " '{\"z\":0,\"a\":1}', [0], '{\"z\":0,\"a\":1}' ", + " '{\"z\":0,\"a\":1}', '[{\"a\":[1, 2]}]', '{\"z\":0,\"a\":1}' ", + + }) + void Test_with_Object_Array_Json(@ConvertWith(StringJsonConverter.class) JsonValue input1, @ConvertWith(StringJsonConverter.class) JsonValue input2, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, expected); + } + } +}