From 1a8d5ad2b0d2716d6a45d8007b9acbb60da0c738 Mon Sep 17 00:00:00 2001 From: Peter Verhas Date: Thu, 23 Oct 2025 14:04:31 +0200 Subject: [PATCH] implementing macro as a feature, to implement context based evaluation, like in the built-in operator 'if'. --- README.md | 35 ++++++++++++ .../github/jamsesso/jsonlogic/JsonLogic.java | 6 ++ .../jamsesso/jsonlogic/evaluator/Macro.java | 16 ++++++ .../jsonlogic/evaluator/MacroArguments.java | 57 +++++++++++++++++++ .../UnEvaluatedArgumentsExpression.java | 35 ++++++++++++ .../jamsesso/jsonlogic/CustomMacroTests.java | 55 ++++++++++++++++++ 6 files changed, 204 insertions(+) create mode 100644 src/main/java/io/github/jamsesso/jsonlogic/evaluator/Macro.java create mode 100644 src/main/java/io/github/jamsesso/jsonlogic/evaluator/MacroArguments.java create mode 100644 src/main/java/io/github/jamsesso/jsonlogic/evaluator/expressions/UnEvaluatedArgumentsExpression.java create mode 100644 src/test/java/io/github/jamsesso/jsonlogic/CustomMacroTests.java diff --git a/README.md b/README.md index cb92eb5..130141a 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,41 @@ String result = (String) jsonLogic.apply("{\"greet\": [\"Sam\"]}", null); assert "Hello, Sam!".equals(result); ``` +You can add your own macros like so: + +```java +// a flag that shows evaluation was performed or not +final AtomicBoolean called = new AtomicBoolean(false); +// add an operation that sets the flag when called +jsonLogic.addOperation("side_effect", args -> { + called.set(true); + return null; + } + ); +// add a macro, evaluates the first argument if it is truthy, +// otherwise evaluates the second argument +jsonLogic.addMacro("unless", args -> { + if (args.size() > 0) { + final boolean condition = JsonLogic.truthy(args.evaluate(0)); + if (args.size() > 1 && !condition) { + return args.evaluate(1); + } else { + return null; + } + } + return null; + }); +called.set(false); +jsonLogic.apply("{\"unless\": [ false , {\"side_effect\": 0}]}", null); +assert called.get(); + +called.set(false); +jsonLogic.apply("{\"unless\": [ true , {\"side_effect\": 0}]}", null); +assert !called.get(); +``` + + + There is a `truthy` static method that mimics the truthy-ness rules of Javascript: ```java diff --git a/src/main/java/io/github/jamsesso/jsonlogic/JsonLogic.java b/src/main/java/io/github/jamsesso/jsonlogic/JsonLogic.java index fa25f2f..4921021 100644 --- a/src/main/java/io/github/jamsesso/jsonlogic/JsonLogic.java +++ b/src/main/java/io/github/jamsesso/jsonlogic/JsonLogic.java @@ -4,6 +4,7 @@ import io.github.jamsesso.jsonlogic.ast.JsonLogicParser; import io.github.jamsesso.jsonlogic.evaluator.JsonLogicEvaluator; import io.github.jamsesso.jsonlogic.evaluator.JsonLogicExpression; +import io.github.jamsesso.jsonlogic.evaluator.Macro; import io.github.jamsesso.jsonlogic.evaluator.expressions.*; import java.lang.reflect.Array; @@ -54,6 +55,11 @@ public JsonLogic() { addOperation(MissingExpression.SOME); } + public JsonLogic addMacro(String name, Macro macro) { + addOperation(new UnEvaluatedArgumentsExpression(name, macro)); + return this; + } + public JsonLogic addOperation(String name, Function function) { return addOperation(new PreEvaluatedArgumentsExpression() { @Override diff --git a/src/main/java/io/github/jamsesso/jsonlogic/evaluator/Macro.java b/src/main/java/io/github/jamsesso/jsonlogic/evaluator/Macro.java new file mode 100644 index 0000000..58d7cb2 --- /dev/null +++ b/src/main/java/io/github/jamsesso/jsonlogic/evaluator/Macro.java @@ -0,0 +1,16 @@ +package io.github.jamsesso.jsonlogic.evaluator; + +/** + * Represents a macro operation that can be evaluated within a JsonLogic context. + * A macro is a reusable, parameterized logic component that operates on provided arguments + * and evaluates to a resulting value. + *

+ * The evaluation of a macro is carried out using a {@link MacroArguments} object, which provides + * access to the arguments, their evaluated values, and contextual logic data. + *

+ * The `evaluate` method is designed to be implemented by any class adhering to this interface, + * allowing for custom logic evaluation handling. + */ +public interface Macro { + Object evaluate(MacroArguments evaluator) throws JsonLogicEvaluationException; +} diff --git a/src/main/java/io/github/jamsesso/jsonlogic/evaluator/MacroArguments.java b/src/main/java/io/github/jamsesso/jsonlogic/evaluator/MacroArguments.java new file mode 100644 index 0000000..d770891 --- /dev/null +++ b/src/main/java/io/github/jamsesso/jsonlogic/evaluator/MacroArguments.java @@ -0,0 +1,57 @@ +package io.github.jamsesso.jsonlogic.evaluator; + +import io.github.jamsesso.jsonlogic.ast.JsonLogicArray; +import io.github.jamsesso.jsonlogic.ast.JsonLogicNode; + +/** + * Represents a container for handling and evaluating macro arguments in the context of JsonLogic operations. + * This class provides utilities to evaluate specific arguments and determine the total number of arguments. + */ +public class MacroArguments { + private final JsonLogicEvaluator evaluator; + private final JsonLogicArray arguments; + private final Object data; + private final String jsonPath; + public MacroArguments(JsonLogicEvaluator evaluator, JsonLogicArray arguments, Object data, String jsonPath) { + this.evaluator = evaluator; + this.arguments = arguments; + this.data = data; + this.jsonPath = jsonPath; + } + + /** + * Evaluates the argument at the specified index using the provided evaluator, data, and JSON path context. + *

+ * Macro implementations invoke this method to get the evaluated value of each argument they need to evaluate. + * Macros can skip and ignore, OR evaluate each argument. + * Macros can evaluate arguments in any order. + * Macros can evaluate any argument multiple times. + * + * @param i the index of the argument to evaluate, starting at 0 and less than the total number of arguments + * @return the result of the evaluation as an Object + * @throws JsonLogicEvaluationException if an error occurs during evaluation + */ + public Object evaluate(int i) throws JsonLogicEvaluationException { + return evaluator.evaluate(arguments.get(i), data, String.format("%s[%d]", jsonPath, i)); + } + + /** + * Retrieves the argument at the specified index. + * + * @param i the index of the argument to retrieve, starting at zero and less than the total number of arguments + * @return the argument at the specified index as a {@code JsonLogicNode} + */ + public JsonLogicNode get(int i) { + return arguments.get(i); + } + + /** + * Get the total number of arguments. + * Indexing in {@link #evaluate(int)} goes from zero to {@code size() - 1}. + * + * @return the total number of arguments + */ + public int size(){ + return arguments.size(); + } +} diff --git a/src/main/java/io/github/jamsesso/jsonlogic/evaluator/expressions/UnEvaluatedArgumentsExpression.java b/src/main/java/io/github/jamsesso/jsonlogic/evaluator/expressions/UnEvaluatedArgumentsExpression.java new file mode 100644 index 0000000..b46ab83 --- /dev/null +++ b/src/main/java/io/github/jamsesso/jsonlogic/evaluator/expressions/UnEvaluatedArgumentsExpression.java @@ -0,0 +1,35 @@ +package io.github.jamsesso.jsonlogic.evaluator.expressions; + +import io.github.jamsesso.jsonlogic.ast.JsonLogicArray; +import io.github.jamsesso.jsonlogic.evaluator.*; + +/** + * This class represents an unevaluated arguments expression that uses a macro to delay + * the evaluation of arguments in a specific JsonLogic context. The expression defers the + * responsibility of argument evaluation to the provided macro. + *

+ * This class implements the {@link JsonLogicExpression} interface, enabling it to be + * used as a type of expression within the JsonLogic evaluation framework. + */ +public class UnEvaluatedArgumentsExpression implements JsonLogicExpression { + private final String operator; + private final Macro macro; + + public UnEvaluatedArgumentsExpression(String operator, + Macro macro) { + this.operator = operator; + this.macro = macro; + } + + @Override + public String key() { + return operator; + } + + @Override + public Object evaluate(JsonLogicEvaluator evaluator, JsonLogicArray arguments, Object data, + String jsonPath) + throws JsonLogicEvaluationException { + return macro.evaluate(new MacroArguments(evaluator,arguments,data,jsonPath)); + } +} diff --git a/src/test/java/io/github/jamsesso/jsonlogic/CustomMacroTests.java b/src/test/java/io/github/jamsesso/jsonlogic/CustomMacroTests.java new file mode 100644 index 0000000..a5c4e2c --- /dev/null +++ b/src/test/java/io/github/jamsesso/jsonlogic/CustomMacroTests.java @@ -0,0 +1,55 @@ +package io.github.jamsesso.jsonlogic; + +import org.hamcrest.core.Is; +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * The CustomMacroTests class contains unit tests for adding and using custom operations and macros + * with the JsonLogic library. It demonstrates extending JsonLogic by defining and applying custom + * behaviors to evaluate logical expressions. + *

+ * This test specifically adds a custom operation "side_effect" and a custom macro "unless". The "side_effect" + * operation sets a flag when invoked, demonstrating side-effect-based behavior. The "unless" macro evaluates + * a conditional expression that executes an operation only if the condition is false. + *

+ * The test verifies: + * - Whether the "side_effect" operation is called based on the "unless" macro's condition. + * - Various scenarios for the conditional logic handled by the "unless" macro. + *

+ * Exceptions: + * - JsonLogicException: Thrown if there is an error during the initialization or evaluation of JSON logic expressions. + * - JsonLogicEvaluationException: Thrown if there are issues during the evaluation of arguments or expressions. + */ +public class CustomMacroTests { + private static final JsonLogic jsonLogic = new JsonLogic(); + + @Test + public void testCustomMacro() throws JsonLogicException { + final AtomicBoolean called = new AtomicBoolean(false); + jsonLogic.addOperation("side_effect", args -> { + called.set(true); + return null; + } + ).addMacro("unless", args -> { + if (args.size() > 0) { + final boolean condition = JsonLogic.truthy(args.evaluate(0)); + if (args.size() > 1 && !condition) { + return args.evaluate(1); + } else { + return null; + } + } + return null; + }); + called.set(false); + jsonLogic.apply("{\"unless\": [ false , {\"side_effect\": 0}]}", null); + Assert.assertThat(called.get(), Is.is(true)); + + called.set(false); + jsonLogic.apply("{\"unless\": [ true , {\"side_effect\": 0}]}", null); + Assert.assertThat(called.get(), Is.is(false)); + } +}