From 87ae709f6d0912e5eb8cbf1b84edab29d9efe4aa Mon Sep 17 00:00:00 2001
From: jbb01 <32650546+jbb01@users.noreply.github.com>
Date: Sun, 13 Apr 2025 02:29:38 +0200
Subject: [PATCH] fixup query
---
.../java/eu/jonahbauer/json/JsonNumber.java | 4 -
.../eu/jonahbauer/json/query/JsonMath.java | 152 ++++++++++++++++++
.../java/eu/jonahbauer/json/query/Main.java | 10 ++
.../json/query/parser/JQParser.java | 68 +++++++-
.../json/query/parser/ast/JQAsExpression.java | 4 +-
.../json/query/parser/ast/JQBuiltIn.java | 24 +++
.../json/query/parser/ast/JQExpression.java | 19 ++-
.../query/parser/ast/JQForEachExpression.java | 41 ++---
.../ast/JQObjectConstructionExpression.java | 52 ++++++
.../json/query/parser/ast/JQPatterns.java | 114 +++++++------
.../query/parser/ast/JQReduceExpression.java | 34 ++--
.../query/parser/ast/JQSliceExpression.java | 46 +-----
.../jonahbauer/json/query/JsonQueryTest.java | 8 +-
13 files changed, 416 insertions(+), 160 deletions(-)
create mode 100644 query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQObjectConstructionExpression.java
diff --git a/core/src/main/java/eu/jonahbauer/json/JsonNumber.java b/core/src/main/java/eu/jonahbauer/json/JsonNumber.java
index 762c170..ead6b1e 100644
--- a/core/src/main/java/eu/jonahbauer/json/JsonNumber.java
+++ b/core/src/main/java/eu/jonahbauer/json/JsonNumber.java
@@ -13,10 +13,6 @@ 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");
- }
-
/**
* Converts the given int to a JSON number.
* @param i an int
diff --git a/query/src/main/java/eu/jonahbauer/json/query/JsonMath.java b/query/src/main/java/eu/jonahbauer/json/query/JsonMath.java
index 58e5b90..5f7334b 100644
--- a/query/src/main/java/eu/jonahbauer/json/query/JsonMath.java
+++ b/query/src/main/java/eu/jonahbauer/json/query/JsonMath.java
@@ -348,6 +348,49 @@ public class JsonMath {
}
//
+ //
+ public static @NotNull Generator<@Nullable JsonValue> recurse(@Nullable JsonValue value) {
+ return recurse(value, v -> values(v, true));
+ }
+
+ public static @NotNull Generator<@Nullable JsonValue> recurse(@Nullable JsonValue value, @NotNull JQFilter f) {
+ return recurse(value, f, v -> Generator.of(JsonBoolean.TRUE));
+ }
+
+ public static @NotNull Generator<@Nullable JsonValue> recurse(@Nullable JsonValue value, @NotNull JQFilter f, @NotNull JQFilter condition) {
+ return Generator.concat(Generator.of(value), f.apply(value)
+ .flatMap(v -> condition.apply(v).filter(JsonMath::isTruthy).map(_ -> v))
+ .flatMap(v -> recurse(v, f, condition))
+ );
+ }
+
+ public static @NotNull Generator<@Nullable JsonValue> walk(@Nullable JsonValue value, @NotNull JQFilter filter) {
+ var result = switch (value) {
+ case JsonObject object -> mapValues(object, v -> walk(v, filter));
+ case JsonArray array -> map(array, v -> walk(v, filter));
+ case null, default -> value;
+ };
+ return filter.apply(result);
+ }
+
+ public static @NotNull Generator<@Nullable JsonValue> while_(@Nullable JsonValue value, @NotNull JQFilter condition, @NotNull JQFilter update) {
+ return condition.apply(value)
+ .filter(JsonMath::isTruthy)
+ .flatMap(_ -> Generator.concat(
+ Generator.of(value),
+ update.apply(value).flatMap(v -> while_(v, condition, update)))
+ );
+ }
+
+ public static @NotNull Generator<@Nullable JsonValue> until(@Nullable JsonValue value, @NotNull JQFilter condition, @NotNull JQFilter next) {
+ return condition.apply(value)
+ .flatMap(bool -> JsonMath.isTruthy(bool)
+ ? Generator.of(value)
+ : next.apply(value).flatMap(v -> until(v, condition, next))
+ );
+ }
+ //
+
//
public static @Nullable JsonValue index(@Nullable JsonValue value, @Nullable JsonValue index) {
return switch (index) {
@@ -379,10 +422,48 @@ public class JsonMath {
}
case null, default -> throw indexError(value, index);
};
+ case JsonObject indexObject -> slice(value, indexObject.get("start"), indexObject.get("end"));
case null, default -> throw indexError(value, index);
};
}
+ public static @Nullable JsonValue slice(@Nullable JsonValue value, @Nullable JsonValue start, @Nullable JsonValue end) {
+ String type;
+ int length;
+ BiFunction slice;
+ if (value instanceof JsonString string) {
+ type = "a string slice";
+ length = string.length();
+ slice = string::subSequence;
+ } else if (value instanceof JsonArray array) {
+ type = "an array slice";
+ length = array.size();
+ slice = array::subList;
+ } else {
+ throw indexError(value, JsonObject.EMPTY);
+ }
+
+ if (start != null && !(start instanceof JsonNumber) || end != null && !(end instanceof JsonNumber)) {
+ throw new JsonQueryException("Start and end indices of " + type + " must be numbers");
+ }
+ var lower = getIndex((JsonNumber) start, length, false);
+ var upper = getIndex((JsonNumber) end, length, true);
+ if (lower > upper) {
+ return slice.apply(0, 0);
+ } else {
+ return slice.apply(lower, upper);
+ }
+ }
+
+ private static int getIndex(@Nullable JsonNumber index, int length, boolean isUpper) {
+ if (index == null) {
+ return isUpper ? length : 0;
+ }
+ var d = index.value();
+ if (d < 0) d = length + d;
+ return isUpper ? (int) Math.ceil(d) : (int) Math.floor(d);
+ }
+
private static boolean isInt(double value) {
return ((int) value) == value;
}
@@ -674,6 +755,12 @@ public class JsonMath {
}
}
return JsonBoolean.TRUE;
+ } else if (container instanceof JsonBoolean bcontainer && content instanceof JsonBoolean bcontent) {
+ return JsonBoolean.valueOf(bcontainer == bcontent);
+ } else if (container instanceof JsonNumber ncontainer && content instanceof JsonNumber ncontent) {
+ return JsonBoolean.valueOf(ncontainer.equals(ncontent));
+ } else if (container == null && content == null) {
+ return JsonBoolean.TRUE;
} else {
return null;
}
@@ -1324,7 +1411,72 @@ public class JsonMath {
}
//
+ //
+ public static @Nullable JsonValue getpath(@Nullable JsonValue value, @Nullable JsonValue path) {
+ if (!(path instanceof JsonArray array)) {
+ throw new JsonQueryException("Path must be specified as an array");
+ }
+
+ var v = value;
+ for (var node : array) {
+ v = index(v, node);
+ }
+ return v;
+ }
+
+// public static @Nullable JsonValue setpath(@Nullable JsonValue container, @Nullable JsonValue path, @Nullable JsonValue value) {
+// if (!(path instanceof JsonArray array)) {
+// throw new JsonQueryException("Path must be specified as an array");
+// }
+// if (array.isEmpty()) {
+// return value;
+// } else if (array.size() > 1) {
+//
+// }
+//
+// if (array.size() > 1) {
+//
+// }
+// }
+
+ public static @NotNull Generator<@NotNull JsonArray> paths(@Nullable JsonValue value) {
+ return switch (value) {
+ case JsonObject object -> Generator.from(object.keySet()).flatMap(key -> Generator.concat(
+ Generator.of(JsonArray.valueOf(key)),
+ paths(object.get(key)).map(path -> {
+ var list = new ArrayList<>(path.size() + 1);
+ list.add(JsonString.valueOf(key));
+ list.addAll(path);
+ return JsonArray.valueOf(list);
+ })
+ ));
+ case JsonArray array -> Generator.from(IntStream.range(0, array.size()).boxed()).flatMap(i -> Generator.concat(
+ Generator.of(JsonArray.valueOf(JsonNumber.valueOf(i))),
+ paths(array.get(i)).map(path -> {
+ var list = new ArrayList<>(path.size() + 1);
+ list.add(JsonNumber.valueOf(i));
+ list.addAll(path);
+ return JsonArray.valueOf(list);
+ })
+ ));
+ case null, default -> Generator.empty();
+ };
+ }
+
+ public static @NotNull Generator<@NotNull JsonArray> paths(@Nullable JsonValue value, @NotNull JQFilter filter) {
+ return paths(value)
+ .flatMap(path -> filter.apply(getpath(value, path))
+ .filter(JsonMath::isTruthy)
+ .map(v -> path)
+ );
+ }
+ //
+
//
+ public static @NotNull JsonObject env() {
+ return JsonObject.valueOf(System.getenv());
+ }
+
public static @NotNull JsonNumber length(@Nullable JsonValue value) {
return new JsonNumber(length0(value));
}
diff --git a/query/src/main/java/eu/jonahbauer/json/query/Main.java b/query/src/main/java/eu/jonahbauer/json/query/Main.java
index 7960791..208cb43 100644
--- a/query/src/main/java/eu/jonahbauer/json/query/Main.java
+++ b/query/src/main/java/eu/jonahbauer/json/query/Main.java
@@ -1,5 +1,7 @@
package eu.jonahbauer.json.query;
+import eu.jonahbauer.json.JsonValue;
+
import java.util.LinkedHashMap;
import java.util.SequencedMap;
@@ -10,6 +12,14 @@ public class Main {
System.out.println();
System.out.println("Object Identifier-Index: .foo, .foo.bar");
+ JsonQuery.parse("{\"foo\": 1, \"bar\": 2} | reduce . as {(\"foo\", \"bar\"): $item} (0; . + $item, 2 * . * $item)")
+ .run(null)
+ .forEach(System.out::println);
+
+ JsonQuery.parse(".[] as {$a, $b, c: {$d, $e}} ?// {$a, $b, c: {$d, $e}} | {$a, $b, $d, $e}")
+ .run(JsonValue.parse("[{\"a\": 1, \"b\": 2, \"c\": {\"d\": 3, \"e\": 4}}, {\"a\": 1, \"b\": 2, \"c\": [{\"d\": 3, \"e\": 4}]}]"))
+ .forEach(System.out::println);
+
JsonQuery.parse(".foo")
.run(JSON."{\"foo\": 42, \"bar\": \"less interesting data\"}")
.forEach(System.out::println);
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
index c0dfbd2..398c481 100644
--- a/query/src/main/java/eu/jonahbauer/json/query/parser/JQParser.java
+++ b/query/src/main/java/eu/jonahbauer/json/query/parser/JQParser.java
@@ -442,7 +442,7 @@ public class JQParser {
return new JQFunctionInvocation(name, args);
}
- private @NotNull JQExpression parseVariableExpression() throws IOException {
+ private @NotNull JQVariableExpression parseVariableExpression() throws IOException {
consume(JQTokenKind.DOLLAR);
var ident = consume(JQTokenKind.IDENT).text();
return new JQVariableExpression("$" + ident);
@@ -470,9 +470,71 @@ public class JQParser {
consume(JQTokenKind.LBRACE);
if (tryConsume(JQTokenKind.RBRACE)) {
return new JQConstant(JsonObject.EMPTY);
- } else {
- throw new UnsupportedOperationException("not yet implemented");
}
+
+ var entries = new ArrayList();
+ while (!tryConsume(JQTokenKind.RBRACE)) {
+ if (!entries.isEmpty()) {
+ consume(JQTokenKind.COMMA);
+ }
+
+ if (peek(JQTokenKind.LOC)) {
+ var loc = consume(JQTokenKind.LOC);
+ entries.add(new JQObjectConstructionExpression.Entry(
+ new JQConstant(JsonValue.valueOf("__loc__")),
+ new JQLocExpression("", loc.line())
+ ));
+ continue;
+ }
+
+ var next = peek();
+ JQExpression key;
+ JQExpression value = null;
+
+ switch (next == null ? null : next.kind()) {
+ case IDENT -> {
+ key = new JQConstant(JsonString.valueOf(consume(JQTokenKind.IDENT).text()));
+ value = new JQIndexExpression(new JQRootExpression(), key, false);
+ }
+ case QQSTRING_START -> {
+ key = parseString();
+ value = new JQIndexExpression(new JQRootExpression(), key, false);
+ }
+ case DOLLAR -> {
+ var var = parseVariableExpression();
+ key = new JQConstant(JsonString.valueOf(var.name().substring(1)));
+ value = var;
+ }
+ case LPAREN -> {
+ key = parseParenthesizedExpression();
+ }
+ case AS, IMPORT, INCLUDE, MODULE, DEF, IF, THEN, ELSE, ELSE_IF, AND, OR, END, REDUCE, FOREACH, TRY,
+ CATCH, LABEL, BREAK -> {
+ key = new JQConstant(JsonString.valueOf(consume(next.kind()).text()));
+ value = key;
+ }
+ case null -> throw new JsonQueryParserException(-1, -1, "unexpected end of file");
+ default -> {
+ var token = Objects.requireNonNull(next);
+ throw new JsonQueryParserException(token.line(),
+ token.column(),
+ "unexpected token " + token.kind()
+ );
+ }
+ }
+
+ if (value == null) {
+ consume(JQTokenKind.COLON);
+ } else if (!tryConsume(JQTokenKind.COLON)) {
+ entries.add(new JQObjectConstructionExpression.Entry(key, value));
+ continue;
+ }
+
+ value = parseLeftAssocBinaryExpression(Map.of(JQTokenKind.PIPE, JQPipeExpression::new), this::parseNegation);
+ entries.add(new JQObjectConstructionExpression.Entry(key, value));
+ }
+
+ return new JQObjectConstructionExpression(entries);
}
private @NotNull JQExpression parseIndexingExpression(@NotNull JQExpression root) throws IOException {
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
index bd0f6c3..9652ae4 100644
--- 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
@@ -17,10 +17,10 @@ public record JQAsExpression(
Objects.requireNonNull(patterns);
}
+
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
- return variable.evaluate(context)
- .flatMap(value -> patterns.bind(context, value).map(context::withVariables).flatMap(expression::evaluate));
+ return variable.evaluate(context).flatMap(value -> patterns.bind(context, value, expression::evaluate));
}
@Override
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
index 65f9408..1dbbed4 100644
--- 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
@@ -39,6 +39,14 @@ public enum JQBuiltIn implements JQInvocable {
NTH$2(2, (context, args) -> args.getFirst().evaluate(context).flatMap(n -> JsonMath.nth(args.getLast().evaluate(context), n))),
SELECT(1, Implementation.mapF$F(JsonMath::select)),
+ // loops
+ RECURSE$0(0, Implementation.map$F(JsonMath::recurse)),
+ RECURSE$1(1, Implementation.mapF$F(JsonMath::recurse)),
+ RECURSE$2(2, Implementation.mapFF$F(JsonMath::recurse)),
+ WALK$1(1, Implementation.mapF$F(JsonMath::walk)),
+ WHILE(2, Implementation.mapFF$F(JsonMath::while_)),
+ UNTIL(2, Implementation.mapFF$F(JsonMath::until)),
+
// iterable operations
MAP(1, Implementation.mapF$V(JsonMath::map)),
MAP_VALUES(1, Implementation.mapF$V(JsonMath::mapValues)),
@@ -147,11 +155,19 @@ public enum JQBuiltIn implements JQInvocable {
TODATE(0, Implementation.map$V(JsonMath::todate)),
NOW(0, Implementation.map$V(_ -> JsonNumber.valueOf(System.currentTimeMillis()))),
+ // paths
+ GETPATH(1, Implementation.mapV$V(JsonMath::getpath)),
+ PATHS$0(0, Implementation.map$F(JsonMath::paths)),
+ PATHS$1(1, Implementation.mapF$F(JsonMath::paths)),
+
// misc
+ ENV(0, Implementation.map$V(_ -> JsonMath.env())),
LENGTH(0, Implementation.map$V(JsonMath::length)),
REPEAT(1, (context, args) -> JsonMath.repeat(() -> args.getFirst().evaluate(context))),
// math library
+ INFINITE(0, Implementation.map$V(_ -> JsonNumber.valueOf(Double.POSITIVE_INFINITY))),
+ NAN(0, Implementation.map$V(_ -> JsonNumber.valueOf(Double.NaN))),
ACOS(0, Implementation.map$V(JsonMath::acos)),
ACOSH(0, Implementation.map$V(JsonMath::acosh)),
ASIN(0, Implementation.map$V(JsonMath::asin)),
@@ -366,6 +382,14 @@ public enum JQBuiltIn implements JQInvocable {
};
}
+ static @NotNull Implementation mapFF$F(@NotNull TriFunction super @Nullable JsonValue, ? super @NotNull JQFilter, ? super @NotNull JQFilter, ? extends @NotNull Generator extends @Nullable JsonValue>> function) {
+ return (context, args) -> {
+ var arg0 = args.get(0).bind(context);
+ var arg1 = args.get(1).bind(context);
+ return context.stream().flatMap(root -> function.apply(root, arg0, arg1));
+ };
+ }
+
static @NotNull Implementation filter(@NotNull Predicate<@Nullable JsonValue> predicate) {
return (context, _) -> context.stream().filter(predicate);
}
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
index de5e553..0b0637e 100644
--- 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
@@ -1,13 +1,13 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
+import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
-import java.util.stream.Collectors;
public interface JQExpression {
@@ -28,18 +28,17 @@ public interface JQExpression {
) {
public Context(@Nullable JsonValue root) {
- this(root, Map.of(), JQBuiltIn.ALL_BUILTINS);
+ this(root, Map.of("$ENV", JsonMath.env()), 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
- ));
+ var map = new LinkedHashMap();
+ variables.forEach((key, value) -> {
+ Objects.requireNonNull(key);
+ if (!key.startsWith("$")) throw new IllegalArgumentException();
+ map.put(key, value);
+ });
+ variables = Collections.unmodifiableMap(map);
functions = Map.copyOf(functions);
}
diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQForEachExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQForEachExpression.java
index 2303f0d..135ee15 100644
--- a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQForEachExpression.java
+++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQForEachExpression.java
@@ -17,36 +17,17 @@ public record JQForEachExpression(
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
- return init.evaluate(context)
- .flatMap(s -> new Generator<>() {
- private final Generator source = expression.evaluate(context);
- private @NotNull Generator current = Generator.empty();
- private @Nullable JsonValue state = s;
-
- private @NotNull Generator advance() {
- var item = source.next();
-
- return patterns.bind(context, item)
- .map(context::withVariables)
- .map(ctx -> ctx.withRoot(state))
- .flatMap(ctx -> update.evaluate(ctx).map(s -> {
- state = s;
- return ctx.withRoot(s);
- }).flatMap(extract::evaluate));
- }
-
- @Override
- public boolean hasNext() {
- while (!current.hasNext() && source.hasNext()) current = advance();
- return current.hasNext();
- }
-
- @Override
- public @Nullable JsonValue next() throws NoSuchElementException {
- while (!current.hasNext() && source.hasNext()) current = advance();
- return current.next();
- }
- });
+ return init.evaluate(context).flatMap(initial -> {
+ var state = new Object() {
+ private JsonValue state = initial;
+ };
+ return expression.evaluate(context).flatMap(value -> patterns.bind(context, value, ctx -> {
+ return update.evaluate(ctx.withRoot(state.state)).map(v -> {
+ state.state = v;
+ return ctx.withRoot(v);
+ }).flatMap(extract::evaluate);
+ }));
+ });
}
@Override
diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQObjectConstructionExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQObjectConstructionExpression.java
new file mode 100644
index 0000000..d6b20cf
--- /dev/null
+++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQObjectConstructionExpression.java
@@ -0,0 +1,52 @@
+package eu.jonahbauer.json.query.parser.ast;
+
+import eu.jonahbauer.json.JsonObject;
+import eu.jonahbauer.json.JsonString;
+import eu.jonahbauer.json.JsonValue;
+import eu.jonahbauer.json.query.JsonQueryException;
+import eu.jonahbauer.json.query.impl.Generator;
+import eu.jonahbauer.json.query.util.Util;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.StringJoiner;
+
+public record JQObjectConstructionExpression(@NotNull List<@NotNull Entry> entries) implements JQExpression {
+
+ @Override
+ public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
+ var generator = Generator.of(new LinkedHashMap<@NotNull String, @Nullable JsonValue>());
+ for (var entry : entries) {
+ generator = generator.flatMap(map -> entry.key().evaluate(context)
+ .flatMap(key -> {
+ if (!(key instanceof JsonString(var string))) {
+ throw new JsonQueryException("Cannot use " + Util.type(key) + "(" + Util.value(key) + ") as object key.");
+ }
+ return entry.value().evaluate(context)
+ .map(value -> {
+ var out = new LinkedHashMap<>(map);
+ out.put(string, value);
+ return out;
+ });
+ })
+ );
+ }
+ return generator.map(JsonObject::new);
+ }
+
+ @Override
+ public boolean isConstant() {
+ return false;
+ }
+
+ @Override
+ public @NotNull String toString() {
+ var out = new StringJoiner(", ", "{", "}");
+ entries.forEach(entry -> out.add(entry.key() + ": " + entry.value()));
+ return out.toString();
+ }
+
+ public record Entry(@NotNull JQExpression key, @NotNull JQExpression value) { }
+}
diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQPatterns.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQPatterns.java
index 962be84..a42ad25 100644
--- a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQPatterns.java
+++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQPatterns.java
@@ -5,11 +5,13 @@ import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.impl.Generator;
+import eu.jonahbauer.json.query.parser.ast.JQExpression.Context;
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.function.Supplier;
import java.util.stream.Collectors;
@@ -20,60 +22,74 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
if (patterns.isEmpty()) throw new IllegalArgumentException();
}
- public @NotNull Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.Context context, @Nullable JsonValue v) {
- var variables = new HashMap();
- Generator.from(patterns).map(Pattern::variables).flatMap(Generator::from).forEach(key -> variables.put(key, null));
+ public @NotNull Generator<@Nullable JsonValue> bind(@NotNull Context context, @Nullable JsonValue value, Function<@NotNull Context, @NotNull Generator<@Nullable JsonValue>> downstream) {
+ return new PatternGeneratorImpl(patterns, context, value, downstream);
+ }
- var result = new Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>>() {
- private final Generator<@NotNull Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>>> source
- = Generator.from(patterns).map(pattern -> pattern.bind(context, v));
- private @NotNull Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>> current = Generator.of(() -> { throw new JsonQueryException("empty"); });
+ private static final class PatternGeneratorImpl implements Generator<@Nullable JsonValue> {
+ // one generator per pattern
+ private final @NotNull Generator<@NotNull Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>>> patterns;
+ private final @NotNull Context context;
+ private final @NotNull Function<@NotNull Context, @NotNull Generator<@Nullable JsonValue>> downstream;
- private boolean hasValue;
- private @Nullable Map<@NotNull String, @Nullable JsonValue> value;
+ // the generator for the current pattern
+ private @Nullable Generator<@Nullable JsonValue> current;
- private boolean advance() {
- if (hasValue) return true;
- while (true) {
- try {
- hasValue = current.hasNext();
- value = hasValue ? current.next() : null;
- return hasValue;
- } catch (JsonQueryException _) {
- // switch to next pattern on error
- if (source.hasNext()) {
- current = source.next();
- } else {
- value = null;
- hasValue = false;
- return false;
+ public PatternGeneratorImpl(
+ @NotNull List<@NotNull Pattern> patterns, @NotNull Context context, @Nullable JsonValue value,
+ @NotNull Function<@NotNull Context, @NotNull Generator<@Nullable JsonValue>> downstream
+ ) {
+ this.context = context;
+ this.downstream = downstream;
+ var variables = new HashMap();
+ patterns.stream().map(Pattern::variables).flatMap(Collection::stream).forEach(key -> variables.put(key, null));
+ this.patterns = Generator.from(patterns).map(pattern -> pattern.bind(context, value).map(vars -> {
+ var out = new HashMap<>(variables);
+ out.putAll(vars);
+ return out;
+ }));
+ }
+
+ private T advance(@NotNull Function, T> function) {
+ var ex = new JsonQueryException("no match");
+ while (true) {
+ if (current == null) {
+ // find the next matching pattern
+ while (patterns.hasNext()) {
+ try {
+ var match = patterns.next();
+ current = match.map(context::withVariables).flatMap(downstream);
+ break;
+ } catch (JsonQueryException e) {
+ ex = e;
}
}
+
+ // no matching pattern; propagate exception from last match attempt
+ if (current == null) {
+ throw ex;
+ }
+ }
+
+ try {
+ return function.apply(current);
+ } catch (JsonQueryException e) {
+ // exception during execution; try the next pattern
+ current = null;
+ ex = e;
}
}
+ }
- @Override
- public boolean hasNext() {
- return advance();
- }
+ @Override
+ public boolean hasNext() {
+ return advance(Generator::hasNext);
+ }
- @Override
- public @NotNull Map<@NotNull String, @Nullable JsonValue> next() {
- if (!advance()) throw new NoSuchElementException();
- assert value != null;
-
- var out = value;
- value = null;
- hasValue = false;
- return out;
- }
- };
-
- return result.map(vars -> {
- var out = new HashMap<>(variables);
- out.putAll(vars);
- return out;
- });
+ @Override
+ public @Nullable JsonValue next() throws NoSuchElementException {
+ return advance(Generator::next);
+ }
}
@Override
@@ -85,7 +101,7 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
@NotNull
Set<@NotNull String> variables();
@NotNull
- Generator