diff --git a/core/src/main/java/eu/jonahbauer/json/JsonNumber.java b/core/src/main/java/eu/jonahbauer/json/JsonNumber.java index 1101c52..762c170 100644 --- a/core/src/main/java/eu/jonahbauer/json/JsonNumber.java +++ b/core/src/main/java/eu/jonahbauer/json/JsonNumber.java @@ -10,6 +10,9 @@ import org.jetbrains.annotations.Nullable; * numbers and therefore all numbers are stored as {@code double}. */ public record JsonNumber(double value) implements JsonValue, JsonToken { + public static final @NotNull JsonNumber ZERO = new JsonNumber(0); + public static final @NotNull JsonNumber ONE = new JsonNumber(1); + public JsonNumber { if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite"); } diff --git a/query/build.gradle.kts b/query/build.gradle.kts new file mode 100644 index 0000000..a64b964 --- /dev/null +++ b/query/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + `java-library` +} + +group = "eu.jonahbauer.chat" +version = "1.0-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(22) + } +} + +dependencies { + api(project(":core")) + api(libs.annotations) + + compileOnly(libs.lombok) + annotationProcessor(libs.lombok) + + testImplementation(libs.junit.jupiter) + testRuntimeOnly(libs.junit.platform.launcher) +} + +tasks { + withType { + options.encoding = "UTF-8" + options.compilerArgs.add("--enable-preview") + } + + withType { + useJUnitPlatform() + jvmArgs("--enable-preview") + } +} \ No newline at end of file diff --git a/query/src/main/java/eu/jonahbauer/json/query/JsonMath.java b/query/src/main/java/eu/jonahbauer/json/query/JsonMath.java new file mode 100644 index 0000000..f6b6259 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/JsonMath.java @@ -0,0 +1,1517 @@ +package eu.jonahbauer.json.query; + +import eu.jonahbauer.json.*; +import eu.jonahbauer.json.exceptions.JsonParserException; +import eu.jonahbauer.json.query.parser.ast.*; +import eu.jonahbauer.json.query.util.Util; +import lombok.experimental.UtilityClass; +import org.intellij.lang.annotations.MagicConstant; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.*; + +@UtilityClass +@SuppressWarnings("preview") +public class JsonMath { + private static final @NotNull Comparator<@Nullable JsonValue> COMPARATOR = JsonMath::compare; + + // + public static @Nullable JsonValue add(@Nullable JsonValue first, @Nullable JsonValue second) { + if (first instanceof JsonNumber(double x) && second instanceof JsonNumber(double y)) { + return new JsonNumber(x + y); + } else if (first instanceof JsonArray x && second instanceof JsonArray y) { + var out = new ArrayList(x.size() + y.size()); + out.addAll(x); + out.addAll(y); + return new JsonArray(out); + } else if (first instanceof JsonString(var x) && second instanceof JsonString(var y)) { + return new JsonString(x + y); + } else if (first instanceof JsonObject x && second instanceof JsonObject y) { + var out = new HashMap<>(x); + out.putAll(y); + return new JsonObject(out); + } else if (first == null) { + return second; + } else if (second == null) { + return first; + } else { + throw error(first, second, "cannot be added"); + } + } + + public static @NotNull JsonValue sub(@Nullable JsonValue first, @Nullable JsonValue second) { + if (first instanceof JsonNumber(double x) && second instanceof JsonNumber(double y)) { + return new JsonNumber(x - y); + } else if (first instanceof JsonArray x && second instanceof JsonArray y) { + var out = new ArrayList<>(x); + out.removeAll(y); + return new JsonArray(out); + } else { + throw error(first, second, "cannot be subtracted"); + } + } + + public static @Nullable JsonValue mul(@Nullable JsonValue first, @Nullable JsonValue second) { + if (first instanceof JsonNumber(double x) && second instanceof JsonNumber(double y)) { + return new JsonNumber(x * y); + } else if (first instanceof JsonNumber(double n) && second instanceof JsonString(var str)) { + if (n == 0) return JsonString.EMPTY; + if (n < 0 || Math.ceil(n) > Integer.MAX_VALUE) return null; + return new JsonString(str.repeat((int) Math.ceil(n))); + } else if (first instanceof JsonString(var str) && second instanceof JsonNumber(var n)) { + if (n == 0) return JsonString.EMPTY; + if (n < 0 || Math.ceil(n) > Integer.MAX_VALUE) return null; + return new JsonString(str.repeat((int) Math.ceil(n))); + } else if (first instanceof JsonObject x && second instanceof JsonObject y) { + var out = new HashMap<>(x); + y.forEach((key, value) -> out.merge(key, value, (a, b) -> + a instanceof JsonObject && b instanceof JsonObject ? mul(a, b) : b + )); + return new JsonObject(out); + } else { + throw error(first, second, "cannot be multiplied"); + } + } + + public static @NotNull JsonValue div(@Nullable JsonValue first, @Nullable JsonValue second) { + if (first instanceof JsonNumber(double x) && second instanceof JsonNumber(double y)) { + if (y == 0) throw error(first, second, "cannot be divided because the divisor is zero"); + return new JsonNumber(x / y); + } else if (first instanceof JsonString(var x) && second instanceof JsonString(var y)) { + return new JsonArray(Arrays.stream(x.split(Pattern.quote(y), -1)).map(JsonValue::valueOf).toList()); + } else { + throw error(first, second, "cannot be divided"); + } + } + + public static @NotNull JsonNumber mod(@Nullable JsonValue first, @Nullable JsonValue second) { + if (first instanceof JsonNumber(double x) && second instanceof JsonNumber(double y)) { + if (y == 0) throw error(first, second, "cannot be divided (remainder) because the divisor is zero"); + return new JsonNumber(x % y); + } else { + throw error(first, second, "cannot be divided (remainder)"); + } + } + + public static @NotNull JsonNumber neg(@Nullable JsonValue value) { + if (value instanceof JsonNumber(double x)) { + return new JsonNumber(- x); + } else { + throw error(value, "cannot be negated"); + } + } + // + + // + public static @NotNull JsonBoolean and(@Nullable JsonValue first, @Nullable JsonValue second) { + return JsonBoolean.valueOf(isTruthy(first) && isTruthy(second)); + } + + public static @NotNull JsonBoolean or(@Nullable JsonValue first, @Nullable JsonValue second) { + return JsonBoolean.valueOf(isTruthy(first) || isTruthy(second)); + } + + public static @NotNull JsonBoolean not(@Nullable JsonValue value) { + return JsonBoolean.valueOf(isFalsy(value)); + } + + public static boolean isTruthy(@Nullable JsonValue value) { + return value != null && value != JsonBoolean.FALSE; + } + + public static boolean isFalsy(@Nullable JsonValue value) { + return !isTruthy(value); + } + // + + // + public static @NotNull JsonBoolean eq(@Nullable JsonValue first, @Nullable JsonValue second) { + return JsonBoolean.valueOf(Objects.equals(first, second)); + } + + public static @NotNull JsonBoolean neq(@Nullable JsonValue first, @Nullable JsonValue second) { + return JsonBoolean.valueOf(!Objects.equals(first, second)); + } + + public static @NotNull JsonBoolean lt(@Nullable JsonValue first, @Nullable JsonValue second) { + return JsonBoolean.valueOf(compare(first, second) < 0); + } + + public static @NotNull JsonBoolean gt(@Nullable JsonValue first, @Nullable JsonValue second) { + return JsonBoolean.valueOf(compare(first, second) > 0); + } + + public static @NotNull JsonBoolean leq(@Nullable JsonValue first, @Nullable JsonValue second) { + return JsonBoolean.valueOf(compare(first, second) <= 0); + } + + public static @NotNull JsonBoolean geq(@Nullable JsonValue first, @Nullable JsonValue second) { + return JsonBoolean.valueOf(compare(first, second) >= 0); + } + + public static @Nullable JsonValue abs(@Nullable JsonValue value) { + return compare(value, JsonNumber.ZERO) < 0 ? neg(value) : value; + } + + public static @NotNull Comparator<@Nullable JsonValue> comparator() { + return COMPARATOR; + } + + public static int compare(@Nullable JsonValue first, @Nullable JsonValue second) { + int firstType = compareByType(first); + int secondType = compareByType(second); + if (firstType != secondType) return Integer.compare(firstType, secondType); + + if (first instanceof JsonNumber x && second instanceof JsonNumber y) { + return compareNumbers(x, y); + } else if (first instanceof JsonString x && second instanceof JsonString y) { + return compareStrings(x, y); + } else if (first instanceof JsonArray x && second instanceof JsonArray y) { + return compareLexicographically(x, y, COMPARATOR); + } else if (first instanceof JsonObject x && second instanceof JsonObject y) { + return compareObjects(x, y); + } else { + // order of booleans and null has already been established via compareByType + throw new AssertionError(); + } + } + + private static int compareByType(@Nullable JsonValue value) { + return switch (value) { + case null -> 0; + case JsonBoolean.FALSE -> 1; + case JsonBoolean.TRUE -> 2; + case JsonNumber _ -> 3; + case JsonString _ -> 4; + case JsonArray _ -> 5; + case JsonObject _ -> 6; + }; + } + + private static int compareNumbers(@NotNull JsonNumber first, @NotNull JsonNumber second) { + return Double.compare(first.value(), second.value()); + } + + private static int compareStrings(@NotNull JsonString first, @NotNull JsonString second) { + return first.value().compareTo(second.value()); + } + + private static int compareObjects(@NotNull JsonObject first, @NotNull JsonObject second) { + // compare sorted key sets lexicographically + var x1 = new TreeSet<>(first.keySet()); + var x2 = new TreeSet<>(second.keySet()); + var d = compareLexicographically(x1, x2, Comparator.naturalOrder()); + if (d != 0) return d; + + // compare each value order of sorted keys + for (var key : x1) { + var v1 = first.get(key); + var v2 = second.get(key); + var dk = compare(v1, v2); + if (dk != 0) return dk; + } + return 0; + } + + private static int compareLexicographically(@NotNull Iterable first, @NotNull Iterable second, @NotNull Comparator comparator) { + var it1 = first.iterator(); + var it2 = second.iterator(); + while (it1.hasNext() && it2.hasNext()) { + var d = comparator.compare(it1.next(), it2.next()); + if (d != 0) return d; + } + if (it1.hasNext()) return 1; + if (it2.hasNext()) return -1; + return 0; + } + // + + // + public static @NotNull Stream<@Nullable JsonValue> error(@Nullable JsonValue value) { + if (value != null) throw new JsonQueryUserException(value); + return Stream.empty(); + } + + public static @NotNull Stream<@Nullable JsonValue> halt() { + throw new JsonQueryHaltException(null, 0); + } + + public static @NotNull Stream<@Nullable JsonValue> halt(@Nullable JsonValue value) { + throw new JsonQueryHaltException(value, 5); + } + + public static @NotNull Stream<@Nullable JsonValue> halt(@Nullable JsonValue value, @Nullable JsonValue code) { + throw new JsonQueryHaltException(value, switch (code) { + case JsonNumber(var number) -> (int) number; + case null, default -> throw new JsonQueryException(STR."\{Util.type(value)} (\{Util.value(value)}) halt_error/1: number required"); + }); + } + // + + // + public static @NotNull Stream<@NotNull JsonValue> range(@Nullable JsonValue upto) { + return range(new JsonNumber(0), upto); + } + + public static @NotNull Stream<@NotNull JsonValue> range(@Nullable JsonValue from, @Nullable JsonValue upto) { + var min = switch (from) { + case JsonNumber(var number) -> number; + case null, default -> throw new JsonQueryException("Range bounds must be numeric."); + }; + var max = switch (upto) { + case JsonNumber(var number) -> number; + case null, default -> throw new JsonQueryException("Range bounds must be numeric."); + }; + return IntStream.range((int) Math.ceil(min), (int) Math.ceil(max)).mapToObj(JsonNumber::new); + } + + public static @NotNull Stream<@Nullable JsonValue> range(@Nullable JsonValue from, @Nullable JsonValue upto, @Nullable JsonValue by) { + if (compare(by, JsonNumber.ZERO) > 0) { + return Stream.iterate(from, i -> compare(i, upto) < 0, i -> add(i, by)); + } else if (compare(by, JsonNumber.ZERO) < 0) { + return Stream.iterate(from, i -> compare(i, upto) > 0, i -> add(i, by)); + } else { + return Stream.empty(); + } + } + + public static @NotNull Stream<@Nullable JsonValue> limit(@NotNull Stream<@Nullable JsonValue> stream, @Nullable JsonValue limit) { + if (compare(limit, JsonNumber.ZERO) > 0) { + class State { + @Nullable JsonValue limit; + public State(@Nullable JsonValue limit) { this.limit = limit; } + } + + return stream.gather(Gatherer.ofSequential( + () -> new State(limit), + (state, value, downstream) -> { + state.limit = JsonMath.sub(state.limit, JsonNumber.ONE); + downstream.push(value); + return compare(state.limit, JsonNumber.ZERO) > 0; + } + )); + } else if (Objects.equals(limit, JsonNumber.ZERO)) { + return Stream.empty(); + } else { + return stream; + } + } + + public static @NotNull Stream<@Nullable JsonValue> first(@NotNull Stream<@Nullable JsonValue> stream) { + return stream.limit(1); + } + + public static @NotNull Stream<@Nullable JsonValue> last(@NotNull Stream<@Nullable JsonValue> stream) { + class State { + boolean hasValue; + @Nullable JsonValue value; + } + return stream.gather(Gatherer.ofSequential( + State::new, + Gatherer.Integrator.ofGreedy((state, value, _) -> { + state.hasValue = true; + state.value = value; + return true; + }), + (state, downstream) -> { + if (state.hasValue) downstream.push(state.value); + } + )); + } + + public static @NotNull Stream<@Nullable JsonValue> nth(@NotNull Stream<@Nullable JsonValue> stream, @Nullable JsonValue limit) { + if (compare(limit, JsonNumber.ZERO) < 0) throw new JsonQueryException("nth doesn't support negative indices"); + class State { + @Nullable JsonValue limit; + public State(@Nullable JsonValue limit) { this.limit = limit; } + } + return stream.gather(Gatherer.ofSequential( + () -> new State(sub(add(limit, JsonNumber.ONE), JsonNumber.ONE)), + (state, value, downstream) -> { + if (compare(state.limit, JsonNumber.ZERO) <= 0) { + downstream.push(value); + return false; + } else { + state.limit = sub(state.limit, JsonNumber.ONE); + return true; + } + } + )); + } + // + + // + public static @Nullable JsonValue index(@Nullable JsonValue value, @Nullable JsonValue index) { + return switch (index) { + case JsonNumber(var number) -> switch (value) { + case JsonArray array when isInt(number) && 0 <= number && number < array.size() -> array.get((int) number); + case JsonArray array when isInt(number) && -array.size() <= number && number < 0 -> array.get(array.size() + (int) number); + case JsonArray _ -> null; + case null -> null; + default -> throw indexError(value, index); + }; + case JsonString(var string) -> switch (value) { + case JsonObject object -> object.get(string); + case null -> null; + default -> throw indexError(value, index); + }; + case JsonArray indexArray -> switch (value) { + case JsonArray _ when indexArray.isEmpty() -> JsonArray.EMPTY; + case JsonArray array -> { + var out = new ArrayList(); + var indexLength = indexArray.size(); + var length = array.size(); + outer: for (int i = 0; i < length - indexLength; i++) { + for (int j = 0; j < indexLength; j++) { + if (!Objects.equals(array.get(i + j), indexArray.get(j))) continue outer; + } + out.add(new JsonNumber(i)); + } + yield new JsonArray(out); + } + case null, default -> throw indexError(value, index); + }; + case null, default -> throw indexError(value, index); + }; + } + + private static boolean isInt(double value) { + return ((int) value) == value; + } + + private static @NotNull JsonQueryException indexError(@Nullable JsonValue value, @Nullable JsonValue index) { + var valueString = Util.type(value); + var indexString = Util.type(index) + (index instanceof JsonString(var string) ? " " + JsonString.quote(string) : ""); + return new JsonQueryException(STR."Cannot index \{valueString} with \{indexString}."); + } + + public static @NotNull JsonArray keys(@Nullable JsonValue value) { + return JsonArray.valueOf(switch (value) { + case JsonArray array -> IntStream.range(0, array.size()).mapToObj(JsonNumber::new).toList(); + case JsonObject object -> new TreeSet<>(object.keySet()).stream().map(JsonString::new).toList(); + case null, default -> throw error(value, "has no keys"); + }); + } + + public static @NotNull JsonArray keysUnsorted(@Nullable JsonValue value) { + return JsonArray.valueOf(switch (value) { + case JsonArray array -> IntStream.range(0, array.size()).mapToObj(JsonNumber::new); + case JsonObject object -> object.keySet().stream().map(JsonString::new); + case null, default -> throw error(value, "has no keys"); + }); + } + + public static @NotNull JsonBoolean has(@Nullable JsonValue value, @Nullable JsonValue index) { + return JsonBoolean.valueOf((boolean) switch (index) { + case JsonNumber(var number) -> switch (value) { + case JsonArray array -> 0 <= number && (int) number < array.size(); + case null -> false; + default -> throw new JsonQueryException(STR."Cannot check whether \{Util.type(value)} has a number key."); + }; + case JsonString(var string) -> switch (value) { + case JsonObject object -> object.containsKey(string); + case null -> false; + default -> throw new JsonQueryException(STR."Cannot check whether \{Util.type(value)} has a string key."); + }; + case null, default -> throw new JsonQueryException(STR."Cannot check whether \{Util.type(value)} has a \{Util.type(index)} key."); + }); + } + + public static @NotNull JsonBoolean in(@Nullable JsonValue index, @Nullable JsonValue value) { + return has(value, index); + } + + public static @NotNull Stream<@Nullable JsonValue> values(@Nullable JsonValue value) { + return values(value, false); + } + + public static @NotNull Stream<@Nullable JsonValue> values(@Nullable JsonValue value, boolean optional) { + return switch (value) { + case JsonArray array -> array.stream(); + case JsonObject object -> object.values().stream(); + case null, default -> { + if (optional) yield Stream.empty(); + throw new JsonQueryException(STR."Cannot iterate over \{Util.type(value)} (\{Util.value(value)})."); + } + }; + } + + public static @NotNull JsonArray map(@Nullable JsonValue value, @NotNull JQFilter expression) { + return new JsonArray(values(value).flatMap(expression).toList()); + } + + public static @NotNull JsonValue mapValues(@Nullable JsonValue value, @NotNull JQFilter expression) { + return switch (value) { + case JsonArray array -> new JsonArray(array.stream().map(expression).flatMap(s -> s.limit(1)).toList()); + case JsonObject object -> { + var out = new LinkedHashMap(); + object.forEach((key, v) -> expression.apply(v).limit(1).forEach(newValue -> out.put(key, newValue))); + yield new JsonObject(out); + } + case null, default -> throw new JsonQueryException(STR."Cannot iterate over \{Util.type(value)} (\{Util.value(value)})."); + }; + } + + public static @NotNull JsonBoolean any(@Nullable JsonValue value) { + return JsonBoolean.valueOf(values(value).anyMatch(JsonMath::isTruthy)); + } + + public static @NotNull JsonBoolean any(@Nullable JsonValue value, @NotNull JQFilter expression) { + return JsonBoolean.valueOf(values(value).flatMap(expression).anyMatch(JsonMath::isTruthy)); + } + + public static @NotNull JsonBoolean any(@NotNull Stream<@Nullable JsonValue> values, @NotNull JQFilter expression) { + return JsonBoolean.valueOf(values.flatMap(expression).anyMatch(JsonMath::isTruthy)); + } + + public static @NotNull JsonBoolean all(@Nullable JsonValue value) { + return JsonBoolean.valueOf(values(value).allMatch(JsonMath::isTruthy)); + } + + public static @NotNull JsonBoolean all(@Nullable JsonValue value, @NotNull JQFilter expression) { + return JsonBoolean.valueOf(values(value).flatMap(expression).allMatch(JsonMath::isTruthy)); + } + + public static @NotNull JsonBoolean all(@NotNull Stream<@Nullable JsonValue> values, @NotNull JQFilter expression) { + return JsonBoolean.valueOf(values.flatMap(expression).allMatch(JsonMath::isTruthy)); + } + + public static @NotNull JsonArray flatten(@Nullable JsonValue value) { + return new JsonArray(values(value).flatMap(JsonMath::flatten0).toList()); + } + + public static @NotNull JsonArray flatten(@Nullable JsonValue value, @Nullable JsonValue depth) { + return switch (depth) { + case JsonNumber(var number) when number < 0 -> throw new JsonQueryException("flatten depth must not be negative"); + case JsonNumber(var number) -> new JsonArray(values(value).flatMap(v -> flatten0(v, (int) number)).toList()); + case null, default -> throw new JsonQueryException("flatten depth must be a number"); + }; + } + + private static @NotNull Stream<@Nullable JsonValue> flatten0(@Nullable JsonValue value) { + return flatten0(value, -1); + } + + private static @NotNull Stream<@Nullable JsonValue> flatten0(@Nullable JsonValue value, int depth) { + return switch (value) { + case JsonArray array when depth == 0 -> Stream.of(array); + case JsonArray array -> { + var d = depth < 0 ? depth : depth - 1; + yield array.stream().flatMap(v -> flatten0(v, d)); + } + case null, default -> Stream.of(value); + }; + } + + public static @NotNull JsonArray sort(@Nullable JsonValue value) { + return switch (value) { + case JsonArray array -> { + var list = new ArrayList<>(array); + list.sort(comparator()); + yield new JsonArray(list); + } + case null, default -> throw error(value, "cannot be sorted, as it is not an array"); + }; + } + + public static @NotNull JsonArray sort(@Nullable JsonValue value, @NotNull JQFilter expression) { + return switch (value) { + case JsonArray array -> new JsonArray(array.stream() + .map(v -> new SortEntry(v, expression.apply(v).toList())) + .sorted(SortEntry.COMPARATOR) + .map(SortEntry::value) + .toList() + ); + case null, default -> throw error(value, "cannot be sorted, as it is not an array"); + }; + } + + public static @Nullable JsonValue min(@Nullable JsonValue value) { + return switch (value) { + case JsonArray array when array.isEmpty() -> null; + case JsonArray array -> Collections.min(array, COMPARATOR); + case null, default -> throw error(value, value, "cannot be iterated over"); + }; + } + + public static @Nullable JsonValue min(@Nullable JsonValue value, @NotNull JQFilter expression) { + return switch (value) { + case JsonArray array when array.isEmpty() -> null; + case JsonArray array -> { + var it = array.iterator(); + var minValue = it.next(); + var minKey = expression.apply(minValue).toList(); + + while (it.hasNext()) { + var nextValue = it.next(); + var nextKey = expression.apply(nextValue).toList(); + if (compareLexicographically(nextKey, minKey, COMPARATOR) < 0) { + minValue = nextValue; + minKey = nextKey; + } + } + + yield minValue; + } + case null, default -> throw error(value, value, "cannot be iterated over"); + }; + } + + public static @Nullable JsonValue max(@Nullable JsonValue value) { + return switch (value) { + case JsonArray array when array.isEmpty() -> null; + case JsonArray array -> Collections.max(array, COMPARATOR); + case null, default -> throw error(value, "cannot be sorted as it is not an array"); + }; + } + + public static @Nullable JsonValue max(@Nullable JsonValue value, @NotNull JQFilter expression) { + return switch (value) { + case JsonArray array when array.isEmpty() -> null; + case JsonArray array -> { + var it = array.iterator(); + var maxValue = it.next(); + var maxKey = expression.apply(maxValue).toList(); + + while (it.hasNext()) { + var nextValue = it.next(); + var nextKey = expression.apply(nextValue).toList(); + if (compareLexicographically(nextKey, maxKey, COMPARATOR) > 0) { + maxValue = nextValue; + maxKey = nextKey; + } + } + + yield maxValue; + } + case null, default -> throw error(value, value, "cannot be iterated over"); + }; + } + + public static @NotNull JsonArray unique(@Nullable JsonValue value) { + return switch (value) { + case JsonArray array when array.size() <= 1 -> array; + case JsonArray array -> { + var set = new TreeSet<>(COMPARATOR); + set.addAll(array); + yield new JsonArray(set.stream().toList()); + } + case null, default -> throw error(value, "cannot be sorted as it is not an array"); + }; + } + + public static @NotNull JsonArray unique(@Nullable JsonValue value, @NotNull JQFilter expression) { + return switch (value) { + case JsonArray array when array.isEmpty() -> array; + case JsonArray array -> { + var map = new TreeMap, JsonValue>(SortEntry.LEXICOGRAPHIC_COMPARATOR); + array.forEach(v -> map.putIfAbsent(expression.apply(v).toList(), v)); + yield new JsonArray(map.values().stream().toList()); + } + case null, default -> throw error(value, "cannot be sorted as it is not an array"); + }; + } + + public static @NotNull JsonArray group(@Nullable JsonValue value, @NotNull JQFilter expression) { + return switch (value) { + case JsonArray array when array.isEmpty() -> array; + case JsonArray array -> { + var map = new TreeMap, List>(SortEntry.LEXICOGRAPHIC_COMPARATOR); + array.forEach(v -> { + var key = expression.apply(v).toList(); + map.computeIfAbsent(key, _ -> new ArrayList<>()).add(v); + }); + yield new JsonArray(map.values().stream().map(JsonArray::new).toList()); + } + case null, default -> throw error(value, "cannot be sorted as it is not an array"); + }; + } + + public static @NotNull JsonArray reverse(@Nullable JsonValue value) { + return switch (value) { + case JsonArray array -> array.reversed(); + case null, default -> throw error(value, "cannot be reversed, as it is not an array"); + }; + } + + public static @NotNull JsonBoolean contains(@Nullable JsonValue container, @Nullable JsonValue content) { + var out = contains0(container, content); + if (out == null) throw error(container, content, "cannot have their containment checked"); + return out; + } + + private static @Nullable JsonBoolean contains0(@Nullable JsonValue container, @Nullable JsonValue content) { + if (container instanceof JsonArray acontainer && content instanceof JsonArray acontent) { + outer: for (var e1 : acontent) { + for (var e2 : acontainer) { + if (contains0(e2, e1) == JsonBoolean.TRUE) continue outer; + } + return JsonBoolean.FALSE; + } + return JsonBoolean.TRUE; + } else if (container instanceof JsonString(var scontainer) && content instanceof JsonString(var scontent)) { + return JsonBoolean.valueOf(scontainer.contains(scontent)); + } else if (container instanceof JsonObject ocontainer && content instanceof JsonObject ocontent) { + for (var entry : ocontent.entrySet()) { + if (contains0(ocontainer.get(entry.getKey()), entry.getValue()) != JsonBoolean.TRUE) { + return JsonBoolean.FALSE; + } + } + return JsonBoolean.TRUE; + } else { + return null; + } + } + + public static @Nullable JsonValue indices(@Nullable JsonValue container, @Nullable JsonValue content) { + if (container instanceof JsonArray acontainer && content instanceof JsonArray acontent) { + return index(acontainer, acontent); + } else if (container instanceof JsonArray acontainer) { + return index(acontainer, JsonArray.of(content)); + } else if (container instanceof JsonString(var scontainer) && content instanceof JsonString(var scontent)) { + var out = new ArrayList(); + var idx = -1; + while (true) { + idx = scontainer.indexOf(scontent, idx + 1); + if (idx == -1) break; + out.add(new JsonNumber(idx)); + } + return new JsonArray(out); + } else { + return index(container, content); + } + } + + public static @Nullable JsonValue firstindex(@Nullable JsonValue container, @Nullable JsonValue content) { + return index(indices(container, content), JsonNumber.ZERO); + } + + public static @Nullable JsonValue lastindex(@Nullable JsonValue container, @Nullable JsonValue content) { + return index(indices(container, content), new JsonNumber(-1)); + } + + public static @NotNull JsonBoolean inside(@Nullable JsonValue content, @Nullable JsonValue container) { + return contains(container, content); + } + + public static @NotNull Stream<@NotNull JsonArray> combinations(@Nullable JsonValue value) { + if (length0(value) == 0) return Stream.of(JsonArray.EMPTY); + if (!(value instanceof JsonArray array)) throw indexError(value, JsonNumber.ZERO); + + return values(array.getFirst()) + .flatMap(head -> combinations(array.subList(1, array.size())).map(tail -> { + var out = new ArrayList(tail.size() + 1); + out.add(head); + out.addAll(tail); + return new JsonArray(out); + })); + } + + public static @NotNull Stream<@NotNull JsonArray> combinations(@Nullable JsonValue value, @Nullable JsonValue n) { + if (!(n instanceof JsonNumber(var number))) throw new JsonQueryException("Range bounds must be numeric."); + return combinations(new JsonArray(Collections.nCopies((int) Math.ceil(number), value))); + } + + public static @NotNull JsonNumber bsearch(@Nullable JsonValue container, @Nullable JsonValue value) { + if (length0(container) == 0) return new JsonNumber(-1); + if (container instanceof JsonArray array) { + return new JsonNumber(Collections.binarySearch(array, value, COMPARATOR)); + } else { + throw indexError(container, JsonNumber.ZERO); + } + } + + public static @NotNull JsonArray transpose(@Nullable JsonValue value) { + if (!(value instanceof JsonArray array)) throw indexError(value, JsonNumber.ZERO); + + var length = (int) array.stream().mapToDouble(JsonMath::length0).max().orElse(0.0); + var out = new ArrayList>(length); + for (int i = 0; i < length; i++) out.add(new ArrayList<>()); + + for (var element : array) { + if (!(element instanceof JsonArray subarray)) throw indexError(element, JsonNumber.ZERO); + int i = 0; + for (; i < subarray.size(); i++) out.get(i).add(subarray.get(i)); + for (; i < length; i ++) out.get(i).add(null); + } + + return new JsonArray(out.stream().map(JsonArray::new).toList()); + } + + private record SortEntry(@Nullable JsonValue value, @NotNull Iterable<@Nullable JsonValue> key) { + private static final Comparator> LEXICOGRAPHIC_COMPARATOR = (a, b) -> compareLexicographically(a, b, JsonMath.COMPARATOR); + private static final Comparator COMPARATOR = Comparator.comparing(SortEntry::key, LEXICOGRAPHIC_COMPARATOR); + } + // + + // + public static boolean isArray(@Nullable JsonValue value) { + return value instanceof JsonArray; + } + + public static boolean isObject(@Nullable JsonValue value) { + return value instanceof JsonObject; + } + + public static boolean isIterable(@Nullable JsonValue value) { + return isArray(value) || isObject(value); + } + + public static boolean isBoolean(@Nullable JsonValue value) { + return value instanceof JsonBoolean; + } + + public static boolean isNumber(@Nullable JsonValue value) { + return value instanceof JsonNumber; + } + + public static boolean isNormal(@Nullable JsonValue value) { + return value instanceof JsonNumber(var number) && Math.abs(number) >= Double.MIN_NORMAL; + } + + public static boolean isFinite(@Nullable JsonValue value) { + return value instanceof JsonNumber(var number) && Double.isFinite(number); + } + + public static boolean isInfinite(@Nullable JsonValue value) { + return value instanceof JsonNumber(var number) && Double.isInfinite(number); + } + + public static boolean isNan(@Nullable JsonValue value) { + return value instanceof JsonNumber(var number) && Double.isNaN(number); + } + + public static boolean isString(@Nullable JsonValue value) { + return value instanceof JsonString; + } + + public static boolean isNull(@Nullable JsonValue value) { + return value == null; + } + + public static boolean isValue(@Nullable JsonValue value) { + return value != null; + } + + public static boolean isScalar(@Nullable JsonValue value) { + return !isIterable(value); + } + + public static boolean isEmpty(@NotNull Stream stream) { + return stream.map(_ -> Boolean.TRUE).findFirst().isEmpty(); + } + // + + // + private static final Pattern TRIM = Pattern.compile("^\\s+|\\s+$"); + public static @NotNull JsonString trim(@Nullable JsonValue value) { + return switch (value) { + case JsonString(var string) -> new JsonString(TRIM.matcher(string).replaceAll("")); + case null, default -> throw error(value, "only strings can be trimmed"); + }; + } + + private static final Pattern LTRIM = Pattern.compile("^\\s+"); + public static @NotNull JsonString ltrim(@Nullable JsonValue value) { + return switch (value) { + case JsonString(var string) -> new JsonString(LTRIM.matcher(string).replaceAll("")); + case null, default -> throw error(value, "only strings can be trimmed"); + }; + } + + private static final Pattern RTRIM = Pattern.compile("\\s+$"); + public static @NotNull JsonString rtrim(@Nullable JsonValue value) { + return switch (value) { + case JsonString(var string) -> new JsonString(RTRIM.matcher(string).replaceAll("")); + case null, default -> throw error(value, "only strings can be trimmed"); + }; + } + + public static @NotNull JsonString ltrimstr(@Nullable JsonValue value, @Nullable JsonValue prefix) { + if (!(value instanceof JsonString(var string))) throw error(value, "only strings can be trimmed"); + if (!(prefix instanceof JsonString(var prefixString))) throw error(value, "prefix must be string"); + return string.startsWith(prefixString) ? new JsonString(string.substring(prefixString.length())) : (JsonString) value; + } + + public static @NotNull JsonString rtrimstr(@Nullable JsonValue value, @Nullable JsonValue suffix) { + if (!(value instanceof JsonString(var string))) throw error(value, "only strings can be trimmed"); + if (!(suffix instanceof JsonString(var suffixString))) throw error(value, "suffix must be string"); + return string.endsWith(suffixString) ? new JsonString(string.substring(0, string.length() - suffixString.length())) : (JsonString) value; + } + + public static @NotNull JsonArray split(@Nullable JsonValue input, @Nullable JsonValue separator) { + if (input instanceof JsonString(var inputString) && separator instanceof JsonString(var separatorString)) { + return new JsonArray(Arrays.stream(inputString.split(Pattern.quote(separatorString), -1)).map(JsonString::new).toList()); + } else { + throw new JsonQueryException("split input and separator must be strings."); + } + } + + public static @NotNull JsonString join(@Nullable JsonValue input, @Nullable JsonValue separator) { + var it = values(input).map(value -> switch (value) { + case JsonString string -> string; + case JsonNumber _, JsonBoolean _ -> tostring(value); + case null, default -> value; + }).iterator(); + + if (!it.hasNext()) return JsonString.EMPTY; + + var out = JsonString.EMPTY; + while (it.hasNext()) { + var next = it.next(); + if (next == null) continue; + out = (JsonString) add(out, next); + if (it.hasNext()) out = (JsonString) add(out, separator); + } + return Objects.requireNonNull(out); + } + + public static @NotNull JsonArray explode(@Nullable JsonValue input) { + return switch (input) { + case JsonString(var string) -> JsonArray.valueOf(string.codePoints().toArray()); + case null, default -> throw new JsonQueryException("explode input must be a string"); + }; + } + + public static @NotNull JsonString implode(@Nullable JsonValue input) { + return switch (input) { + case JsonArray array -> { + var codepoints = array.stream().mapToInt(value -> switch (value) { + case JsonNumber(var number) when isInt(number) && 0 <= number && number <= 0x10FFFF -> (int) number; + case null, default -> throw new JsonQueryException("implode input must be an array of unicode codepoints"); + }); + yield toString(codepoints); + } + case null, default -> throw new JsonQueryException("implode input must be an array"); + }; + } + + public static @NotNull JsonString asciiUpcase(@Nullable JsonValue value) { + return switch (value) { + case JsonString(var string) -> toString(string.codePoints().map(i -> 'a' <= i && i <= 'z' ? i + ('A' - 'a') : i)); + case null, default -> throw new JsonQueryException("explode input must be a string"); + }; + } + + public static @NotNull JsonString asciiDowncase(@Nullable JsonValue value) { + return switch (value) { + case JsonString(var string) -> toString(string.codePoints().map(i -> 'A' <= i && i <= 'Z' ? i + ('a' - 'A') : i)); + case null, default -> throw new JsonQueryException("explode input must be a string"); + }; + } + + private static @NotNull JsonString toString(@NotNull IntStream codepoints) { + return new JsonString(codepoints.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString()); + } + + public static @NotNull JsonNumber utf8ByteLength(@Nullable JsonValue value) { + return new JsonNumber(switch (value) { + case JsonString(var string) -> string.codePoints().map(code -> { + if (code <= 0x7F) return 1; + if (code <= 0x7FF) return 2; + if (code <= 0xFFFF) return 3; + return 4; + }).sum(); + case null, default -> throw error(value, "only strings have UTF-8 byte length"); + }); + } + + public static @NotNull JsonBoolean startswith(@Nullable JsonValue expression, @Nullable JsonValue prefix) { + if (expression instanceof JsonString(var string) && prefix instanceof JsonString(var string2)) { + return JsonBoolean.valueOf(string.startsWith(string2)); + } else { + throw new JsonQueryException("startswith() requires string inputs"); + } + } + + public static @NotNull JsonBoolean endswith(@Nullable JsonValue expression, @Nullable JsonValue prefix) { + if (expression instanceof JsonString(var string) && prefix instanceof JsonString(var string2)) { + return JsonBoolean.valueOf(string.endsWith(string2)); + } else { + throw new JsonQueryException("endswith() requires string inputs"); + } + } + // + + // + public static @NotNull Stream<@NotNull JsonValue> match( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args + ) { + var input = regexInput(context); + return match(context, args, false).flatMap(Function.identity()).map(result -> match0(input, result)); + } + + private static @NotNull Stream> match( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args, boolean global + ) { + var input = regexInput(context); + return switch (args.size()) { + case 1 -> args.getFirst().evaluate(context).map(arg -> switch (arg) { + case JsonString pattern -> match(input, pattern, null, global); + case JsonArray array -> switch (array.size()) { + case 0 -> throw new JsonQueryException("array not a string or array"); + case 1 -> match(input, array.getFirst(), null, global); + default -> match(input, array.get(0), array.get(1), global); + }; + case null, default -> throw error(arg, "not a string or array"); + }); + case 2 -> Util.cross(args, context).map(list -> match(input, list.get(0), list.get(1), global)); + default -> throw new IllegalArgumentException(); + }; + } + + private static @NotNull Stream<@NotNull MatchResult> match( + @NotNull String input, @Nullable JsonValue pattern, @Nullable JsonValue flags, boolean global + ) { + if (!(pattern instanceof JsonString(var patternString))) throw error(pattern, "is not a string"); + + var skipEmpty = false; + @MagicConstant(flagsFromClass = Pattern.class) + var patternFlags = Pattern.MULTILINE; + + switch (flags) { + case JsonString(var flagsString) -> { + for (int i = 0; i < flagsString.length(); i++) { + switch (flagsString.charAt(i)) { + case 'g' -> global = true; + case 'i' -> patternFlags |= Pattern.CASE_INSENSITIVE; + case 'm' -> patternFlags |= Pattern.DOTALL; + case 'n' -> skipEmpty = true; + case 'p' -> patternFlags = patternFlags & ~Pattern.MULTILINE | Pattern.DOTALL; + case 's' -> patternFlags &= ~Pattern.MULTILINE; + case 'l' -> throw new UnsupportedOperationException("unsupported flag l"); + case 'x' -> patternFlags |= Pattern.COMMENTS; + default -> throw new JsonQueryException(flagsString + " is not a valid modifier string."); + } + } + } + case null -> {} + default -> throw error(flags, "is not a string"); + } + + var compiledPattern = Pattern.compile(patternString, patternFlags); + var out = matchesAsStream(compiledPattern.matcher(input)); + if (skipEmpty) out = out.filter(Predicate.not(result -> result.group().isEmpty())); + if (!global) out = out.limit(1); + return out; + } + + private static @NotNull Stream<@NotNull MatchResult> matchesAsStream(@NotNull Matcher matcher) { + var it = new Iterator() { + @Override + public boolean hasNext() { return matcher.find(); } + + @Override + public MatchResult next() { return matcher.toMatchResult(); } + }; + + var split = Spliterators.spliteratorUnknownSize(it, Spliterator.ORDERED); + return StreamSupport.stream(split, false); + } + + private static @NotNull JsonValue match0(@NotNull String input, @NotNull MatchResult result) { + var groupNames = new HashMap(); + result.namedGroups().forEach((name, index) -> groupNames.put(index, name)); + + var groups = new ArrayList(); + var groupCount = result.groupCount(); + for (int i = 1; i <= groupCount; i++) { + var start = result.start(i); + groups.add(JsonObject.of( + "offset", new JsonNumber(start == -1 ? -1 : input.codePointCount(0, result.start(i))), + "length", new JsonNumber(start == -1 ? 0 : input.codePointCount(result.start(i), result.end(i))), + "string", JsonString.valueOf(result.group(i)), + "name", JsonString.valueOf(groupNames.get(i)) + )); + } + + return JsonObject.of( + "offset", new JsonNumber(input.codePointCount(0, result.start())), + "length", new JsonNumber(input.codePointCount(result.start(), result.end())), + "string", new JsonString(result.group()), + "captures", new JsonArray(groups) + ); + } + + private static @NotNull String regexInput(@NotNull JQExpression.Context context) { + if (context.root() instanceof JsonString(var input)) return input; + throw error(context.root(), "cannot be matched, as it is not a string"); + } + + public static @NotNull Stream<@NotNull JsonValue> test( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args + ) { + return Util.lazy(() -> JsonBoolean.valueOf(!isEmpty(match(context, args, false)))); + } + + public static @NotNull Stream<@NotNull JsonValue> capture( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args + ) { + return match(context, args, false).flatMap(Function.identity()).map(JsonMath::capture0); + } + + private static @NotNull JsonValue capture0(@NotNull MatchResult result) { + var out = new LinkedHashMap(); + result.namedGroups().forEach((name, number) -> out.put(name, JsonString.valueOf(result.group(number)))); + return new JsonObject(out); + } + + public static @NotNull Stream<@NotNull JsonValue> scan( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args + ) { + return match(context, args, true).flatMap(Function.identity()).map(MatchResult::group).map(JsonString::new); + } + + public static @NotNull Stream<@NotNull JsonValue> split( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args + ) { + return Util.lazy(() -> new JsonArray(splits(context, args).toList())); + } + + public static @NotNull Stream<@NotNull JsonValue> splits( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args + ) { + if (!(context.root() instanceof JsonString(var inputString))) throw error(context.root(), "cannot be matched, as it is not a string"); + return match(context, args, true).flatMap(s -> s.gather(splits0(inputString))); + } + + private static @NotNull Gatherer splits0(@NotNull String input) { + class State { int offset; } + return Gatherer.ofSequential( + State::new, + Gatherer.Integrator.ofGreedy((state, value, downstream) -> { + downstream.push(new JsonString(input.substring(state.offset, value.start()))); + state.offset = value.end(); + return true; + }), + (state, downstream) -> downstream.push(new JsonString(input.substring(state.offset))) + ); + } + + public static @NotNull Stream<@NotNull JsonValue> sub( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args + ) { + return sub0(context, args, false); + } + + public static @NotNull Stream<@NotNull JsonValue> gsub( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args + ) { + return sub0(context, args, true); + } + + private static @NotNull Stream<@NotNull JsonValue> sub0( + @NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args, boolean global + ) { + var input = regexInput(context); + var matchArgs = switch (args.size()) { + case 2 -> List.of(args.get(0), new JQConstant(null)); + case 3 -> List.of(args.get(0), args.get(2)); + default -> throw new IllegalArgumentException(); + }; + var subst = args.get(1); + + return match(context, matchArgs, global).flatMap(s -> { + var offset = 0; + var fragments = new ArrayList(); + var values = new ArrayList(); + + var it = s.iterator(); + while (it.hasNext()) { + var result = it.next(); + var capture = capture0(result); + fragments.add(input.substring(offset, result.start())); + values.add(new JQPipeExpression(new JQConstant(capture), subst)); + offset = result.end(); + } + fragments.add(input.substring(offset)); + return new JQStringInterpolation(null, fragments, values).evaluate(context); + }); + } + // + + // + public static @NotNull JsonString type(@Nullable JsonValue value) { + return new JsonString(Util.type(value)); + } + + public static @NotNull JsonString tojson(@Nullable JsonValue value) { + return new JsonString(JsonValue.toJsonString(value)); + } + + public static @NotNull JsonString tostring(@Nullable JsonValue value) { + return switch (value) { + case JsonString string -> string; + case null, default -> new JsonString(JsonValue.toJsonString(value)); + }; + } + + public static @NotNull JsonNumber tonumber(@Nullable JsonValue value) { + return switch (value) { + case JsonNumber number -> number; + case JsonString(var string) -> { + try { + var number = Double.parseDouble(string); + yield new JsonNumber(number); + } catch (IllegalArgumentException _) { + throw error(value, "cannot be parsed as a number"); + } + } + case null, default -> throw error(value, "cannot be parsed as a number"); + }; + } + + public static @Nullable JsonValue fromjson(@Nullable JsonValue value) { + return switch (value) { + case JsonString(var string) -> { + try { + yield JsonValue.parse(string); + } catch (JsonParserException ex) { + throw new JsonQueryException(ex.getMessage(), ex); + } + } + case null, default -> throw error(value, "only strings can be parsed"); + }; + } + // + + // + public static @NotNull JsonNumber length(@Nullable JsonValue value) { + return new JsonNumber(length0(value)); + } + + private static double length0(@Nullable JsonValue value) { + return switch (value) { + case JsonString(var string) -> string.length(); + case JsonNumber(double number) -> Math.abs(number); + case JsonArray array -> array.size(); + case JsonObject object -> object.size(); + case JsonBoolean b -> throw error(b, "has no length"); + case null -> 0; + }; + } + // + + // + private static @Nullable JsonNumber math(@Nullable JsonValue operand, @NotNull DoubleUnaryOperator operator) { + return switch (operand) { + case JsonNumber(double number) -> { + var out = operator.applyAsDouble(number); + yield Double.isNaN(out) ? null : new JsonNumber(out); + } + case null, default -> throw error(operand, "number required"); + }; + } + + private static @Nullable JsonNumber math(@Nullable JsonValue left, @Nullable JsonValue right, @NotNull DoubleBinaryOperator operator) { + if (!(left instanceof JsonNumber(var number1))) { + throw error(left, "number required"); + } + if (!(right instanceof JsonNumber(var number2))) { + throw error(right, "number required"); + } + var out = operator.applyAsDouble(number1, number2); + return Double.isNaN(out) ? null : new JsonNumber(out); + } + + public static @Nullable JsonNumber acos(@Nullable JsonValue a) { + return math(a, Math::acos); + } + + public static @Nullable JsonNumber acosh(@Nullable JsonValue a) { + throw new UnsupportedOperationException("acosh"); + // return math(a, Math::acosh); + } + + public static @Nullable JsonNumber asin(@Nullable JsonValue a) { + return math(a, Math::asin); + } + + public static @Nullable JsonNumber asinh(@Nullable JsonValue a) { + throw new UnsupportedOperationException("asinh"); + // return math(a, Math::asinh); + } + + public static @Nullable JsonNumber atan(@Nullable JsonValue a) { + return math(a, Math::atan); + } + + public static @Nullable JsonNumber atanh(@Nullable JsonValue a) { + throw new UnsupportedOperationException("atanh"); + // return math(a, Math::atanh); + } + + public static @Nullable JsonNumber cbrt(@Nullable JsonValue a) { + return math(a, Math::cbrt); + } + + public static @Nullable JsonNumber ceil(@Nullable JsonValue a) { + return math(a, Math::ceil); + } + + public static @Nullable JsonNumber cos(@Nullable JsonValue a) { + return math(a, Math::cos); + } + + public static @Nullable JsonNumber cosh(@Nullable JsonValue x) { + return math(x, Math::cosh); + } + + public static @Nullable JsonNumber erf(@Nullable JsonValue a) { + throw new UnsupportedOperationException("erf"); + // return math(a, Math::erf); + } + + public static @Nullable JsonNumber erfc(@Nullable JsonValue a) { + throw new UnsupportedOperationException("erfc"); + // return math(a, Math::erfc); + } + + public static @Nullable JsonNumber exp(@Nullable JsonValue a) { + return math(a, Math::exp); + } + + public static @Nullable JsonNumber exp10(@Nullable JsonValue a) { + throw new UnsupportedOperationException("exp10"); + // return math(a, Math::exp10); + } + + public static @Nullable JsonNumber exp2(@Nullable JsonValue a) { + throw new UnsupportedOperationException("exp2"); + // return math(a, Math::exp2); + } + + public static @Nullable JsonNumber expm1(@Nullable JsonValue x) { + return math(x, Math::expm1); + } + + public static @Nullable JsonNumber fabs(@Nullable JsonValue a) { + throw new UnsupportedOperationException("fabs"); + // return math(a, Math::fabs); + } + + public static @Nullable JsonNumber floor(@Nullable JsonValue a) { + return math(a, Math::floor); + } + + public static @Nullable JsonNumber gamma(@Nullable JsonValue a) { + throw new UnsupportedOperationException("gamma"); + // return math(a, Math::gamma); + } + + public static @Nullable JsonNumber j0(@Nullable JsonValue a) { + throw new UnsupportedOperationException("j0"); + // return math(a, Math::j0); + } + + public static @Nullable JsonNumber j1(@Nullable JsonValue a) { + throw new UnsupportedOperationException("j1"); + // return math(a, Math::j1); + } + + public static @Nullable JsonNumber lgamma(@Nullable JsonValue a) { + throw new UnsupportedOperationException("lgamma"); + // return math(a, Math::lgamma); + } + + public static @Nullable JsonNumber log(@Nullable JsonValue a) { + return math(a, Math::log); + } + + public static @Nullable JsonNumber log10(@Nullable JsonValue a) { + return math(a, Math::log10); + } + + public static @Nullable JsonNumber log1p(@Nullable JsonValue x) { + return math(x, Math::log1p); + } + + public static @Nullable JsonNumber log2(@Nullable JsonValue a) { + throw new UnsupportedOperationException("log2"); + // return math(a, Math::log2); + } + + public static @Nullable JsonNumber logb(@Nullable JsonValue a) { + throw new UnsupportedOperationException("logb"); + // return math(a, Math::logb); + } + + public static @Nullable JsonNumber nearbyint(@Nullable JsonValue a) { + throw new UnsupportedOperationException("nearbyint"); + // return math(a, Math::nearbyint); + } + + public static @Nullable JsonNumber rint(@Nullable JsonValue a) { + return math(a, Math::rint); + } + + public static @Nullable JsonNumber round(@Nullable JsonValue a) { + return math(a, Math::round); + } + + public static @Nullable JsonNumber significand(@Nullable JsonValue a) { + throw new UnsupportedOperationException("significand"); + // return math(a, Math::significand); + } + + public static @Nullable JsonNumber sin(@Nullable JsonValue a) { + return math(a, Math::sin); + } + + public static @Nullable JsonNumber sinh(@Nullable JsonValue x) { + return math(x, Math::sinh); + } + + public static @Nullable JsonNumber sqrt(@Nullable JsonValue a) { + return math(a, Math::sqrt); + } + + public static @Nullable JsonNumber tan(@Nullable JsonValue a) { + return math(a, Math::tan); + } + + public static @Nullable JsonNumber tanh(@Nullable JsonValue x) { + return math(x, Math::tanh); + } + + public static @Nullable JsonNumber tgamma(@Nullable JsonValue a) { + throw new UnsupportedOperationException("tgamma"); + // return math(a, Math::tgamma); + } + + public static @Nullable JsonNumber trunc(@Nullable JsonValue a) { + throw new UnsupportedOperationException("trunc"); + // return math(a, Math::trunc); + } + + public static @Nullable JsonNumber y0(@Nullable JsonValue a) { + throw new UnsupportedOperationException("y0"); + // return math(a, Math::y0); + } + + public static @Nullable JsonNumber y1(@Nullable JsonValue a) { + throw new UnsupportedOperationException("y1"); + // return math(a, Math::y1); + } + + public static @Nullable JsonNumber atan2(@Nullable JsonValue y, @Nullable JsonValue x) { + return math(y, x, Math::atan2); + } + + public static @Nullable JsonNumber copysign(@Nullable JsonValue magnitude, @Nullable JsonValue sign) { + return math(magnitude, sign, Math::copySign); + } + + public static @Nullable JsonNumber drem(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("drem"); + // return math(a, b, Math::drem); + } + + public static @Nullable JsonNumber fdim(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("fdim"); + // return math(a, b, Math::fdim); + } + + public static @Nullable JsonNumber fmax(@Nullable JsonValue a, @Nullable JsonValue b) { + return math(a, b, Math::max); + } + + public static @Nullable JsonNumber fmin(@Nullable JsonValue a, @Nullable JsonValue b) { + return math(a, b, Math::min); + } + + public static @Nullable JsonNumber fmod(@Nullable JsonValue a, @Nullable JsonValue b) { + return math(a, b, (l, r) -> l % r); + } + + public static @Nullable JsonNumber frexp(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("frexp"); + // return math(a, b, Math::frexp); + } + + public static @Nullable JsonNumber hypot(@Nullable JsonValue x, @Nullable JsonValue y) { + return math(x, y, Math::hypot); + } + + public static @Nullable JsonNumber jn(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("jn"); + // return math(a, b, Math::jn); + } + + public static @Nullable JsonNumber ldexp(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("ldexp"); + // return math(a, b, Math::ldexp); + } + + public static @Nullable JsonNumber modf(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("modf"); + // return math(a, b, Math::modf); + } + + public static @Nullable JsonNumber nextafter(@Nullable JsonValue start, @Nullable JsonValue direction) { + return math(start, direction, Math::nextAfter); + } + + public static @Nullable JsonNumber nexttoward(@Nullable JsonValue start, @Nullable JsonValue direction) { + return math(start, direction, Math::nextAfter); + } + + public static @Nullable JsonNumber pow(@Nullable JsonValue a, @Nullable JsonValue b) { + return math(a, b, Math::pow); + } + + public static @Nullable JsonNumber remainder(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("remainder"); + // return math(a, b, Math::remainder); + } + + public static @Nullable JsonNumber scalb(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("scalb"); + // return math(a, b, Math::scalb); + } + + public static @Nullable JsonNumber scalbln(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("scalbln"); + // return math(a, b, Math::scalbln); + } + + public static @Nullable JsonNumber yn(@Nullable JsonValue a, @Nullable JsonValue b) { + throw new UnsupportedOperationException("yn"); + // return math(a, b, Math::yn); + } + + public static @Nullable JsonNumber fma(@Nullable JsonValue a, @Nullable JsonValue b, @Nullable JsonValue c) { + if (!(a instanceof JsonNumber(var number1))) { + throw error(a, "number required"); + } + if (!(b instanceof JsonNumber(var number2))) { + throw error(b, "number required"); + } + if (!(c instanceof JsonNumber(var number3))) { + throw error(c, "number required"); + } + var out = Math.fma(number1, number2, number3); + return Double.isNaN(out) ? null : new JsonNumber(out); + } + // + + // + private static @NotNull JsonQueryException error(@Nullable JsonValue value, @NotNull String message) { + return new JsonQueryException(STR."\{Util.type(value)} (\{Util.value(value)}) \{message}."); + } + + private static @NotNull JsonQueryException error(@Nullable JsonValue first, @Nullable JsonValue second, @NotNull String message) { + return new JsonQueryException(STR."\{Util.type(first)} (\{Util.value(first)}) and \{Util.type(second)} (\{Util.value(second)}) \{message}."); + } + // + +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/JsonQuery.java b/query/src/main/java/eu/jonahbauer/json/query/JsonQuery.java new file mode 100644 index 0000000..1aa38c8 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/JsonQuery.java @@ -0,0 +1,34 @@ +package eu.jonahbauer.json.query; + +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.parser.JQParser; +import eu.jonahbauer.json.query.parser.ast.JQExpression; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.stream.Stream; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class JsonQuery { + private final @NotNull JQExpression expression; + + public static @NotNull JsonQuery parse(@NotNull String query) { + try { + var parser = new JQParser(query); + var programm = parser.parseTopLevel(); + if (programm.expression() == null) throw new IllegalArgumentException(); + return new JsonQuery(programm.expression()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + public @NotNull Stream<@NotNull JsonValue> run(@Nullable JsonValue value) { + var context = new JQExpression.Context(value); + return expression.evaluate(context); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/JsonQueryException.java b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryException.java new file mode 100644 index 0000000..a5e3347 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryException.java @@ -0,0 +1,11 @@ +package eu.jonahbauer.json.query; + +public class JsonQueryException extends RuntimeException { + public JsonQueryException(String message) { + super(message); + } + + public JsonQueryException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/JsonQueryHaltException.java b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryHaltException.java new file mode 100644 index 0000000..419a769 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryHaltException.java @@ -0,0 +1,27 @@ +package eu.jonahbauer.json.query; + +import eu.jonahbauer.json.JsonString; +import eu.jonahbauer.json.JsonValue; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Getter +public class JsonQueryHaltException extends JsonQueryException { + private final @Nullable JsonValue value; + private final int statusCode; + + public JsonQueryHaltException(@Nullable JsonValue value, int statusCode) { + super(getMessage(value)); + this.value = value; + this.statusCode = statusCode; + } + + private static @NotNull String getMessage(@Nullable JsonValue value) { + return switch (value) { + case JsonString(var string) -> string; + case null -> ""; + default -> JsonValue.toJsonString(value); + }; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/JsonQueryParserException.java b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryParserException.java new file mode 100644 index 0000000..8bd2afa --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryParserException.java @@ -0,0 +1,28 @@ +package eu.jonahbauer.json.query; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +@Getter +public class JsonQueryParserException extends JsonQueryException { + private final int line; + private final int column; + + public JsonQueryParserException(int line, int column, @NotNull String message) { + super(message); + if (line < 0 || column < 0) throw new IllegalArgumentException(); + + this.line = line; + this.column = column; + } + + public JsonQueryParserException(int line, int column, @NotNull String message, @NotNull Throwable cause) { + this(line, column, message); + initCause(cause); + } + + @Override + public @NotNull String getMessage() { + return STR."\{super.getMessage()} at line \{line}, column \{column}"; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/JsonQueryTokenizerException.java b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryTokenizerException.java new file mode 100644 index 0000000..aa9e746 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryTokenizerException.java @@ -0,0 +1,13 @@ +package eu.jonahbauer.json.query; + +import org.jetbrains.annotations.NotNull; + +public class JsonQueryTokenizerException extends JsonQueryParserException { + public JsonQueryTokenizerException(int line, int column, @NotNull String message) { + super(line, column, message); + } + + public JsonQueryTokenizerException(int line, int column, @NotNull String message, @NotNull Throwable cause) { + super(line, column, message, cause); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/JsonQueryUserException.java b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryUserException.java new file mode 100644 index 0000000..0a4cd4c --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/JsonQueryUserException.java @@ -0,0 +1,23 @@ +package eu.jonahbauer.json.query; + +import eu.jonahbauer.json.JsonString; +import eu.jonahbauer.json.JsonValue; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +@Getter +public class JsonQueryUserException extends JsonQueryException { + private final @NotNull JsonValue value; + + public JsonQueryUserException(@NotNull JsonValue value) { + super(getMessage(value)); + this.value = value; + } + + private static @NotNull String getMessage(@NotNull JsonValue value) { + return switch (value) { + case JsonString(var string) -> string; + case JsonValue v -> "(not a string): " + v.toJsonString(); + }; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/Main.java b/query/src/main/java/eu/jonahbauer/json/query/Main.java new file mode 100644 index 0000000..7960791 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/Main.java @@ -0,0 +1,145 @@ +package eu.jonahbauer.json.query; + +import java.util.LinkedHashMap; +import java.util.SequencedMap; + +import static eu.jonahbauer.json.JsonTemplateProcessor.JSON; + +public class Main { + public static void main(String[] args) { + System.out.println(); + System.out.println("Object Identifier-Index: .foo, .foo.bar"); + + JsonQuery.parse(".foo") + .run(JSON."{\"foo\": 42, \"bar\": \"less interesting data\"}") + .forEach(System.out::println); + + JsonQuery.parse(".foo") + .run(JSON."{\"notfoo\": true, \"alsonotfoo\": false}") + .forEach(System.out::println); + + JsonQuery.parse(".[\"foo\"]") + .run(JSON."{\"foo\": 42}") + .forEach(System.out::println); + + JsonQuery.parse(".foo?") + .run(JSON."{\"foo\": 42, \"bar\": \"less interesting data\"}") + .forEach(System.out::println); + + System.out.println(); + System.out.println("Optional Object Identifier-Index: .foo?"); + + JsonQuery.parse(".foo?") + .run(JSON."{\"notfoo\": true, \"alsonotfoo\": false}") + .forEach(System.out::println); + + JsonQuery.parse(".[\"foo\"]?") + .run(JSON."{\"foo\": 42}") + .forEach(System.out::println); + + JsonQuery.parse("[.foo?]") + .run(JSON."[1, 2]") + .forEach(System.out::println); + + System.out.println(); + System.out.println("Array Index: .[]"); + + JsonQuery.parse(".[0]") + .run(JSON."[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]") + .forEach(System.out::println); + + JsonQuery.parse(".[2]") + .run(JSON."[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]") + .forEach(System.out::println); + + JsonQuery.parse(".[-2]") + .run(JSON."[1, 2, 3]") + .forEach(System.out::println); + + System.out.println(); + System.out.println("Array/String Slice: .[:]"); + + JsonQuery.parse(".[2:4]") + .run(JSON."[\"a\",\"b\",\"c\",\"d\",\"e\"]") + .forEach(System.out::println); + + JsonQuery.parse(".[2:4]") + .run(JSON."\"abcdefghi\"") + .forEach(System.out::println); + + JsonQuery.parse(".[:3]") + .run(JSON."[\"a\",\"b\",\"c\",\"d\",\"e\"]") + .forEach(System.out::println); + + JsonQuery.parse(".[-2:]") + .run(JSON."[\"a\",\"b\",\"c\",\"d\",\"e\"]") + .forEach(System.out::println); + + System.out.println(); + System.out.println("Array/Object Value Iterator: .[]"); + + JsonQuery.parse(".[]") + .run(JSON."[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]") + .forEach(System.out::println); + + JsonQuery.parse(".[]") + .run(JSON."[]") + .forEach(System.out::println); + + JsonQuery.parse(".foo[]") + .run(JSON."{\"foo\":[1,2,3]}") + .forEach(System.out::println); + + JsonQuery.parse(".[]") + .run(JSON."{\"a\": 1, \"b\": 1}") + .forEach(System.out::println); + +// var root = JsonValue.valueOf(List.of( +// List.of( +// List.of(10, 20), +// List.of(30, 40) +// ), +// List.of( +// List.of(1, 2), +// List.of(3, 4) +// ) +// )); +// +// var key = new JQCommaExpression(new JQConstant(new JsonNumber(0)), new JQConstant(new JsonNumber(1))); +// +// var pattern = new ArrayPattern(List.of( +// new JQAsExpression.Pattern.ObjectPattern(of( +// key, new JQAsExpression.Pattern.ObjectPattern(of( +// key, +// new JQAsExpression.Pattern.ValuePattern("$x") +// )) +// )), +// new JQAsExpression.Pattern.ObjectPattern(of( +// key, new JQAsExpression.Pattern.ObjectPattern(of( +// key, +// new JQAsExpression.Pattern.ValuePattern("$y") +// )) +// )) +// )); +// +// var expression = new JQBinaryExpression( +// new JQVariableExpression("$x"), +// new JQVariableExpression("$y"), +// JQBinaryExpression.Operator.ADD +// ); +// +// var as = new JQAsExpression(new JQConstant(root), List.of(pattern), expression); +// as.evaluate(new JQExpression.Context(null)).forEach(System.out::println); +// +// var parser = new JQParser("[{(0, 1): {(0,1): $x}}, {(0, 1): {(0, 1): $y}}]"); +// var pattern2 = parser.parsePattern(); +// System.out.println(pattern); +// System.out.println(pattern2); + } + + private static SequencedMap of(K key1, V value1) { + var map = new LinkedHashMap(); + map.put(key1, value1); + return map; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/ConcatGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/ConcatGenerator.java new file mode 100644 index 0000000..d66f108 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/ConcatGenerator.java @@ -0,0 +1,28 @@ +package eu.jonahbauer.json.query.impl; + +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +final class ConcatGenerator implements Generator { + private final @NotNull Queue<@NotNull Generator> generators; + + public ConcatGenerator(@NotNull List<@NotNull Generator> generators) { + this.generators = new LinkedList<>(generators); + } + + @Override + public T next() throws EndOfStreamException { + while (!generators.isEmpty()) { + try { + var current = generators.peek(); + return current.next(); + } catch (EndOfStreamException ex) { + generators.remove(); + } + } + throw new EndOfStreamException(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/EmptyGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/EmptyGenerator.java new file mode 100644 index 0000000..ad543cf --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/EmptyGenerator.java @@ -0,0 +1,10 @@ +package eu.jonahbauer.json.query.impl; + +enum EmptyGenerator implements Generator { + INSTANCE; + + @Override + public Object next() throws EndOfStreamException { + throw new EndOfStreamException(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/FlatMappingGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/FlatMappingGenerator.java new file mode 100644 index 0000000..7807410 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/FlatMappingGenerator.java @@ -0,0 +1,27 @@ +package eu.jonahbauer.json.query.impl; + +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +final class FlatMappingGenerator implements Generator { + private final @NotNull Generator source; + private final @NotNull Function> function; + private @NotNull Generator current = Generator.empty(); + + public FlatMappingGenerator(@NotNull Generator source, @NotNull Function> function) { + this.source = source; + this.function = function; + } + + @Override + public S next() throws EndOfStreamException { + while (true) { + try { + return current.next(); + } catch (EndOfStreamException _) { + current = function.apply(source.next()); + } + } + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/Generator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/Generator.java new file mode 100644 index 0000000..06763e1 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/Generator.java @@ -0,0 +1,33 @@ +package eu.jonahbauer.json.query.impl; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.function.Function; + +public interface Generator { + T next() throws EndOfStreamException; + + default @NotNull Generator map(@NotNull Function function) { + return new MappingGenerator<>(this, function); + } + + default @NotNull Generator flatMap(@NotNull Function> function) { + return new FlatMappingGenerator<>(this, function); + } + + @SuppressWarnings("unchecked") + static @NotNull Generator empty() { + return (Generator) EmptyGenerator.INSTANCE; + } + + static @NotNull Generator concat(@NotNull Generator first, @NotNull Generator second) { + return new ConcatGenerator<>(List.of(first, second)); + } + + static @NotNull Generator of(T value) { + return new SingletonGenerator<>(value); + } + + class EndOfStreamException extends RuntimeException {} +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/MappingGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/MappingGenerator.java new file mode 100644 index 0000000..3759ba5 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/MappingGenerator.java @@ -0,0 +1,12 @@ +package eu.jonahbauer.json.query.impl; + +import org.jetbrains.annotations.NotNull; + +import java.util.function.Function; + +record MappingGenerator(@NotNull Generator delegate, @NotNull Function function) implements Generator { + @Override + public S next() throws EndOfStreamException { + return function.apply(delegate.next()); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/SingletonGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/SingletonGenerator.java new file mode 100644 index 0000000..8f8a140 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/SingletonGenerator.java @@ -0,0 +1,21 @@ +package eu.jonahbauer.json.query.impl; + +final class SingletonGenerator implements Generator { + private boolean done; + private T value; + + public SingletonGenerator(T value) { + this.value = value; + } + + @Override + public T next() throws EndOfStreamException { + if (!done) { + var out = value; + done = true; + value = null; + return value; + } + throw new EndOfStreamException(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/JQParser.java b/query/src/main/java/eu/jonahbauer/json/query/parser/JQParser.java new file mode 100644 index 0000000..2eea9a3 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/JQParser.java @@ -0,0 +1,563 @@ +package eu.jonahbauer.json.query.parser; + +import eu.jonahbauer.json.*; +import eu.jonahbauer.json.query.JsonQueryParserException; +import eu.jonahbauer.json.query.parser.ast.*; +import eu.jonahbauer.json.query.parser.tokenizer.JQToken; +import eu.jonahbauer.json.query.parser.tokenizer.JQTokenKind; +import eu.jonahbauer.json.query.parser.tokenizer.JQTokenizer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.Reader; +import java.util.*; +import java.util.function.BinaryOperator; + +public class JQParser { + private final @NotNull JQTokenizer tokenizer; + private final @NotNull Queue pushback = new LinkedList<>(); + + public JQParser(@NotNull String string) { + this.tokenizer = new JQTokenizer(string); + } + + public JQParser(@NotNull Reader reader) { + this.tokenizer = new JQTokenizer(reader); + } + + public @NotNull JQProgram parseTopLevel() throws IOException { + var module = parseModule(); + var imports = parseImports(); + var functions = parseFuncDefs(); + var expr = peek() == null ? null : parseExpression(); + + return new JQProgram(module, imports, functions, expr); + } + + private @Nullable JQModule parseModule() throws IOException { + if (!tryConsume(JQTokenKind.MODULE)) return null; + var metadata = parseExpression(); + consume(JQTokenKind.SEMICOLON); + return new JQModule(metadata); + } + + private @NotNull List<@NotNull JQImport> parseImports() throws IOException { + var out = new ArrayList(); + + while (peek(JQTokenKind.IMPORT) || peek(JQTokenKind.INCLUDE)) { + out.add(parseImport()); + } + + return out; + } + + private @NotNull JQImport parseImport() throws IOException { + var include = tryConsume(JQTokenKind.INCLUDE); + if (!include) consume(JQTokenKind.IMPORT); + + var path = parseString(); + + String as; + if (include) { + as = null; + } else { + consume(JQTokenKind.AS); + var dollar = tryConsume(JQTokenKind.DOLLAR); + var ident = consume(JQTokenKind.IDENT); + as = (dollar ? "$" : "") + ident.text(); + } + + if (tryConsume(JQTokenKind.SEMICOLON)) { + return new JQImport(path, as, null); + } else { + var metadata = parseExpression(); + consume(JQTokenKind.SEMICOLON); + return new JQImport(path, as, metadata); + } + } + + private @NotNull List<@NotNull JQFunction> parseFuncDefs() throws IOException { + var out = new ArrayList(); + while (peek(JQTokenKind.DEF)) { + out.add(parseFuncDef()); + } + return out; + } + + private @NotNull JQFunction parseFuncDef() throws IOException { + consume(JQTokenKind.DEF); + var name = consume(JQTokenKind.IDENT).text(); + + var params = new ArrayList(); + if (tryConsume(JQTokenKind.LPAREN)) { + do { + var dollar = tryConsume(JQTokenKind.DOLLAR); + var param = consume(JQTokenKind.IDENT).text(); + params.add((dollar ? "$" : "") + param); + } while (tryConsume(JQTokenKind.SEMICOLON)); + + consume(JQTokenKind.RPAREN); + } + + consume(JQTokenKind.COLON); + var body = parseExpression(); + consume(JQTokenKind.SEMICOLON); + + return new JQFunction(name, params, body); + } + + private @NotNull JQExpression parseExpression() throws IOException { + if (peek(JQTokenKind.DEF)) { + + } else if (peek(JQTokenKind.REDUCE)) { + + } else if (peek(JQTokenKind.FOREACH)) { + + } else if (peek(JQTokenKind.IF)) { + + } else if (peek(JQTokenKind.LABEL)) { + + } else { + return parsePipeExpression(); + } + throw new UnsupportedOperationException("not yet implemented"); + } + + private @NotNull JQExpression parseNonAssocBinaryExpression( + @NotNull Map<@NotNull JQTokenKind, @NotNull BinaryOperator<@NotNull JQExpression>> operations, + @NotNull Downstream next + ) throws IOException { + var first = next.get(); + + var operator = peek(); + var operation = operator == null ? null : operations.get(operator.kind()); + if (operation != null) { + next(); + var second = next.get(); + first = operation.apply(first, second); + } + + return first; + } + + private @NotNull JQExpression parseLeftAssocBinaryExpression( + @NotNull Map<@NotNull JQTokenKind, @NotNull BinaryOperator<@NotNull JQExpression>> operations, + @NotNull Downstream next + ) throws IOException { + var first = next.get(); + + while (true) { + var operator = peek(); + var operation = operator == null ? null : operations.get(operator.kind()); + if (operation == null) break; + + next(); + var second = next.get(); + first = operation.apply(first, second); + } + + return first; + } + + private interface Downstream { + @NotNull + JQExpression get() throws IOException; + } + + private @NotNull JQExpression parsePipeExpression() throws IOException { + return parseLeftAssocBinaryExpression(Map.of( + JQTokenKind.PIPE, JQPipeExpression::new + ), this::parseCommaExpression); + } + + private @NotNull JQExpression parseCommaExpression() throws IOException { + return parseLeftAssocBinaryExpression(Map.of( + JQTokenKind.COMMA, JQCommaExpression::new + ), this::parseAlternativeExpression); + } + + private @NotNull JQExpression parseAlternativeExpression() throws IOException { + return parseLeftAssocBinaryExpression(Map.of( + JQTokenKind.DEFINEDOR, JQAlternativeExpression::new + ), this::parseAssignment); + } + + private @NotNull JQExpression parseAssignment() throws IOException { + return parseNonAssocBinaryExpression(Map.of( + JQTokenKind.ASSIGN, JQAssignment::new, + JQTokenKind.SETPLUS, JQAssignment::add, + JQTokenKind.SETMINUS, JQAssignment::sub, + JQTokenKind.SETMULT, JQAssignment::mul, + JQTokenKind.SETDIV, JQAssignment::div, + JQTokenKind.SETMOD, JQAssignment::mod, + JQTokenKind.SETPIPE, JQAssignmentPipe::new, + JQTokenKind.SETDEFINEDOR, JQAssignmentCoerce::new + ), this::parseBooleanOrExpression); + } + + private @NotNull JQExpression parseBooleanOrExpression() throws IOException { + return parseLeftAssocBinaryExpression(Map.of( + JQTokenKind.OR, JQBooleanOrExpression::new + ), this::parseBooleanAndExpression); + } + + private @NotNull JQExpression parseBooleanAndExpression() throws IOException { + return parseLeftAssocBinaryExpression(Map.of( + JQTokenKind.AND, JQBooleanAndExpression::new + ), this::parseComparisonExpression); + } + + private @NotNull JQExpression parseComparisonExpression() throws IOException { + return parseNonAssocBinaryExpression(Map.of( + JQTokenKind.EQ, JQBinaryExpression::eq, + JQTokenKind.NEQ, JQBinaryExpression::neq, + JQTokenKind.LESS, JQBinaryExpression::lt, + JQTokenKind.GREATER, JQBinaryExpression::gt, + JQTokenKind.LESSEQ, JQBinaryExpression::leq, + JQTokenKind.GREATEREQ, JQBinaryExpression::geq + ), this::parseAdditiveExpression); + } + + private @NotNull JQExpression parseAdditiveExpression() throws IOException { + return parseLeftAssocBinaryExpression(Map.of( + JQTokenKind.PLUS, JQBinaryExpression::add, + JQTokenKind.MINUS, JQBinaryExpression::sub + ), this::parseMultiplicativeExpression); + } + + private @NotNull JQExpression parseMultiplicativeExpression() throws IOException { + return parseLeftAssocBinaryExpression(Map.of( + JQTokenKind.MULT, JQBinaryExpression::mul, + JQTokenKind.DIV, JQBinaryExpression::div, + JQTokenKind.MOD, JQBinaryExpression::mod + ), this::parseTryCatch); + } + + private @NotNull JQExpression parseTryCatch() throws IOException { + if (tryConsume(JQTokenKind.TRY)) { + var expr = parseNegation(); + var fallback = tryConsume(JQTokenKind.CATCH) ? parseNegation() : null; + return new JQTryExpression(expr, fallback); + } else { + return parseNegation(); + } + } + + private @NotNull JQExpression parseNegation() throws IOException { + if (tryConsume(JQTokenKind.MINUS)) { + var expr = parseErrorSuppression(); + return new JQNegation(expr); + } else { + return parseErrorSuppression(); + } + } + + private @NotNull JQExpression parseErrorSuppression() throws IOException { + var expression = parseTermOrAs(); + if (tryConsume(JQTokenKind.QUESTION_MARK)) { + return new JQTryExpression(expression); + } else { + return expression; + } + } + + private @NotNull JQExpression parseTermOrAs() throws IOException { + var term = parseTerm(); + if (tryConsume(JQTokenKind.AS)) { + var patterns = new ArrayList(); + patterns.add(parsePattern()); + while (tryConsume(JQTokenKind.ALTERNATION)) { + patterns.add(parsePattern()); + } + consume(JQTokenKind.PIPE); + var expr = parseExpression(); + return new JQAsExpression(term, patterns, expr); + } + return term; + } + + private @NotNull JQAsExpression.Pattern parsePattern() throws IOException { + if (tryConsume(JQTokenKind.LBRACKET)) { + var patterns = new ArrayList(); + do { + patterns.add(parsePattern()); + } while (tryConsume(JQTokenKind.COMMA)); + consume(JQTokenKind.RBRACKET); + return new JQAsExpression.Pattern.ArrayPattern(patterns); + } else if (tryConsume(JQTokenKind.LBRACE)) { + var patterns = new LinkedHashMap(); + do { + if (tryConsume(JQTokenKind.DOLLAR)) { + var ident = consume(JQTokenKind.IDENT).text(); + var pattern = new JQAsExpression.Pattern.ValuePattern("$" + ident); + patterns.put(new JQConstant(JsonString.valueOf(ident)), pattern); + } else if (peek(JQTokenKind.IDENT)) { + var ident = consume(JQTokenKind.IDENT).text(); + consume(JQTokenKind.COLON); + var pattern = parsePattern(); + patterns.put(new JQConstant(JsonString.valueOf(ident)), pattern); + } else if (tryConsume(JQTokenKind.LPAREN)) { + var expr = parseExpression(); + consume(JQTokenKind.RPAREN); + consume(JQTokenKind.COLON); + var pattern = parsePattern(); + patterns.put(new JQParenthesizedExpression(expr), pattern); + } else { + var key = parseString(); + consume(JQTokenKind.COLON); + var pattern = parsePattern(); + patterns.put(key, pattern); + } + } while (tryConsume(JQTokenKind.COMMA)); + consume(JQTokenKind.RBRACE); + return new JQAsExpression.Pattern.ObjectPattern(patterns); + } else { + consume(JQTokenKind.DOLLAR); + var ident = consume(JQTokenKind.IDENT).text(); + return new JQAsExpression.Pattern.ValuePattern("$" + ident); + } + } + + private @NotNull JQExpression parseTerm() throws IOException { + var next = peek(); + var term = switch (next == null ? null : next.kind()) { + case NUMBER -> new JQConstant(new JsonNumber(Objects.requireNonNull(next().nval()))); + case LPAREN -> parseParenthesizedExpression(); + case LBRACKET -> parseArrayConstructionExpression(); + case LBRACE -> parseObjectConstructionExpression(); + case QQSTRING_START -> parseString(); + case FORMAT -> throw new UnsupportedOperationException("not yet implemented"); + case BREAK -> { + consume(JQTokenKind.IDENT); + consume(JQTokenKind.DOLLAR); + var label = consume(JQTokenKind.IDENT).text(); + throw new UnsupportedOperationException("not yet implemented"); + } + case IDENT -> switch (Objects.requireNonNull(peek()).text()) { + case "null" -> { + consume(JQTokenKind.IDENT); + yield new JQConstant(null); + } + case "true" -> { + consume(JQTokenKind.IDENT); + yield new JQConstant(JsonBoolean.TRUE); + } + case "false" -> { + consume(JQTokenKind.IDENT); + yield new JQConstant(JsonBoolean.FALSE); + } + default -> parseFunctionInvocation(); + }; + case LOC -> { + var loc = consume(JQTokenKind.LOC); + yield new JQLocExpression("", loc.line()); + } + case DOLLAR -> parseVariableExpression(); + case FIELD -> { + var name = consume(JQTokenKind.FIELD).text().substring(1); + var optional = tryConsume(JQTokenKind.QUESTION_MARK); + yield new JQIndexExpression(new JQRootExpression(), name, optional); + } + case DOT -> { + consume(JQTokenKind.DOT); + if (peek(JQTokenKind.QQSTRING_START)) { + var name = parseString(); + var optional = tryConsume(JQTokenKind.QUESTION_MARK); + yield new JQIndexExpression(new JQRootExpression(), name, optional); + } else { + yield new JQRootExpression(); + } + } + case REC -> { + consume(JQTokenKind.REC); + yield new JQRecursionExpression(); + } + case null -> throw new JsonQueryParserException(-1, -1, "unexpected end of file"); + default -> { + var token = Objects.requireNonNull(peek()); + throw new JsonQueryParserException(token.line(), token.column(), "unexpected token " + token.kind()); + } + }; + + while (peek(JQTokenKind.LBRACKET) || peek(JQTokenKind.DOT) || peek(JQTokenKind.FIELD)) { + term = parseIndexingExpression(term); + } + + return term; + } + + private @NotNull JQExpression parseFunctionInvocation() throws IOException { + var name = consume(JQTokenKind.IDENT).text(); + var args = new ArrayList(); + if (tryConsume(JQTokenKind.LPAREN)) { + do { + args.add(parseExpression()); + } while (tryConsume(JQTokenKind.SEMICOLON)); + consume(JQTokenKind.RPAREN); + } + return new JQFunctionInvocation(name, args); + } + + private @NotNull JQExpression parseVariableExpression() throws IOException { + var dollar = consume(JQTokenKind.DOLLAR); + var ident = consume(JQTokenKind.IDENT).text(); + return new JQVariableExpression("$" + ident); + } + + private @NotNull JQExpression parseParenthesizedExpression() throws IOException { + consume(JQTokenKind.LPAREN); + var expression = parseExpression(); + consume(JQTokenKind.RPAREN); + return new JQParenthesizedExpression(expression); + } + + private @NotNull JQExpression parseArrayConstructionExpression() throws IOException { + consume(JQTokenKind.LBRACKET); + if (tryConsume(JQTokenKind.RBRACKET)) { + return new JQConstant(JsonArray.EMPTY); + } else { + var expression = parseExpression(); + consume(JQTokenKind.RBRACKET); + return new JQArrayConstructionExpression(expression); + } + } + + private @NotNull JQExpression parseObjectConstructionExpression() throws IOException { + consume(JQTokenKind.LBRACE); + if (tryConsume(JQTokenKind.RBRACE)) { + return new JQConstant(JsonObject.EMPTY); + } else { + throw new UnsupportedOperationException("not yet implemented"); + } + } + + private @NotNull JQExpression parseIndexingExpression(@NotNull JQExpression root) throws IOException { + if (tryConsume(JQTokenKind.LBRACKET)) { + if (tryConsume(JQTokenKind.RBRACKET)) { + var optional = tryConsume(JQTokenKind.QUESTION_MARK); + return new JQIterateExpression(root, optional); + } else if (tryConsume(JQTokenKind.COLON)) { + var end = parseExpression(); + consume(JQTokenKind.RBRACKET); + var optional = tryConsume(JQTokenKind.QUESTION_MARK); + return new JQSliceExpression(root, null, end, optional); + } else { + var expr = parseExpression(); + if (tryConsume(JQTokenKind.COLON)) { + var end = peek(JQTokenKind.RBRACKET) ? null : parseExpression(); + consume(JQTokenKind.RBRACKET); + var optional = tryConsume(JQTokenKind.QUESTION_MARK); + return new JQSliceExpression(root, expr, end, optional); + } else { + consume(JQTokenKind.RBRACKET); + var optional = tryConsume(JQTokenKind.QUESTION_MARK); + return new JQIndexExpression(root, expr, optional); + } + } + } else if (peek(JQTokenKind.FIELD)) { + var name = consume(JQTokenKind.FIELD).text().substring(1); + var optional = tryConsume(JQTokenKind.QUESTION_MARK); + return new JQIndexExpression(root, name, optional); + } else { + consume(JQTokenKind.DOT); + var name = parseString(); + var optional = tryConsume(JQTokenKind.QUESTION_MARK); + return new JQIndexExpression(root, name, optional); + } + } + + private @NotNull JQStringInterpolation parseString() throws IOException { + var format = peek(JQTokenKind.FORMAT) ? next().text() : null; + + var current = new StringBuilder(); + var fragments = new ArrayList(); + var values = new ArrayList(); + + consume(JQTokenKind.QQSTRING_START); + + current.setLength(0); + while (peek(JQTokenKind.QQSTRING_TEXT)) { + current.append(next().sval()); + } + fragments.add(current.toString()); + + while (tryConsume(JQTokenKind.QQSTRING_INTERP_START)) { + values.add(parseExpression()); + consume(JQTokenKind.QQSTRING_INTERP_END); + + current.setLength(0); + while (peek(JQTokenKind.QQSTRING_TEXT)) { + current.append(next().sval()); + } + fragments.add(current.toString()); + } + + consume(JQTokenKind.QQSTRING_END); + return new JQStringInterpolation(format, fragments, values); + } + + + + + + + private void consume(@NotNull String ident) throws IOException { + var token = next(); + if (token.kind() != JQTokenKind.IDENT || !Objects.equals(ident, token.text())) { + throw new JsonQueryParserException(0, 0, ""); + } + } + + private @NotNull JQToken consume(@NotNull JQTokenKind kind) throws IOException { + var token = next(); + if (token.kind() != kind) throw new JsonQueryParserException(token.line(), token.column(), "unexpected " + token.kind() + " expected " + kind); + return token; + } + + private boolean tryConsume(@NotNull String ident) throws IOException { + var out = peek(ident); + if (out) next(); + return out; + } + + private boolean tryConsume(@NotNull JQTokenKind kind) throws IOException { + var out = peek(kind); + if (out) next(); + return out; + } + + private boolean peek(@NotNull String ident) throws IOException { + var token = peek(); + return token != null && token.kind() == JQTokenKind.IDENT && Objects.equals(ident, token.text()); + } + + private boolean peek(@NotNull JQTokenKind kind) throws IOException { + var token = peek(); + return token != null && token.kind() == kind; + } + + private @Nullable JQToken peek() throws IOException { + if (pushback.isEmpty()) { + var next = tokenizer.next(); + pushback(next); + return next; + } else { + return pushback.peek(); + } + } + + private @NotNull JQToken next() throws IOException { + if (pushback.isEmpty()) { + var next = tokenizer.next(); + if (next == null) throw new JsonQueryParserException(0, 0, "unexpected $end"); + return next; + } else { + return pushback.remove(); + } + } + + private void pushback(@Nullable JQToken token) { + pushback.add(token); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAlternativeExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAlternativeExpression.java new file mode 100644 index 0000000..35708ff --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAlternativeExpression.java @@ -0,0 +1,80 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonMath; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Supplier; +import java.util.stream.Gatherer; +import java.util.stream.Stream; + +@SuppressWarnings("preview") +public record JQAlternativeExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression { + public JQAlternativeExpression { + Objects.requireNonNull(first); + Objects.requireNonNull(second); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return first.evaluate(context).gather(new AlternativeGatherer(second, context)); + } + + @Override + public boolean isConstant() { + return first.isConstant() && second.isConstant(); + } + + @Override + public @NotNull String toString() { + return first + " // " + second; + } + + @NoArgsConstructor + @AllArgsConstructor + private static class State { + private boolean empty = true; + } + + private record AlternativeGatherer( + @NotNull JQExpression expression, + @NotNull Context context + ) implements Gatherer { + + @Override + public @NotNull Supplier initializer() { + return State::new; + } + + @Override + public @NotNull Integrator integrator() { + return Integrator.ofGreedy((state, element, downstream) -> { + if (JsonMath.isTruthy(element)) { + state.empty = false; + return downstream.push(element); + } + return true; + }); + } + + @Override + public @NotNull BinaryOperator combiner() { + return (state1, state2) -> new State(state1.empty && state2.empty); + } + + @Override + public @NotNull BiConsumer> finisher() { + return (state, downstream) -> { + if (!state.empty) return; + var it = expression.evaluate(context).iterator(); + while (it.hasNext() && downstream.push(it.next())); + }; + } + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQArrayConstructionExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQArrayConstructionExpression.java new file mode 100644 index 0000000..661d14e --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQArrayConstructionExpression.java @@ -0,0 +1,31 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonArray; +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQArrayConstructionExpression(@NotNull JQExpression expression) implements JQExpression { + public JQArrayConstructionExpression { + Objects.requireNonNull(expression); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return Util.lazy(() -> new JsonArray(expression.evaluate(context).toList())); + } + + @Override + public boolean isConstant() { + return expression.isConstant(); + } + + @Override + public @NotNull String toString() { + return "[" + expression + "]"; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAsExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAsExpression.java new file mode 100644 index 0000000..8cef3cb --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAsExpression.java @@ -0,0 +1,173 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.*; +import eu.jonahbauer.json.query.JsonMath; +import eu.jonahbauer.json.query.JsonQueryException; +import eu.jonahbauer.json.query.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public record JQAsExpression( + @NotNull JQExpression variable, @NotNull List<@NotNull Pattern> patterns, @NotNull JQExpression expression +) implements JQExpression { + + public JQAsExpression { + Objects.requireNonNull(variable); + Objects.requireNonNull(expression); + + patterns = List.copyOf(patterns); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + var variables = new HashMap(); + patterns.stream().map(Pattern::variables).flatMap(Set::stream).forEach(key -> variables.put(key, null)); + + return variable.evaluate(context) + .flatMap(value -> { + Stream> result = null; + + // find first pattern that does not throw + var it = patterns.iterator(); + while (it.hasNext()) { + try { + result = it.next().bind(context, value); + } catch (JsonQueryException ex) { + if (!it.hasNext()) throw ex; + } + } + + // execute expression for all possible pattern matches + assert result != null; + return result + .map(vars -> { + var out = new HashMap<>(variables); + out.putAll(vars); + return context.withVariables(out); + }) + .flatMap(expression::evaluate); + }); + } + + @Override + public boolean isConstant() { + return variable.isConstant() && expression.isConstant(); + } + + @Override + public @NotNull String toString() { + return variable + + " as " + patterns.stream().map(Objects::toString).collect(Collectors.joining(" ?// ")) + + " | " + expression; + } + + public sealed interface Pattern { + @NotNull Set<@NotNull String> variables(); + @NotNull Stream> bind(@NotNull Context context, @Nullable JsonValue value); + + record ValuePattern(@NotNull String name) implements Pattern { + public ValuePattern { + Objects.requireNonNull(name); + if (!name.startsWith("$")) throw new IllegalArgumentException(); + } + + @Override + public @NotNull Set<@NotNull String> variables() { + return Set.of(name); + } + + @Override + public @NotNull Stream> bind(@NotNull Context context, @Nullable JsonValue value) { + var map = new HashMap(); + map.put(name, value); + return Stream.of(Collections.unmodifiableMap(map)); + } + + @Override + public @NotNull String toString() { + return name; + } + } + + record ArrayPattern(@NotNull List<@NotNull Pattern> patterns) implements Pattern { + public ArrayPattern { + patterns = List.copyOf(patterns); + if (patterns.isEmpty()) throw new IllegalArgumentException(); + } + + @Override + public @NotNull Set<@NotNull String> variables() { + var out = new HashSet(); + patterns.forEach(p -> out.addAll(p.variables())); + return Collections.unmodifiableSet(out); + } + + @Override + public @NotNull Stream> bind(@NotNull Context context, @Nullable JsonValue value) { + var streams = new ArrayList>>>(); + + for (int i = 0; i < patterns.size(); i++) { + var k = new JsonNumber(i); + var v = JsonMath.index(value, k); + + var pattern = patterns.get(i); + streams.add(() -> pattern.bind(context, v)); + } + + return Util.crossReversed(streams).map(list -> { + var map = new HashMap(); + list.forEach(map::putAll); + return map; + }); + } + + @Override + public @NotNull String toString() { + return patterns.stream().map(Objects::toString).collect(Collectors.joining(", ", "[", "]")); + } + } + + record ObjectPattern(@NotNull SequencedMap<@NotNull JQExpression, @NotNull Pattern> patterns) implements Pattern { + + public ObjectPattern { + patterns = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(patterns)); + if (patterns.isEmpty()) throw new IllegalArgumentException(); + } + + @Override + public @NotNull Set<@NotNull String> variables() { + var out = new HashSet(); + patterns.values().forEach(p -> out.addAll(p.variables())); + return Collections.unmodifiableSet(out); + } + + @Override + public @NotNull Stream> bind(@NotNull Context context, @Nullable JsonValue value) { + var streams = new ArrayList>>>(); + + this.patterns.reversed().forEach((keyExpression, pattern) -> streams.add(() -> { + var keyStream = keyExpression.evaluate(context); + return keyStream.flatMap(key -> pattern.bind(context, JsonMath.index(value, key))); + })); + + return Util.cross(streams).map(list -> { + var map = new HashMap(); + list.reversed().forEach(map::putAll); + return map; + }); + } + + @Override + public @NotNull String toString() { + var out = new StringJoiner(", ", "{", "}"); + patterns.forEach((key, pattern) -> out.add(key + ": " + pattern)); + return out.toString(); + } + } + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAssignment.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAssignment.java new file mode 100644 index 0000000..df5e544 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAssignment.java @@ -0,0 +1,72 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonMath; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.BinaryOperator; +import java.util.stream.Stream; + +public record JQAssignment(@NotNull JQExpression target, @NotNull JQExpression value, @NotNull Operator operator) implements JQExpression { + public static @NotNull JQAssignment add(@NotNull JQExpression target, @NotNull JQExpression value) { + return new JQAssignment(target, value, Operator.ADD); + } + + public static @NotNull JQAssignment sub(@NotNull JQExpression target, @NotNull JQExpression value) { + return new JQAssignment(target, value, Operator.SUB); + } + + public static @NotNull JQAssignment mul(@NotNull JQExpression target, @NotNull JQExpression value) { + return new JQAssignment(target, value, Operator.MUL); + } + + public static @NotNull JQAssignment div(@NotNull JQExpression target, @NotNull JQExpression value) { + return new JQAssignment(target, value, Operator.DIV); + } + + public static @NotNull JQAssignment mod(@NotNull JQExpression target, @NotNull JQExpression value) { + return new JQAssignment(target, value, Operator.MOD); + } + + public JQAssignment { + Objects.requireNonNull(target); + Objects.requireNonNull(value); + Objects.requireNonNull(operator); + } + + public JQAssignment(@NotNull JQExpression target, @NotNull JQExpression value) { + this(target, value, Operator.ID); + } + + @Override + public @NotNull Stream evaluate(@NotNull Context context) { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public boolean isConstant() { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public @NotNull String toString() { + return STR."\{target} \{operator.symbol} \{value}"; + } + + @RequiredArgsConstructor + public enum Operator { + ID((_, value) -> value, "="), + ADD(JsonMath::add, "+="), + SUB(JsonMath::sub, "-="), + MUL(JsonMath::mul, "*="), + DIV(JsonMath::div, "/="), + MOD(JsonMath::mod, "%="), + ; + + private final @NotNull BinaryOperator<@Nullable JsonValue> operator; + private final @NotNull String symbol; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAssignmentCoerce.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAssignmentCoerce.java new file mode 100644 index 0000000..5583b58 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAssignmentCoerce.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQAssignmentCoerce(@NotNull JQExpression target, @NotNull JQExpression value) implements JQExpression { + + public JQAssignmentCoerce { + Objects.requireNonNull(target); + Objects.requireNonNull(value); + } + + @Override + public @NotNull Stream evaluate(@NotNull Context context) { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public boolean isConstant() { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public @NotNull String toString() { + return target + " //= " + value; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAssignmentPipe.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAssignmentPipe.java new file mode 100644 index 0000000..f0865d5 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQAssignmentPipe.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQAssignmentPipe(@NotNull JQExpression target, @NotNull JQExpression value) implements JQExpression { + + public JQAssignmentPipe { + Objects.requireNonNull(target); + Objects.requireNonNull(value); + } + + @Override + public @NotNull Stream evaluate(@NotNull Context context) { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public boolean isConstant() { + throw new UnsupportedOperationException("not yet implemented"); + } + + @Override + public @NotNull String toString() { + return target + " |= " + value; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBinaryExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBinaryExpression.java new file mode 100644 index 0000000..96351ce --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBinaryExpression.java @@ -0,0 +1,105 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonMath; +import eu.jonahbauer.json.query.util.Util; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.function.BinaryOperator; +import java.util.stream.Stream; + +public record JQBinaryExpression(@NotNull JQExpression first, @NotNull JQExpression second, @NotNull Operator operator) implements JQExpression { + public static @NotNull JQBinaryExpression add(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.ADD); + } + + public static @NotNull JQBinaryExpression sub(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.SUB); + } + + public static @NotNull JQBinaryExpression mul(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.MUL); + } + + public static @NotNull JQBinaryExpression div(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.DIV); + } + + public static @NotNull JQBinaryExpression mod(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.MOD); + } + + public static @NotNull JQBinaryExpression eq(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.EQ); + } + + public static @NotNull JQBinaryExpression neq(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.NEQ); + } + + public static @NotNull JQBinaryExpression lt(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.LT); + } + + public static @NotNull JQBinaryExpression gt(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.GT); + } + + public static @NotNull JQBinaryExpression leq(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.LEQ); + } + + public static @NotNull JQBinaryExpression geq(@NotNull JQExpression first, @NotNull JQExpression second) { + return new JQBinaryExpression(first, second, Operator.GEQ); + } + + public JQBinaryExpression { + Objects.requireNonNull(first); + Objects.requireNonNull(second); + Objects.requireNonNull(operator); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return Util.crossReversed(List.of(first, second), context) + .map(values -> operator.apply(values.getFirst(), values.getLast())); + } + + @Override + public boolean isConstant() { + return first.isConstant() && second.isConstant(); + } + + @Override + public @NotNull String toString() { + return STR."\{first} \{operator.symbol} \{second}"; + } + + @RequiredArgsConstructor + public enum Operator implements BinaryOperator<@Nullable JsonValue> { + ADD(JsonMath::add, "+"), + SUB(JsonMath::sub, "-"), + MUL(JsonMath::mul, "*"), + DIV(JsonMath::div, "/"), + MOD(JsonMath::mod, "%"), + EQ(JsonMath::eq, "=="), + NEQ(JsonMath::neq, "!="), + LT(JsonMath::lt, "<"), + GT(JsonMath::gt, ">"), + LEQ(JsonMath::leq, "<="), + GEQ(JsonMath::geq, ">="), + ; + + private final @NotNull BinaryOperator<@Nullable JsonValue> operator; + private final @NotNull String symbol; + + @Override + public @Nullable JsonValue apply(@Nullable JsonValue value, @Nullable JsonValue value2) { + return operator.apply(value, value2); + } + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBooleanAndExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBooleanAndExpression.java new file mode 100644 index 0000000..68496d4 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBooleanAndExpression.java @@ -0,0 +1,36 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonBoolean; +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonMath; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQBooleanAndExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression { + + public JQBooleanAndExpression { + Objects.requireNonNull(first); + Objects.requireNonNull(second); + } + + @Override + public @NotNull Stream evaluate(@NotNull Context context) { + return first.evaluate(context) + .flatMap(value -> JsonMath.isFalsy(value) + ? Stream.of(JsonBoolean.FALSE) + : second.evaluate(context).map(JsonMath::isTruthy).map(JsonBoolean::valueOf) + ); + } + + @Override + public boolean isConstant() { + return first.isConstant() && second.isConstant(); + } + + @Override + public @NotNull String toString() { + return first + " and " + second; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBooleanOrExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBooleanOrExpression.java new file mode 100644 index 0000000..cd753d9 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBooleanOrExpression.java @@ -0,0 +1,36 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonBoolean; +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonMath; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQBooleanOrExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression { + + public JQBooleanOrExpression { + Objects.requireNonNull(first); + Objects.requireNonNull(second); + } + + @Override + public @NotNull Stream evaluate(@NotNull Context context) { + return first.evaluate(context) + .flatMap(value -> JsonMath.isTruthy(value) + ? Stream.of(JsonBoolean.TRUE) + : second.evaluate(context).map(JsonMath::isTruthy).map(JsonBoolean::valueOf) + ); + } + + @Override + public boolean isConstant() { + return first.isConstant() && second.isConstant(); + } + + @Override + public @NotNull String toString() { + return first + " or " + second; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBuiltIn.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBuiltIn.java new file mode 100644 index 0000000..9511680 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQBuiltIn.java @@ -0,0 +1,276 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.*; +import eu.jonahbauer.json.query.JsonMath; +import eu.jonahbauer.json.query.JsonQueryException; +import eu.jonahbauer.json.query.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Gatherer; +import java.util.stream.Stream; + +public enum JQBuiltIn implements JQInvocable { + ABS(0, (context, _) -> context.stream().map(JsonMath::abs)), + ADD(0, (context, _) -> context.stream().map(value -> JsonMath.values(value).reduce(null, JsonMath::add))), + NOT(0, (context, _) -> context.stream().map(JsonMath::not)), + + // error handling + ERROR$0(0, (context, _) -> context.stream().flatMap(JsonMath::error)), + ERROR$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(JsonMath::error)), + HALT(0, (_, _) -> Stream.of((JsonValue) null).flatMap(_ -> JsonMath.halt())), + HALT_ERROR$0(0, (context, _) -> context.stream().flatMap(JsonMath::halt)), + HALT_ERROR$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(value -> JsonMath.halt(context.root(), value))), + + // stream operations + EMPTY(0, (_, _) -> Stream.empty()), + RANGE$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(JsonMath::range)), + RANGE$2(2, (context, args) -> Util.cross(args, context).flatMap(bounds -> JsonMath.range(bounds.get(0), bounds.get(1)))), + RANGE$3(3, (context, args) -> Util.cross(args, context).flatMap(bounds -> JsonMath.range(bounds.get(0), bounds.get(1), bounds.get(2)))), + LIMIT(2, (context, args) -> args.getFirst().evaluate(context).flatMap(limit -> JsonMath.limit(args.getLast().evaluate(context), limit))), + FIRST$1(1, (context, args) -> JsonMath.first(args.getFirst().evaluate(context))), + LAST$1(1, (context, args) -> JsonMath.last(args.getFirst().evaluate(context))), + NTH$2(2, (context, args) -> args.getFirst().evaluate(context).flatMap(n -> JsonMath.nth(args.getLast().evaluate(context), n))), + + // iterable operations + MAP(1, (context, args) -> { + var filter = args.getFirst().bind(context); + return context.stream().map(value -> JsonMath.map(value, filter)); + }), + MAP_VALUES(1, (context, args) -> { + var filter = args.getFirst().bind(context); + return context.stream().map(value -> JsonMath.mapValues(value, filter)); + }), + KEYS(0, (context, _) -> context.stream().map(JsonMath::keys)), + KEYS_UNSORTED(0, (context, _) -> context.stream().map(JsonMath::keysUnsorted)), + HAS(1, (context, args) -> args.getFirst().evaluate(context).map(index -> JsonMath.has(context.root(), index))), + IN(1, (context, args) -> args.getFirst().evaluate(context).map(value -> JsonMath.in(context.root(), value))), + FIRST$0(0, (context, _) -> context.stream().map(value -> JsonMath.index(value, JsonNumber.ZERO))), + LAST$0(0, (context, _) -> context.stream().map(value -> JsonMath.index(value, new JsonNumber(-1)))), + NTH$1(1, (context, args) -> args.getFirst().evaluate(context).map(index -> JsonMath.index(context.root(), index))), + ANY$0(0, (context, _) -> context.stream().map(JsonMath::any)), + ANY$1(1, (context, args) -> { + var filter = args.getFirst().bind(context); + return context.stream().map(value -> JsonMath.any(value, filter)); + }), + ANY$2(2, (context, args) -> { + var filter = args.getFirst().bind(context); + return Util.lazy(() -> JsonMath.any(args.getFirst().evaluate(context), filter)); + }), + ALL$0(0, (context, _) -> context.stream().map(JsonMath::all)), + ALL$1(1, (context, args) -> { + var filter = args.getFirst().bind(context); + return context.stream().map(value -> JsonMath.all(value, filter)); + }), + ALL$2(2, (context, args) -> { + var filter = args.getFirst().bind(context); + return Util.lazy(() -> JsonMath.all(args.getFirst().evaluate(context), filter)); + }), + FLATTEN$0(0, (context, _) -> context.stream().map(JsonMath::flatten)), + FLATTEN$1(1, (context, args) -> context.stream().flatMap(value -> args.getFirst().evaluate(context).map(depth -> JsonMath.flatten(value, depth)))), + SORT(0, (context, _) -> context.stream().map(JsonMath::sort)), + SORT_BY(1, (context, args) -> { + var filter = args.getFirst().bind(context); + return context.stream().map(value -> JsonMath.sort(value, filter)); + }), + MIN(0, (context, _) -> context.stream().map(JsonMath::min)), + MIN_BY(1, (context, args) -> { + var filter = args.getFirst().bind(context); + return context.stream().map(value -> JsonMath.min(value, filter)); + }), + MAX(0, (context, _) -> context.stream().map(JsonMath::max)), + MAX_BY(1, (context, args) -> { + var filter = args.getFirst().bind(context); + return context.stream().map(value -> JsonMath.max(value, filter)); + }), + UNIQUE(0, (context, _) -> context.stream().map(JsonMath::unique)), + UNIQUE_BY(1, (context, args) -> { + var filter = args.getFirst().bind(context); + return context.stream().map(value -> JsonMath.unique(value, filter)); + }), + GROUP_BY(1, (context, args) -> { + var filter = args.getFirst().bind(context); + return context.stream().map(value -> JsonMath.group(value, filter)); + }), + REVERSE(0, (context, _) -> context.stream().map(JsonMath::reverse)), + CONTAINS(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.contains(context.root(), content))), + INDICES(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.indices(context.root(), content))), + INDEX(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.firstindex(context.root(), content))), + RINDEX(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.lastindex(context.root(), content))), + INSIDE(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.inside(context.root(), content))), + COMBINATIONS$0(0, (context, _) -> context.stream().flatMap(JsonMath::combinations)), + COMBINATIONS$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(n -> JsonMath.combinations(context.root(), n))), + BSEARCH(1, (context, args) -> args.getFirst().evaluate(context).map(value -> JsonMath.bsearch(context.root(), value))), + TRANSPOSE(0, (context, _) -> context.stream().map(JsonMath::transpose)), + + // filters + ARRAYS(0, (context, _) -> context.stream().filter(JsonMath::isArray)), + OBJECTS(0, (context, _) -> context.stream().filter(JsonMath::isObject)), + ITERABLES(0, (context, _) -> context.stream().filter(JsonMath::isIterable)), + BOOLEANS(0, (context, _) -> context.stream().filter(JsonMath::isBoolean)), + NUMBERS(0, (context, _) -> context.stream().filter(JsonMath::isNumber)), + NORMALS(0, (context, _) -> context.stream().filter(JsonMath::isNormal)), + FINITES(0, (context, _) -> context.stream().filter(JsonMath::isFinite)), + STRINGS(0, (context, _) -> context.stream().filter(JsonMath::isString)), + NULLS(0, (context, _) -> context.stream().filter(JsonMath::isNull)), + VALUES(0, (context, _) -> context.stream().filter(JsonMath::isValue)), + SCALARS(0, (context, _) -> context.stream().filter(JsonMath::isScalar)), + + // checks + ISINFINITE(0, (context, _) -> context.stream().map(JsonMath::isInfinite).map(JsonBoolean::valueOf)), + ISNAN(0, (context, _) -> context.stream().map(JsonMath::isNan).map(JsonBoolean::valueOf)), + ISFINITE(0, (context, _) -> context.stream().map(JsonMath::isFinite).map(JsonBoolean::valueOf)), + ISNORMAL(0, (context, _) -> context.stream().map(JsonMath::isNormal).map(JsonBoolean::valueOf)), + ISEMPTY(1, (context, args) -> Util.lazy(() -> JsonBoolean.valueOf(JsonMath.isEmpty(args.getFirst().evaluate(context))))), + + // string operations + TRIM(0, (context, _) -> context.stream().map(JsonMath::trim)), + LTRIM(0, (context, _) -> context.stream().map(JsonMath::ltrim)), + RTRIM(0, (context, _) -> context.stream().map(JsonMath::rtrim)), + LTRIMSTR(1, (context, args) -> args.getFirst().evaluate(context).map(prefix -> JsonMath.ltrimstr(context.root(), prefix))), + RTRIMSTR(1, (context, args) -> args.getFirst().evaluate(context).map(suffix -> JsonMath.rtrimstr(context.root(), suffix))), + SPLIT$1(1, (context, args) -> args.getFirst().evaluate(context).map(separator -> JsonMath.split(context.root(), separator))), + JOIN(1, (context, args) -> args.getFirst().evaluate(context).map(separator -> JsonMath.join(context.root(), separator))), + IMPLODE(0, (context, _) -> context.stream().map(JsonMath::implode)), + EXPLODE(0, (context, _) -> context.stream().map(JsonMath::explode)), + ASCII_UPCASE(0, (context, _) -> context.stream().map(JsonMath::asciiUpcase)), + ASCII_DOWNCASE(0, (context, _) -> context.stream().map(JsonMath::asciiDowncase)), + UTF8BYTELENGTH(0, (context, _) -> context.stream().map(JsonMath::utf8ByteLength)), + STARTSWITH(1, (context, args) -> args.getFirst().evaluate(context).map(prefix -> JsonMath.startswith(context.root(), prefix))), + ENDSWITH(1, (context, args) -> args.getFirst().evaluate(context).map(prefix -> JsonMath.endswith(context.root(), prefix))), + + // regex + TEST$1(1, JsonMath::test), + TEST$2(2, JsonMath::test), + MATCH$1(1, JsonMath::match), + MATCH$2(2, JsonMath::match), + CAPTURE$1(1, JsonMath::capture), + CAPTURE$2(2, JsonMath::capture), + SCAN$1(1, JsonMath::scan), + SCAN$2(2, JsonMath::scan), + SPLIT$2(2, JsonMath::split), + SPLITS$1(1, JsonMath::splits), + SPLITS$2(2, JsonMath::splits), + SUB$2(2, JsonMath::sub), + SUB$3(3, JsonMath::sub), + GSUB$2(2, JsonMath::gsub), + GSUB$3(3, JsonMath::gsub), + + // conversions + TYPE(0, (context, _) -> context.stream().map(JsonMath::type)), + TOJSON(0, (context, _) -> context.stream().map(JsonMath::tojson)), + TOSTRING(0, (context, _) -> context.stream().map(JsonMath::tostring)), + TONUMBER(0, (context, _) -> context.stream().map(JsonMath::tonumber)), + FROMJSON(0, (context, _) -> context.stream().map(JsonMath::fromjson)), + + // misc + LENGTH(0, (context, _) -> context.stream().map(JsonMath::length)), + REPEAT(1, (context, args) -> Stream.generate(() -> args.getFirst().evaluate(context)).flatMap(Function.identity())), + + // math library + ACOS(0, (context, _) -> context.stream().map(JsonMath::acos)), + ACOSH(0, (context, _) -> context.stream().map(JsonMath::acosh)), + ASIN(0, (context, _) -> context.stream().map(JsonMath::asin)), + ASINH(0, (context, _) -> context.stream().map(JsonMath::asinh)), + ATAN(0, (context, _) -> context.stream().map(JsonMath::atan)), + ATANH(0, (context, _) -> context.stream().map(JsonMath::atanh)), + CBRT(0, (context, _) -> context.stream().map(JsonMath::cbrt)), + CEIL(0, (context, _) -> context.stream().map(JsonMath::ceil)), + COS(0, (context, _) -> context.stream().map(JsonMath::cos)), + COSH(0, (context, _) -> context.stream().map(JsonMath::cosh)), + ERF(0, (context, _) -> context.stream().map(JsonMath::erf)), + ERFC(0, (context, _) -> context.stream().map(JsonMath::erfc)), + EXP(0, (context, _) -> context.stream().map(JsonMath::exp)), + EXP10(0, (context, _) -> context.stream().map(JsonMath::exp10)), + EXP2(0, (context, _) -> context.stream().map(JsonMath::exp2)), + EXPM1(0, (context, _) -> context.stream().map(JsonMath::expm1)), + FABS(0, (context, _) -> context.stream().map(JsonMath::fabs)), + FLOOR(0, (context, _) -> context.stream().map(JsonMath::floor)), + GAMMA(0, (context, _) -> context.stream().map(JsonMath::gamma)), + J0(0, (context, _) -> context.stream().map(JsonMath::j0)), + J1(0, (context, _) -> context.stream().map(JsonMath::j1)), + LGAMMA(0, (context, _) -> context.stream().map(JsonMath::lgamma)), + LOG(0, (context, _) -> context.stream().map(JsonMath::log)), + LOG10(0, (context, _) -> context.stream().map(JsonMath::log10)), + LOG1P(0, (context, _) -> context.stream().map(JsonMath::log1p)), + LOG2(0, (context, _) -> context.stream().map(JsonMath::log2)), + LOGB(0, (context, _) -> context.stream().map(JsonMath::logb)), + NEARBYINT(0, (context, _) -> context.stream().map(JsonMath::nearbyint)), + RINT(0, (context, _) -> context.stream().map(JsonMath::rint)), + ROUND(0, (context, _) -> context.stream().map(JsonMath::round)), + SIGNIFICAND(0, (context, _) -> context.stream().map(JsonMath::significand)), + SIN(0, (context, _) -> context.stream().map(JsonMath::sin)), + SINH(0, (context, _) -> context.stream().map(JsonMath::sinh)), + SQRT(0, (context, _) -> context.stream().map(JsonMath::sqrt)), + TAN(0, (context, _) -> context.stream().map(JsonMath::tan)), + TANH(0, (context, _) -> context.stream().map(JsonMath::tanh)), + TGAMMA(0, (context, _) -> context.stream().map(JsonMath::tgamma)), + TRUNC(0, (context, _) -> context.stream().map(JsonMath::trunc)), + Y0(0, (context, _) -> context.stream().map(JsonMath::y0)), + Y1(0, (context, _) -> context.stream().map(JsonMath::y1)), + ATAN2(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.atan2(values.get(0), values.get(1)))), + COPYSIGN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.copysign(values.get(0), values.get(1)))), + DREM(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.drem(values.get(0), values.get(1)))), + FDIM(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fdim(values.get(0), values.get(1)))), + FMAX(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fmax(values.get(0), values.get(1)))), + FMIN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fmin(values.get(0), values.get(1)))), + FMOD(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fmod(values.get(0), values.get(1)))), + FREXP(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.frexp(values.get(0), values.get(1)))), + HYPOT(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.hypot(values.get(0), values.get(1)))), + JN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.jn(values.get(0), values.get(1)))), + LDEXP(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.ldexp(values.get(0), values.get(1)))), + MODF(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.modf(values.get(0), values.get(1)))), + NEXTAFTER(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.nextafter(values.get(0), values.get(1)))), + NEXTTOWARD(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.nexttoward(values.get(0), values.get(1)))), + POW(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.pow(values.get(0), values.get(1)))), + REMAINDER(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.remainder(values.get(0), values.get(1)))), + SCALB(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.scalb(values.get(0), values.get(1)))), + SCALBLN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.scalbln(values.get(0), values.get(1)))), + YN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.yn(values.get(0), values.get(1)))), + FMA(3, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fma(values.get(0), values.get(1), values.get(2)))) + ; + + public static final @NotNull Map<@NotNull String, @NotNull JQInvocable> ALL_BUILTINS = Arrays.stream(JQBuiltIn.values()) + .collect(Collectors.toMap( + JQInvocable::reference, + Function.identity() + )); + + private final @NotNull String identifier; + private final int arity; + private final @NotNull Implementation implementation; + + JQBuiltIn(int arity, @NotNull Implementation implementation) { + var identifier = this.name().toLowerCase(Locale.ROOT); + var idx = identifier.lastIndexOf("$"); + if (idx != -1) identifier = identifier.substring(0, idx); + + this.identifier = identifier; + this.arity = arity; + this.implementation = implementation; + } + + @Override + public @NotNull Stream<@Nullable JsonValue> invoke(JQExpression.@NotNull Context context, @NotNull List<@NotNull JQExpression> args) { + if (args.size() != arity) throw new JsonQueryException("invalid argument count"); + return implementation.invoke(context, args); + } + + @Override + public @NotNull String identifier() { + return identifier; + } + + @Override + public int arity() { + return arity; + } + + @FunctionalInterface + private interface Implementation { + @NotNull Stream<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQCommaExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQCommaExpression.java new file mode 100644 index 0000000..7a1fed6 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQCommaExpression.java @@ -0,0 +1,58 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public record JQCommaExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression { + public JQCommaExpression { + Objects.requireNonNull(first); + Objects.requireNonNull(second); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + var it = new Iterator() { + private Iterator delegate; + private Queue>> queue = new LinkedList<>(List.of( + () -> first.evaluate(context).iterator(), + () -> second.evaluate(context).iterator() + )); + + @Override + public boolean hasNext() { + if (delegate == null) delegate = queue.remove().get(); + while (!delegate.hasNext() && !queue.isEmpty()) { + delegate = queue.remove().get(); + } + return delegate.hasNext(); + } + + @Override + public JsonValue next() { + if (delegate == null) delegate = queue.remove().get(); + while (!delegate.hasNext() && !queue.isEmpty()) { + delegate = queue.remove().get(); + } + return delegate.next(); + } + }; + var spliterator = Spliterators.spliteratorUnknownSize(it, Spliterator.ORDERED); + return StreamSupport.stream(spliterator, false); + } + + @Override + public boolean isConstant() { + return first.isConstant() && second.isConstant(); + } + + @Override + public @NotNull String toString() { + return first + ", " + second; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQConstant.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQConstant.java new file mode 100644 index 0000000..edd2c94 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQConstant.java @@ -0,0 +1,25 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.stream.Stream; + +public record JQConstant(@Nullable JsonValue value) implements JQExpression { + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return Stream.of(value); + } + + @Override + public boolean isConstant() { + return true; + } + + @Override + public @NotNull String toString() { + return JsonValue.toJsonString(value); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQExpression.java new file mode 100644 index 0000000..c41acfe --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQExpression.java @@ -0,0 +1,89 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonQueryException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public interface JQExpression { + + @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context); + + boolean isConstant(); + + @NotNull String toString(); + + default @NotNull JQFilter bind(@NotNull Context context) { + return value -> evaluate(context.withRoot(value)); + } + + record Context( + @Nullable JsonValue root, + @NotNull Map<@NotNull String, @Nullable JsonValue> variables, + @NotNull Map<@NotNull String, @NotNull JQInvocable> functions + ) { + + public Context(@Nullable JsonValue root) { + this(root, Map.of(), JQBuiltIn.ALL_BUILTINS); + } + + public Context { + variables = variables.entrySet().stream().collect(Collectors.toMap( + entry -> { + Objects.requireNonNull(entry.getKey()); + if (!entry.getKey().startsWith("$")) throw new IllegalArgumentException(); + return entry.getKey(); + }, + Map.Entry::getValue + )); + functions = Map.copyOf(functions); + } + + public @NotNull JQInvocable function(@NotNull String name, int arity) { + var out = functions.get(name + "/" + arity); + if (out == null) throw new JsonQueryException(name + "/" + arity + " is not defined."); + return out; + } + + public @Nullable JsonValue variable(@NotNull String name) { + if (!variables.containsKey(name)) throw new JsonQueryException(name + " is not defined."); + return variables.get(name); + } + + public @NotNull Context withRoot(@Nullable JsonValue root) { + return new Context(root, variables, functions); + } + + public @NotNull Context withFunction(@NotNull JQInvocable function) { + var f = new HashMap<>(functions); + f.put(function.reference(), function); + return new Context(root, variables, f); + } + + public @NotNull Context withFunctions(@NotNull List functions) { + var f = new HashMap<>(this.functions); + functions.forEach(func -> f.put(func.reference(), func)); + return new Context(root, variables, f); + } + + public @NotNull Context withVariable(@NotNull String name, @Nullable JsonValue variable) { + var v = new HashMap<>(variables); + v.put(name, variable); + return new Context(root, v, functions); + } + + public @NotNull Context withVariables(@NotNull Map<@NotNull String, @Nullable JsonValue> variables) { + var v = new HashMap<>(this.variables); + v.putAll(variables); + return new Context(root, v, functions); + } + + public @NotNull Stream<@Nullable JsonValue> stream() { + return Stream.of(root()); + } + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFilter.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFilter.java new file mode 100644 index 0000000..54d2663 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFilter.java @@ -0,0 +1,12 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Function; +import java.util.stream.Stream; + +@FunctionalInterface +public interface JQFilter extends Function<@Nullable JsonValue, @NotNull Stream<@Nullable JsonValue>> { +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFunction.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFunction.java new file mode 100644 index 0000000..1ed25cc --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFunction.java @@ -0,0 +1,49 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.stream.Stream; + +public record JQFunction(@NotNull String identifier, @NotNull List<@NotNull String> params, @NotNull JQExpression body) implements JQInvocable { + public JQFunction { + Objects.requireNonNull(identifier); + Objects.requireNonNull(body); + params = List.copyOf(params); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> arguments) { + var expression = body; + + var functions = new ArrayList(); + for (int i = params.size() - 1; i >= 0; i--) { + String param = params.get(i); + if (param.startsWith("$")) { + expression = new JQAsExpression( + new JQFunctionInvocation(param.substring(1), List.of()), + List.of(new JQAsExpression.Pattern.ValuePattern(param)), + expression + ); + param = param.substring(1); + } + + functions.add(new JQFunction(param, List.of(), arguments.get(i))); + } + + return expression.evaluate(context.withFunctions(functions)); + + } + + @Override + public int arity() { + return params().size(); + } + + @Override + public @NotNull String toString() { + return "def " + identifier + (params.isEmpty() ? "" : String.join("; ", params)) + ": " + body + ";"; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFunctionInvocation.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFunctionInvocation.java new file mode 100644 index 0000000..474a308 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFunctionInvocation.java @@ -0,0 +1,36 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; +import java.util.stream.Stream; + +public record JQFunctionInvocation(@NotNull String name, @NotNull List<@NotNull JQExpression> args) implements JQExpression { + public JQFunctionInvocation { + Objects.requireNonNull(name); + args = List.copyOf(args); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + var function = context.function(name, args.size()); + return function.invoke(context, args); + } + + @Override + public boolean isConstant() { + return false; + } + + @Override + public @NotNull String toString() { + var out = new StringJoiner("; ", name + "(", ")"); + out.setEmptyValue(name); + for (var arg : args) out.add(arg.toString()); + return out.toString(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQImport.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQImport.java new file mode 100644 index 0000000..e47ac3b --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQImport.java @@ -0,0 +1,11 @@ +package eu.jonahbauer.json.query.parser.ast; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public record JQImport( + @NotNull JQStringInterpolation path, + @Nullable String as, + @Nullable JQExpression metadata +) { +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQIndexExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQIndexExpression.java new file mode 100644 index 0000000..40b689a --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQIndexExpression.java @@ -0,0 +1,53 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonNumber; +import eu.jonahbauer.json.JsonString; +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonMath; +import eu.jonahbauer.json.query.JsonQueryException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQIndexExpression(@NotNull JQExpression expression, @NotNull JQExpression index, boolean optional) implements JQExpression { + + public JQIndexExpression { + Objects.requireNonNull(expression); + Objects.requireNonNull(index); + } + + public JQIndexExpression(@NotNull JQExpression expression, @Nullable JsonValue index, boolean optional) { + this(expression, new JQConstant(index), optional); + } + + public JQIndexExpression(@NotNull JQExpression expression, double index, boolean optional) { + this(expression, new JsonNumber(index), optional); + } + + public JQIndexExpression(@NotNull JQExpression expression, @NotNull String index, boolean optional) { + this(expression, new JsonString(index), optional); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return expression.evaluate(context).flatMap(value -> index.evaluate(context).mapMulti((index, downstream) -> { + try { + downstream.accept(JsonMath.index(value, index)); + } catch (JsonQueryException ex) { + if (!optional) throw ex; + } + })); + } + + @Override + public boolean isConstant() { + return expression.isConstant() && index.isConstant(); + } + + @Override + public @NotNull String toString() { + return expression + "[" + index + "]" + (optional ? "?" : ""); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQInvocable.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQInvocable.java new file mode 100644 index 0000000..aa923d2 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQInvocable.java @@ -0,0 +1,18 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.stream.Stream; + +public interface JQInvocable { + @NotNull Stream<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args); + + int arity(); + @NotNull String identifier(); + default @NotNull String reference() { + return identifier() + "/" + arity(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQIterateExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQIterateExpression.java new file mode 100644 index 0000000..de6a1df --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQIterateExpression.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonMath; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQIterateExpression(@NotNull JQExpression expression, boolean optional) implements JQExpression { + public JQIterateExpression { + Objects.requireNonNull(expression); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return expression.evaluate(context).flatMap(value -> JsonMath.values(value, optional)); + } + + @Override + public boolean isConstant() { + return expression.isConstant(); + } + + @Override + public @NotNull String toString() { + return expression + "[]" + (optional ? "?" : ""); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQLocExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQLocExpression.java new file mode 100644 index 0000000..b8cc79c --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQLocExpression.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonNumber; +import eu.jonahbauer.json.JsonObject; +import eu.jonahbauer.json.JsonString; +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.stream.Stream; + +public record JQLocExpression(@NotNull String file, int line) implements JQExpression { + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return Stream.of(JsonObject.of( + "file", new JsonString(file), + "line", new JsonNumber(line) + )); + } + + @Override + public boolean isConstant() { + return true; + } + + @Override + public @NotNull String toString() { + return "$__loc__"; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQModule.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQModule.java new file mode 100644 index 0000000..8c98516 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQModule.java @@ -0,0 +1,11 @@ +package eu.jonahbauer.json.query.parser.ast; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public record JQModule(@NotNull JQExpression metadata) { + public JQModule { + Objects.requireNonNull(metadata); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQNegation.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQNegation.java new file mode 100644 index 0000000..03aede9 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQNegation.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonMath; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQNegation(@NotNull JQExpression expression) implements JQExpression { + public JQNegation { + Objects.requireNonNull(expression); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return expression.evaluate(context).map(JsonMath::neg); + } + + @Override + public boolean isConstant() { + return expression.isConstant(); + } + + @Override + public @NotNull String toString() { + return "-" + expression; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQParenthesizedExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQParenthesizedExpression.java new file mode 100644 index 0000000..63c8779 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQParenthesizedExpression.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQParenthesizedExpression(@NotNull JQExpression expression) implements JQExpression { + + public JQParenthesizedExpression { + Objects.requireNonNull(expression); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return expression.evaluate(context); + } + + @Override + public boolean isConstant() { + return expression.isConstant(); + } + + @Override + public @NotNull String toString() { + return "(" + expression + ")"; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQPipeExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQPipeExpression.java new file mode 100644 index 0000000..a7ce7a6 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQPipeExpression.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQPipeExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression { + public JQPipeExpression { + Objects.requireNonNull(first); + Objects.requireNonNull(second); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return first.evaluate(context).flatMap(value -> second.evaluate(context.withRoot(value))); + } + + @Override + public boolean isConstant() { + return first.isConstant() && second.isConstant(); + } + + @Override + public @NotNull String toString() { + return first + " | " + second; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQProgram.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQProgram.java new file mode 100644 index 0000000..8570ebf --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQProgram.java @@ -0,0 +1,14 @@ +package eu.jonahbauer.json.query.parser.ast; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public record JQProgram( + @Nullable JQModule module, + @NotNull List<@NotNull JQImport> imports, + @NotNull List<@NotNull JQFunction> functions, + @Nullable JQExpression expression +) { +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQRecursionExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQRecursionExpression.java new file mode 100644 index 0000000..c473d49 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQRecursionExpression.java @@ -0,0 +1,29 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonArray; +import eu.jonahbauer.json.JsonObject; +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.stream.Stream; + +public record JQRecursionExpression() implements JQExpression { + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return recurse(context.root()); + } + + private @NotNull Stream<@Nullable JsonValue> recurse(@Nullable JsonValue value) { + return switch (value) { + case JsonArray array -> Stream.concat(Stream.of(array), array.stream().flatMap(this::recurse)); + case JsonObject object -> Stream.concat(Stream.of(object), object.values().stream().flatMap(this::recurse)); + case null, default -> Stream.of(value); + }; + } + + @Override + public boolean isConstant() { + return false; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQRootExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQRootExpression.java new file mode 100644 index 0000000..47a38a5 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQRootExpression.java @@ -0,0 +1,24 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.stream.Stream; + +public record JQRootExpression() implements JQExpression { + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return context.stream(); + } + + @Override + public boolean isConstant() { + return false; + } + + @Override + public @NotNull String toString() { + return "."; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQSliceExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQSliceExpression.java new file mode 100644 index 0000000..b827b91 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQSliceExpression.java @@ -0,0 +1,67 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonArray; +import eu.jonahbauer.json.JsonNumber; +import eu.jonahbauer.json.JsonString; +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonQueryException; +import eu.jonahbauer.json.query.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public record JQSliceExpression(@NotNull JQExpression expression, @Nullable JQExpression start, @Nullable JQExpression end, boolean optional) implements JQExpression { + public JQSliceExpression { + Objects.requireNonNull(expression); + if (start == null && end == null) throw new IllegalArgumentException(); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return expression.evaluate(context).flatMap(value -> switch (value) { + case JsonArray array -> slice(context, "an array slice", array.size(), array::subList); + case JsonString string -> slice(context, "a string slice", string.length(), string::subSequence); + case null -> Stream.of((JsonValue) null); + default -> { + if (optional) yield Stream.empty(); + throw new JsonQueryException(STR."Cannot index \{Util.type(value)} with object."); + } + }); + } + + private @NotNull Stream<@Nullable JsonValue> slice(@NotNull Context context, @NotNull String type, int length, @NotNull BiFunction slice) { + return getIndices(start, context, type, length, 0) + .mapToObj(start -> getIndices(end, context, type, length, length) + .mapToObj(end -> start > end ? slice.apply(0, 0) : slice.apply(start, end)) + ) + .flatMap(Function.identity()); + } + + private @NotNull IntStream getIndices(@Nullable JQExpression expression, @NotNull Context context, @NotNull String type, int length, int fallback) { + if (expression == null) return IntStream.of(fallback); + return expression.evaluate(context).mapToInt(value -> getIndex(value, type, length)); + } + + private int getIndex(@Nullable JsonValue value, @NotNull String type, int length) { + if (!(value instanceof JsonNumber(double d))) { + throw new JsonQueryException(STR."Start and end indices of \{type} must be numbers."); + } + var i = (int) Math.floor(d); + return i < 0 ? Math.max(0, length + i) : Math.min(length, i); + } + + @Override + public boolean isConstant() { + return expression.isConstant() && (start == null || start.isConstant()) && (end == null || end.isConstant()); + } + + @Override + public @NotNull String toString() { + return expression + "[" + (start == null ? "" : start) + ":" + (end == null ? "" : end) + "]"; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQStringInterpolation.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQStringInterpolation.java new file mode 100644 index 0000000..ca489b5 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQStringInterpolation.java @@ -0,0 +1,60 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonString; +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.stream.Stream; + +@SuppressWarnings("preview") +public record JQStringInterpolation( + @Nullable String format, + @NotNull List<@NotNull String> fragments, + @NotNull List<@NotNull JQExpression> values +) implements JQExpression { + public JQStringInterpolation { + fragments = List.copyOf(fragments); + values = List.copyOf(values); + if (fragments.size() != values.size() + 1) throw new IllegalArgumentException(); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return Util.crossReversed(values, context) + .map(values -> StringTemplate.of(fragments, values.stream().map(JQStringInterpolation::toString).toList())) + .map(STR::process) + .map(JsonString::valueOf); + } + + private static @NotNull String toString(@Nullable JsonValue value) { + return value instanceof JsonString(var string) ? string : JsonValue.toJsonString(value); + } + + @Override + public boolean isConstant() { + return format == null && values.stream().allMatch(JQExpression::isConstant); + } + + @Override + public @NotNull String toString() { + var out = new StringBuilder(); + if (format != null) out.append("@").append(format); + out.append("\""); + + var it1 = fragments.iterator(); + for (JQExpression value : values) { + var fragment = JsonString.quote(it1.next()); + out.append(fragment, 1, fragment.length() - 1); + out.append("\\("); + out.append(value); + out.append(")"); + } + var last = JsonString.quote(it1.next()); + out.append(last, 1, last.length()); + + return out.toString(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQTryExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQTryExpression.java new file mode 100644 index 0000000..6c09ebe --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQTryExpression.java @@ -0,0 +1,98 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonString; +import eu.jonahbauer.json.JsonValue; +import eu.jonahbauer.json.query.JsonQueryException; +import eu.jonahbauer.json.query.JsonQueryHaltException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public record JQTryExpression(@NotNull JQExpression expression, @Nullable JQExpression fallback) implements JQExpression { + public JQTryExpression { + Objects.requireNonNull(expression); + } + + public JQTryExpression(@NotNull JQExpression expression) { + this(expression, null); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + var iterator = new QuietIterator( + expression.evaluate(context).iterator(), + fallback == null ? null : ex -> fallback.evaluate(context.withRoot(new JsonString(ex.getMessage()))).iterator() + ); + var spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED); + return StreamSupport.stream(spliterator, false); + } + + @Override + public boolean isConstant() { + return expression.isConstant() && (fallback == null || fallback.isConstant()); + } + + @Override + public @NotNull String toString() { + return "try " + expression + (fallback == null ? "" : " catch " + fallback); + } + + private static class QuietIterator implements Iterator<@Nullable JsonValue> { + private @Nullable Iterator<@Nullable JsonValue> delegate; + private @Nullable Function<@NotNull JsonQueryException, @NotNull Iterator<@Nullable JsonValue>> fallback; + + private @Nullable JsonValue next; + private boolean hasNext; + + private boolean valid = false; + + private QuietIterator( + @NotNull Iterator<@Nullable JsonValue> delegate, + @Nullable Function<@NotNull JsonQueryException, @NotNull Iterator<@Nullable JsonValue>> fallback + ) { + this.delegate = delegate; + this.fallback = fallback; + } + + private void ensureValid() { + if (valid) return; + + while (true) { + try { + if (delegate != null && delegate.hasNext()) { // still have values in current stream + next = delegate.next(); + hasNext = true; + } else { // end of stream + next = null; + hasNext = false; + } + break; + } catch (JsonQueryHaltException ex) { + throw ex; + } catch (JsonQueryException ex) { // switch to fallback + delegate = fallback != null ? fallback.apply(ex) : null; + fallback = null; + } + } + valid = true; + } + + @Override + public boolean hasNext() { + ensureValid(); + return hasNext; + } + + @Override + public @Nullable JsonValue next() { + ensureValid(); + if (!hasNext) throw new NoSuchElementException(); + valid = false; + return next; + } + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQVariableExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQVariableExpression.java new file mode 100644 index 0000000..c84b7a2 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQVariableExpression.java @@ -0,0 +1,30 @@ +package eu.jonahbauer.json.query.parser.ast; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.stream.Stream; + +public record JQVariableExpression(@NotNull String name) implements JQExpression { + public JQVariableExpression { + Objects.requireNonNull(name); + if (!name.startsWith("$")) throw new IllegalArgumentException(); + } + + @Override + public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) { + return Stream.of(context.variable(name)); + } + + @Override + public boolean isConstant() { + return false; + } + + @Override + public @NotNull String toString() { + return name; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/tokenizer/JQToken.java b/query/src/main/java/eu/jonahbauer/json/query/parser/tokenizer/JQToken.java new file mode 100644 index 0000000..fb3ff82 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/tokenizer/JQToken.java @@ -0,0 +1,33 @@ +package eu.jonahbauer.json.query.parser.tokenizer; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public record JQToken( + @NotNull JQTokenKind kind, @NotNull String text, + @Nullable Double nval, @Nullable String sval, + int line, int column +) { + public JQToken(@NotNull JQTokenKind kind, @NotNull String text, int line, int column) { + this(kind, text, null, null, line, column); + } + + public JQToken(@NotNull JQTokenKind kind, @NotNull String text, @NotNull Double nval, int line, int column) { + this(kind, text, nval, null, line, column); + } + + public JQToken(@NotNull JQTokenKind kind, @NotNull String text, @NotNull String sval, int line, int column) { + this(kind, text, null, sval, line, column); + } + + @Override + public @NotNull String toString() { + if (kind.isDynamic()) { + return kind + "(" + text + ")"; + } else { + return kind.toString(); + } + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/tokenizer/JQTokenKind.java b/query/src/main/java/eu/jonahbauer/json/query/parser/tokenizer/JQTokenKind.java new file mode 100644 index 0000000..f144e77 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/tokenizer/JQTokenKind.java @@ -0,0 +1,105 @@ +package eu.jonahbauer.json.query.parser.tokenizer; + +import lombok.AccessLevel; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public enum JQTokenKind { + NEQ, + EQ, + DEFINEDOR, + SETPIPE, + SETPLUS, + SETMINUS, + SETMULT, + SETDIV, + SETMOD, + SETDEFINEDOR, + LESSEQ, + GREATEREQ, + REC, + ALTERNATION, + + // keywords + AS("as"), + IMPORT("import"), + INCLUDE("include"), + MODULE("module"), + DEF("def"), + IF("if"), + THEN("then"), + ELSE("else"), + ELSE_IF("elif"), + AND("and"), + OR("or"), + END("end"), + REDUCE("reduce"), + FOREACH("foreach"), + TRY("try"), + CATCH("catch"), + LABEL("label"), + BREAK("break"), + LOC("$__loc__"), + + // without name + DOT("."), + QUESTION_MARK("?"), + ASSIGN("="), + SEMICOLON(";"), + COMMA(","), + COLON(":"), + PIPE("|"), + PLUS("+"), + MINUS("-"), + MULT("*"), + DIV("/"), + MOD("%"), + DOLLAR("$"), + LESS("<"), + GREATER(">"), + + LBRACKET("["), + LBRACE("{"), + LPAREN("("), + + RBRACKET("]"), + RBRACE("}"), + RPAREN(")"), + + QQSTRING_START, + QQSTRING_TEXT(true), + QQSTRING_END, + QQSTRING_INTERP_START, + QQSTRING_INTERP_END, + + FIELD(true), + FORMAT(true), + NUMBER(true), + IDENT(true), + ; + + private final @NotNull String name; + @Getter(AccessLevel.PACKAGE) + private final boolean dynamic; + + JQTokenKind(@NotNull String name) { + this.name = "\"" + name + "\""; + this.dynamic = false; + } + + JQTokenKind(boolean dynamic) { + this.name = name(); + this.dynamic = dynamic; + } + + JQTokenKind() { + this.name = name(); + this.dynamic = false; + } + + @Override + public @NotNull String toString() { + return name; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/tokenizer/JQTokenizer.java b/query/src/main/java/eu/jonahbauer/json/query/parser/tokenizer/JQTokenizer.java new file mode 100644 index 0000000..fb4dfe3 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/tokenizer/JQTokenizer.java @@ -0,0 +1,419 @@ +package eu.jonahbauer.json.query.parser.tokenizer; + +import eu.jonahbauer.json.query.JsonQueryTokenizerException; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.util.*; +import java.util.function.IntPredicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public final class JQTokenizer implements Iterable { + private final TrackingReader reader; + + private final Queue stack = Collections.asLifoQueue(new ArrayDeque<>()); + + public JQTokenizer(@NotNull String string) { + this.reader = new TrackingReader(new StringReader(string)); + } + + public JQTokenizer(@NotNull Reader reader) { + this.reader = new TrackingReader(new BufferedReader(reader)); + } + + public @Nullable JQToken next() throws IOException { + int chr; + int line; + int column; + + if (stack.peek() == State.IN_QQSTRING) { + line = reader.getLineNumber(); + column = reader.getColumnNumber(); + chr = reader.read(); + if (chr == -1) throw new JsonQueryTokenizerException(line, column, "unexpected $end"); + + var text = new StringBuilder().append((char) chr); + if (chr == '"') { + stack.remove(State.IN_QQSTRING); + return new JQToken(JQTokenKind.QQSTRING_END, text.toString(), line, column); + } else if (chr == '\\') { + int chr2 = reader.read(); + if (chr2 != -1) text.append((char) chr2); + + return switch (chr2) { + case '(' -> { + stack.add(State.IN_QQINTERP); + yield new JQToken(JQTokenKind.QQSTRING_INTERP_START, text.toString(), line, column); + } + case 'u' -> { + if (tryRead(text, this::isHexDigit, 4) != 4) { + throw new JsonQueryTokenizerException(line, column, "invalid \\uXXXX escape"); + } + var code = Integer.parseInt(text.substring(2), 16); + yield new JQToken(JQTokenKind.QQSTRING_TEXT, text.toString(), String.valueOf((char) code), line, column); + } + case -1 -> throw new JsonQueryTokenizerException(line, column, "invalid character"); + default -> new JQToken(JQTokenKind.QQSTRING_TEXT, text.toString(), switch (chr2) { + case '"' -> "\""; + case '\\' -> "\\"; + case '/' -> "/"; + case 'b' -> "\b"; + case 'f' -> "\f"; + case 'n' -> "\n"; + case 'r' -> "\r"; + case 't' -> "\t"; + default -> throw new JsonQueryTokenizerException(line, column, "invalid escape"); + }, line, column); + }; + } else { + tryRead(text, c -> c != '\\' && c != '"'); + return new JQToken(JQTokenKind.QQSTRING_TEXT, text.toString(), text.toString(), line, column); + } + } + + while (true) { + line = reader.getLineNumber(); + column = reader.getColumnNumber(); + chr = reader.read(); + + // rules producing no token + if (isWhitespace(chr)) continue; + if (chr == '#') { + readEndOfLineComment(); + continue; + } + + break; + } + + // EOF + if (chr == -1) return null; + + var text = new StringBuilder().append((char) chr); + assert chr >= 0; + var result = switch ((Character) (char) chr) { + case Character c when c == '!' && tryRead(text, "=") -> JQTokenKind.NEQ; + case Character c when c == '=' && tryRead(text, "=") -> JQTokenKind.EQ; + case Character c when c == '/' && tryRead(text, "/") -> JQTokenKind.DEFINEDOR; + case Character c when c == '|' && tryRead(text, "=") -> JQTokenKind.SETPIPE; + case Character c when c == '+' && tryRead(text, "=") -> JQTokenKind.SETPLUS; + case Character c when c == '-' && tryRead(text, "=") -> JQTokenKind.SETMINUS; + case Character c when c == '*' && tryRead(text, "=") -> JQTokenKind.SETMULT; + case Character c when c == '/' && tryRead(text, "=") -> JQTokenKind.SETDIV; + case Character c when c == '%' && tryRead(text, "=") -> JQTokenKind.SETMOD; + case Character c when c == '/' && tryRead(text, "/=") -> JQTokenKind.SETDEFINEDOR; + case Character c when c == '<' && tryRead(text, "=") -> JQTokenKind.LESSEQ; + case Character c when c == '>' && tryRead(text, "=") -> JQTokenKind.GREATEREQ; + case Character c when c == '.' && tryRead(text, ".") -> JQTokenKind.REC; + case Character c when c == '?' && tryRead(text, "//") -> JQTokenKind.ALTERNATION; + case Character c when c == '@' && tryRead(text, this::isIdentifierPart) -> JQTokenKind.FORMAT; + case Character c when c == '$' && tryRead(text, "__loc__") -> JQTokenKind.LOC; + case '?' -> JQTokenKind.QUESTION_MARK; + case '=' -> JQTokenKind.ASSIGN; + case ';' -> JQTokenKind.SEMICOLON; + case ',' -> JQTokenKind.COMMA; + case ':' -> JQTokenKind.COLON; + case '|' -> JQTokenKind.PIPE; + case '+' -> JQTokenKind.PLUS; + case '-' -> JQTokenKind.MINUS; + case '*' -> JQTokenKind.MULT; + case '/' -> JQTokenKind.DIV; + case '%' -> JQTokenKind.MOD; + case '$' -> JQTokenKind.DOLLAR; + case '<' -> JQTokenKind.LESS; + case '>' -> JQTokenKind.GREATER; + + case '[' -> { + stack.add(State.IN_BRACKET); + yield JQTokenKind.LBRACKET; + } + case '(' -> { + stack.add(State.IN_PAREN); + yield JQTokenKind.LPAREN; + } + case '{' -> { + stack.add(State.IN_BRACE); + yield JQTokenKind.LBRACE; + } + case Character c when c == ']' && stack.peek() == State.IN_BRACKET -> { + stack.remove(); + yield JQTokenKind.RBRACKET; + } + case Character c when c == ')' && stack.peek() == State.IN_PAREN -> { + stack.remove(); + yield JQTokenKind.RPAREN; + } + case Character c when c == ')' && stack.peek() == State.IN_QQINTERP -> { + stack.remove(); + yield JQTokenKind.QQSTRING_INTERP_END; + } + case Character c when c == '}' && stack.peek() == State.IN_BRACE -> { + stack.remove(); + yield JQTokenKind.RBRACE; + } + + case '"' -> { + stack.add(State.IN_QQSTRING); + yield JQTokenKind.QQSTRING_START; + } + + case Character c when c == '.' && tryRead(text, this::isDigit) -> { + readExponential(text); + yield new JQToken(JQTokenKind.NUMBER, text.toString(), Double.parseDouble(text.toString()), line, column); + } + case Character c when isDigit(c) -> { + readDigits(text); + if (tryRead(text, ".")) readDigits(text); + readExponential(text); + yield new JQToken(JQTokenKind.NUMBER, text.toString(), Double.parseDouble(text.toString()), line, column); + } + case Character c when c == '.' && tryRead(text, this::isIdentifierStart) -> { + tryRead(text, this::isIdentifierPart); + yield JQTokenKind.FIELD; + } + case '.' -> JQTokenKind.DOT; + case Character c when isIdentifierStart(c) -> { + tryRead(text, this::isIdentifierPart); + while (true) { + reader.mark(3); + if (reader.read() != ':') { reader.reset(); break; } + if (reader.read() != ':') { reader.reset(); break; } + chr = reader.read(); + if (!isIdentifierStart(chr)) { reader.reset(); break; } + text.append("::").append((char) chr); + tryRead(text, this::isIdentifierPart); + } + yield switch (text.toString()) { + case "as" -> JQTokenKind.AS; + case "import" -> JQTokenKind.IMPORT; + case "include" -> JQTokenKind.INCLUDE; + case "module" -> JQTokenKind.MODULE; + case "def" -> JQTokenKind.DEF; + case "if" -> JQTokenKind.IF; + case "then" -> JQTokenKind.THEN; + case "else" -> JQTokenKind.ELSE; + case "elif" -> JQTokenKind.ELSE_IF; + case "and" -> JQTokenKind.AND; + case "or" -> JQTokenKind.OR; + case "end" -> JQTokenKind.END; + case "reduce" -> JQTokenKind.REDUCE; + case "foreach" -> JQTokenKind.FOREACH; + case "try" -> JQTokenKind.TRY; + case "catch" -> JQTokenKind.CATCH; + case "label" -> JQTokenKind.LABEL; + case "break" -> JQTokenKind.BREAK; + default -> JQTokenKind.IDENT; + }; + } + default -> throw new JsonQueryTokenizerException(line, column, "invalid character"); + }; + + if (result instanceof JQToken token) { + return token; + } else { + return new JQToken((JQTokenKind) result, text.toString(), line, column); + } + } + + private void readEndOfLineComment() throws IOException { + int chr; + do { + chr = reader.read(); + } while (chr != -1 && chr != '\r' && chr != '\n'); + } + + private void readDigits(@NotNull StringBuilder text) throws IOException { + tryRead(text, this::isDigit); + } + + private void readExponential(@NotNull StringBuilder text) throws IOException { + reader.mark(3); + + // [eE] + int e = reader.read(); + if (e != 'e' && e != 'E') { + reader.reset(); + return; + } + + // [+-]? + int sign = reader.read(); + int digit; + if (sign == '+' || sign == '-') { + digit = reader.read(); + } else { + digit = sign; + sign = -1; + } + + if (!isDigit(digit)) { + reader.reset(); + return; + } + + text.append((char) e); + if (sign != -1) text.append((char) sign); + text.append((char) digit); + readDigits(text); + } + + private boolean tryRead(@NotNull StringBuilder text, @NotNull String expected) throws IOException { + int length = expected.length(); + reader.mark(length); + for (int i = 0; i < length; i++) { + if (reader.read() != expected.charAt(i)) { + reader.reset(); + return false; + } + } + + text.append(expected); + return true; + } + + private boolean tryRead(@NotNull StringBuilder text, @NotNull IntPredicate predicate) throws IOException { + return tryRead(text, predicate, Integer.MAX_VALUE) != 0; + } + + private int tryRead(@NotNull StringBuilder text, @NotNull IntPredicate predicate, int limit) throws IOException { + int i = 0; + for (; i < limit; i++) { + reader.mark(1); + int chr = reader.read(); + if (chr == -1 || !predicate.test(chr)) { + reader.reset(); + break; + } + text.append((char) chr); + } + return i; + } + + private boolean isWhitespace(int chr) { + return chr == ' ' || chr == '\t' || chr == '\r' || chr == '\n'; + } + + private boolean isDigit(int chr) { + return '0' <= chr && chr <= '9'; + } + + private boolean isHexDigit(int chr) { + return '0' <= chr && chr <= '9' || 'a' <= chr && chr <= 'f' || 'A' <= chr && chr <= 'F'; + } + + private boolean isIdentifierStart(int chr) { + return 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z' || chr == '_'; + } + + private boolean isIdentifierPart(int chr) { + return isIdentifierStart(chr) || isDigit(chr); + } + + public @NotNull Iterator iterator() { + return new JQTokenizerIterator(); + } + + public @NotNull Stream<@NotNull JQToken> stream() { + return StreamSupport.stream(this.spliterator(), false); + } + + private class JQTokenizerIterator implements Iterator { + private JQToken next; + private boolean valid = false; + + private void ensureValid() { + try { + if (!valid) { + next = JQTokenizer.this.next(); + valid = true; + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + @Override + public boolean hasNext() { + ensureValid(); + return next != null; + } + + @Override + public @NotNull JQToken next() { + ensureValid(); + if (next == null) { + throw new NoSuchElementException(); + } else { + valid = false; + return next; + } + } + } + + private enum State { + IN_PAREN, + IN_BRACKET, + IN_BRACE, + IN_QQSTRING, + IN_QQINTERP, + ; + } + + @RequiredArgsConstructor + private static class TrackingReader { + private final Reader delegate; + + private boolean skipLF; + @Getter + private int lineNumber = 1; + @Getter + private int columnNumber = 1; + + private boolean markedSkipLF; + private int markedLineNumber; + private int markedColumnNumber; + + public int read() throws IOException { + int c = delegate.read(); + + // handle line feed + if (skipLF) { + skipLF = false; + } else if (c == '\n') { + lineNumber++; + columnNumber = 1; + } + + // handle carriage return + if (c == '\r') { + lineNumber++; + columnNumber = 1; + skipLF = true; + } + + if (c != '\n' && c != '\r') { + columnNumber++; + } + + return c; + } + + public void mark(int readAheadLimit) throws IOException { + delegate.mark(readAheadLimit); + markedSkipLF = skipLF; + markedLineNumber = lineNumber; + markedColumnNumber = columnNumber; + } + + public void reset() throws IOException { + delegate.reset(); + skipLF = markedSkipLF; + lineNumber = markedLineNumber; + columnNumber = markedColumnNumber; + } + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/util/Util.java b/query/src/main/java/eu/jonahbauer/json/query/util/Util.java new file mode 100644 index 0000000..3baf3eb --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/util/Util.java @@ -0,0 +1,72 @@ +package eu.jonahbauer.json.query.util; + +import eu.jonahbauer.json.*; +import eu.jonahbauer.json.query.parser.ast.JQExpression; +import lombok.experimental.UtilityClass; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + +@UtilityClass +public class Util { + + public static @NotNull Stream lazy(@NotNull Supplier supplier) { + return Stream.of((Object) null).map(_ -> supplier.get()); + } + + public static @NotNull Stream lazyStream(@NotNull Supplier> supplier) { + return Stream.of((Object) null).flatMap(_ -> supplier.get()); + } + + public static @NotNull Stream<@NotNull List> cross(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) { + return cross(asSupplier(expressions, context)).map(Collections::unmodifiableList); + } + + public static @NotNull Stream<@NotNull List> cross(@NotNull List<@NotNull Supplier<@NotNull Stream>> expressions) { + if (expressions.isEmpty()) return Stream.of(new ArrayList().reversed()); + + return expressions.getFirst().get() + .flatMap(value -> cross(expressions.subList(1, expressions.size())) + .peek(list -> list.addFirst(value)) + ); + } + + public static @NotNull Stream<@NotNull List> crossReversed(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) { + return crossReversed(asSupplier(expressions, context)).map(Collections::unmodifiableList); + } + + public static @NotNull Stream<@NotNull List> crossReversed(@NotNull List<@NotNull Supplier<@NotNull Stream>> expressions) { + if (expressions.isEmpty()) return Stream.of(new ArrayList<>()); + + return expressions.getLast().get() + .flatMap(value -> crossReversed(expressions.subList(0, expressions.size() - 1)) + .peek(list -> list.addLast(value)) + ); + } + + private static @NotNull List<@NotNull Supplier<@NotNull Stream>> asSupplier(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) { + var list = new ArrayList>>(expressions.size()); + expressions.forEach(expr -> list.add(() -> expr.evaluate(context))); + return list; + } + + public static @NotNull String type(@Nullable JsonValue value) { + return switch (value) { + case JsonArray _ -> "array"; + case JsonObject _ -> "object"; + case JsonNumber _ -> "number"; + case JsonString _ -> "string"; + case JsonBoolean _ -> "boolean"; + case null -> "null"; + }; + } + + public static @NotNull String value(@Nullable JsonValue value) { + return JsonValue.toJsonString(value); + } +} diff --git a/query/src/test/java/eu/jonahbauer/json/query/JsonMathTest.java b/query/src/test/java/eu/jonahbauer/json/query/JsonMathTest.java new file mode 100644 index 0000000..cc63b93 --- /dev/null +++ b/query/src/test/java/eu/jonahbauer/json/query/JsonMathTest.java @@ -0,0 +1,186 @@ +package eu.jonahbauer.json.query; + +import eu.jonahbauer.json.JsonArray; +import eu.jonahbauer.json.JsonBoolean; +import eu.jonahbauer.json.JsonNumber; +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JsonMathTest { + + @ParameterizedTest + @MethodSource("addArguments") + void add(@Nullable JsonValue first, @Nullable JsonValue second, @Nullable JsonValue expected) { + assertEquals(expected, JsonMath.add(first, second)); + } + + static Stream addArguments() { + return arguments( + Arguments.of("foo", "bar", "foobar"), + Arguments.of(7, 1, 8), + Arguments.of(List.of(1, 2), List.of(3, 4), List.of(1, 2, 3, 4)), + Arguments.of(1, null, 1), + Arguments.of(Map.of("a", 1), Map.of("b", 1), Map.of("a", 1, "b", 1)), + Arguments.of(Map.of("a", 1), Map.of("a", 42), Map.of("a", 42)) + ); + } + + @ParameterizedTest + @MethodSource("subArguments") + void sub(@Nullable JsonValue first, @Nullable JsonValue second, @Nullable JsonValue expected) { + assertEquals(expected, JsonMath.sub(first, second)); + } + + static Stream subArguments() { + return arguments( + Arguments.of(4, 1, 3), + Arguments.of(List.of("xml", "yaml", "json"), List.of("xml", "yaml"), List.of("json")) + ); + } + + @ParameterizedTest + @MethodSource("mulArguments") + void mul(@Nullable JsonValue first, @Nullable JsonValue second, @Nullable JsonValue expected) { + assertEquals(expected, JsonMath.mul(first, second)); + } + + static Stream mulArguments() { + return arguments( + Arguments.of("foo", 0, ""), + Arguments.of(0, "foo", ""), + Arguments.of(1.5, "foo", "foofoo"), + Arguments.of("foo", 1.5, "foofoo"), + Arguments.of(-1, "foo", null), + Arguments.of("foo", -1, null), + Arguments.of("foo", 1E300, null), + Arguments.of(1E300, "foo", null), + Arguments.of(2, 3, 6), + Arguments.of( + Map.of("k", Map.of("a", 1, "b", 2)), + Map.of("k", Map.of("a", 0, "c", 3)), + Map.of("k", Map.of("a", 0, "b", 2, "c", 3)) + ) + ); + } + + @ParameterizedTest + @MethodSource("divArguments") + void div(@Nullable JsonValue first, @Nullable JsonValue second, @Nullable JsonValue expected) { + assertEquals(expected, JsonMath.div(first, second)); + } + + static Stream divArguments() { + return arguments( + Arguments.of(10, 5, 2), + Arguments.of("a, b,c,d, e", ", ", List.of("a", "b,c,d", "e")) + ); + } + + @ParameterizedTest + @MethodSource("eqArguments") + void eq(@Nullable JsonValue first, @Nullable JsonValue second, @Nullable JsonValue expected) { + assertEquals(expected, JsonMath.eq(first, second)); + } + + static Stream eqArguments() { + return arguments( + Arguments.of(null, false, false), + Arguments.of(1, 1.0, true), + Arguments.of(1, "1", false), + Arguments.of(1, "banana", false) + ); + } + + @ParameterizedTest + @MethodSource("sortArguments") + void sort(@NotNull JsonArray value, @NotNull JsonArray expected) { + assertEquals(expected, JsonMath.sort(value)); + } + + static Stream sortArguments() { + return arguments( + Arguments.of(Arrays.asList(8, 3, null, 6, true, false), Arrays.asList(null, false, true, 3, 6, 8)), + Arguments.of(List.of(List.of("a", "b", "c"), List.of("a", "b")), List.of(List.of("a", "b"), List.of("a", "b", "c"))), + Arguments.of(List.of("foo", "bar", "baz"), List.of("bar", "baz", "foo")), + Arguments.of(List.of(Map.of("a", 1), Map.of("b", 1)), List.of(Map.of("a", 1), Map.of("b", 1))), + Arguments.of(List.of(Map.of("b", 1), Map.of("a", 1)), List.of(Map.of("a", 1), Map.of("b", 1))), + Arguments.of(List.of(Map.of("a", 1), Map.of("a", 2)), List.of(Map.of("a", 1), Map.of("a", 2))), + Arguments.of(List.of(Map.of("a", 2), Map.of("a", 1)), List.of(Map.of("a", 1), Map.of("a", 2))), + Arguments.of(List.of(Map.of("a", 2), Map.of("a", 1, "c", 1)), List.of(Map.of("a", 2), Map.of("a", 1, "c", 1))), + Arguments.of(List.of(Map.of("a", 1, "c", 1), Map.of("a", 2)), List.of(Map.of("a", 2), Map.of("a", 1, "c", 1))) + ); + } + + @Test + void lt() { + assertEquals(JsonBoolean.FALSE, JsonMath.lt(JsonNumber.valueOf(1), JsonNumber.valueOf(1))); + assertEquals(JsonBoolean.TRUE, JsonMath.lt(JsonNumber.valueOf(1), JsonNumber.valueOf(2))); + assertEquals(JsonBoolean.FALSE, JsonMath.lt(JsonNumber.valueOf(2), JsonNumber.valueOf(1))); + } + + @Test + void gt() { + assertEquals(JsonBoolean.FALSE, JsonMath.gt(JsonNumber.valueOf(1), JsonNumber.valueOf(1))); + assertEquals(JsonBoolean.FALSE, JsonMath.gt(JsonNumber.valueOf(1), JsonNumber.valueOf(2))); + assertEquals(JsonBoolean.TRUE, JsonMath.gt(JsonNumber.valueOf(2), JsonNumber.valueOf(1))); + } + + @Test + void leq() { + assertEquals(JsonBoolean.TRUE, JsonMath.leq(JsonNumber.valueOf(1), JsonNumber.valueOf(1))); + assertEquals(JsonBoolean.TRUE, JsonMath.leq(JsonNumber.valueOf(1), JsonNumber.valueOf(2))); + assertEquals(JsonBoolean.FALSE, JsonMath.leq(JsonNumber.valueOf(2), JsonNumber.valueOf(1))); + } + + @Test + void geq() { + assertEquals(JsonBoolean.TRUE, JsonMath.geq(JsonNumber.valueOf(1), JsonNumber.valueOf(1))); + assertEquals(JsonBoolean.FALSE, JsonMath.geq(JsonNumber.valueOf(1), JsonNumber.valueOf(2))); + assertEquals(JsonBoolean.TRUE, JsonMath.geq(JsonNumber.valueOf(2), JsonNumber.valueOf(1))); + } + + @ParameterizedTest + @MethodSource("andArguments") + void and(@Nullable JsonValue first, @Nullable JsonValue second, @Nullable JsonValue expected) { + assertEquals(expected, JsonMath.and(first, second)); + } + + static Stream andArguments() { + return arguments( + Arguments.of(42, "a string", true), + Arguments.of(true, true, true), + Arguments.of(true, false, false) + ); + } + + @ParameterizedTest + @MethodSource("orArguments") + void or(@Nullable JsonValue first, @Nullable JsonValue second, @Nullable JsonValue expected) { + assertEquals(expected, JsonMath.or(first, second)); + } + + static Stream orArguments() { + return arguments( + Arguments.of(false, "a string", true), + Arguments.of(false, null, false), + Arguments.of(1, false, true) + ); + } + + static Stream arguments(@NotNull Arguments @NotNull ... arguments) { + return Stream.of(arguments).map(args -> Arguments.of(Arrays.stream(args.get()).map(JsonValue::valueOf).toArray())); + } + +} \ No newline at end of file diff --git a/query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java b/query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java new file mode 100644 index 0000000..7c65314 --- /dev/null +++ b/query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java @@ -0,0 +1,1433 @@ +package eu.jonahbauer.json.query; + +import eu.jonahbauer.json.JsonValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentest4j.AssertionFailedError; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JsonQueryTest { + + @ParameterizedTest + @MethodSource("arguments") + void examples(@NotNull String jq, @Nullable JsonValue input, @NotNull List<@Nullable JsonValue> output) { + JsonQuery query; + try { + query = JsonQuery.parse(jq); + } catch (UnsupportedOperationException _) { + throw Assumptions.abort("not yet implemented"); + } catch (Throwable t) { + throw new AssertionFailedError("Unexpected exception thrown: " + t.getMessage(), t); + } + var actual = query.run(input).toList(); + assertEquals(output, actual); + } + + /** + * Examples taken from the manual + * + * @see jq Manual (development version) + */ + @SuppressWarnings("ArraysAsListWithZeroOrOneArgument") + static Stream arguments() { + return Stream.of( + Arguments.of( + ".", + JsonValue.parse("\"Hello, world!\""), + Arrays.asList(JsonValue.parse("\"Hello, world!\"")) + ), + Arguments.of( + ".", + JsonValue.parse("0.12345678901234567890123456789"), + Arrays.asList(JsonValue.parse("0.12345678901234567890123456789")) + ), + Arguments.of( + "[., tojson]", + JsonValue.parse("12345678909876543212345"), + Arrays.asList(JsonValue.parse("[12345678909876543212345,\"12345678909876543212345\"]")) + ), + Arguments.of( + ". < 0.12345678901234567890123456788", + JsonValue.parse("0.12345678901234567890123456789"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + "map([., . == 1]) | tojson", + JsonValue.parse("[1, 1.000, 1.0, 100e-2]"), + Arrays.asList(JsonValue.parse("\"[[1,true],[1.000,true],[1.0,true],[1.00,true]]\"")) + ), + Arguments.of( + ". as $big | [$big, $big + 1] | map(. > 10000000000000000000000000000000)", + JsonValue.parse("10000000000000000000000000000001"), + Arrays.asList(JsonValue.parse("[true, false]")) + ), + Arguments.of( + ".foo", + JsonValue.parse("{\"foo\": 42, \"bar\": \"less interesting data\"}"), + Arrays.asList(JsonValue.parse("42")) + ), + Arguments.of( + ".foo", + JsonValue.parse("{\"notfoo\": true, \"alsonotfoo\": false}"), + Arrays.asList(JsonValue.parse("null")) + ), + Arguments.of( + ".[\"foo\"]", + JsonValue.parse("{\"foo\": 42}"), + Arrays.asList(JsonValue.parse("42")) + ), + Arguments.of( + ".foo?", + JsonValue.parse("{\"foo\": 42, \"bar\": \"less interesting data\"}"), + Arrays.asList(JsonValue.parse("42")) + ), + Arguments.of( + ".foo?", + JsonValue.parse("{\"notfoo\": true, \"alsonotfoo\": false}"), + Arrays.asList(JsonValue.parse("null")) + ), + Arguments.of( + ".[\"foo\"]?", + JsonValue.parse("{\"foo\": 42}"), + Arrays.asList(JsonValue.parse("42")) + ), + Arguments.of( + "[.foo?]", + JsonValue.parse("[1,2]"), + Arrays.asList(JsonValue.parse("[]")) + ), + Arguments.of( + ".[0]", + JsonValue.parse("[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]"), + Arrays.asList(JsonValue.parse("{\"name\":\"JSON\", \"good\":true}")) + ), + Arguments.of( + ".[2]", + JsonValue.parse("[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]"), + Arrays.asList(JsonValue.parse("null")) + ), + Arguments.of( + ".[-2]", + JsonValue.parse("[1,2,3]"), + Arrays.asList(JsonValue.parse("2")) + ), + Arguments.of( + ".[2:4]", + JsonValue.parse("[\"a\",\"b\",\"c\",\"d\",\"e\"]"), + Arrays.asList(JsonValue.parse("[\"c\", \"d\"]")) + ), + Arguments.of( + ".[2:4]", + JsonValue.parse("\"abcdefghi\""), + Arrays.asList(JsonValue.parse("\"cd\"")) + ), + Arguments.of( + ".[:3]", + JsonValue.parse("[\"a\",\"b\",\"c\",\"d\",\"e\"]"), + Arrays.asList(JsonValue.parse("[\"a\", \"b\", \"c\"]")) + ), + Arguments.of( + ".[-2:]", + JsonValue.parse("[\"a\",\"b\",\"c\",\"d\",\"e\"]"), + Arrays.asList(JsonValue.parse("[\"d\", \"e\"]")) + ), + Arguments.of( + ".[]", + JsonValue.parse("[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]"), + Arrays.asList( + JsonValue.parse("{\"name\":\"JSON\", \"good\":true}"), + JsonValue.parse("{\"name\":\"XML\", \"good\":false}") + ) + ), + Arguments.of( + ".[]", + JsonValue.parse("[]"), + Arrays.asList() + ), + Arguments.of( + ".foo[]", + JsonValue.parse("{\"foo\":[1,2,3]}"), + Arrays.asList( + JsonValue.parse("1"), + JsonValue.parse("2"), + JsonValue.parse("3") + ) + ), + Arguments.of( + ".[]", + JsonValue.parse("{\"a\": 1, \"b\": 1}"), + Arrays.asList( + JsonValue.parse("1"), + JsonValue.parse("1") + ) + ), + Arguments.of( + ".foo, .bar", + JsonValue.parse("{\"foo\": 42, \"bar\": \"something else\", \"baz\": true}"), + Arrays.asList( + JsonValue.parse("42"), + JsonValue.parse("\"something else\"") + ) + ), + Arguments.of( + ".user, .projects[]", + JsonValue.parse("{\"user\":\"stedolan\", \"projects\": [\"jq\", \"wikiflow\"]}"), + Arrays.asList( + JsonValue.parse("\"stedolan\""), + JsonValue.parse("\"jq\""), + JsonValue.parse("\"wikiflow\"") + ) + ), + Arguments.of( + ".[4,2]", + JsonValue.parse("[\"a\",\"b\",\"c\",\"d\",\"e\"]"), + Arrays.asList( + JsonValue.parse("\"e\""), + JsonValue.parse("\"c\"") + ) + ), + Arguments.of( + ".[] | .name", + JsonValue.parse("[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]"), + Arrays.asList( + JsonValue.parse("\"JSON\""), + JsonValue.parse("\"XML\"") + ) + ), + Arguments.of( + "(. + 2) * 5", + JsonValue.parse("1"), + Arrays.asList(JsonValue.parse("15")) + ), + Arguments.of( + "[.user, .projects[]]", + JsonValue.parse("{\"user\":\"stedolan\", \"projects\": [\"jq\", \"wikiflow\"]}"), + Arrays.asList(JsonValue.parse("[\"stedolan\", \"jq\", \"wikiflow\"]")) + ), + Arguments.of( + "[ .[] | . * 2]", + JsonValue.parse("[1, 2, 3]"), + Arrays.asList(JsonValue.parse("[2, 4, 6]")) + ), + Arguments.of( + "{user, title: .titles[]}", + JsonValue.parse("{\"user\":\"stedolan\",\"titles\":[\"JQ Primer\", \"More JQ\"]}"), + Arrays.asList( + JsonValue.parse("{\"user\":\"stedolan\", \"title\": \"JQ Primer\"}"), + JsonValue.parse("{\"user\":\"stedolan\", \"title\": \"More JQ\"}") + ) + ), + Arguments.of( + "{(.user): .titles}", + JsonValue.parse("{\"user\":\"stedolan\",\"titles\":[\"JQ Primer\", \"More JQ\"]}"), + Arrays.asList(JsonValue.parse("{\"stedolan\": [\"JQ Primer\", \"More JQ\"]}")) + ), + Arguments.of( + ".. | .a?", + JsonValue.parse("[[{\"a\":1}]]"), + Arrays.asList(JsonValue.parse("1")) + ), + Arguments.of( + ".a + 1", + JsonValue.parse("{\"a\": 7}"), + Arrays.asList(JsonValue.parse("8")) + ), + Arguments.of( + ".a + .b", + JsonValue.parse("{\"a\": [1,2], \"b\": [3,4]}"), + Arrays.asList(JsonValue.parse("[1,2,3,4]")) + ), + Arguments.of( + ".a + null", + JsonValue.parse("{\"a\": 1}"), + Arrays.asList(JsonValue.parse("1")) + ), + Arguments.of( + ".a + 1", + JsonValue.parse("{}"), + Arrays.asList(JsonValue.parse("1")) + ), + Arguments.of( + "{a: 1} + {b: 2} + {c: 3} + {a: 42}", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("{\"a\": 42, \"b\": 2, \"c\": 3}")) + ), + Arguments.of( + "4 - .a", + JsonValue.parse("{\"a\":3}"), + Arrays.asList(JsonValue.parse("1")) + ), + Arguments.of( + ". - [\"xml\", \"yaml\"]", + JsonValue.parse("[\"xml\", \"yaml\", \"json\"]"), + Arrays.asList(JsonValue.parse("[\"json\"]")) + ), + Arguments.of( + "10 / . * 3", + JsonValue.parse("5"), + Arrays.asList(JsonValue.parse("6")) + ), + Arguments.of( + ". / \", \"", + JsonValue.parse("\"a, b,c,d, e\""), + Arrays.asList(JsonValue.parse("[\"a\",\"b,c,d\",\"e\"]")) + ), + Arguments.of( + "{\"k\": {\"a\": 1, \"b\": 2}} * {\"k\": {\"a\": 0,\"c\": 3}}", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("{\"k\": {\"a\": 0, \"b\": 2, \"c\": 3}}")) + ), + Arguments.of( + ".[] | (1 / .)?", + JsonValue.parse("[1,0,-1]"), + Arrays.asList( + JsonValue.parse("1"), + JsonValue.parse("-1") + ) + ), + Arguments.of( + "map(abs)", + JsonValue.parse("[-10, -1.1, -1e-1]"), + Arrays.asList(JsonValue.parse("[10,1.1,1e-1]")) + ), + Arguments.of( + ".[] | length", + JsonValue.parse("[[1,2], \"string\", {\"a\":2}, null, -5]"), + Arrays.asList( + JsonValue.parse("2"), + JsonValue.parse("6"), + JsonValue.parse("1"), + JsonValue.parse("0"), + JsonValue.parse("5") + ) + ), + Arguments.of( + "utf8bytelength", + JsonValue.parse("\"\\u03bc\""), + Arrays.asList(JsonValue.parse("2")) + ), + Arguments.of( + "keys", + JsonValue.parse("{\"abc\": 1, \"abcd\": 2, \"Foo\": 3}"), + Arrays.asList(JsonValue.parse("[\"Foo\", \"abc\", \"abcd\"]")) + ), + Arguments.of( + "keys", + JsonValue.parse("[42,3,35]"), + Arrays.asList(JsonValue.parse("[0,1,2]")) + ), + Arguments.of( + "map(has(\"foo\"))", + JsonValue.parse("[{\"foo\": 42}, {}]"), + Arrays.asList(JsonValue.parse("[true, false]")) + ), + Arguments.of( + "map(has(2))", + JsonValue.parse("[[0,1], [\"a\",\"b\",\"c\"]]"), + Arrays.asList(JsonValue.parse("[false, true]")) + ), + Arguments.of( + ".[] | in({\"foo\": 42})", + JsonValue.parse("[\"foo\", \"bar\"]"), + Arrays.asList( + JsonValue.parse("true"), + JsonValue.parse("false") + ) + ), + Arguments.of( + "map(in([0,1]))", + JsonValue.parse("[2, 0]"), + Arrays.asList(JsonValue.parse("[false, true]")) + ), + Arguments.of( + "map(.+1)", + JsonValue.parse("[1,2,3]"), + Arrays.asList(JsonValue.parse("[2,3,4]")) + ), + Arguments.of( + "map_values(.+1)", + JsonValue.parse("{\"a\": 1, \"b\": 2, \"c\": 3}"), + Arrays.asList(JsonValue.parse("{\"a\": 2, \"b\": 3, \"c\": 4}")) + ), + Arguments.of( + "map(., .)", + JsonValue.parse("[1,2]"), + Arrays.asList(JsonValue.parse("[1,1,2,2]")) + ), + Arguments.of( + "map_values(. // empty)", + JsonValue.parse("{\"a\": null, \"b\": true, \"c\": false}"), + Arrays.asList(JsonValue.parse("{\"b\":true}")) + ), + Arguments.of( + "pick(.a, .b.c, .x)", + JsonValue.parse("{\"a\": 1, \"b\": {\"c\": 2, \"d\": 3}, \"e\": 4}"), + Arrays.asList(JsonValue.parse("{\"a\":1,\"b\":{\"c\":2},\"x\":null}")) + ), + Arguments.of( + "pick(.[2], .[0], .[0])", + JsonValue.parse("[1,2,3,4]"), + Arrays.asList(JsonValue.parse("[1,null,3]")) + ), + Arguments.of( + "path(.a[0].b)", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[\"a\",0,\"b\"]")) + ), + Arguments.of( + "[path(..)]", + JsonValue.parse("{\"a\":[{\"b\":1}]}"), + Arrays.asList(JsonValue.parse("[[],[\"a\"],[\"a\",0],[\"a\",0,\"b\"]]")) + ), + Arguments.of( + "del(.foo)", + JsonValue.parse("{\"foo\": 42, \"bar\": 9001, \"baz\": 42}"), + Arrays.asList(JsonValue.parse("{\"bar\": 9001, \"baz\": 42}")) + ), + Arguments.of( + "del(.[1, 2])", + JsonValue.parse("[\"foo\", \"bar\", \"baz\"]"), + Arrays.asList(JsonValue.parse("[\"foo\"]")) + ), + Arguments.of( + "getpath([\"a\",\"b\"])", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("null")) + ), + Arguments.of( + "[getpath([\"a\",\"b\"], [\"a\",\"c\"])]", + JsonValue.parse("{\"a\":{\"b\":0, \"c\":1}}"), + Arrays.asList(JsonValue.parse("[0, 1]")) + ), + Arguments.of( + "setpath([\"a\",\"b\"]; 1)", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("{\"a\": {\"b\": 1}}")) + ), + Arguments.of( + "setpath([\"a\",\"b\"]; 1)", + JsonValue.parse("{\"a\":{\"b\":0}}"), + Arrays.asList(JsonValue.parse("{\"a\": {\"b\": 1}}")) + ), + Arguments.of( + "setpath([0,\"a\"]; 1)", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[{\"a\":1}]")) + ), + Arguments.of( + "delpaths([[\"a\",\"b\"]])", + JsonValue.parse("{\"a\":{\"b\":1},\"x\":{\"y\":2}}"), + Arrays.asList(JsonValue.parse("{\"a\":{},\"x\":{\"y\":2}}")) + ), + Arguments.of( + "to_entries", + JsonValue.parse("{\"a\": 1, \"b\": 2}"), + Arrays.asList(JsonValue.parse("[{\"key\":\"a\", \"value\":1}, {\"key\":\"b\", \"value\":2}]")) + ), + Arguments.of( + "from_entries", + JsonValue.parse("[{\"key\":\"a\", \"value\":1}, {\"key\":\"b\", \"value\":2}]"), + Arrays.asList(JsonValue.parse("{\"a\": 1, \"b\": 2}")) + ), + Arguments.of( + "with_entries(.key |= \"KEY_\" + .)", + JsonValue.parse("{\"a\": 1, \"b\": 2}"), + Arrays.asList(JsonValue.parse("{\"KEY_a\": 1, \"KEY_b\": 2}")) + ), + Arguments.of( + "map(select(. >= 2))", + JsonValue.parse("[1,5,3,0,7]"), + Arrays.asList(JsonValue.parse("[5,3,7]")) + ), + Arguments.of( + ".[] | select(.id == \"second\")", + JsonValue.parse("[{\"id\": \"first\", \"val\": 1}, {\"id\": \"second\", \"val\": 2}]"), + Arrays.asList(JsonValue.parse("{\"id\": \"second\", \"val\": 2}")) + ), + Arguments.of( + ".[]|numbers", + JsonValue.parse("[[],{},1,\"foo\",null,true,false]"), + Arrays.asList(JsonValue.parse("1")) + ), + Arguments.of( + "1, empty, 2", + JsonValue.parse("null"), + Arrays.asList( + JsonValue.parse("1"), + JsonValue.parse("2") + ) + ), + Arguments.of( + "[1,2,empty,3]", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[1,2,3]")) + ), + Arguments.of( + "try error catch .", + JsonValue.parse("\"error message\""), + Arrays.asList(JsonValue.parse("\"error message\"")) + ), + Arguments.of( + "try error(\"invalid value: \\(.)\") catch .", + JsonValue.parse("42"), + Arrays.asList(JsonValue.parse("\"invalid value: 42\"")) + ), + Arguments.of( + "try error(\"\\($__loc__)\") catch .", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("\"{\\\"file\\\":\\\"\\\",\\\"line\\\":1}\"")) + ), + Arguments.of( + "[paths]", + JsonValue.parse("[1,[[],{\"a\":2}]]"), + Arrays.asList(JsonValue.parse("[[0],[1],[1,0],[1,1],[1,1,\"a\"]]")) + ), + Arguments.of( + "[paths(type == \"number\")]", + JsonValue.parse("[1,[[],{\"a\":2}]]"), + Arrays.asList(JsonValue.parse("[[0],[1,1,\"a\"]]")) + ), + Arguments.of( + "add", + JsonValue.parse("[\"a\",\"b\",\"c\"]"), + Arrays.asList(JsonValue.parse("\"abc\"")) + ), + Arguments.of( + "add", + JsonValue.parse("[1, 2, 3]"), + Arrays.asList(JsonValue.parse("6")) + ), + Arguments.of( + "add", + JsonValue.parse("[]"), + Arrays.asList(JsonValue.parse("null")) + ), + Arguments.of( + "any", + JsonValue.parse("[true, false]"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "any", + JsonValue.parse("[false, false]"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + "any", + JsonValue.parse("[]"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + "all", + JsonValue.parse("[true, false]"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + "all", + JsonValue.parse("[true, true]"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "all", + JsonValue.parse("[]"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "flatten", + JsonValue.parse("[1, [2], [[3]]]"), + Arrays.asList(JsonValue.parse("[1, 2, 3]")) + ), + Arguments.of( + "flatten(1)", + JsonValue.parse("[1, [2], [[3]]]"), + Arrays.asList(JsonValue.parse("[1, 2, [3]]")) + ), + Arguments.of( + "flatten", + JsonValue.parse("[[]]"), + Arrays.asList(JsonValue.parse("[]")) + ), + Arguments.of( + "flatten", + JsonValue.parse("[{\"foo\": \"bar\"}, [{\"foo\": \"baz\"}]]"), + Arrays.asList(JsonValue.parse("[{\"foo\": \"bar\"}, {\"foo\": \"baz\"}]")) + ), + Arguments.of( + "range(2; 4)", + JsonValue.parse("null"), + Arrays.asList( + JsonValue.parse("2"), + JsonValue.parse("3") + ) + ), + Arguments.of( + "[range(2; 4)]", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[2,3]")) + ), + Arguments.of( + "[range(4)]", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[0,1,2,3]")) + ), + Arguments.of( + "[range(0; 10; 3)]", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[0,3,6,9]")) + ), + Arguments.of( + "[range(0; 10; -1)]", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[]")) + ), + Arguments.of( + "[range(0; -5; -1)]", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[0,-1,-2,-3,-4]")) + ), + Arguments.of( + "floor", + JsonValue.parse("3.14159"), + Arrays.asList(JsonValue.parse("3")) + ), + Arguments.of( + "sqrt", + JsonValue.parse("9"), + Arrays.asList(JsonValue.parse("3")) + ), + Arguments.of( + ".[] | tonumber", + JsonValue.parse("[1, \"1\"]"), + Arrays.asList( + JsonValue.parse("1"), + JsonValue.parse("1") + ) + ), + Arguments.of( + ".[] | tostring", + JsonValue.parse("[1, \"1\", [1]]"), + Arrays.asList( + JsonValue.parse("\"1\""), + JsonValue.parse("\"1\""), + JsonValue.parse("\"[1]\"") + ) + ), + Arguments.of( + "map(type)", + JsonValue.parse("[0, false, [], {}, null, \"hello\"]"), + Arrays.asList(JsonValue.parse( + "[\"number\", \"boolean\", \"array\", \"object\", \"null\", \"string\"]")) + ), + Arguments.of( + ".[] | (infinite * .) < 0", + JsonValue.parse("[-1, 1]"), + Arrays.asList( + JsonValue.parse("true"), + JsonValue.parse("false") + ) + ), + Arguments.of( + "infinite, nan | type", + JsonValue.parse("null"), + Arrays.asList( + JsonValue.parse("\"number\""), + JsonValue.parse("\"number\"") + ) + ), + Arguments.of( + "sort", + JsonValue.parse("[8,3,null,6]"), + Arrays.asList(JsonValue.parse("[null,3,6,8]")) + ), + Arguments.of( + "sort_by(.foo)", + JsonValue.parse("[{\"foo\":4, \"bar\":10}, {\"foo\":3, \"bar\":10}, {\"foo\":2, \"bar\":1}]"), + Arrays.asList( + JsonValue.parse( + "[{\"foo\":2, \"bar\":1}, {\"foo\":3, \"bar\":10}, {\"foo\":4, \"bar\":10}]") + ) + ), + Arguments.of( + "sort_by(.foo, .bar)", + JsonValue.parse( + "[{\"foo\":4, \"bar\":10}, {\"foo\":3, \"bar\":20}, {\"foo\":2, \"bar\":1}, {\"foo\":3, \"bar\":10}]"), + Arrays.asList( + JsonValue.parse( + "[{\"foo\":2, \"bar\":1}, {\"foo\":3, \"bar\":10}, {\"foo\":3, \"bar\":20}, {\"foo\":4, \"bar\":10}]") + ) + ), + Arguments.of( + "group_by(.foo)", + JsonValue.parse("[{\"foo\":1, \"bar\":10}, {\"foo\":3, \"bar\":100}, {\"foo\":1, \"bar\":1}]"), + Arrays.asList( + JsonValue.parse( + "[[{\"foo\":1, \"bar\":10}, {\"foo\":1, \"bar\":1}], [{\"foo\":3, \"bar\":100}]]") + ) + ), + Arguments.of( + "min", + JsonValue.parse("[5,4,2,7]"), + Arrays.asList(JsonValue.parse("2")) + ), + Arguments.of( + "max_by(.foo)", + JsonValue.parse("[{\"foo\":1, \"bar\":14}, {\"foo\":2, \"bar\":3}]"), + Arrays.asList(JsonValue.parse("{\"foo\":2, \"bar\":3}")) + ), + Arguments.of( + "unique", + JsonValue.parse("[1,2,5,3,5,3,1,3]"), + Arrays.asList(JsonValue.parse("[1,2,3,5]")) + ), + Arguments.of( + "unique_by(.foo)", + JsonValue.parse("[{\"foo\": 1, \"bar\": 2}, {\"foo\": 1, \"bar\": 3}, {\"foo\": 4, \"bar\": 5}]"), + Arrays.asList(JsonValue.parse("[{\"foo\": 1, \"bar\": 2}, {\"foo\": 4, \"bar\": 5}]")) + ), + Arguments.of( + "unique_by(length)", + JsonValue.parse("[\"chunky\", \"bacon\", \"kitten\", \"cicada\", \"asparagus\"]"), + Arrays.asList(JsonValue.parse("[\"bacon\", \"chunky\", \"asparagus\"]")) + ), + Arguments.of( + "reverse", + JsonValue.parse("[1,2,3,4]"), + Arrays.asList(JsonValue.parse("[4,3,2,1]")) + ), + Arguments.of( + "contains(\"bar\")", + JsonValue.parse("\"foobar\""), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "contains([\"baz\", \"bar\"])", + JsonValue.parse("[\"foobar\", \"foobaz\", \"blarp\"]"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "contains([\"bazzzzz\", \"bar\"])", + JsonValue.parse("[\"foobar\", \"foobaz\", \"blarp\"]"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + "contains({foo: 12, bar: [{barp: 12}]})", + JsonValue.parse("{\"foo\": 12, \"bar\":[1,2,{\"barp\":12, \"blip\":13}]}"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "contains({foo: 12, bar: [{barp: 15}]})", + JsonValue.parse("{\"foo\": 12, \"bar\":[1,2,{\"barp\":12, \"blip\":13}]}"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + "indices(\", \")", + JsonValue.parse("\"a,b, cd, efg, hijk\""), + Arrays.asList(JsonValue.parse("[3,7,12]")) + ), + Arguments.of( + "indices(1)", + JsonValue.parse("[0,1,2,1,3,1,4]"), + Arrays.asList(JsonValue.parse("[1,3,5]")) + ), + Arguments.of( + "indices([1,2])", + JsonValue.parse("[0,1,2,3,1,4,2,5,1,2,6,7]"), + Arrays.asList(JsonValue.parse("[1,8]")) + ), + Arguments.of( + "index(\", \")", + JsonValue.parse("\"a,b, cd, efg, hijk\""), + Arrays.asList(JsonValue.parse("3")) + ), + Arguments.of( + "index(1)", + JsonValue.parse("[0,1,2,1,3,1,4]"), + Arrays.asList(JsonValue.parse("1")) + ), + Arguments.of( + "index([1,2])", + JsonValue.parse("[0,1,2,3,1,4,2,5,1,2,6,7]"), + Arrays.asList(JsonValue.parse("1")) + ), + Arguments.of( + "rindex(\", \")", + JsonValue.parse("\"a,b, cd, efg, hijk\""), + Arrays.asList(JsonValue.parse("12")) + ), + Arguments.of( + "rindex(1)", + JsonValue.parse("[0,1,2,1,3,1,4]"), + Arrays.asList(JsonValue.parse("5")) + ), + Arguments.of( + "rindex([1,2])", + JsonValue.parse("[0,1,2,3,1,4,2,5,1,2,6,7]"), + Arrays.asList(JsonValue.parse("8")) + ), + Arguments.of( + "inside(\"foobar\")", + JsonValue.parse("\"bar\""), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "inside([\"foobar\", \"foobaz\", \"blarp\"])", + JsonValue.parse("[\"baz\", \"bar\"]"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "inside([\"foobar\", \"foobaz\", \"blarp\"])", + JsonValue.parse("[\"bazzzzz\", \"bar\"]"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + "inside({\"foo\": 12, \"bar\":[1,2,{\"barp\":12, \"blip\":13}]})", + JsonValue.parse("{\"foo\": 12, \"bar\": [{\"barp\": 12}]}"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "inside({\"foo\": 12, \"bar\":[1,2,{\"barp\":12, \"blip\":13}]})", + JsonValue.parse("{\"foo\": 12, \"bar\": [{\"barp\": 15}]}"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + "[.[]|startswith(\"foo\")]", + JsonValue.parse("[\"fo\", \"foo\", \"barfoo\", \"foobar\", \"barfoob\"]"), + Arrays.asList(JsonValue.parse("[false, true, false, true, false]")) + ), + Arguments.of( + "[.[]|endswith(\"foo\")]", + JsonValue.parse("[\"foobar\", \"barfoo\"]"), + Arrays.asList(JsonValue.parse("[false, true]")) + ), + Arguments.of( + "combinations", + JsonValue.parse("[[1,2], [3, 4]]"), + Arrays.asList( + JsonValue.parse("[1, 3]"), + JsonValue.parse("[1, 4]"), + JsonValue.parse("[2, 3]"), + JsonValue.parse("[2, 4]") + ) + ), + Arguments.of( + "combinations(2)", + JsonValue.parse("[0, 1]"), + Arrays.asList( + JsonValue.parse("[0, 0]"), + JsonValue.parse("[0, 1]"), + JsonValue.parse("[1, 0]"), + JsonValue.parse("[1, 1]") + ) + ), + Arguments.of( + "[.[]|ltrimstr(\"foo\")]", + JsonValue.parse("[\"fo\", \"foo\", \"barfoo\", \"foobar\", \"afoo\"]"), + Arrays.asList(JsonValue.parse("[\"fo\",\"\",\"barfoo\",\"bar\",\"afoo\"]")) + ), + Arguments.of( + "[.[]|rtrimstr(\"foo\")]", + JsonValue.parse("[\"fo\", \"foo\", \"barfoo\", \"foobar\", \"foob\"]"), + Arrays.asList(JsonValue.parse("[\"fo\",\"\",\"bar\",\"foobar\",\"foob\"]")) + ), + Arguments.of( + "trim, ltrim, rtrim", + JsonValue.parse("\" abc \""), + Arrays.asList( + JsonValue.parse("\"abc\""), + JsonValue.parse("\"abc \""), + JsonValue.parse("\" abc\"") + ) + ), + Arguments.of( + "explode", + JsonValue.parse("\"foobar\""), + Arrays.asList(JsonValue.parse("[102,111,111,98,97,114]")) + ), + Arguments.of( + "implode", + JsonValue.parse("[65, 66, 67]"), + Arrays.asList(JsonValue.parse("\"ABC\"")) + ), + Arguments.of( + "split(\", \")", + JsonValue.parse("\"a, b,c,d, e, \""), + Arrays.asList(JsonValue.parse("[\"a\",\"b,c,d\",\"e\",\"\"]")) + ), + Arguments.of( + "join(\", \")", + JsonValue.parse("[\"a\",\"b,c,d\",\"e\"]"), + Arrays.asList(JsonValue.parse("\"a, b,c,d, e\"")) + ), + Arguments.of( + "join(\" \")", + JsonValue.parse("[\"a\",1,2.3,true,null,false]"), + Arrays.asList(JsonValue.parse("\"a 1 2.3 true false\"")) + ), + Arguments.of( + "ascii_upcase", + JsonValue.parse("\"useful but not for �\""), + Arrays.asList(JsonValue.parse("\"USEFUL BUT NOT FOR �\"")) + ), + Arguments.of( + "[while(.<100; .*2)]", + JsonValue.parse("1"), + Arrays.asList(JsonValue.parse("[1,2,4,8,16,32,64]")) + ), + Arguments.of( + "[repeat(.*2, error)?]", + JsonValue.parse("1"), + Arrays.asList(JsonValue.parse("[2]")) + ), + Arguments.of( + "[.,1]|until(.[0] < 1; [.[0] - 1, .[1] * .[0]])|.[1]", + JsonValue.parse("4"), + Arrays.asList(JsonValue.parse("24")) + ), + Arguments.of( + "recurse(.foo[])", + JsonValue.parse("{\"foo\":[{\"foo\": []}, {\"foo\":[{\"foo\":[]}]}]}"), + Arrays.asList( + JsonValue.parse("{\"foo\":[{\"foo\":[]},{\"foo\":[{\"foo\":[]}]}]}"), + JsonValue.parse("{\"foo\":[]}"), + JsonValue.parse("{\"foo\":[{\"foo\":[]}]}"), + JsonValue.parse("{\"foo\":[]}") + ) + ), + Arguments.of( + "recurse", + JsonValue.parse("{\"a\":0,\"b\":[1]}"), + Arrays.asList( + JsonValue.parse("{\"a\":0,\"b\":[1]}"), + JsonValue.parse("0"), + JsonValue.parse("[1]"), + JsonValue.parse("1") + ) + ), + Arguments.of( + "recurse(. * .; . < 20)", + JsonValue.parse("2"), + Arrays.asList( + JsonValue.parse("2"), + JsonValue.parse("4"), + JsonValue.parse("16") + ) + ), + Arguments.of( + "walk(if type == \"array\" then sort else . end)", + JsonValue.parse("[[4, 1, 7], [8, 5, 2], [3, 6, 9]]"), + Arrays.asList(JsonValue.parse("[[1,4,7],[2,5,8],[3,6,9]]")) + ), + Arguments.of( + "walk( if type == \"object\" then with_entries( .key |= sub( \"^_+\"; \"\") ) else . end )", + JsonValue.parse("[ { \"_a\": { \"__b\": 2 } } ]"), + Arrays.asList(JsonValue.parse("[{\"a\":{\"b\":2}}]")) + ), + Arguments.of( + "$ENV.PAGER", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("\"less\"")) + ), + Arguments.of( + "env.PAGER", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("\"less\"")) + ), + Arguments.of( + "transpose", + JsonValue.parse("[[1], [2,3]]"), + Arrays.asList(JsonValue.parse("[[1,2],[null,3]]")) + ), + Arguments.of( + "bsearch(0)", + JsonValue.parse("[0,1]"), + Arrays.asList(JsonValue.parse("0")) + ), + Arguments.of( + "bsearch(0)", + JsonValue.parse("[1,2,3]"), + Arrays.asList(JsonValue.parse("-1")) + ), + Arguments.of( + "bsearch(4) as $ix | if $ix < 0 then .[-(1+$ix)] = 4 else . end", + JsonValue.parse("[1,2,3]"), + Arrays.asList(JsonValue.parse("[1,2,3,4]")) + ), + Arguments.of( + "\"The input was \\(.), which is one less than \\(.+1)\"", + JsonValue.parse("42"), + Arrays.asList(JsonValue.parse("\"The input was 42, which is one less than 43\"")) + ), + Arguments.of( + "[.[]|tostring]", + JsonValue.parse("[1, \"foo\", [\"foo\"]]"), + Arrays.asList(JsonValue.parse("[\"1\",\"foo\",\"[\\\"foo\\\"]\"]")) + ), + Arguments.of( + "[.[]|tojson]", + JsonValue.parse("[1, \"foo\", [\"foo\"]]"), + Arrays.asList(JsonValue.parse("[\"1\",\"\\\"foo\\\"\",\"[\\\"foo\\\"]\"]")) + ), + Arguments.of( + "[.[]|tojson|fromjson]", + JsonValue.parse("[1, \"foo\", [\"foo\"]]"), + Arrays.asList(JsonValue.parse("[1,\"foo\",[\"foo\"]]")) + ), + Arguments.of( + "@html", + JsonValue.parse("\"This works if x < y\""), + Arrays.asList(JsonValue.parse("\"This works if x < y\"")) + ), + Arguments.of( + "@sh \"echo \\(.)\"", + JsonValue.parse("\"O'Hara's Ale\""), + Arrays.asList(JsonValue.parse("\"echo 'O'\\\\''Hara'\\\\''s Ale'\"")) + ), + Arguments.of( + "@base64", + JsonValue.parse("\"This is a message\""), + Arrays.asList(JsonValue.parse("\"VGhpcyBpcyBhIG1lc3NhZ2U=\"")) + ), + Arguments.of( + "@base64d", + JsonValue.parse("\"VGhpcyBpcyBhIG1lc3NhZ2U=\""), + Arrays.asList(JsonValue.parse("\"This is a message\"")) + ), + Arguments.of( + "fromdate", + JsonValue.parse("\"2015-03-05T23:51:47Z\""), + Arrays.asList(JsonValue.parse("1425599507")) + ), + Arguments.of( + "strptime(\"%Y-%m-%dT%H:%M:%SZ\")", + JsonValue.parse("\"2015-03-05T23:51:47Z\""), + Arrays.asList(JsonValue.parse("[2015,2,5,23,51,47,4,63]")) + ), + Arguments.of( + "strptime(\"%Y-%m-%dT%H:%M:%SZ\")|mktime", + JsonValue.parse("\"2015-03-05T23:51:47Z\""), + Arrays.asList(JsonValue.parse("1425599507")) + ), + Arguments.of( + ". == false", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + ". == {\"b\": {\"d\": (4 + 1e-20), \"c\": 3}, \"a\":1}", + JsonValue.parse("{\"a\":1, \"b\": {\"c\": 3, \"d\": 4}}"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + ".[] == 1", + JsonValue.parse("[1, 1.0, \"1\", \"banana\"]"), + Arrays.asList( + JsonValue.parse("true"), + JsonValue.parse("true"), + JsonValue.parse("false"), + JsonValue.parse("false") + ) + ), + Arguments.of( + "if . == 0 then \"zero\" elif . == 1 then \"one\" else \"many\" end", + JsonValue.parse("2"), + Arrays.asList(JsonValue.parse("\"many\"")) + ), + Arguments.of( + ". < 5", + JsonValue.parse("2"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "42 and \"a string\"", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "(true, false) or false", + JsonValue.parse("null"), + Arrays.asList( + JsonValue.parse("true"), + JsonValue.parse("false") + ) + ), + Arguments.of( + "(true, true) and (true, false)", + JsonValue.parse("null"), + Arrays.asList( + JsonValue.parse("true"), + JsonValue.parse("false"), + JsonValue.parse("true"), + JsonValue.parse("false") + ) + ), + Arguments.of( + "[true, false | not]", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[false, true]")) + ), + Arguments.of( + "empty // 42", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("42")) + ), + Arguments.of( + ".foo // 42", + JsonValue.parse("{\"foo\": 19}"), + Arrays.asList(JsonValue.parse("19")) + ), + Arguments.of( + ".foo // 42", + JsonValue.parse("{}"), + Arrays.asList(JsonValue.parse("42")) + ), + Arguments.of( + "(false, null, 1) // 42", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("1")) + ), + Arguments.of( + "(false, null, 1) | . // 42", + JsonValue.parse("null"), + Arrays.asList( + JsonValue.parse("42"), + JsonValue.parse("42"), + JsonValue.parse("1") + ) + ), + Arguments.of( + "try .a catch \". is not an object\"", + JsonValue.parse("true"), + Arrays.asList(JsonValue.parse("\". is not an object\"")) + ), + Arguments.of( + "[.[]|try .a]", + JsonValue.parse("[{}, true, {\"a\":1}]"), + Arrays.asList(JsonValue.parse("[null, 1]")) + ), + Arguments.of( + "try error(\"some exception\") catch .", + JsonValue.parse("true"), + Arrays.asList(JsonValue.parse("\"some exception\"")) + ), + Arguments.of( + "[.[] | .a?]", + JsonValue.parse("[{}, true, {\"a\":1}]"), + Arrays.asList(JsonValue.parse("[null, 1]")) + ), + Arguments.of( + "[.[] | tonumber?]", + JsonValue.parse("[\"1\", \"invalid\", \"3\", 4]"), + Arrays.asList(JsonValue.parse("[1, 3, 4]")) + ), + Arguments.of( + "test(\"foo\")", + JsonValue.parse("\"foo\""), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + ".[] | test(\"a b c # spaces are ignored\"; \"ix\")", + JsonValue.parse("[\"xabcd\", \"ABC\"]"), + Arrays.asList( + JsonValue.parse("true"), + JsonValue.parse("true") + ) + ), + Arguments.of( + "match(\"(abc)+\"; \"g\")", + JsonValue.parse("\"abc abc\""), + Arrays.asList( + JsonValue.parse( + "{\"offset\": 0, \"length\": 3, \"string\": \"abc\", \"captures\": [{\"offset\": 0, \"length\": 3, \"string\": \"abc\", \"name\": null}]}"), + JsonValue.parse( + "{\"offset\": 4, \"length\": 3, \"string\": \"abc\", \"captures\": [{\"offset\": 4, \"length\": 3, \"string\": \"abc\", \"name\": null}]}") + ) + ), + Arguments.of( + "match(\"foo\")", + JsonValue.parse("\"foo bar foo\""), + Arrays.asList(JsonValue.parse( + "{\"offset\": 0, \"length\": 3, \"string\": \"foo\", \"captures\": []}")) + ), + Arguments.of( + "match([\"foo\", \"ig\"])", + JsonValue.parse("\"foo bar FOO\""), + Arrays.asList( + JsonValue.parse("{\"offset\": 0, \"length\": 3, \"string\": \"foo\", \"captures\": []}"), + JsonValue.parse("{\"offset\": 8, \"length\": 3, \"string\": \"FOO\", \"captures\": []}") + ) + ), + Arguments.of( + "match(\"foo (?bar)? foo\"; \"ig\")", + JsonValue.parse("\"foo bar foo foo foo\""), + Arrays.asList( + JsonValue.parse( + "{\"offset\": 0, \"length\": 11, \"string\": \"foo bar foo\", \"captures\": [{\"offset\": 4, \"length\": 3, \"string\": \"bar\", \"name\": \"bar123\"}]}"), + JsonValue.parse( + "{\"offset\": 12, \"length\": 8, \"string\": \"foo foo\", \"captures\": [{\"offset\": -1, \"length\": 0, \"string\": null, \"name\": \"bar123\"}]}") + ) + ), + Arguments.of( + "[ match(\".\"; \"g\")] | length", + JsonValue.parse("\"abc\""), + Arrays.asList(JsonValue.parse("3")) + ), + Arguments.of( + "capture(\"(?[a-z]+)-(?[0-9]+)\")", + JsonValue.parse("\"xyzzy-14\""), + Arrays.asList(JsonValue.parse("{ \"a\": \"xyzzy\", \"n\": \"14\" }")) + ), + Arguments.of( + "scan(\"c\")", + JsonValue.parse("\"abcdefabc\""), + Arrays.asList( + JsonValue.parse("\"c\""), + JsonValue.parse("\"c\"") + ) + ), + Arguments.of( + "split(\", *\"; null)", + JsonValue.parse("\"ab,cd, ef\""), + Arrays.asList(JsonValue.parse("[\"ab\",\"cd\",\"ef\"]")) + ), + Arguments.of( + "splits(\", *\")", + JsonValue.parse("\"ab,cd, ef, gh\""), + Arrays.asList( + JsonValue.parse("\"ab\""), + JsonValue.parse("\"cd\""), + JsonValue.parse("\"ef\""), + JsonValue.parse("\"gh\"") + ) + ), + Arguments.of( + "sub(\"[^a-z]*(?[a-z]+)\"; \"Z\\(.x)\"; \"g\")", + JsonValue.parse("\"123abc456def\""), + Arrays.asList(JsonValue.parse("\"ZabcZdef\"")) + ), + Arguments.of( + "[sub(\"(?.)\"; \"\\(.a|ascii_upcase)\", \"\\(.a|ascii_downcase)\")]", + JsonValue.parse("\"aB\""), + Arrays.asList(JsonValue.parse("[\"AB\",\"aB\"]")) + ), + Arguments.of( + "gsub(\"(?.)[^a]*\"; \"+\\(.x)-\")", + JsonValue.parse("\"Abcabc\""), + Arrays.asList(JsonValue.parse("\"+A-+a-\"")) + ), + Arguments.of( + "[gsub(\"p\"; \"a\", \"b\")]", + JsonValue.parse("\"p\""), + Arrays.asList(JsonValue.parse("[\"a\",\"b\"]")) + ), + Arguments.of( + ".bar as $x | .foo | . + $x", + JsonValue.parse("{\"foo\":10, \"bar\":200}"), + Arrays.asList(JsonValue.parse("210")) + ), + Arguments.of( + ". as $i|[(.*2|. as $i| $i), $i]", + JsonValue.parse("5"), + Arrays.asList(JsonValue.parse("[10,5]")) + ), + Arguments.of( + ". as [$a, $b, {c: $c}] | $a + $b + $c", + JsonValue.parse("[2, 3, {\"c\": 4, \"d\": 5}]"), + Arrays.asList(JsonValue.parse("9")) + ), + Arguments.of( + ".[] as [$a, $b] | {a: $a, b: $b}", + JsonValue.parse("[[0], [0, 1], [2, 1, 0]]"), + Arrays.asList( + JsonValue.parse("{\"a\":0,\"b\":null}"), + JsonValue.parse("{\"a\":0,\"b\":1}"), + JsonValue.parse("{\"a\":2,\"b\":1}") + ) + ), + Arguments.of( + ".[] as {$a, $b, c: {$d, $e}} ?// {$a, $b, c: [{$d, $e}]} | {$a, $b, $d, $e}", + JsonValue.parse( + "[{\"a\": 1, \"b\": 2, \"c\": {\"d\": 3, \"e\": 4}}, {\"a\": 1, \"b\": 2, \"c\": [{\"d\": 3, \"e\": 4}]}]"), + Arrays.asList( + JsonValue.parse("{\"a\":1,\"b\":2,\"d\":3,\"e\":4}"), + JsonValue.parse("{\"a\":1,\"b\":2,\"d\":3,\"e\":4}") + ) + ), + Arguments.of( + ".[] as {$a, $b, c: {$d}} ?// {$a, $b, c: [{$e}]} | {$a, $b, $d, $e}", + JsonValue.parse( + "[{\"a\": 1, \"b\": 2, \"c\": {\"d\": 3, \"e\": 4}}, {\"a\": 1, \"b\": 2, \"c\": [{\"d\": 3, \"e\": 4}]}]"), + Arrays.asList( + JsonValue.parse("{\"a\":1,\"b\":2,\"d\":3,\"e\":null}"), + JsonValue.parse("{\"a\":1,\"b\":2,\"d\":null,\"e\":4}") + ) + ), + Arguments.of( + ".[] as [$a] ?// [$b] | if $a != null then error(\"err: \\($a)\") else {$a,$b} end", + JsonValue.parse("[[3]]"), + Arrays.asList(JsonValue.parse("{\"a\":null,\"b\":3}")) + ), + Arguments.of( + "def addvalue(f): . + [f]; map(addvalue(.[0]))", + JsonValue.parse("[[1,2],[10,20]]"), + Arrays.asList(JsonValue.parse("[[1,2,1], [10,20,10]]")) + ), + Arguments.of( + "def addvalue(f): f as $x | map(. + $x); addvalue(.[0])", + JsonValue.parse("[[1,2],[10,20]]"), + Arrays.asList(JsonValue.parse("[[1,2,1,2], [10,20,1,2]]")) + ), + Arguments.of( + "isempty(empty)", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "isempty(.[])", + JsonValue.parse("[]"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "isempty(.[])", + JsonValue.parse("[1,2,3]"), + Arrays.asList(JsonValue.parse("false")) + ), + Arguments.of( + "[limit(3;.[])]", + JsonValue.parse("[0,1,2,3,4,5,6,7,8,9]"), + Arrays.asList(JsonValue.parse("[0,1,2]")) + ), + Arguments.of( + "[first(range(.)), last(range(.)), nth(./2; range(.))]", + JsonValue.parse("10"), + Arrays.asList(JsonValue.parse("[0,9,5]")) + ), + Arguments.of( + "[range(.)]|[first, last, nth(5)]", + JsonValue.parse("10"), + Arrays.asList(JsonValue.parse("[0,9,5]")) + ), + Arguments.of( + "reduce .[] as $item (0; . + $item)", + JsonValue.parse("[1,2,3,4,5]"), + Arrays.asList(JsonValue.parse("15")) + ), + Arguments.of( + "reduce .[] as [$i,$j] (0; . + $i * $j)", + JsonValue.parse("[[1,2],[3,4],[5,6]]"), + Arrays.asList(JsonValue.parse("44")) + ), + Arguments.of( + "reduce .[] as {$x,$y} (null; .x += $x | .y += [$y])", + JsonValue.parse("[{\"x\":\"a\",\"y\":1},{\"x\":\"b\",\"y\":2},{\"x\":\"c\",\"y\":3}]"), + Arrays.asList(JsonValue.parse("{\"x\":\"abc\",\"y\":[1,2,3]}")) + ), + Arguments.of( + "foreach .[] as $item (0; . + $item)", + JsonValue.parse("[1,2,3,4,5]"), + Arrays.asList( + JsonValue.parse("1"), + JsonValue.parse("3"), + JsonValue.parse("6"), + JsonValue.parse("10"), + JsonValue.parse("15") + ) + ), + Arguments.of( + "foreach .[] as $item (0; . + $item; [$item, . * 2])", + JsonValue.parse("[1,2,3,4,5]"), + Arrays.asList( + JsonValue.parse("[1,2]"), + JsonValue.parse("[2,6]"), + JsonValue.parse("[3,12]"), + JsonValue.parse("[4,20]"), + JsonValue.parse("[5,30]") + ) + ), + Arguments.of( + "foreach .[] as $item (0; . + 1; {index: ., $item})", + JsonValue.parse("[\"foo\", \"bar\", \"baz\"]"), + Arrays.asList( + JsonValue.parse("{\"index\":1,\"item\":\"foo\"}"), + JsonValue.parse("{\"index\":2,\"item\":\"bar\"}"), + JsonValue.parse("{\"index\":3,\"item\":\"baz\"}") + ) + ), + Arguments.of( + "def range(init; upto; by): def _range: if (by > 0 and . < upto) or (by < 0 and . > upto) then ., ((.+by)|_range) else . end; if by == 0 then init else init|_range end | select((by > 0 and . < upto) or (by < 0 and . > upto)); range(0; 10; 3)", + JsonValue.parse("null"), + Arrays.asList( + JsonValue.parse("0"), + JsonValue.parse("3"), + JsonValue.parse("6"), + JsonValue.parse("9") + ) + ), + Arguments.of( + "def while(cond; update): def _while: if cond then ., (update | _while) else empty end; _while; [while(.<100; .*2)]", + JsonValue.parse("1"), + Arrays.asList(JsonValue.parse("[1,2,4,8,16,32,64]")) + ), + Arguments.of( + "truncate_stream([[0],1],[[1,0],2],[[1,0]],[[1]])", + JsonValue.parse("1"), + Arrays.asList( + JsonValue.parse("[[0],2]"), + JsonValue.parse("[[0]]") + ) + ), + Arguments.of( + "fromstream(1|truncate_stream([[0],1],[[1,0],2],[[1,0]],[[1]]))", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("[2]")) + ), + Arguments.of( + ". as $dot|fromstream($dot|tostream)|.==$dot", + JsonValue.parse("[0,[1,{\"a\":1},{\"b\":2}]]"), + Arrays.asList(JsonValue.parse("true")) + ), + Arguments.of( + "(..|select(type==\"boolean\")) |= if . then 1 else 0 end", + JsonValue.parse("[true,false,[5,true,[true,[false]],false]]"), + Arrays.asList(JsonValue.parse("[1,0,[5,1,[1,[0]],0]]")) + ), + Arguments.of( + ".foo += 1", + JsonValue.parse("{\"foo\": 42}"), + Arrays.asList(JsonValue.parse("{\"foo\": 43}")) + ), + Arguments.of( + ".a = .b", + JsonValue.parse("{\"a\": {\"b\": 10}, \"b\": 20}"), + Arrays.asList(JsonValue.parse("{\"a\":20,\"b\":20}")) + ), + Arguments.of( + ".a |= .b", + JsonValue.parse("{\"a\": {\"b\": 10}, \"b\": 20}"), + Arrays.asList(JsonValue.parse("{\"a\":10,\"b\":20}")) + ), + Arguments.of( + "(.a, .b) = range(3)", + JsonValue.parse("null"), + Arrays.asList( + JsonValue.parse("{\"a\":0,\"b\":0}"), + JsonValue.parse("{\"a\":1,\"b\":1}"), + JsonValue.parse("{\"a\":2,\"b\":2}") + ) + ), + Arguments.of( + "(.a, .b) |= range(3)", + JsonValue.parse("null"), + Arrays.asList(JsonValue.parse("{\"a\":0,\"b\":0}")) + ) + ); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 911a2e1..c55b33c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,3 +9,4 @@ dependencyResolutionManagement { rootProject.name = "json" include("core") +include("query")