diff --git a/src/net/sourceforge/plantuml/tim/TContext.java b/src/net/sourceforge/plantuml/tim/TContext.java index f9fb996c7..c3edbd42e 100644 --- a/src/net/sourceforge/plantuml/tim/TContext.java +++ b/src/net/sourceforge/plantuml/tim/TContext.java @@ -113,6 +113,7 @@ 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.JsonRemove; +import net.sourceforge.plantuml.tim.stdlib.JsonSet; import net.sourceforge.plantuml.tim.stdlib.Lighten; import net.sourceforge.plantuml.tim.stdlib.LoadJson; import net.sourceforge.plantuml.tim.stdlib.LogicalAnd; @@ -222,6 +223,7 @@ public class TContext { functionsSet.addFunction(new Str2Json()); functionsSet.addFunction(new JsonAdd()); functionsSet.addFunction(new JsonRemove()); + functionsSet.addFunction(new JsonSet()); // %standard_exists_function // %str_replace // !exit diff --git a/src/net/sourceforge/plantuml/tim/stdlib/JsonSet.java b/src/net/sourceforge/plantuml/tim/stdlib/JsonSet.java new file mode 100644 index 000000000..79b6487b4 --- /dev/null +++ b/src/net/sourceforge/plantuml/tim/stdlib/JsonSet.java @@ -0,0 +1,91 @@ +/* ======================================================================== + * 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 JsonSet extends SimpleReturnFunction { + + public TFunctionSignature getSignature() { + return new TFunctionSignature("%json_set", 3); + } + + @Override + public boolean canCover(int nbArg, Set namedArgument) { + return nbArg == 3; + } + + @Override + public TValue executeReturnFunction(TContext context, TMemory memory, StringLocated location, List values, + Map named) throws EaterException { + final TValue data = values.get(0); + if (!data.isJson()) + throw new EaterException("Not JSON data", location); + + final JsonValue json = data.toJson(); + + if (!json.isArray() && !json.isObject()) + return data; + if (json.isArray()) { + if (values.get(1).isNumber()) { + final Integer index = values.get(1).toInt(); + final JsonValue value = values.get(2).toJsonValue(); + if (0 <= index && index < json.asArray().size()) + json.asArray().set(index, value); + } + return TValue.fromJson(json); + } + if (json.isObject()) { + final String name = values.get(1).toString(); + final JsonValue value = values.get(2).toJsonValue(); + json.asObject().set(name, value); + return TValue.fromJson(json); + } + throw new EaterException("Bad JSON type", location); + } +} diff --git a/test/net/sourceforge/plantuml/tim/TimTestUtils.java b/test/net/sourceforge/plantuml/tim/TimTestUtils.java index 7a48c1086..abe3a1bef 100644 --- a/test/net/sourceforge/plantuml/tim/TimTestUtils.java +++ b/test/net/sourceforge/plantuml/tim/TimTestUtils.java @@ -69,6 +69,27 @@ public class TimTestUtils { assertEquals(expected, tValue.toString()); } + // Tfunc: (JsonValue, Int, Int) -> (String) + public static void assertTimExpectedOutputFromInput(TFunction func, JsonValue input1, Integer input2, Integer input3, String expected) throws EaterException { + final List values = Arrays.asList(TValue.fromJson(input1), TValue.fromInt(input2), TValue.fromInt(input3)); + final TValue tValue = func.executeReturnFunction(null, null, null, values, null); + assertEquals(expected, tValue.toString()); + } + + // Tfunc: (JsonValue, Int, String) -> (String) + public static void assertTimExpectedOutputFromInput(TFunction func, JsonValue input1, Integer input2, String input3, String expected) throws EaterException { + final List values = Arrays.asList(TValue.fromJson(input1), TValue.fromInt(input2), TValue.fromString(input3)); + final TValue tValue = func.executeReturnFunction(null, null, null, values, null); + assertEquals(expected, tValue.toString()); + } + + // Tfunc: (JsonValue, Int, JsonValue) -> (String) + public static void assertTimExpectedOutputFromInput(TFunction func, JsonValue input1, Integer input2, JsonValue input3, String expected) throws EaterException { + final List values = Arrays.asList(TValue.fromJson(input1), TValue.fromInt(input2), TValue.fromJson(input3)); + final TValue tValue = func.executeReturnFunction(null, null, null, values, null); + assertEquals(expected, tValue.toString()); + } + // Tfunc: (JsonValue, String) -> (String) public static void assertTimExpectedOutputFromInput(TFunction func, JsonValue input1, String input2, String expected) throws EaterException { final List values = Arrays.asList(TValue.fromJson(input1), TValue.fromString(input2)); diff --git a/test/net/sourceforge/plantuml/tim/stdlib/JsonSetTest.java b/test/net/sourceforge/plantuml/tim/stdlib/JsonSetTest.java new file mode 100644 index 000000000..b070a4147 --- /dev/null +++ b/test/net/sourceforge/plantuml/tim/stdlib/JsonSetTest.java @@ -0,0 +1,130 @@ +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.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 JsonSetTest { + TFunction cut = new JsonSet(); + final String cutName = "json_set"; + final String paramTestName = "[{index}] " + cutName + "({0}, {1}, {2}) = {3}"; + + @ParameterizedTest(name = paramTestName) + @CsvSource(value = { + " [0], 0, 1, [1]", + " [0], 0, \"a\", '[\"a\"]' ", + " [0], 0, {\"a\": 123}, '[{\"a\":123}]' ", + " [0], 0, [1], '[[1]]' ", + " '[{\"a\":[1, 2]}]', 0, 1, '[1]' ", + + }) + void Test_with_Array_Json(@ConvertWith(StringJsonConverter.class) JsonValue input1, Integer input2, @ConvertWith(StringJsonConverter.class) JsonValue input3, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, input3, expected); + } + + @ParameterizedTest(name = paramTestName) + @CsvSource(value = { + " [0], 0, -1, '[\"-1\"]' ", + " [0], 0, 1, '[\"1\"]' ", + " [0], 0, 123, '[\"123\"]' ", + " [0], 0, a, '[\"a\"]' ", + " [0], 0, \"a\", '[\"\\\"a\\\"\"]' ", + " [0], 0, a b c, '[\"a b c\"]' ", + " [0], 0, \"a b c\", '[\"\\\"a b c\\\"\"]' ", + " '[0,1]', 1, -1, '[0,\"-1\"]' ", + " '[0,1]', 1, 1, '[0,\"1\"]' ", + " '[0,1]', 1, 123, '[0,\"123\"]' ", + " '[0,1]', 1, a, '[0,\"a\"]' ", + " '[0,1]', 1, \"a\", '[0,\"\\\"a\\\"\"]' ", + " '[0,1]', 1, a b c, '[0,\"a b c\"]' ", + " '[0,1]', 1, \"a b c\", '[0,\"\\\"a b c\\\"\"]' ", + " '[{\"a\":[1, 2]}]', 0, 1, '[\"1\"]' ", + " '[{\"a\":[1, 2]}]', 0, a, '[\"a\"]' ", + " '[{\"a\":[1, 2]}, 1]', 1, 1, '[{\"a\":[1,2]},\"1\"]' ", + " '[{\"a\":[1, 2]}, 1]', 1, a, '[{\"a\":[1,2]},\"a\"]' ", + }) + void Test_with_Array_Json_add_Str(@ConvertWith(StringJsonConverter.class) JsonValue input1, Integer input2, String input3, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, input3, expected); + } + + @ParameterizedTest(name = paramTestName) + @CsvSource(value = { + " [0], 0, -1, [-1]", + " [0], 0, 1, [1]", + " [0], 0, 123, '[123]' ", + " '[{\"a\":[1, 2]}]', 0, 1, '[1]' ", + " '[{\"a\":[1, 2]}]', 0, 123, '[123]' ", + }) + void Test_with_Array_Json_add_Int(@ConvertWith(StringJsonConverter.class) JsonValue input1, Integer input2, Integer input3, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, input3, expected); + } + + @ParameterizedTest(name = paramTestName) + @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\"}}'", + " {\"a\":0}, a, 1, '{\"a\":1}'", + " {\"a\":0}, a, '[1,2,3]', '{\"a\":[1,2,3]}'", + " {\"a\":0}, a, '{\"b\":123}', '{\"a\":{\"b\":123}}'", + " {\"a\":0}, a, '{\"b\":\"abc\"}', '{\"a\":{\"b\":\"abc\"}}'", + " '{\"a\": 1, \"b\": \"two\"}', b, 3, '{\"a\":1,\"b\":3}'", + " '{\"a\": 1, \"b\": \"two\"}', b, '{\"da\": 1, \"db\": \"two\"}', '{\"a\":1,\"b\":{\"da\":1,\"db\":\"two\"}}'", + " '{\"a\":0, \"a\":5}', a, 1, '{\"a\":0,\"a\":1}'", + " '{\"a\":0, \"a\":5}', a, '[1,2,3]', '{\"a\":0,\"a\":[1,2,3]}'", + " '{\"a\":0, \"a\":5}', a, '{\"b\":123}', '{\"a\":0,\"a\":{\"b\":123}}'", + " '{\"a\":0, \"a\":5}', a, '{\"b\":\"abc\"}', '{\"a\":0,\"a\":{\"b\":\"abc\"}}'", + " '{\"a\": 1, \"b\": 5, \"b\": \"two\"}', b, 3, '{\"a\":1,\"b\":5,\"b\":3}'", + " '{\"a\": 1, \"b\": 5, \"b\": \"two\"}', b, '{\"da\": 1, \"db\": \"two\"}', '{\"a\":1,\"b\":5,\"b\":{\"da\":1,\"db\":\"two\"}}'", + + }) + void Test_with_Object_Json(@ConvertWith(StringJsonConverter.class) JsonValue input1, String input2, @ConvertWith(StringJsonConverter.class) JsonValue input3, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, input3, expected); + } + + @ParameterizedTest(name = paramTestName) + @CsvSource(value = { + " {}, a, 1, {\"a\":\"1\"}", + " {}, a, 'abc', '{\"a\":\"abc\"}'", + " {}, a, 'a b c', '{\"a\":\"a b c\"}'", + " {\"age\" : 30}, name, Sally, '{\"age\":30,\"name\":\"Sally\"}'", + " '{\"age\" : 30, \"name\":\"Bob\"}', name, Sally, '{\"age\":30,\"name\":\"Sally\"}'", + }) + void Test_with_Object_Json_add_Str(@ConvertWith(StringJsonConverter.class) JsonValue input1, String input2, String input3, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, input3, expected); + } + + @ParameterizedTest(name = paramTestName) + @CsvSource(value = { + " {}, a, 1, {\"a\":1}", + " {}, a, 123, '{\"a\":123}'", + " {\"age\" : 30}, name, 123, '{\"age\":30,\"name\":123}'", + " '{\"age\" : 30, \"name\":\"Bob\"}', name, 123, '{\"age\":30,\"name\":123}'", + }) + void Test_with_Object_Json_add_Int(@ConvertWith(StringJsonConverter.class) JsonValue input1, String input2, Integer input3, String expected) throws EaterException { + assertTimExpectedOutputFromInput(cut, input1, input2, input3, expected); + } +}