Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/io/github/jamsesso/jsonlogic/JsonLogic.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Object[], Object> function) {
return addOperation(new PreEvaluatedArgumentsExpression() {
@Override
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/io/github/jamsesso/jsonlogic/evaluator/Macro.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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;
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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();
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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));
}
}
55 changes: 55 additions & 0 deletions src/test/java/io/github/jamsesso/jsonlogic/CustomMacroTests.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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));
}
}