diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/FilterGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/FilterGenerator.java new file mode 100644 index 0000000..0936c20 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/FilterGenerator.java @@ -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 implements Generator { + private final @NotNull Generator generator; + private final @NotNull Predicate filter; + + private @Nullable T value; + private boolean hasValue; + + public FilterGenerator(@NotNull Generator generator, @NotNull Predicate 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; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/IteratorGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/IteratorGenerator.java new file mode 100644 index 0000000..5d57248 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/IteratorGenerator.java @@ -0,0 +1,17 @@ +package eu.jonahbauer.json.query.impl; + +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; + +public record IteratorGenerator(@NotNull Iterator iterator) implements Generator { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public T next() { + return iterator.next(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/LimitGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/LimitGenerator.java new file mode 100644 index 0000000..c868f73 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/LimitGenerator.java @@ -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 implements Generator { + private final @NotNull Generator delegate; + private int count; + + public LimitGenerator(@NotNull Generator 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(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/MapMultiGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/MapMultiGenerator.java new file mode 100644 index 0000000..c43b329 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/MapMultiGenerator.java @@ -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 implements Generator { + private final @NotNull Generator generator; + private final @NotNull BiConsumer> mapping; + private final @NotNull Queue queue = new LinkedList<>(); + + public MapMultiGenerator(@NotNull Generator generator, @NotNull BiConsumer> mapping) { + this.generator = Objects.requireNonNull(generator); + this.mapping = Objects.requireNonNull(mapping); + } + + private boolean advance() { + while (queue.isEmpty() && generator.hasNext()) { + mapping.accept(generator.next(), (Consumer) queue::add); + } + return !queue.isEmpty(); + } + + @Override + public boolean hasNext() { + return advance(); + } + + @Override + public S next() { + if (!advance()) throw new NoSuchElementException(); + return queue.remove(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/impl/SupplierGenerator.java b/query/src/main/java/eu/jonahbauer/json/query/impl/SupplierGenerator.java new file mode 100644 index 0000000..137098c --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/impl/SupplierGenerator.java @@ -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 implements Generator { + private @Nullable Supplier value; + + public SupplierGenerator(@NotNull Supplier 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(); + } +} 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 new file mode 100644 index 0000000..2303f0d --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQForEachExpression.java @@ -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 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(); + } + }); + } + + @Override + public boolean isConstant() { + return expression.isConstant() && init.isConstant() && update.isConstant(); + } + + @Override + public @NotNull String toString() { + return "foreach " + expression + " as " + patterns + " (" + init + "; " + update + "; " + extract + ")"; + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFunctionDefinition.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFunctionDefinition.java new file mode 100644 index 0000000..6f027bf --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQFunctionDefinition.java @@ -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(); + } +} diff --git a/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQIfExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQIfExpression.java new file mode 100644 index 0000000..f5060e6 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQIfExpression.java @@ -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"; + } + } +} 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 new file mode 100644 index 0000000..962be84 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQPatterns.java @@ -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(); + 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> 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> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) { + var map = new HashMap(); + 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(); + patterns.forEach(p -> out.addAll(p.variables())); + return Collections.unmodifiableSet(out); + } + + @Override + public @NotNull Generator> bind(@NotNull JQExpression.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 Generator> bind(@NotNull JQExpression.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/JQReduceExpression.java b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQReduceExpression.java new file mode 100644 index 0000000..5d29b82 --- /dev/null +++ b/query/src/main/java/eu/jonahbauer/json/query/parser/ast/JQReduceExpression.java @@ -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 + ")"; + } +}