feat: add new JSON builtin function (`%json_set`)

From this request:
- https://github.com/plantuml/plantuml/issues/328#issuecomment-2089276726
This commit is contained in:
The-Lum 2024-05-03 17:06:30 +00:00
parent 34e3cfc9f4
commit dcfe17b73c
4 changed files with 244 additions and 0 deletions

View File

@ -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

View File

@ -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<String> namedArgument) {
return nbArg == 3;
}
@Override
public TValue executeReturnFunction(TContext context, TMemory memory, StringLocated location, List<TValue> values,
Map<String, TValue> 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);
}
}

View File

@ -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<TValue> 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<TValue> 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<TValue> 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<TValue> values = Arrays.asList(TValue.fromJson(input1), TValue.fromString(input2));

View File

@ -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);
}
}