fixup query

This commit is contained in:
jbb01 2025-04-07 20:18:22 +02:00
parent bdb505fce7
commit ef88d89ce1
No known key found for this signature in database
GPG Key ID: 83C72CB6D5442CF1
10 changed files with 527 additions and 0 deletions

View File

@ -0,0 +1,49 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.NoSuchElementException;
import java.util.function.Predicate;
final class FilterGenerator<T> implements Generator<T> {
private final @NotNull Generator<T> generator;
private final @NotNull Predicate<? super T> filter;
private @Nullable T value;
private boolean hasValue;
public FilterGenerator(@NotNull Generator<T> generator, @NotNull Predicate<? super T> filter) {
this.generator = generator;
this.filter = filter;
}
public boolean advance() {
if (hasValue) return true;
while (generator.hasNext()) {
var value = generator.next();
if (filter.test(value)) {
this.value = value;
this.hasValue = true;
return true;
}
}
this.value = null;
this.hasValue = false;
return false;
}
@Override
public boolean hasNext() {
return advance();
}
@Override
public T next() {
if (!advance()) throw new NoSuchElementException();
var out = value;
hasValue = false;
value = null;
return out;
}
}

View File

@ -0,0 +1,17 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
public record IteratorGenerator<T>(@NotNull Iterator<T> iterator) implements Generator<T> {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
return iterator.next();
}
}

View File

@ -0,0 +1,29 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.NoSuchElementException;
import java.util.Objects;
final class LimitGenerator<T> implements Generator<T> {
private final @NotNull Generator<T> delegate;
private int count;
public LimitGenerator(@NotNull Generator<T> delegate, int count) {
if (count < 0) throw new IllegalArgumentException();
this.delegate = Objects.requireNonNull(delegate);
this.count = count;
}
@Override
public boolean hasNext() {
return count != 0 && delegate.hasNext();
}
@Override
public T next() {
if (count == 0 || !delegate.hasNext()) throw new NoSuchElementException();
count--;
return delegate.next();
}
}

View File

@ -0,0 +1,39 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Queue;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
final class MapMultiGenerator<T, S> implements Generator<S> {
private final @NotNull Generator<T> generator;
private final @NotNull BiConsumer<? super T, ? super Consumer<S>> mapping;
private final @NotNull Queue<S> queue = new LinkedList<>();
public MapMultiGenerator(@NotNull Generator<T> generator, @NotNull BiConsumer<? super T, ? super Consumer<S>> mapping) {
this.generator = Objects.requireNonNull(generator);
this.mapping = Objects.requireNonNull(mapping);
}
private boolean advance() {
while (queue.isEmpty() && generator.hasNext()) {
mapping.accept(generator.next(), (Consumer<S>) queue::add);
}
return !queue.isEmpty();
}
@Override
public boolean hasNext() {
return advance();
}
@Override
public S next() {
if (!advance()) throw new NoSuchElementException();
return queue.remove();
}
}

View File

@ -0,0 +1,31 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Supplier;
final class SupplierGenerator<T> implements Generator<T> {
private @Nullable Supplier<? extends T> value;
public SupplierGenerator(@NotNull Supplier<? extends T> supplier) {
this.value = Objects.requireNonNull(supplier);
}
@Override
public boolean hasNext() {
return value != null;
}
@Override
public T next() {
if (value != null) {
var out = value.get();
value = null;
return out;
}
throw new NoSuchElementException();
}
}

View File

@ -0,0 +1,61 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.NoSuchElementException;
public record JQForEachExpression(
@NotNull JQExpression expression,
@NotNull JQPatterns patterns,
@NotNull JQExpression init,
@NotNull JQExpression update,
@NotNull JQExpression extract
) implements JQExpression {
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return init.evaluate(context)
.flatMap(s -> new Generator<>() {
private final Generator<JsonValue> source = expression.evaluate(context);
private @NotNull Generator<JsonValue> current = Generator.empty();
private @Nullable JsonValue state = s;
private @NotNull Generator<JsonValue> 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();
}
});
}
@Override
public boolean isConstant() {
return expression.isConstant() && init.isConstant() && update.isConstant();
}
@Override
public @NotNull String toString() {
return "foreach " + expression + " as " + patterns + " (" + init + "; " + update + "; " + extract + ")";
}
}

View File

@ -0,0 +1,18 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public record JQFunctionDefinition(@NotNull JQFunction function, @NotNull JQExpression expression) implements JQExpression {
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return expression.evaluate(context.withFunction(function));
}
@Override
public boolean isConstant() {
return expression.isConstant();
}
}

View File

@ -0,0 +1,44 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public record JQIfExpression(@NotNull JQExpression condition, @NotNull JQExpression then, @Nullable JQExpression otherwise) implements JQExpression {
public JQIfExpression {
Objects.requireNonNull(condition);
Objects.requireNonNull(then);
}
public JQIfExpression(@NotNull JQExpression condition, @NotNull JQExpression then) {
this(condition, then, null);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return condition.evaluate(context).flatMap(result -> JsonMath.isTruthy(result)
? then.evaluate(context)
: (otherwise != null ? otherwise.evaluate(context) : Generator.empty())
);
}
@Override
public boolean isConstant() {
return condition.isConstant() && then.isConstant() && (otherwise == null || otherwise.isConstant());
}
@Override
public @NotNull String toString() {
if (otherwise instanceof JQIfExpression) {
return "if " + condition + " then " + then + " el" + otherwise;
} else if (otherwise != null) {
return "if " + condition + " then " + then + " else " + otherwise + " end";
} else {
return "if " + condition + " then " + then + " end";
}
}
}

View File

@ -0,0 +1,190 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonNumber;
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.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;
public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
public JQPatterns {
patterns = List.copyOf(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<String, JsonValue>();
Generator.from(patterns).map(Pattern::variables).flatMap(Generator::from).forEach(key -> variables.put(key, null));
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 boolean hasValue;
private @Nullable Map<@NotNull String, @Nullable JsonValue> value;
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;
}
}
}
}
@Override
public boolean hasNext() {
return advance();
}
@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 @NotNull String toString() {
return patterns.stream().map(Objects::toString).collect(Collectors.joining(" ?// "));
}
public sealed interface Pattern {
@NotNull
Set<@NotNull String> variables();
@NotNull
Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.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 Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) {
var map = new HashMap<String, JsonValue>();
map.put(name, value);
return Generator.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<String>();
patterns.forEach(p -> out.addAll(p.variables()));
return Collections.unmodifiableSet(out);
}
@Override
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) {
var streams = new ArrayList<Supplier<Generator<Map<String, JsonValue>>>>();
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<String, JsonValue>();
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<String>();
patterns.values().forEach(p -> out.addAll(p.variables()));
return Collections.unmodifiableSet(out);
}
@Override
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) {
var streams = new ArrayList<Supplier<Generator<Map<String, JsonValue>>>>();
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<String, JsonValue>();
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();
}
}
}
}

View File

@ -0,0 +1,49 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public record JQReduceExpression(
@NotNull JQExpression expression,
@NotNull JQPatterns patterns,
@NotNull JQExpression init,
@NotNull JQExpression update
) implements JQExpression {
@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();
}
JsonValue nextState = null;
var up = update.evaluate(context.withVariables(binding).withRoot(state));
while (up.hasNext()) {
nextState = up.next();
}
state = nextState;
}
return state;
});
}
@Override
public boolean isConstant() {
return expression.isConstant() && init.isConstant() && update.isConstant();
}
@Override
public @NotNull String toString() {
return "reduce " + expression + " as " + patterns + " (" + init + "; " + update + ")";
}
}