[JOHNZON-313] implement json-logic based on JSON-P
diff --git a/johnzon-jsonlogic/pom.xml b/johnzon-jsonlogic/pom.xml
new file mode 100644
index 0000000..95dfdfa
--- /dev/null
+++ b/johnzon-jsonlogic/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>johnzon</artifactId>
+ <groupId>org.apache.johnzon</groupId>
+ <version>1.2.7-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>johnzon-jsonlogic</artifactId>
+ <name>Johnzon :: JSON Logic</name>
+ <packaging>bundle</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.johnzon</groupId>
+ <artifactId>johnzon-core</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/johnzon-jsonlogic/src/main/java/org/apache/johnzon/jsonlogic/JohnzonJsonLogic.java b/johnzon-jsonlogic/src/main/java/org/apache/johnzon/jsonlogic/JohnzonJsonLogic.java
new file mode 100644
index 0000000..d77c878
--- /dev/null
+++ b/johnzon-jsonlogic/src/main/java/org/apache/johnzon/jsonlogic/JohnzonJsonLogic.java
@@ -0,0 +1,665 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonlogic;
+
+import org.apache.johnzon.jsonlogic.spi.Operator;
+
+import javax.json.JsonArray;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonBuilderFactory;
+import javax.json.JsonException;
+import javax.json.JsonNumber;
+import javax.json.JsonObject;
+import javax.json.JsonPointer;
+import javax.json.JsonString;
+import javax.json.JsonStructure;
+import javax.json.JsonValue;
+import javax.json.spi.JsonProvider;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiPredicate;
+import java.util.stream.Collector;
+import java.util.stream.DoubleStream;
+import java.util.stream.Stream;
+
+import static java.util.Collections.emptyMap;
+import static java.util.stream.Collectors.joining;
+
+public class JohnzonJsonLogic {
+ private final JsonProvider provider;
+ private final Map<String, Operator> operators = new HashMap<>();
+ private final Map<String, JsonPointer> pointers = new HashMap<>();
+ private final JsonBuilderFactory builderFactory;
+ private boolean cachePointers;
+
+ public JohnzonJsonLogic() {
+ this(JsonProvider.provider());
+ registerDefaultOperators();
+ }
+
+ public JohnzonJsonLogic(final JsonProvider provider) {
+ this.provider = provider;
+ this.builderFactory = provider.createBuilderFactory(emptyMap());
+ }
+
+ public JohnzonJsonLogic cachePointers() {
+ this.cachePointers = true;
+ return this;
+ }
+
+ public JohnzonJsonLogic registerOperator(final String name, final Operator impl) {
+ operators.put(name, impl);
+ return this;
+ }
+
+ public JsonValue apply(final JsonValue logic, final JsonValue args) {
+ if (logic.getValueType() != JsonValue.ValueType.OBJECT) {
+ return logic;
+ }
+
+ final JsonObject object = logic.asJsonObject();
+ if (object.size() > 1) {
+ return object;
+ }
+
+ final Set<String> keys = object.keySet();
+ if (keys.size() != 1) {
+ throw new IllegalArgumentException("Invaid argument, multiple keys found: " + keys);
+ }
+ final String operator = keys.iterator().next();
+ final Operator impl = operators.get(operator);
+ if (impl == null) {
+ throw new IllegalArgumentException("Missing operator '" + operator + "'");
+ }
+ return impl.apply(this, object.get(operator), args);
+ }
+
+ public boolean isTruthy(final JsonValue value) {
+ return !isFalsy(value);
+ }
+
+ public boolean isFalsy(final JsonValue value) {
+ switch (value.getValueType()) {
+ case NUMBER:
+ return JsonNumber.class.cast(value).intValue() == 0;
+ case ARRAY:
+ return value.asJsonArray().isEmpty();
+ case STRING:
+ return JsonString.class.cast(value).getString().isEmpty();
+ case FALSE:
+ case NULL:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public boolean areEqualsWithCoercion(final JsonValue a, final JsonValue b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null) {
+ return false;
+ }
+ if (b == null) {
+ return false;
+ }
+ if (a.getValueType() == b.getValueType()) {
+ return a.equals(b);
+ }
+ switch (a.getValueType()) {
+ case STRING:
+ switch (b.getValueType()) {
+ case NUMBER:
+ try {
+ return Double.parseDouble(JsonString.class.cast(a).getString()) == JsonNumber.class.cast(b).doubleValue();
+ } catch (final NumberFormatException nfe) {
+ return false;
+ }
+ case TRUE:
+ case FALSE:
+ return isFalsy(a) == isFalsy(b);
+ default:
+ return false;
+ }
+ case NUMBER:
+ switch (b.getValueType()) {
+ case STRING:
+ try {
+ return Double.parseDouble(JsonString.class.cast(b).getString()) == JsonNumber.class.cast(a).doubleValue();
+ } catch (final NumberFormatException nfe) {
+ return false;
+ }
+ case TRUE:
+ case FALSE:
+ default:
+ return isFalsy(a) == isFalsy(b);
+ }
+ case TRUE:
+ case FALSE:
+ return isFalsy(a) == isFalsy(b);
+ default:
+ return false;
+ }
+ }
+
+ // to not depend on a logger we don't register "log" operation but it is trivial to do:
+ public JohnzonJsonLogic registerDefaultOperators() {
+ registerOperator("log", (logic, config, params) -> {
+ throw new UnsupportedOperationException("Log is not supported by default, register the following operator with your preferred logger:\n\n" +
+ "jsonLogic.registerOperator(\"log\", (l, c, p) -> log.info(String.valueOf(l.apply(c, p)));\n");
+ });
+ registerOperator("var", (logic, config, params) -> varImpl(config, params));
+ registerOperator("missing", this::missingImpl);
+ registerOperator("missing_some", this::missingSomeImpl);
+ registerOperator("if", this::ifImpl);
+ registerOperator("<", (logic, config, params) -> numericComparison((a, b) -> a < b, config, logic, params));
+ registerOperator(">", (logic, config, params) -> numericComparison((a, b) -> a > b, config, logic, params));
+ registerOperator("<=", (logic, config, params) -> numericComparison((a, b) -> a <= b, config, logic, params));
+ registerOperator(">=", (logic, config, params) -> numericComparison((a, b) -> a >= b, config, logic, params));
+ registerOperator("==", (logic, config, params) -> comparison(this::areEqualsWithCoercion, config, logic, params));
+ registerOperator("!=", (logic, config, params) -> comparison((a, b) -> !areEqualsWithCoercion(a, b), config, logic, params));
+ registerOperator("===", (logic, config, params) -> comparison(Objects::equals, config, logic, params));
+ registerOperator("!==", (logic, config, params) -> comparison((a, b) -> !Objects.equals(a, b), config, logic, params));
+ registerOperator("!", this::notImpl);
+ registerOperator("!!", this::toBooleanImpl);
+ registerOperator("or", this::orImpl);
+ registerOperator("and", this::andImpl);
+ registerOperator("min", this::minImpl);
+ registerOperator("max", this::maxImpl);
+ registerOperator("+", this::plusImpl);
+ registerOperator("*", this::multiplyImpl);
+ registerOperator("-", this::minusImpl);
+ registerOperator("/", this::divideImpl);
+ registerOperator("%", this::moduloImpl);
+ registerOperator("map", this::mapImpl);
+ registerOperator("filter", this::filterImpl);
+ registerOperator("reduce", this::reduceImpl);
+ registerOperator("all", (logic, config, params) ->
+ arrayTest(logic, config, params, (subConf, stream) -> stream.allMatch(it -> isTruthy(logic.apply(subConf, it)))));
+ registerOperator("some", (logic, config, params) ->
+ arrayTest(logic, config, params, (subConf, stream) -> stream.anyMatch(it -> isTruthy(logic.apply(subConf, it)))));
+ registerOperator("none", (logic, config, params) ->
+ arrayTest(logic, config, params, (subConf, stream) -> stream.noneMatch(it -> isTruthy(logic.apply(subConf, it)))));
+ registerOperator("merge", (logic, config, params) -> mergeImpl(config));
+ registerOperator("in", this::inImpl);
+ registerOperator("cat", this::catImpl);
+ registerOperator("substr", this::substrImpl);
+ return this;
+ }
+
+ private JsonValue minImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("min only supports arrays: '" + config + "'");
+ }
+ return provider.createValue(mapToDouble(logic, config, params).min().orElse(0));
+ }
+
+ private JsonValue maxImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("max only supports arrays: '" + config + "'");
+ }
+ return provider.createValue(mapToDouble(logic, config, params).max().orElse(0));
+ }
+
+ private JsonValue plusImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ return castToNumber(logic.apply(config, params));
+ }
+ if (config.asJsonArray().isEmpty()) {
+ return provider.createValue(0);
+ }
+ return provider.createValue(mapToDouble(logic, config, params).sum());
+ }
+
+ private JsonValue multiplyImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("* only supports arrays: '" + config + "'");
+ }
+ if (config.asJsonArray().isEmpty()) {
+ return provider.createValue(0);
+ }
+ return provider.createValue(mapToDouble(logic, config, params)
+ .reduce(1, (a, b) -> a * b));
+ }
+
+ private JsonValue minusImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() == JsonValue.ValueType.ARRAY) {
+ final JsonArray array = config.asJsonArray();
+ if (array.size() != 2) {
+ throw new IllegalArgumentException("- only supports arrays with 2 elements: '" + config + "'");
+ }
+ return provider.createValue(JsonNumber.class.cast(logic.apply(array.get(0), params)).doubleValue() -
+ JsonNumber.class.cast(logic.apply(array.get(1), params)).doubleValue());
+ }
+ final JsonValue applied = logic.apply(config, params);
+ if (applied.getValueType() == JsonValue.ValueType.NUMBER) {
+ return provider.createValue(-1 * JsonNumber.class.cast(applied).doubleValue());
+ }
+ throw new IllegalArgumentException("Unsupported - operation: '" + config + "'");
+ }
+
+ private JsonValue divideImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() == JsonValue.ValueType.ARRAY) {
+ final JsonArray array = config.asJsonArray();
+ if (array.size() != 2) {
+ throw new IllegalArgumentException("/ only supports arrays with 2 elements: '" + config + "'");
+ }
+ return provider.createValue(JsonNumber.class.cast(logic.apply(array.get(0), params)).doubleValue() /
+ JsonNumber.class.cast(logic.apply(array.get(1), params)).doubleValue());
+ }
+ throw new IllegalArgumentException("Unsupported / operation: '" + config + "'");
+ }
+
+ private JsonValue moduloImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() == JsonValue.ValueType.ARRAY) {
+ final JsonArray array = config.asJsonArray();
+ if (array.size() != 2) {
+ throw new IllegalArgumentException("% only supports arrays with 2 elements: '" + config + "'");
+ }
+ return provider.createValue(JsonNumber.class.cast(logic.apply(array.get(0), params)).doubleValue() %
+ JsonNumber.class.cast(logic.apply(array.get(1), params)).doubleValue());
+ }
+ throw new IllegalArgumentException("Unsupported % operation: '" + config + "'");
+ }
+
+ private JsonValue mapImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() == JsonValue.ValueType.ARRAY) {
+ final JsonArray array = config.asJsonArray();
+ if (array.size() != 2) {
+ throw new IllegalArgumentException("map only supports arrays with 2 elements: '" + config + "'");
+ }
+ final JsonValue items = logic.apply(array.get(0), params);
+ if (items.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("Expected '" + array.get(0) + "' to be an array, got " + items.getValueType());
+ }
+ final JsonValue subLogic = array.get(1);
+ return items.asJsonArray().stream()
+ .map(it -> logic.apply(subLogic, it))
+ .collect(toArray());
+ }
+ throw new IllegalArgumentException("Unsupported map operation: '" + config + "'");
+ }
+
+ private JsonValue filterImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() == JsonValue.ValueType.ARRAY) {
+ final JsonArray array = config.asJsonArray();
+ if (array.size() != 2) {
+ throw new IllegalArgumentException("filter only supports arrays with 2 elements: '" + config + "'");
+ }
+ final JsonValue items = logic.apply(array.get(0), params);
+ if (items.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("Expected '" + array.get(0) + "' to be an array, got " + items.getValueType());
+ }
+ final JsonValue subLogic = array.get(1);
+ return items.asJsonArray().stream()
+ .filter(it -> isTruthy(logic.apply(subLogic, it)))
+ .collect(toArray());
+ }
+ throw new IllegalArgumentException("Unsupported filter operation: '" + config + "'");
+ }
+
+ private JsonValue mergeImpl(final JsonValue config) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("merge only support an array as configuration, got '" + config + "'");
+ }
+ return config.asJsonArray().stream()
+ .flatMap(it -> it.getValueType() == JsonValue.ValueType.ARRAY ?
+ it.asJsonArray().stream() : builderFactory.createArrayBuilder().add(it).build().stream())
+ .collect(toArray());
+ }
+
+ private JsonValue substrImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY || config.asJsonArray().size() < 2) {
+ throw new IllegalArgumentException("substr only support an array as configuration, got '" + config + "'");
+ }
+ final JsonArray array = config.asJsonArray();
+ final JsonValue value = logic.apply(array.get(0), params);
+ if (value.getValueType() != JsonValue.ValueType.STRING) {
+ throw new IllegalArgumentException("expected a string for substr, got '" + value + "'");
+ }
+ final String valueStr = JsonString.class.cast(value).getString();
+ final JsonValue from = logic.apply(array.get(1), params);
+ if (from.getValueType() != JsonValue.ValueType.NUMBER) {
+ throw new IllegalArgumentException("expected a number for substr, got '" + from + "'");
+ }
+ final int fromIdx = JsonNumber.class.cast(from).intValue();
+ final int start;
+ if (fromIdx < 0) {
+ start = valueStr.length() + fromIdx;
+ } else {
+ start = fromIdx;
+ }
+ final int end;
+ if (array.size() == 3) {
+ final JsonValue to = logic.apply(array.get(2), params);
+ if (to.getValueType() != JsonValue.ValueType.NUMBER) {
+ throw new IllegalArgumentException("expected a number for substr, got '" + to + "'");
+ }
+ final int length = JsonNumber.class.cast(to).intValue();
+ end = length < 0 ? valueStr.length() + length : start + length;
+ } else {
+ end = valueStr.length();
+ }
+ return provider.createValue(valueStr.substring(start, end));
+ }
+
+ private JsonValue catImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("cat only support an array of string elements as configuration, got '" + config + "'");
+ }
+ return provider.createValue(config.asJsonArray().stream()
+ .map(it -> logic.apply(it, params))
+ .filter(it -> it.getValueType() == JsonValue.ValueType.STRING)
+ .map(it -> JsonString.class.cast(it).getString())
+ .collect(joining()));
+ }
+
+ private JsonValue inImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY || config.asJsonArray().size() != 2) {
+ throw new IllegalArgumentException("in only support an array of 2 elements as configuration, got '" + config + "'");
+ }
+ final JsonArray array = config.asJsonArray();
+ final JsonValue expected = logic.apply(array.get(0), params);
+ final JsonValue value = logic.apply(array.get(1), params);
+ switch (value.getValueType()) {
+ case STRING:
+ return expected.getValueType() == JsonValue.ValueType.STRING && JsonString.class.cast(value).getString()
+ .contains(JsonString.class.cast(expected).getString()) ? JsonValue.TRUE : JsonValue.FALSE;
+ case ARRAY:
+ return value.getValueType() == JsonValue.ValueType.ARRAY && value.asJsonArray().stream()
+ .anyMatch(it -> Objects.equals(it, expected)) ? JsonValue.TRUE : JsonValue.FALSE;
+ default:
+ return JsonValue.FALSE;
+ }
+ }
+
+ private JsonValue reduceImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() == JsonValue.ValueType.ARRAY) {
+ final JsonArray array = config.asJsonArray();
+ if (array.size() < 2 || array.size() > 3) {
+ throw new IllegalArgumentException("filter only supports arrays with 2 or 3 elements: '" + config + "'");
+ }
+ final JsonValue items = logic.apply(array.get(0), params);
+ if (items.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("Expected '" + array.get(0) + "' to be an array, got " + items.getValueType());
+ }
+ final JsonValue subLogic = array.get(1);
+ return items.asJsonArray().stream()
+ .reduce(
+ array.size() == 3 ? array.get(2) : JsonValue.NULL,
+ (accumulator, current) -> logic.apply(subLogic, builderFactory.createObjectBuilder()
+ .add("accumulator", accumulator)
+ .add("current", current)
+ .build()));
+ }
+ throw new IllegalArgumentException("Unsupported reduce operation: '" + config + "'");
+ }
+
+ private JsonValue andImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("and only supports arrays: '" + config + "'");
+ }
+ final JsonArray array = config.asJsonArray();
+ return array.stream()
+ .map(it -> logic.apply(it, params))
+ .filter(this::isFalsy)
+ .findFirst()
+ .orElseGet(() -> array.isEmpty() ? JsonValue.FALSE : array.get(array.size() - 1));
+ }
+
+ private JsonValue orImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("or only supports arrays: '" + config + "'");
+ }
+ final JsonArray array = config.asJsonArray();
+ return array.stream()
+ .map(it -> logic.apply(it, params))
+ .filter(this::isTruthy)
+ .findFirst()
+ .orElseGet(() -> array.isEmpty() ? JsonValue.FALSE : array.get(array.size() - 1));
+ }
+
+ private JsonValue toBooleanImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() == JsonValue.ValueType.ARRAY) {
+ final JsonArray array = config.asJsonArray();
+ if (array.size() != 1) {
+ throw new IllegalArgumentException("!! takes only one parameter '" + config + "'");
+ }
+ return isTruthy(logic.apply(array.get(0), params)) ? JsonValue.TRUE : JsonValue.FALSE;
+ }
+ return isTruthy(logic.apply(config, params)) ? JsonValue.TRUE : JsonValue.FALSE;
+ }
+
+ private JsonValue notImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() == JsonValue.ValueType.ARRAY) {
+ final JsonArray array = config.asJsonArray();
+ if (array.size() != 1) {
+ throw new IllegalArgumentException("! takes only one parameter '" + config + "'");
+ }
+ return isFalsy(logic.apply(array.get(0), params)) ? JsonValue.TRUE : JsonValue.FALSE;
+ }
+ return isFalsy(logic.apply(config, params)) ? JsonValue.TRUE : JsonValue.FALSE;
+ }
+
+ private JsonValue ifImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("if config must be an array");
+ }
+ final JsonArray configArray = config.asJsonArray();
+ if (configArray.size() < 2) {
+ throw new IllegalArgumentException("if config must be an array >= 2 elements");
+ }
+ for (int i = 0; i < configArray.size() - 1; i += 2) {
+ if (isTruthy(logic.apply(configArray.get(i), params))) {
+ return logic.apply(configArray.get(i + 1), params);
+ }
+ }
+ if (configArray.size() % 2 == 1) {
+ return configArray.get(configArray.size() - 1);
+ }
+ return JsonValue.FALSE;
+ }
+
+ private JsonValue missingSomeImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("missing_some takes an array as parameter: '" + config + "'");
+ }
+ final JsonArray configArray = config.asJsonArray();
+ if (configArray.size() != 2) {
+ throw new IllegalArgumentException("missing_some takes an array with a number and a path array as parameter: '" + config + "'");
+ }
+ final JsonArray tested = configArray.get(1).asJsonArray();
+ final JsonArray missing = tested.stream()
+ .filter(it -> varImpl(logic.apply(it, params), params) == JsonValue.NULL)
+ .collect(toArray());
+ if ((tested.size() - missing.size()) < JsonNumber.class.cast(logic.apply(configArray.get(0), params)).intValue()) {
+ return missing;
+ }
+ return JsonValue.EMPTY_JSON_ARRAY;
+ }
+
+ private JsonValue missingImpl(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("missing takes an array as parameter: '" + config + "'");
+ }
+ return config.asJsonArray().stream()
+ .filter(it -> varImpl(logic.apply(it, params), params) == JsonValue.NULL)
+ .collect(toArray());
+ }
+
+ private JsonValue arrayTest(final JohnzonJsonLogic self, final JsonValue config, final JsonValue params,
+ final BiPredicate<JsonValue, Stream<JsonValue>> tester) {
+ if (config.getValueType() == JsonValue.ValueType.ARRAY) {
+ final JsonArray array = config.asJsonArray();
+ if (array.size() != 2) {
+ throw new IllegalArgumentException("array test only supports arrays with 2: '" + config + "'");
+ }
+ final JsonValue items = self.apply(array.get(0), params);
+ if (items.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("Expected '" + array.get(0) + "' to be an array, got " + items.getValueType());
+ }
+ final JsonValue subLogic = array.get(1);
+ return tester.test(subLogic, items.asJsonArray().stream()) ? JsonValue.TRUE : JsonValue.FALSE;
+ }
+ throw new IllegalArgumentException("Unsupported array test operation: '" + config + "'");
+ }
+
+ private JsonValue castToNumber(final JsonValue value) {
+ switch (value.getValueType()) {
+ case NUMBER:
+ return value;
+ case STRING:
+ return provider.createValue(Double.parseDouble(JsonString.class.cast(value).getString()));
+ default:
+ throw new IllegalArgumentException("Unsupported value to number: '" + value + "'");
+ }
+ }
+
+ private DoubleStream mapToDouble(final JohnzonJsonLogic logic, final JsonValue config, final JsonValue params) {
+ return config.asJsonArray().stream()
+ .map(it -> logic.apply(it, params))
+ .filter(it -> it.getValueType() == JsonValue.ValueType.NUMBER)
+ .mapToDouble(it -> JsonNumber.class.cast(it).doubleValue());
+ }
+
+ private JsonValue comparison(final BiPredicate<JsonValue, JsonValue> comparator,
+ final JsonValue config, final JohnzonJsonLogic self,
+ final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("comparison config must be an array");
+ }
+ final JsonArray values = config.asJsonArray();
+ if (values.size() != 2) {
+ throw new IllegalArgumentException("comparison requires 2 arguments");
+ }
+ final JsonValue first = self.apply(values.get(0), params);
+ final JsonValue second = self.apply(values.get(1), params);
+ return comparator.test(first, second) ? JsonValue.TRUE : JsonValue.FALSE;
+ }
+
+ private JsonValue numericComparison(final BiPredicate<Double, Double> comparator,
+ final JsonValue config, final JohnzonJsonLogic self,
+ final JsonValue params) {
+ if (config.getValueType() != JsonValue.ValueType.ARRAY) {
+ throw new IllegalArgumentException("numeric comparison config must be an array");
+ }
+ final JsonArray configArray = config.asJsonArray();
+ switch (configArray.size()) {
+ case 2: {
+ final JsonValue first = self.apply(configArray.get(0), params);
+ final JsonValue second = self.apply(configArray.get(1), params);
+ if (Stream.of(first, second).anyMatch(it -> it.getValueType() != JsonValue.ValueType.NUMBER)) {
+ throw new IllegalArgumentException("Only numbers can be compared: " + first + " / " + second);
+ }
+ return comparator.test(JsonNumber.class.cast(first).doubleValue(), JsonNumber.class.cast(second).doubleValue()) ?
+ JsonValue.TRUE : JsonValue.FALSE;
+ }
+ case 3: { // between
+ final JsonValue first = self.apply(configArray.get(0), params);
+ final JsonValue second = self.apply(configArray.get(1), params);
+ final JsonValue third = self.apply(configArray.get(1), params);
+ if (Stream.of(first, second, third).anyMatch(it -> it.getValueType() != JsonValue.ValueType.NUMBER)) {
+ throw new IllegalArgumentException("Only numbers can be compared");
+ }
+ return comparator.test(JsonNumber.class.cast(first).doubleValue(), JsonNumber.class.cast(second).doubleValue()) &&
+ comparator.test(JsonNumber.class.cast(second).doubleValue(), JsonNumber.class.cast(third).doubleValue()) ?
+ JsonValue.TRUE : JsonValue.FALSE;
+ }
+ default:
+ throw new IllegalArgumentException("numeric comparison config must be an array >= 2 elements");
+ }
+ }
+
+ private JsonValue varImpl(final JsonValue config, final JsonValue params) {
+ switch (config.getValueType()) {
+ case ARRAY:
+ final JsonArray values = config.asJsonArray();
+ if (values.isEmpty()) {
+ throw new IllegalArgumentException("var should have at least one parameter");
+ }
+ final JsonValue accessor = apply(values.get(0), params);
+ switch (accessor.getValueType()) {
+ case NUMBER:
+ final int index = JsonNumber.class.cast(accessor).intValue();
+ final JsonArray array = params.asJsonArray();
+ final JsonValue arrayAttribute = index >= array.size() ? null : array.get(index);
+ return arrayAttribute == null ? (values.size() > 1 ? apply(values.get(1), params) : JsonValue.NULL) : arrayAttribute;
+ case STRING:
+ final JsonValue objectAttribute = extractValue(params, JsonString.class.cast(accessor).getString());
+ return objectAttribute == JsonValue.NULL && values.size() > 1 ? apply(values.get(1), params) : objectAttribute;
+ default:
+ throw new IllegalArgumentException("Unsupported var first paraemter: '" + accessor + "', should be string or number");
+ }
+ case STRING:
+ return extractValue(params, JsonString.class.cast(config).getString());
+ case NUMBER:
+ final int index = JsonNumber.class.cast(config).intValue();
+ final JsonArray array = params.asJsonArray();
+ final JsonValue arrayAttribute = array.size() <= index ? null : array.get(index);
+ return arrayAttribute == null ? JsonValue.NULL : arrayAttribute;
+ case OBJECT:
+ return varImpl(apply(config, params), params);
+ default:
+ throw new IllegalArgumentException("Unsupported configuration for var: '" + config + "'");
+ }
+ }
+
+ private JsonValue extractValue(final JsonValue params, final String string) {
+ if (string.isEmpty()) {
+ return params;
+ }
+ final JsonValue objectAttribute;
+ if (string.contains(".")) {
+ try {
+ objectAttribute = toPointer(string).getValue(JsonStructure.class.cast(params));
+ } catch (final JsonException je) { // missing
+ return JsonValue.NULL;
+ }
+ } else if (params.getValueType() == JsonValue.ValueType.OBJECT) {
+ objectAttribute = params.asJsonObject().get(string);
+ } else if (params.getValueType() == JsonValue.ValueType.ARRAY) {
+ objectAttribute = params.asJsonArray().get(Integer.parseInt(string.trim()));
+ } else {
+ objectAttribute = null;
+ }
+ return objectAttribute == null ? JsonValue.NULL : objectAttribute;
+ }
+
+ // cache?
+ private JsonPointer toPointer(final String string) {
+ if (cachePointers) {
+ return pointers.computeIfAbsent(string, this::doToPointer);
+ }
+ return doToPointer(string);
+ }
+
+ private JsonPointer doToPointer(final String string) {
+ return provider.createPointer(
+ (!string.startsWith("/") ? "/" : "") +
+ string.replace('.', '/'));
+ }
+
+ // same as JsonCollector one except it uses this builderFactory instead of default one which goes through the SPI
+ private Collector<JsonValue, JsonArrayBuilder, JsonArray> toArray() {
+ return Collector.of(builderFactory::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::addAll, JsonArrayBuilder::build);
+ }
+}
diff --git a/johnzon-jsonlogic/src/main/java/org/apache/johnzon/jsonlogic/spi/Operator.java b/johnzon-jsonlogic/src/main/java/org/apache/johnzon/jsonlogic/spi/Operator.java
new file mode 100644
index 0000000..8531e0b
--- /dev/null
+++ b/johnzon-jsonlogic/src/main/java/org/apache/johnzon/jsonlogic/spi/Operator.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonlogic.spi;
+
+import org.apache.johnzon.jsonlogic.JohnzonJsonLogic;
+
+import javax.json.JsonValue;
+
+@FunctionalInterface
+public interface Operator {
+ JsonValue apply(JohnzonJsonLogic logic, JsonValue config, JsonValue params);
+}
diff --git a/johnzon-jsonlogic/src/test/java/org/apache/johnzon/jsonlogic/JohnzonJsonLogicTest.java b/johnzon-jsonlogic/src/test/java/org/apache/johnzon/jsonlogic/JohnzonJsonLogicTest.java
new file mode 100644
index 0000000..49295b3
--- /dev/null
+++ b/johnzon-jsonlogic/src/test/java/org/apache/johnzon/jsonlogic/JohnzonJsonLogicTest.java
@@ -0,0 +1,982 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.johnzon.jsonlogic;
+
+import org.junit.Test;
+
+import javax.json.Json;
+import javax.json.JsonBuilderFactory;
+import javax.json.JsonObject;
+import javax.json.JsonValue;
+
+import static java.util.Collections.emptyMap;
+import static org.junit.Assert.assertEquals;
+
+public class JohnzonJsonLogicTest {
+ private final JohnzonJsonLogic jsonLogic = new JohnzonJsonLogic();
+ private final JsonBuilderFactory builderFactory = Json.createBuilderFactory(emptyMap());
+
+ @Test
+ public void varObjectString() {
+ assertEquals(Json.createValue("b"), jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("var", "a")
+ .build(),
+ builderFactory.createObjectBuilder()
+ .add("a", "b")
+ .add("c", "d")
+ .build()));
+ }
+
+ @Test
+ public void varObjectPtr() {
+ assertEquals(Json.createValue("ok"), jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("var", "a.b.0")
+ .build(),
+ builderFactory.createObjectBuilder()
+ .add("a", builderFactory.createObjectBuilder()
+ .add("b", builderFactory.createArrayBuilder()
+ .add("ok")))
+ .build()));
+ }
+
+ @Test
+ public void varObjectStringMissing() {
+ assertEquals(JsonValue.NULL, jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("var", "a")
+ .build(),
+ builderFactory.createObjectBuilder()
+ .add("c", "d")
+ .build()));
+ }
+
+ @Test
+ public void varArrayInt() {
+ assertEquals(Json.createValue("b"), jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("var", 1)
+ .build(),
+ builderFactory.createArrayBuilder()
+ .add("a")
+ .add("b")
+ .build()));
+ }
+
+ @Test
+ public void varObjectDefault() {
+ assertEquals(Json.createValue(26), jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("var", builderFactory.createArrayBuilder().add("z").add(26))
+ .build(),
+ builderFactory.createObjectBuilder()
+ .add("a", "b")
+ .add("c", "d")
+ .build()));
+ }
+
+ @Test
+ public void varArrayDefault() {
+ assertEquals(Json.createValue(26), jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("var", builderFactory.createArrayBuilder().add(10).add(26))
+ .build(),
+ builderFactory.createArrayBuilder()
+ .add("a")
+ .add("b")
+ .build()));
+ }
+
+ @Test
+ public void missing() {
+ final JsonObject value = builderFactory.createObjectBuilder()
+ .add("a", 1)
+ .add("b", 2)
+ .build();
+ assertEquals(
+ JsonValue.EMPTY_JSON_ARRAY,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("missing",
+ builderFactory.createArrayBuilder()
+ .add("a")
+ .add("b"))
+ .build(),
+ value));
+ assertEquals(
+ builderFactory.createArrayBuilder()
+ .add("c")
+ .build(),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("missing", builderFactory.createArrayBuilder()
+ .add("a")
+ .add("b")
+ .add("c"))
+ .build(),
+ value));
+ }
+
+ @Test
+ public void missingSome() {
+ final JsonObject value = builderFactory.createObjectBuilder()
+ .add("a", 1)
+ .add("b", 2)
+ .build();
+ assertEquals(
+ JsonValue.EMPTY_JSON_ARRAY,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("missing_some",
+ builderFactory.createArrayBuilder()
+ .add(1)
+ .add(builderFactory.createArrayBuilder()
+ .add("a")
+ .add("c")
+ .add("d")))
+ .build(),
+ value));
+ assertEquals(
+ builderFactory.createArrayBuilder()
+ .add("c")
+ .add("d")
+ .build(),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("missing_some", builderFactory.createArrayBuilder()
+ .add(2)
+ .add(builderFactory.createArrayBuilder()
+ .add("a")
+ .add("c")
+ .add("d")))
+ .build(),
+ value));
+ }
+
+ @Test
+ public void ifStatic() {
+ assertEquals(
+ Json.createValue("yes"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("if",
+ builderFactory.createArrayBuilder()
+ .add(true)
+ .add("yes")
+ .add("false"))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ Json.createValue("no"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("if",
+ builderFactory.createArrayBuilder()
+ .add(false)
+ .add("yes")
+ .add("no"))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void ifElsIfElseWithVarEval() {
+ assertEquals(
+ Json.createValue("liquid"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("if",
+ builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("<", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(0)))
+ .add("freezing")
+ .add(builderFactory.createObjectBuilder()
+ .add("<", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(100)))
+ .add("liquid")
+ .add("gas"))
+ .build(),
+ builderFactory.createObjectBuilder().add("temp", 55).build()));
+ }
+
+ @Test
+ public void lessThan() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("<", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(100))
+ .build(),
+ builderFactory.createObjectBuilder().add("temp", 99).build()));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("<", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(100))
+ .build(),
+ builderFactory.createObjectBuilder().add("temp", 100).build()));
+ }
+
+ @Test
+ public void lessOrEqualsThan() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("<=", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(100))
+ .build(),
+ builderFactory.createObjectBuilder().add("temp", 100).build()));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("<=", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(100))
+ .build(),
+ builderFactory.createObjectBuilder().add("temp", 101).build()));
+ }
+
+ @Test
+ public void greaterThan() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add(">", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(100))
+ .build(),
+ builderFactory.createObjectBuilder().add("temp", 101).build()));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add(">", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(100))
+ .build(),
+ builderFactory.createObjectBuilder().add("temp", 100).build()));
+ }
+
+ @Test
+ public void greaterOrEqualsThan() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add(">=", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(100))
+ .build(),
+ builderFactory.createObjectBuilder().add("temp", 100).build()));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add(">=", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "temp"))
+ .add(100))
+ .build(),
+ builderFactory.createObjectBuilder().add("temp", 99).build()));
+ }
+
+ @Test
+ public void equalsCoercion() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("==", builderFactory.createArrayBuilder()
+ .add(1)
+ .add(1))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("==", builderFactory.createArrayBuilder()
+ .add("1")
+ .add(1))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("==", builderFactory.createArrayBuilder()
+ .add(1)
+ .add("1"))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("==", builderFactory.createArrayBuilder()
+ .add(0)
+ .add(false))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("==", builderFactory.createArrayBuilder()
+ .add(1)
+ .add(false))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void equalsNoCoercion() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("===", builderFactory.createArrayBuilder()
+ .add(1)
+ .add(1))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("===", builderFactory.createArrayBuilder()
+ .add("1")
+ .add(1))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("===", builderFactory.createArrayBuilder()
+ .add(1)
+ .add("1"))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("===", builderFactory.createArrayBuilder()
+ .add(0)
+ .add(false))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("===", builderFactory.createArrayBuilder()
+ .add(1)
+ .add(false))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void negate() {
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("!", true)
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("!", builderFactory.createArrayBuilder().add(true).build())
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void booleanEvaluation() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("!!", true)
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("!!", "a")
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("!!", "")
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("!!", builderFactory.createArrayBuilder().add(true).build())
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void and() {
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("and", builderFactory.createArrayBuilder()
+ .add(false)
+ .add(true))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ Json.createValue("a"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("and", builderFactory.createArrayBuilder()
+ .add(true)
+ .add("a"))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void or() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("or", builderFactory.createArrayBuilder()
+ .add(false)
+ .add(true))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("or", builderFactory.createArrayBuilder()
+ .add(true)
+ .add("a"))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ Json.createValue("a"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("or", builderFactory.createArrayBuilder()
+ .add("a")
+ .add(true))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ Json.createValue("a"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("or", builderFactory.createArrayBuilder()
+ .add(false)
+ .add(JsonValue.EMPTY_JSON_ARRAY)
+ .add("a")
+ .add(true))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void min() {
+ assertEquals(
+ Json.createValue(100.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("min", builderFactory.createArrayBuilder()
+ .add(100)
+ .add(200)
+ .add(300))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void max() {
+ assertEquals(
+ Json.createValue(300.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("max", builderFactory.createArrayBuilder()
+ .add(100)
+ .add(200)
+ .add(300))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void plus() {
+ assertEquals(
+ Json.createValue(6.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("+", builderFactory.createArrayBuilder()
+ .add(4)
+ .add(2))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ Json.createValue(3.14),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("+", "3.14")
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ Json.createValue(7.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("+", builderFactory.createArrayBuilder()
+ .add(4)
+ .add(2)
+ .add(1))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void minus() {
+ assertEquals(
+ Json.createValue(2.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("-", builderFactory.createArrayBuilder()
+ .add(4)
+ .add(2))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ Json.createValue(-2.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("-", 2)
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void multiply() {
+ assertEquals(
+ Json.createValue(8.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("*", builderFactory.createArrayBuilder()
+ .add(4)
+ .add(2))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ Json.createValue(24.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("*", builderFactory.createArrayBuilder()
+ .add(4)
+ .add(3)
+ .add(2))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void divide() {
+ assertEquals(
+ Json.createValue(2.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("/", builderFactory.createArrayBuilder()
+ .add(4)
+ .add(2))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void modulo() {
+ assertEquals(
+ Json.createValue(0.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("%", builderFactory.createArrayBuilder()
+ .add(4)
+ .add(2))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ assertEquals(
+ Json.createValue(1.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("%", builderFactory.createArrayBuilder()
+ .add(5)
+ .add(2))
+ .build(),
+ JsonValue.EMPTY_JSON_OBJECT));
+ }
+
+ @Test
+ public void map() {
+ assertEquals(
+ builderFactory.createArrayBuilder()
+ .add(2.)
+ .add(4.)
+ .add(6.)
+ .add(8.)
+ .add(10.)
+ .build(),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("map", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "integers"))
+ .add(builderFactory.createObjectBuilder()
+ .add("*", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", ""))
+ .add(2))))
+ .build(),
+ builderFactory.createObjectBuilder()
+ .add("integers", builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3)
+ .add(4)
+ .add(5))
+ .build()));
+ }
+
+ @Test
+ public void filter() {
+ assertEquals(
+ builderFactory.createArrayBuilder()
+ .add(1)
+ .add(3)
+ .add(5)
+ .build(),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("filter", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "integers"))
+ .add(builderFactory.createObjectBuilder()
+ .add("%", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", ""))
+ .add(2))))
+ .build(),
+ builderFactory.createObjectBuilder()
+ .add("integers", builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3)
+ .add(4)
+ .add(5))
+ .build()));
+ }
+
+ @Test
+ public void reduce() {
+ assertEquals(
+ Json.createValue(15.),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("reduce", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "integers"))
+ .add(builderFactory.createObjectBuilder()
+ .add("+", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "current"))
+ .add(builderFactory.createObjectBuilder()
+ .add("var", "accumulator"))))
+ .add(0))
+ .build(),
+ builderFactory.createObjectBuilder()
+ .add("integers", builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3)
+ .add(4)
+ .add(5))
+ .build()));
+ }
+
+ @Test
+ public void all() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("all", builderFactory.createArrayBuilder()
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3))
+ .add(builderFactory.createObjectBuilder()
+ .add(">", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", ""))
+ .add(0))))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("all", builderFactory.createArrayBuilder()
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3))
+ .add(builderFactory.createObjectBuilder()
+ .add("<", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", ""))
+ .add(3))))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ }
+
+ @Test
+ public void some() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("some", builderFactory.createArrayBuilder()
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3))
+ .add(builderFactory.createObjectBuilder()
+ .add(">", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", ""))
+ .add(2))))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("some", builderFactory.createArrayBuilder()
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3))
+ .add(builderFactory.createObjectBuilder()
+ .add(">", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", ""))
+ .add(3))))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ }
+
+ @Test
+ public void none() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("none", builderFactory.createArrayBuilder()
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3))
+ .add(builderFactory.createObjectBuilder()
+ .add(">", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", ""))
+ .add(3))))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("none", builderFactory.createArrayBuilder()
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3))
+ .add(builderFactory.createObjectBuilder()
+ .add("<", builderFactory.createArrayBuilder()
+ .add(builderFactory.createObjectBuilder()
+ .add("var", ""))
+ .add(2))))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ }
+
+ @Test
+ public void merge() {
+ assertEquals(
+ builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3)
+ .add("4")
+ .build(),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("merge", builderFactory.createArrayBuilder()
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2))
+ .add(3)
+ .add("4"))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ }
+
+ @Test
+ public void in() {
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("in", builderFactory.createArrayBuilder()
+ .add(2)
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("in", builderFactory.createArrayBuilder()
+ .add(3)
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2)))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ assertEquals(
+ JsonValue.TRUE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("in", builderFactory.createArrayBuilder()
+ .add("ay")
+ .add("may"))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ assertEquals(
+ JsonValue.FALSE,
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("in", builderFactory.createArrayBuilder()
+ .add("cem")
+ .add("may"))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ }
+
+ @Test
+ public void cat() {
+ assertEquals(
+ Json.createValue("hello json"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("cat", builderFactory.createArrayBuilder()
+ .add("hell")
+ .add("o json"))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ }
+
+ @Test
+ public void substr() {
+ assertEquals(
+ Json.createValue("logic"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("substr", builderFactory.createArrayBuilder()
+ .add("jsonlogic")
+ .add(4))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ assertEquals(
+ Json.createValue("logic"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("substr", builderFactory.createArrayBuilder()
+ .add("jsonlogic")
+ .add(-5))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ assertEquals(
+ Json.createValue("son"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("substr", builderFactory.createArrayBuilder()
+ .add("jsonlogic")
+ .add(1)
+ .add(3))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ assertEquals(
+ Json.createValue("log"),
+ jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("substr", builderFactory.createArrayBuilder()
+ .add("jsonlogic")
+ .add(4)
+ .add(-2))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 34965f8..9ebcf46 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,6 +64,7 @@
<module>johnzon-json-extras</module>
<module>johnzon-jsonschema</module>
<module>johnzon-osgi</module>
+ <module>johnzon-jsonlogic</module>
</modules>
<dependencyManagement>
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index aba8f91..3ee6c0e 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -535,6 +535,48 @@
* Doesn't support references in the schema
* Doesn't support: dependencies, propertyNames, if/then/else, allOf/anyOf/oneOf/not, format validations
+### JSON Logic
+
+<pre class="prettyprint linenums"><![CDATA[
+<dependency>
+ <groupId>org.apache.johnzon</groupId>
+ <artifactId>johnzon-jsonlogic</artifactId>
+ <version>${johnzon.version}</version>
+</dependency>
+<dependency> <!-- requires an implementation of JSON-P -->
+ <groupId>org.apache.johnzon</groupId>
+ <artifactId>johnzon-core</artifactId>
+ <version>${johnzon.version}</version>
+</dependency>
+]]></pre>
+
+This module provides a way to execute any [JSON Logic](http://jsonlogic.com/) expression.
+
+<pre class="prettyprint linenums"><![CDATA[
+final JohnzonJsonLogic jsonLogic = new JohnzonJsonLogic();
+final JsonValue result = jsonLogic.apply(
+ builderFactory.createObjectBuilder()
+ .add("merge", builderFactory.createArrayBuilder()
+ .add(builderFactory.createArrayBuilder()
+ .add(1)
+ .add(2))
+ .add(3)
+ .add("4"))
+ .build(),
+ JsonValue.EMPTY_JSON_ARRAY);
+]]></pre>
+
+Default operators are supported - except "log" one to let you pick the logger (impl + name) you want.
+
+To register a custom operator just do it on your json logic instance:
+
+<pre class="prettyprint linenums"><![CDATA[
+final JohnzonJsonLogic jsonLogic = new JohnzonJsonLogic();
+jsonLogic.registerOperator(
+ "log",
+ (jsonLogic, config, args) -> log.info(String.valueOf(jsonLogic.apply(config, args)));
+]]></pre>
+
### OSGi JAX-RS Whiteboard
Though Johnzon artifacts are OSGi bundles to begin with, this module provides further integration with the [OSGi JAX-RS Whiteboard](https://osgi.org/specification/osgi.cmpn/7.0.0/service.jaxrs.html) and [OSGi CDI Integration](https://osgi.org/specification/osgi.enterprise/7.0.0/service.cdi.html) specifications.