This is an automated email from the ASF dual-hosted git repository. rmannibucau pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/johnzon.git
The following commit(s) were added to refs/heads/master by this push: new 51050c4 [JOHNZON-313] implement json-logic based on JSON-P 51050c4 is described below commit 51050c423d3ee18415aa9ec4d4cc820a5e63e6d4 Author: Romain Manni-Bucau <rmannibu...@gmail.com> AuthorDate: Tue May 26 12:20:21 2020 +0200 [JOHNZON-313] implement json-logic based on JSON-P --- johnzon-jsonlogic/pom.xml | 40 + .../apache/johnzon/jsonlogic/JohnzonJsonLogic.java | 665 ++++++++++++++ .../org/apache/johnzon/jsonlogic/spi/Operator.java | 28 + .../johnzon/jsonlogic/JohnzonJsonLogicTest.java | 982 +++++++++++++++++++++ pom.xml | 1 + src/site/markdown/index.md | 42 + 6 files changed, 1758 insertions(+) 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 @@ Known limitations are (feel free to do a PR on github to add these missing featu * 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.