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> 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> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value); + Generator> bind(@NotNull Context context, @Nullable JsonValue value); record ValuePattern(@NotNull String name) implements Pattern { public ValuePattern { @@ -99,7 +115,7 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) { } @Override - public @NotNull Generator> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) { + public @NotNull Generator> bind(@NotNull Context context, @Nullable JsonValue value) { var map = new HashMap(); map.put(name, value); return Generator.of(Collections.unmodifiableMap(map)); @@ -125,7 +141,7 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) { } @Override - public @NotNull Generator> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) { + public @NotNull Generator> bind(@NotNull Context context, @Nullable JsonValue value) { var streams = new ArrayList>>>(); for (int i = 0; i < patterns.size(); i++) { @@ -164,7 +180,7 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) { } @Override - public @NotNull Generator> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) { + public @NotNull Generator> bind(@NotNull Context context, @Nullable JsonValue value) { var streams = new ArrayList>>>(); this.patterns.reversed().forEach((keyExpression, pattern) -> streams.add(() -> { diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQReduceExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQReduceExpression.java index 5d29b82..cfc196a 100644 --- a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQReduceExpression.java +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQReduceExpression.java @@ -14,27 +14,21 @@ public record JQReduceExpression( @Override public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) { - return init.evaluate(context) - .map(state -> { - var it = expression.evaluate(context); - while (it.hasNext()) { - var item = it.next(); - - var bindings = patterns.bind(context, item); - var binding = bindings.next(); - while (bindings.hasNext()) { - binding = bindings.next(); + return init.evaluate(context).flatMap(initial -> { + var state = new Object() { + private JsonValue state = initial; + }; + return Generator.concat( + expression.evaluate(context).flatMap(value -> patterns.bind(context, value, ctx -> { + var gen = update.evaluate(ctx.withRoot(state.state)); + while (gen.hasNext()) { + state.state = gen.next(); } - - JsonValue nextState = null; - var up = update.evaluate(context.withVariables(binding).withRoot(state)); - while (up.hasNext()) { - nextState = up.next(); - } - state = nextState; - } - return state; - }); + return Generator.empty(); + })), + Generator.of(() -> state.state) + ); + }); } @Override 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 index f968f44..a2a5f6f 100644 --- 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 @@ -1,20 +1,12 @@ 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.JsonMath; 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.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 { @@ -24,36 +16,12 @@ public record JQSliceExpression(@NotNull JQExpression expression, @Nullable JQEx @Override public @NotNull Generator<@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 -> Generator.of((JsonValue) null); - default -> { - if (optional) yield Generator.empty(); - throw new JsonQueryException(STR."Cannot index \{Util.type(value)} with object."); - } - }); - } - - private @NotNull Generator<@Nullable JsonValue> slice(@NotNull Context context, @NotNull String type, int length, @NotNull BiFunction slice) { - return getIndices(start, context, type, length, 0) - .map(start -> getIndices(end, context, type, length, length) - .map(end -> start > end ? slice.apply(0, 0) : slice.apply(start, end)) - ) - .flatMap(Function.identity()); - } - - private @NotNull Generator getIndices(@Nullable JQExpression expression, @NotNull Context context, @NotNull String type, int length, int fallback) { - if (expression == null) return Generator.of(fallback); - return expression.evaluate(context).map(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); + return expression.evaluate(context) + .flatMap(value -> (start == null ? Generator.of((JsonValue) null) : start.evaluate(context)) + .flatMap(start -> (end == null ? Generator.of((JsonValue) null) : end.evaluate(context)) + .map(end -> JsonMath.slice(value, start, end)) + ) + ); } @Override diff --git a/query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java b/query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java index 7c65314..661201b 100644 --- a/query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java +++ b/query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java @@ -21,14 +21,16 @@ class JsonQueryTest { @MethodSource("arguments") void examples(@NotNull String jq, @Nullable JsonValue input, @NotNull List<@Nullable JsonValue> output) { JsonQuery query; + List actual; try { query = JsonQuery.parse(jq); - } catch (UnsupportedOperationException _) { - throw Assumptions.abort("not yet implemented"); + actual = query.run(input).toList(); + } catch (UnsupportedOperationException ex) { + var trace = ex.getStackTrace()[0]; + throw Assumptions.abort("not yet implemented: " + trace.getClassName() + "." + trace.getMethodName()); } catch (Throwable t) { throw new AssertionFailedError("Unexpected exception thrown: " + t.getMessage(), t); } - var actual = query.run(input).toList(); assertEquals(output, actual); }