Compare commits
4 Commits
main
...
feature/jq
Author | SHA1 | Date | |
---|---|---|---|
87ae709f6d | |||
ef88d89ce1 | |||
bdb505fce7 | |||
dc5791815e |
@ -10,9 +10,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
* numbers and therefore all numbers are stored as {@code double}.
|
||||
*/
|
||||
public record JsonNumber(double value) implements JsonValue, JsonToken {
|
||||
public JsonNumber {
|
||||
if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite");
|
||||
}
|
||||
public static final @NotNull JsonNumber ZERO = new JsonNumber(0);
|
||||
public static final @NotNull JsonNumber ONE = new JsonNumber(1);
|
||||
|
||||
/**
|
||||
* Converts the given int to a JSON number.
|
||||
|
35
query/build.gradle.kts
Normal file
35
query/build.gradle.kts
Normal file
@ -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<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
options.compilerArgs.add("--enable-preview")
|
||||
}
|
||||
|
||||
withType<Test> {
|
||||
useJUnitPlatform()
|
||||
jvmArgs("--enable-preview")
|
||||
}
|
||||
}
|
1824
query/src/main/java/eu/jonahbauer/json/query/JsonMath.java
Normal file
1824
query/src/main/java/eu/jonahbauer/json/query/JsonMath.java
Normal file
File diff suppressed because it is too large
Load Diff
33
query/src/main/java/eu/jonahbauer/json/query/JsonQuery.java
Normal file
33
query/src/main/java/eu/jonahbauer/json/query/JsonQuery.java
Normal file
@ -0,0 +1,33 @@
|
||||
package eu.jonahbauer.json.query;
|
||||
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
import eu.jonahbauer.json.query.impl.Generator;
|
||||
import eu.jonahbauer.json.query.parser.JQParser;
|
||||
import eu.jonahbauer.json.query.parser.ast.JQProgram;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class JsonQuery {
|
||||
private final @NotNull JQProgram programm;
|
||||
|
||||
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);
|
||||
} catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public @NotNull Generator<@NotNull JsonValue> run(@Nullable JsonValue value) {
|
||||
return programm.run(value);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
@ -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}";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
};
|
||||
}
|
||||
}
|
155
query/src/main/java/eu/jonahbauer/json/query/Main.java
Normal file
155
query/src/main/java/eu/jonahbauer/json/query/Main.java
Normal file
@ -0,0 +1,155 @@
|
||||
package eu.jonahbauer.json.query;
|
||||
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
|
||||
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\": 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);
|
||||
|
||||
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: .[<number>]");
|
||||
|
||||
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: .[<number>:<number>]");
|
||||
|
||||
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 <K, V> SequencedMap<K, V> of(K key1, V value1) {
|
||||
var map = new LinkedHashMap<K, V>();
|
||||
map.put(key1, value1);
|
||||
return map;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package eu.jonahbauer.json.query.impl;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Queue;
|
||||
|
||||
final class ConcatGenerator<T> implements Generator<T> {
|
||||
private final @NotNull Queue<@NotNull Generator<? extends T>> generators;
|
||||
|
||||
public ConcatGenerator(@NotNull List<@NotNull Generator<? extends T>> generators) {
|
||||
this.generators = new LinkedList<>(generators);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
while (!generators.isEmpty()) {
|
||||
var current = generators.peek();
|
||||
if (!current.hasNext()) {
|
||||
generators.remove();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
while (!generators.isEmpty()) {
|
||||
var current = generators.peek();
|
||||
if (!current.hasNext()) {
|
||||
generators.remove();
|
||||
} else {
|
||||
return current.next();
|
||||
}
|
||||
}
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package eu.jonahbauer.json.query.impl;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
enum EmptyGenerator implements Generator<Object> {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object next() {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package eu.jonahbauer.json.query.impl;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
final class FlatMappingGenerator<T, S> implements Generator<S> {
|
||||
private final @NotNull Generator<T> source;
|
||||
private final @NotNull Function<? super T, ? extends Generator<? extends S>> function;
|
||||
private @NotNull Generator<? extends S> current = Generator.empty();
|
||||
|
||||
public FlatMappingGenerator(@NotNull Generator<T> source, @NotNull Function<? super T, ? extends Generator<? extends S>> function) {
|
||||
this.source = source;
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
while (!current.hasNext() && source.hasNext()) {
|
||||
current = function.apply(source.next());
|
||||
}
|
||||
return current.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public S next() {
|
||||
while (!current.hasNext() && source.hasNext()) {
|
||||
current = function.apply(source.next());
|
||||
}
|
||||
return current.next();
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package eu.jonahbauer.json.query.impl;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public interface Generator<T> {
|
||||
boolean hasNext();
|
||||
T next() throws NoSuchElementException;
|
||||
|
||||
default void forEach(@NotNull Consumer<? super T> action) {
|
||||
while (hasNext()) {
|
||||
action.accept(next());
|
||||
}
|
||||
}
|
||||
|
||||
default T reduce(T seed, @NotNull BinaryOperator<T> operator) {
|
||||
while (hasNext()) {
|
||||
seed = operator.apply(seed, next());
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
|
||||
default boolean anyMatch(@NotNull Predicate<? super T> predicate) {
|
||||
while (hasNext()) {
|
||||
if (predicate.test(next())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean allMatch(@NotNull Predicate<? super T> predicate) {
|
||||
while (hasNext()) {
|
||||
if (!predicate.test(next())) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
default @NotNull List<T> toList() {
|
||||
var out = new ArrayList<T>();
|
||||
while (hasNext()) {
|
||||
out.add(next());
|
||||
}
|
||||
return Collections.unmodifiableList(out);
|
||||
}
|
||||
|
||||
default @NotNull Generator<T> filter(@NotNull Predicate<? super T> predicate) {
|
||||
return new FilterGenerator<>(this, predicate);
|
||||
}
|
||||
|
||||
default @NotNull Generator<T> limit(int count) {
|
||||
return new LimitGenerator<>(this, count);
|
||||
}
|
||||
|
||||
default <S> @NotNull Generator<S> map(@NotNull Function<? super T, ? extends S> function) {
|
||||
return new MappingGenerator<>(this, function);
|
||||
}
|
||||
|
||||
default <S> @NotNull Generator<S> flatMap(@NotNull Function<? super T, ? extends Generator<? extends S>> function) {
|
||||
return new FlatMappingGenerator<>(this, function);
|
||||
}
|
||||
|
||||
default <S> @NotNull Generator<S> mapMulti(@NotNull BiConsumer<? super T, ? super Consumer<S>> function) {
|
||||
return new MapMultiGenerator<>(this, function);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> @NotNull Generator<T> empty() {
|
||||
return (Generator<T>) EmptyGenerator.INSTANCE;
|
||||
}
|
||||
|
||||
static <T> @NotNull Generator<T> concat(@NotNull Generator<? extends T> first, @NotNull Generator<? extends T> second) {
|
||||
return new ConcatGenerator<>(List.of(first, second));
|
||||
}
|
||||
|
||||
static <T> @NotNull Generator<T> of(T value) {
|
||||
return new SingletonGenerator<>(value);
|
||||
}
|
||||
|
||||
static <T> @NotNull Generator<T> of(@NotNull Supplier<T> value) {
|
||||
return new SupplierGenerator<>(value);
|
||||
}
|
||||
|
||||
static <T> @NotNull Generator<T> from(@NotNull Iterable<T> value) {
|
||||
return new IteratorGenerator<>(value.iterator());
|
||||
}
|
||||
|
||||
static <T> @NotNull Generator<T> from(@NotNull Iterator<T> value) {
|
||||
return new IteratorGenerator<>(value);
|
||||
}
|
||||
|
||||
static <T> @NotNull Generator<T> from(@NotNull Stream<T> value) {
|
||||
return new IteratorGenerator<>(value.iterator());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package eu.jonahbauer.json.query.impl;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
final class MappingGenerator<T, S> implements Generator<S> {
|
||||
private final @NotNull Generator<T> delegate;
|
||||
private final @NotNull Function<? super T, ? extends S> function;
|
||||
|
||||
public MappingGenerator(@NotNull Generator<T> delegate, @NotNull Function<? super T, ? extends S> function) {
|
||||
this.delegate = Objects.requireNonNull(delegate);
|
||||
this.function = Objects.requireNonNull(function);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return delegate.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public S next() {
|
||||
return function.apply(delegate.next());
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.jonahbauer.json.query.impl;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
final class SingletonGenerator<T> implements Generator<T> {
|
||||
private boolean done;
|
||||
private T value;
|
||||
|
||||
public SingletonGenerator(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return !done;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
if (done) throw new NoSuchElementException();
|
||||
|
||||
var out = value;
|
||||
done = true;
|
||||
value = null;
|
||||
return out;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,668 @@
|
||||
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<JQToken> 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<JQImport>();
|
||||
|
||||
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<JQFunction>();
|
||||
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<String>();
|
||||
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);
|
||||
}
|
||||
|
||||
public @NotNull JQExpression parseExpression() throws IOException {
|
||||
if (peek(JQTokenKind.DEF)) {
|
||||
var function = parseFuncDef();
|
||||
var expr = parsePipeExpression();
|
||||
return new JQFunctionDefinition(function, expr);
|
||||
} 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 = parseControlFlow();
|
||||
if (tryConsume(JQTokenKind.QUESTION_MARK)) {
|
||||
return new JQTryExpression(expression);
|
||||
} else {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull JQExpression parseControlFlow() throws IOException {
|
||||
if (tryConsume(JQTokenKind.REDUCE)) {
|
||||
var expr = parseTerm();
|
||||
consume(JQTokenKind.AS);
|
||||
var patterns = parsePatterns();
|
||||
consume(JQTokenKind.LPAREN);
|
||||
var init = parseExpression();
|
||||
consume(JQTokenKind.SEMICOLON);
|
||||
var update = parseExpression();
|
||||
consume(JQTokenKind.RPAREN);
|
||||
return new JQReduceExpression(expr, patterns, init, update);
|
||||
} else if (tryConsume(JQTokenKind.FOREACH)) {
|
||||
var expr = parseTerm();
|
||||
consume(JQTokenKind.AS);
|
||||
var patterns = parsePatterns();
|
||||
consume(JQTokenKind.LPAREN);
|
||||
var init = parseExpression();
|
||||
consume(JQTokenKind.SEMICOLON);
|
||||
var update = parseExpression();
|
||||
var extract = tryConsume(JQTokenKind.SEMICOLON) ? parseExpression() : new JQRootExpression();
|
||||
consume(JQTokenKind.RPAREN);
|
||||
return new JQForEachExpression(expr, patterns, init, update, extract);
|
||||
} else if (tryConsume(JQTokenKind.IF)) {;
|
||||
var conds = new ArrayList<JQExpression>();
|
||||
var thens = new ArrayList<JQExpression>();
|
||||
|
||||
do {
|
||||
conds.add(parseExpression());
|
||||
consume(JQTokenKind.THEN);
|
||||
thens.add(parseExpression());
|
||||
} while (tryConsume(JQTokenKind.ELSE_IF));
|
||||
|
||||
var otherwise = tryConsume(JQTokenKind.ELSE) ? parseExpression() : null;
|
||||
consume(JQTokenKind.END);
|
||||
|
||||
var out = new JQIfExpression(conds.removeLast(), thens.removeLast(), otherwise);
|
||||
while (!conds.isEmpty()) {
|
||||
out = new JQIfExpression(conds.removeLast(), thens.removeLast(), out);
|
||||
}
|
||||
|
||||
return out;
|
||||
} else {
|
||||
var term = parseTerm();
|
||||
if (tryConsume(JQTokenKind.AS)) {
|
||||
var patterns = parsePatterns();
|
||||
consume(JQTokenKind.PIPE);
|
||||
var expr = parseExpression();
|
||||
return new JQAsExpression(term, patterns, expr);
|
||||
}
|
||||
return term;
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull JQPatterns parsePatterns() throws IOException {
|
||||
var patterns = new ArrayList<JQPatterns.Pattern>();
|
||||
patterns.add(parsePattern());
|
||||
while (tryConsume(JQTokenKind.ALTERNATION)) {
|
||||
patterns.add(parsePattern());
|
||||
}
|
||||
return new JQPatterns(patterns);
|
||||
}
|
||||
|
||||
private @NotNull JQPatterns.Pattern parsePattern() throws IOException {
|
||||
if (tryConsume(JQTokenKind.LBRACKET)) {
|
||||
var patterns = new ArrayList<JQPatterns.Pattern>();
|
||||
do {
|
||||
patterns.add(parsePattern());
|
||||
} while (tryConsume(JQTokenKind.COMMA));
|
||||
consume(JQTokenKind.RBRACKET);
|
||||
return new JQPatterns.Pattern.ArrayPattern(patterns);
|
||||
} else if (tryConsume(JQTokenKind.LBRACE)) {
|
||||
var patterns = new LinkedHashMap<JQExpression, JQPatterns.Pattern>();
|
||||
do {
|
||||
if (tryConsume(JQTokenKind.DOLLAR)) {
|
||||
var ident = consume(JQTokenKind.IDENT).text();
|
||||
var pattern = new JQPatterns.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 JQPatterns.Pattern.ObjectPattern(patterns);
|
||||
} else {
|
||||
consume(JQTokenKind.DOLLAR);
|
||||
var ident = consume(JQTokenKind.IDENT).text();
|
||||
return new JQPatterns.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("<top-level>", 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<JQExpression>();
|
||||
if (tryConsume(JQTokenKind.LPAREN)) {
|
||||
do {
|
||||
args.add(parseExpression());
|
||||
} while (tryConsume(JQTokenKind.SEMICOLON));
|
||||
consume(JQTokenKind.RPAREN);
|
||||
}
|
||||
return new JQFunctionInvocation(name, args);
|
||||
}
|
||||
|
||||
private @NotNull JQVariableExpression parseVariableExpression() throws IOException {
|
||||
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);
|
||||
}
|
||||
|
||||
var entries = new ArrayList<JQObjectConstructionExpression.Entry>();
|
||||
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("<top-level>", 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 {
|
||||
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<String>();
|
||||
var values = new ArrayList<JQExpression>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
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.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("preview")
|
||||
public record JQAlternativeExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
|
||||
public JQAlternativeExpression {
|
||||
Objects.requireNonNull(first);
|
||||
Objects.requireNonNull(second);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
var first = this.first.evaluate(context);
|
||||
var second = this.second.evaluate(context);
|
||||
return new Generator<>() {
|
||||
private boolean empty = true;
|
||||
|
||||
private JsonValue value;
|
||||
private boolean hasValue;
|
||||
|
||||
private boolean advance() {
|
||||
if (hasValue) return true;
|
||||
while (first.hasNext()) {
|
||||
var value = first.next();
|
||||
if (JsonMath.isTruthy(value)) {
|
||||
this.empty = false;
|
||||
this.value = value;
|
||||
this.hasValue = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (empty && second.hasNext()) {
|
||||
this.value = second.next();
|
||||
this.hasValue = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return advance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue next() throws NoSuchElementException {
|
||||
if (!advance()) throw new NoSuchElementException();
|
||||
var out = value;
|
||||
hasValue = false;
|
||||
value = null;
|
||||
return out;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return first.isConstant() && second.isConstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return first + " // " + second;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package eu.jonahbauer.json.query.parser.ast;
|
||||
|
||||
import eu.jonahbauer.json.JsonArray;
|
||||
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.Objects;
|
||||
|
||||
public record JQArrayConstructionExpression(@NotNull JQExpression expression) implements JQExpression {
|
||||
public JQArrayConstructionExpression {
|
||||
Objects.requireNonNull(expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return Generator.of(() -> new JsonArray(expression.evaluate(context).toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return expression.isConstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return "[" + expression + "]";
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
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.Objects;
|
||||
|
||||
public record JQAsExpression(
|
||||
@NotNull JQExpression variable, @NotNull JQPatterns patterns, @NotNull JQExpression expression
|
||||
) implements JQExpression {
|
||||
|
||||
public JQAsExpression {
|
||||
Objects.requireNonNull(variable);
|
||||
Objects.requireNonNull(expression);
|
||||
Objects.requireNonNull(patterns);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return variable.evaluate(context).flatMap(value -> patterns.bind(context, value, expression::evaluate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return variable.isConstant() && expression.isConstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return variable + " as " + patterns + " | " + expression;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
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 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 Generator<JsonValue> 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
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 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 Generator<JsonValue> 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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 java.util.Objects;
|
||||
|
||||
public record JQAssignmentPipe(@NotNull JQExpression target, @NotNull JQExpression value) implements JQExpression {
|
||||
|
||||
public JQAssignmentPipe {
|
||||
Objects.requireNonNull(target);
|
||||
Objects.requireNonNull(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<JsonValue> 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;
|
||||
}
|
||||
}
|
@ -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.impl.Generator;
|
||||
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;
|
||||
|
||||
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 Generator<@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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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 eu.jonahbauer.json.query.impl.Generator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record JQBooleanAndExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
|
||||
|
||||
public JQBooleanAndExpression {
|
||||
Objects.requireNonNull(first);
|
||||
Objects.requireNonNull(second);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<JsonValue> evaluate(@NotNull Context context) {
|
||||
return first.evaluate(context)
|
||||
.flatMap(value -> JsonMath.isFalsy(value)
|
||||
? Generator.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;
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package eu.jonahbauer.json.query.parser.ast;
|
||||
|
||||
import eu.jonahbauer.json.JsonBoolean;
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
import eu.jonahbauer.json.query.JsonMath;
|
||||
import eu.jonahbauer.json.query.impl.Generator;
|
||||
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 Generator<JsonValue> evaluate(@NotNull Context context) {
|
||||
return first.evaluate(context)
|
||||
.flatMap(value -> JsonMath.isTruthy(value)
|
||||
? Generator.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;
|
||||
}
|
||||
}
|
@ -0,0 +1,402 @@
|
||||
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.impl.Generator;
|
||||
import eu.jonahbauer.json.query.parser.JQParser;
|
||||
import eu.jonahbauer.json.query.parser.ast.JQExpression.Context;
|
||||
import eu.jonahbauer.json.query.util.Util;
|
||||
import lombok.SneakyThrows;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum JQBuiltIn implements JQInvocable {
|
||||
ABS(0, Implementation.map$V(JsonMath::abs)),
|
||||
ADD(0, (context, _) -> context.stream().map(value -> JsonMath.values(value).reduce(null, JsonMath::add))),
|
||||
NOT(0, Implementation.map$V(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, (_, _) -> Generator.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, (_, _) -> Generator.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))),
|
||||
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)),
|
||||
KEYS(0, Implementation.map$V(JsonMath::keys)),
|
||||
KEYS_UNSORTED(0, Implementation.map$V(JsonMath::keysUnsorted)),
|
||||
HAS(1, Implementation.mapV$V(JsonMath::has)),
|
||||
IN(1, Implementation.mapV$V(JsonMath::in)),
|
||||
FIRST$0(0, Implementation.map$V(JsonMath::first)),
|
||||
LAST$0(0, Implementation.map$V(JsonMath::last)),
|
||||
NTH$1(1, Implementation.mapV$V(JsonMath::index)),
|
||||
ANY$0(0, Implementation.map$V(JsonMath::any)),
|
||||
ANY$1(1, Implementation.mapF$V(JsonMath::any)),
|
||||
ANY$2(2, Implementation.mapFF$V((root, gen, filter) -> JsonMath.any(gen.apply(root), filter))),
|
||||
ALL$0(0, Implementation.map$V(JsonMath::all)),
|
||||
ALL$1(1, Implementation.mapF$V(JsonMath::all)),
|
||||
ALL$2(2, Implementation.mapFF$V((root, gen, filter) -> JsonMath.all(gen.apply(root), filter))),
|
||||
FLATTEN$0(0, Implementation.map$V(JsonMath::flatten)),
|
||||
FLATTEN$1(1, Implementation.mapV$V(JsonMath::flatten)),
|
||||
SORT(0, Implementation.map$V(JsonMath::sort)),
|
||||
SORT_BY(1, Implementation.mapF$V(JsonMath::sort)),
|
||||
MIN(0, Implementation.map$V(JsonMath::min)),
|
||||
MIN_BY(1, Implementation.mapF$V(JsonMath::min)),
|
||||
MAX(0, Implementation.map$V(JsonMath::max)),
|
||||
MAX_BY(1, Implementation.mapF$V(JsonMath::max)),
|
||||
UNIQUE(0, Implementation.map$V(JsonMath::unique)),
|
||||
UNIQUE_BY(1, Implementation.mapF$V(JsonMath::unique)),
|
||||
GROUP_BY(1, Implementation.mapF$V(JsonMath::group)),
|
||||
REVERSE(0, Implementation.map$V(JsonMath::reverse)),
|
||||
CONTAINS(1, Implementation.mapV$V(JsonMath::contains)),
|
||||
INDICES(1, Implementation.mapV$V(JsonMath::indices)),
|
||||
INDEX(1, Implementation.mapV$V(JsonMath::firstindex)),
|
||||
RINDEX(1, Implementation.mapV$V(JsonMath::lastindex)),
|
||||
INSIDE(1, Implementation.mapV$V(JsonMath::inside)),
|
||||
COMBINATIONS$0(0, Implementation.map$F(JsonMath::combinations)),
|
||||
COMBINATIONS$1(1, Implementation.mapV$F(JsonMath::combinations)),
|
||||
BSEARCH(1, Implementation.mapV$V(JsonMath::bsearch)),
|
||||
TRANSPOSE(0, Implementation.map$V(JsonMath::transpose)),
|
||||
TO_ENTRIES(0, Implementation.map$V(JsonMath::toEntries)),
|
||||
FROM_ENTRIES(0, Implementation.map$V(JsonMath::fromEntries)),
|
||||
WITH_ENTRIES(1, Implementation.mapF$V(JsonMath::withEntries)),
|
||||
|
||||
// filters
|
||||
ARRAYS(0, Implementation.filter(JsonMath::isArray0)),
|
||||
OBJECTS(0, Implementation.filter(JsonMath::isObject0)),
|
||||
ITERABLES(0, Implementation.filter(JsonMath::isIterable0)),
|
||||
BOOLEANS(0, Implementation.filter(JsonMath::isBoolean0)),
|
||||
NUMBERS(0, Implementation.filter(JsonMath::isNumber0)),
|
||||
NORMALS(0, Implementation.filter(JsonMath::isNormal0)),
|
||||
FINITES(0, Implementation.filter(JsonMath::isFinite0)),
|
||||
STRINGS(0, Implementation.filter(JsonMath::isString0)),
|
||||
NULLS(0, Implementation.filter(JsonMath::isNull0)),
|
||||
VALUES(0, Implementation.filter(JsonMath::isValue0)),
|
||||
SCALARS(0, Implementation.filter(JsonMath::isScalar0)),
|
||||
|
||||
// checks
|
||||
ISINFINITE(0, Implementation.map$V(JsonMath::isInfinite)),
|
||||
ISNAN(0, Implementation.map$V(JsonMath::isNan)),
|
||||
ISFINITE(0, Implementation.map$V(JsonMath::isFinite)),
|
||||
ISNORMAL(0, Implementation.map$V(JsonMath::isNormal)),
|
||||
ISEMPTY(1, (context, args) -> Generator.of(() -> JsonMath.isEmpty(args.getFirst().evaluate(context)))),
|
||||
|
||||
// string operations
|
||||
TRIM(0, Implementation.map$V(JsonMath::trim)),
|
||||
LTRIM(0, Implementation.map$V(JsonMath::ltrim)),
|
||||
RTRIM(0, Implementation.map$V(JsonMath::rtrim)),
|
||||
LTRIMSTR(1, Implementation.mapV$V(JsonMath::ltrimstr)),
|
||||
RTRIMSTR(1, Implementation.mapV$V(JsonMath::rtrimstr)),
|
||||
SPLIT$1(1, Implementation.mapV$V(JsonMath::split)),
|
||||
JOIN(1, Implementation.mapV$V(JsonMath::join)),
|
||||
IMPLODE(0, Implementation.map$V(JsonMath::implode)),
|
||||
EXPLODE(0, Implementation.map$V(JsonMath::explode)),
|
||||
ASCII_UPCASE(0, Implementation.map$V(JsonMath::asciiUpcase)),
|
||||
ASCII_DOWNCASE(0, Implementation.map$V(JsonMath::asciiDowncase)),
|
||||
UTF8BYTELENGTH(0, Implementation.map$V(JsonMath::utf8ByteLength)),
|
||||
STARTSWITH(1, Implementation.mapV$V(JsonMath::startswith)),
|
||||
ENDSWITH(1, Implementation.mapV$V(JsonMath::endswith)),
|
||||
|
||||
// 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, Implementation.map$V(JsonMath::type)),
|
||||
TOJSON(0, Implementation.map$V(JsonMath::tojson)),
|
||||
TOSTRING(0, Implementation.map$V(JsonMath::tostring)),
|
||||
TONUMBER(0, Implementation.map$V(JsonMath::tonumber)),
|
||||
FROMJSON(0, Implementation.map$V(JsonMath::fromjson)),
|
||||
|
||||
// time
|
||||
FROMDATEISO8601(0, Implementation.map$V(JsonMath::fromdateiso8601)),
|
||||
FROMDATE(0, Implementation.map$V(JsonMath::fromdate)),
|
||||
TODATEISO8601(0, Implementation.map$V(JsonMath::todateiso8601)),
|
||||
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)),
|
||||
ASINH(0, Implementation.map$V(JsonMath::asinh)),
|
||||
ATAN(0, Implementation.map$V(JsonMath::atan)),
|
||||
ATANH(0, Implementation.map$V(JsonMath::atanh)),
|
||||
CBRT(0, Implementation.map$V(JsonMath::cbrt)),
|
||||
CEIL(0, Implementation.map$V(JsonMath::ceil)),
|
||||
COS(0, Implementation.map$V(JsonMath::cos)),
|
||||
COSH(0, Implementation.map$V(JsonMath::cosh)),
|
||||
ERF(0, Implementation.map$V(JsonMath::erf)),
|
||||
ERFC(0, Implementation.map$V(JsonMath::erfc)),
|
||||
EXP(0, Implementation.map$V(JsonMath::exp)),
|
||||
EXP10(0, Implementation.map$V(JsonMath::exp10)),
|
||||
EXP2(0, Implementation.map$V(JsonMath::exp2)),
|
||||
EXPM1(0, Implementation.map$V(JsonMath::expm1)),
|
||||
FABS(0, Implementation.map$V(JsonMath::fabs)),
|
||||
FLOOR(0, Implementation.map$V(JsonMath::floor)),
|
||||
GAMMA(0, Implementation.map$V(JsonMath::gamma)),
|
||||
J0(0, Implementation.map$V(JsonMath::j0)),
|
||||
J1(0, Implementation.map$V(JsonMath::j1)),
|
||||
LGAMMA(0, Implementation.map$V(JsonMath::lgamma)),
|
||||
LOG(0, Implementation.map$V(JsonMath::log)),
|
||||
LOG10(0, Implementation.map$V(JsonMath::log10)),
|
||||
LOG1P(0, Implementation.map$V(JsonMath::log1p)),
|
||||
LOG2(0, Implementation.map$V(JsonMath::log2)),
|
||||
LOGB(0, Implementation.map$V(JsonMath::logb)),
|
||||
NEARBYINT(0, Implementation.map$V(JsonMath::nearbyint)),
|
||||
RINT(0, Implementation.map$V(JsonMath::rint)),
|
||||
ROUND(0, Implementation.map$V(JsonMath::round)),
|
||||
SIGNIFICAND(0, Implementation.map$V(JsonMath::significand)),
|
||||
SIN(0, Implementation.map$V(JsonMath::sin)),
|
||||
SINH(0, Implementation.map$V(JsonMath::sinh)),
|
||||
SQRT(0, Implementation.map$V(JsonMath::sqrt)),
|
||||
TAN(0, Implementation.map$V(JsonMath::tan)),
|
||||
TANH(0, Implementation.map$V(JsonMath::tanh)),
|
||||
TGAMMA(0, Implementation.map$V(JsonMath::tgamma)),
|
||||
TRUNC(0, Implementation.map$V(JsonMath::trunc)),
|
||||
Y0(0, Implementation.map$V(JsonMath::y0)),
|
||||
Y1(0, Implementation.map$V(JsonMath::y1)),
|
||||
ATAN2(2, Implementation.mapVV$V((_, a, b) -> JsonMath.atan2(a, b))),
|
||||
COPYSIGN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.copysign(a, b))),
|
||||
DREM(2, Implementation.mapVV$V((_, a, b) -> JsonMath.drem(a, b))),
|
||||
FDIM(2, Implementation.mapVV$V((_, a, b) -> JsonMath.fdim(a, b))),
|
||||
FMAX(2, Implementation.mapVV$V((_, a, b) -> JsonMath.fmax(a, b))),
|
||||
FMIN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.fmin(a, b))),
|
||||
FMOD(2, Implementation.mapVV$V((_, a, b) -> JsonMath.fmod(a, b))),
|
||||
FREXP(2, Implementation.mapVV$V((_, a, b) -> JsonMath.frexp(a, b))),
|
||||
HYPOT(2, Implementation.mapVV$V((_, a, b) -> JsonMath.hypot(a, b))),
|
||||
JN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.jn(a, b))),
|
||||
LDEXP(2, Implementation.mapVV$V((_, a, b) -> JsonMath.ldexp(a, b))),
|
||||
MODF(2, Implementation.mapVV$V((_, a, b) -> JsonMath.modf(a, b))),
|
||||
NEXTAFTER(2, Implementation.mapVV$V((_, a, b) -> JsonMath.nextafter(a, b))),
|
||||
NEXTTOWARD(2, Implementation.mapVV$V((_, a, b) -> JsonMath.nexttoward(a, b))),
|
||||
POW(2, Implementation.mapVV$V((_, a, b) -> JsonMath.pow(a, b))),
|
||||
REMAINDER(2, Implementation.mapVV$V((_, a, b) -> JsonMath.remainder(a, b))),
|
||||
SCALB(2, Implementation.mapVV$V((_, a, b) -> JsonMath.scalb(a, b))),
|
||||
SCALBLN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.scalbln(a, b))),
|
||||
YN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.yn(a, b))),
|
||||
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 JQInvocable implementation;
|
||||
|
||||
JQBuiltIn(@NotNull String body) {
|
||||
this(List.of(), parse(body));
|
||||
}
|
||||
|
||||
JQBuiltIn(@NotNull String arg0, @NotNull String body) {
|
||||
this(List.of(arg0), parse(body));
|
||||
}
|
||||
|
||||
JQBuiltIn(@NotNull String arg0, @NotNull String arg1, @NotNull String body) {
|
||||
this(List.of(arg0, arg1), parse(body));
|
||||
}
|
||||
|
||||
JQBuiltIn(@NotNull String arg0, @NotNull String arg1, @NotNull String arg2, @NotNull String body) {
|
||||
this(List.of(arg0, arg1, arg2), parse(body));
|
||||
}
|
||||
|
||||
JQBuiltIn(@NotNull List<@NotNull String> params, @NotNull JQExpression body) {
|
||||
this.implementation = new JQFunction(getIdentifier(name()), params, body);
|
||||
}
|
||||
|
||||
JQBuiltIn(int arity, @NotNull Implementation implementation) {
|
||||
this.implementation = new InvocableDelegate(getIdentifier(name()), arity, implementation);
|
||||
}
|
||||
|
||||
@SneakyThrows(IOException.class)
|
||||
private static @NotNull JQExpression parse(@NotNull String expression) {
|
||||
return new JQParser(expression).parseExpression();
|
||||
}
|
||||
|
||||
private static @NotNull String getIdentifier(@NotNull String name) {
|
||||
var identifier = name.toLowerCase(Locale.ROOT);
|
||||
var idx = identifier.lastIndexOf("$");
|
||||
if (idx != -1) identifier = identifier.substring(0, idx);
|
||||
return identifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> invoke(@NotNull Context context, @NotNull List<@NotNull JQExpression> args) {
|
||||
return implementation.invoke(context, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String identifier() {
|
||||
return implementation.identifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int arity() {
|
||||
return implementation.arity();
|
||||
}
|
||||
|
||||
private record InvocableDelegate(
|
||||
@NotNull String identifier, int arity,
|
||||
@NotNull Implementation delegate
|
||||
) implements JQInvocable {
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> invoke(@NotNull Context context, @NotNull List<@NotNull JQExpression> args) {
|
||||
if (args.size() != arity) throw new JsonQueryException("invalid argument count");
|
||||
return delegate.invoke(context, args);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface Implementation {
|
||||
@NotNull Generator<@Nullable JsonValue> invoke(@NotNull Context context, @NotNull List<@NotNull JQExpression> args);
|
||||
|
||||
// format: map<args>$<return>
|
||||
// where <args> and <return> use V for value, F for filter
|
||||
|
||||
/**
|
||||
* Produces a zero-arg implementation applying the function to the current value.
|
||||
*/
|
||||
static @NotNull Implementation map$F(@NotNull Function<? super @Nullable JsonValue, ? extends @NotNull Generator<? extends @Nullable JsonValue>> operator) {
|
||||
return (context, _) -> context.stream().flatMap(operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a zero-arg implementation applying the function to the current value.
|
||||
*/
|
||||
static @NotNull Implementation map$V(@NotNull Function<? super @Nullable JsonValue, ? extends @Nullable JsonValue> operator) {
|
||||
return (context, _) -> context.stream().map(operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a one-arg implementation applying the function to the current value and the first argument
|
||||
* (passed by value).
|
||||
*/
|
||||
static @NotNull Implementation mapV$V(@NotNull BiFunction<? super @Nullable JsonValue, ? super @Nullable JsonValue, ? extends @Nullable JsonValue> function) {
|
||||
return (context, args) -> {
|
||||
var arg = args.getFirst();
|
||||
return context.stream().flatMap(root -> arg.evaluate(context).map(value -> function.apply(root, value)));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a one-arg implementation applying the function to the current value and the first argument
|
||||
* (passed by value).
|
||||
*/
|
||||
static @NotNull Implementation mapV$F(@NotNull BiFunction<? super @Nullable JsonValue, ? super @Nullable JsonValue, ? extends @NotNull Generator<? extends @Nullable JsonValue>> function) {
|
||||
return (context, args) -> {
|
||||
var arg = args.getFirst();
|
||||
return context.stream().flatMap(root -> arg.evaluate(context).flatMap(value -> function.apply(root, value)));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a one-arg implementation applying the function to the current value and the first argument
|
||||
* (passed as filter).
|
||||
*/
|
||||
static @NotNull Implementation mapF$F(@NotNull BiFunction<? super @Nullable JsonValue, ? super @NotNull JQFilter, ? extends @NotNull Generator<? extends @Nullable JsonValue>> function) {
|
||||
return (context, args) -> {
|
||||
var arg = args.getFirst().bind(context);
|
||||
return context.stream().flatMap(root -> function.apply(root, arg));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a one-arg implementation applying the function to the current value and the first argument
|
||||
* (passed as filter).
|
||||
*/
|
||||
static @NotNull Implementation mapF$V(@NotNull BiFunction<? super @Nullable JsonValue, ? super @NotNull JQFilter, ? extends @Nullable JsonValue> function) {
|
||||
return (context, args) -> {
|
||||
var arg = args.getFirst().bind(context);
|
||||
return context.stream().map(root -> function.apply(root, arg));
|
||||
};
|
||||
}
|
||||
|
||||
static @NotNull Implementation mapVV$V(@NotNull TriFunction<? super @Nullable JsonValue, ? super @Nullable JsonValue, ? super @Nullable JsonValue, ? extends @Nullable JsonValue> function) {
|
||||
return (context, args) -> {
|
||||
var arg0 = args.get(0);
|
||||
var arg1 = args.get(1);
|
||||
return context.stream().flatMap(root -> arg0.evaluate(context).flatMap(value0 -> arg1.evaluate(context).map(value1 -> function.apply(root, value0, value1))));
|
||||
};
|
||||
}
|
||||
|
||||
static @NotNull Implementation mapFF$V(@NotNull TriFunction<? super @Nullable JsonValue, ? super @NotNull JQFilter, ? super @NotNull JQFilter, ? extends @Nullable JsonValue> function) {
|
||||
return (context, args) -> {
|
||||
var arg0 = args.get(0).bind(context);
|
||||
var arg1 = args.get(1).bind(context);
|
||||
return context.stream().map(root -> function.apply(root, arg0, arg1));
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface TriFunction<S, T, U, R> {
|
||||
R apply(S s, T t, U u);
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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.*;
|
||||
|
||||
public record JQCommaExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
|
||||
public JQCommaExpression {
|
||||
Objects.requireNonNull(first);
|
||||
Objects.requireNonNull(second);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return Generator.concat(first.evaluate(context), second.evaluate(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return first.isConstant() && second.isConstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return first + ", " + second;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
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 JQConstant(@Nullable JsonValue value) implements JQExpression {
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return Generator.of(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return JsonValue.toJsonString(value);
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
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.*;
|
||||
|
||||
public interface JQExpression {
|
||||
|
||||
@NotNull Generator<@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("$ENV", JsonMath.env()), JQBuiltIn.ALL_BUILTINS);
|
||||
}
|
||||
|
||||
public Context {
|
||||
var map = new LinkedHashMap<String, @Nullable JsonValue>();
|
||||
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);
|
||||
}
|
||||
|
||||
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<? extends @NotNull JQInvocable> 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 Generator<@Nullable JsonValue> stream() {
|
||||
return Generator.of(root());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
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.function.Function;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface JQFilter extends Function<@Nullable JsonValue, @NotNull Generator<@Nullable JsonValue>> {
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
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(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
|
||||
public boolean isConstant() {
|
||||
return expression.isConstant() && init.isConstant() && update.isConstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return "foreach " + expression + " as " + patterns + " (" + init + "; " + update + "; " + extract + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package eu.jonahbauer.json.query.parser.ast;
|
||||
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
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.*;
|
||||
|
||||
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 Generator<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> arguments) {
|
||||
if (arguments.size() != params.size()) throw new JsonQueryException("invalid argument count");
|
||||
|
||||
var expression = body;
|
||||
|
||||
var functions = new ArrayList<JQFunction>();
|
||||
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()),
|
||||
new JQPatterns(List.of(new JQPatterns.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 + ";";
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
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.List;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
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 Generator<@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();
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
) {
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
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 eu.jonahbauer.json.query.impl.Generator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
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 Generator<@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 ? "?" : "");
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
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.List;
|
||||
|
||||
public interface JQInvocable {
|
||||
@NotNull
|
||||
Generator<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args);
|
||||
|
||||
int arity();
|
||||
@NotNull String identifier();
|
||||
default @NotNull String reference() {
|
||||
return identifier() + "/" + arity();
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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 JQIterateExpression(@NotNull JQExpression expression, boolean optional) implements JQExpression {
|
||||
public JQIterateExpression {
|
||||
Objects.requireNonNull(expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@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 ? "?" : "");
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
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 eu.jonahbauer.json.query.impl.Generator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public record JQLocExpression(@NotNull String file, int line) implements JQExpression {
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return Generator.of(JsonObject.of(
|
||||
"file", new JsonString(file),
|
||||
"line", new JsonNumber(line)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return "$__loc__";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
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;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public record JQNegation(@NotNull JQExpression expression) implements JQExpression {
|
||||
public JQNegation {
|
||||
Objects.requireNonNull(expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@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;
|
||||
}
|
||||
}
|
@ -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) { }
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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.Objects;
|
||||
|
||||
public record JQParenthesizedExpression(@NotNull JQExpression expression) implements JQExpression {
|
||||
|
||||
public JQParenthesizedExpression {
|
||||
Objects.requireNonNull(expression);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return expression.evaluate(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return expression.isConstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return "(" + expression + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,206 @@
|
||||
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.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;
|
||||
|
||||
public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
|
||||
|
||||
public JQPatterns {
|
||||
patterns = List.copyOf(patterns);
|
||||
if (patterns.isEmpty()) throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// the generator for the current pattern
|
||||
private @Nullable Generator<@Nullable JsonValue> current;
|
||||
|
||||
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<String, JsonValue>();
|
||||
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> T advance(@NotNull Function<Generator<@Nullable JsonValue>, 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(Generator::hasNext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue next() throws NoSuchElementException {
|
||||
return advance(Generator::next);
|
||||
}
|
||||
}
|
||||
|
||||
@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 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 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 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 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
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.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 Generator<@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;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
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.List;
|
||||
|
||||
public record JQProgram(
|
||||
@Nullable JQModule module,
|
||||
@NotNull List<@NotNull JQImport> imports,
|
||||
@NotNull List<@NotNull JQFunction> functions,
|
||||
@Nullable JQExpression expression
|
||||
) {
|
||||
public @NotNull Generator<@Nullable JsonValue> run(@Nullable JsonValue value) {
|
||||
if (expression == null) return Generator.empty();
|
||||
return expression.evaluate(new JQExpression.Context(value).withFunctions(functions));
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package eu.jonahbauer.json.query.parser.ast;
|
||||
|
||||
import eu.jonahbauer.json.JsonArray;
|
||||
import eu.jonahbauer.json.JsonObject;
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
import eu.jonahbauer.json.query.impl.Generator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public record JQRecursionExpression() implements JQExpression {
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return recurse(context.root());
|
||||
}
|
||||
|
||||
private @NotNull Generator<@Nullable JsonValue> recurse(@Nullable JsonValue value) {
|
||||
return switch (value) {
|
||||
case JsonArray array -> Generator.concat(Generator.of(array), Generator.from(array).flatMap(this::recurse));
|
||||
case JsonObject object -> Generator.concat(Generator.of(object), Generator.from(object.values()).flatMap(this::recurse));
|
||||
case null, default -> Generator.of(value);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
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).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();
|
||||
}
|
||||
return Generator.empty();
|
||||
})),
|
||||
Generator.of(() -> state.state)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return expression.isConstant() && init.isConstant() && update.isConstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return "reduce " + expression + " as " + patterns + " (" + init + "; " + update + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
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 JQRootExpression() implements JQExpression {
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return context.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return ".";
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
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 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 Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
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
|
||||
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) + "]";
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package eu.jonahbauer.json.query.parser.ast;
|
||||
|
||||
import eu.jonahbauer.json.JsonString;
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
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.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 Generator<@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();
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
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 eu.jonahbauer.json.query.impl.Generator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
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 Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return new QuietIterator(
|
||||
expression.evaluate(context),
|
||||
fallback == null ? null : ex -> fallback.evaluate(context.withRoot(new JsonString(ex.getMessage())))
|
||||
);
|
||||
}
|
||||
|
||||
@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 Generator<@Nullable JsonValue> {
|
||||
private @Nullable Generator<@Nullable JsonValue> delegate;
|
||||
private @Nullable Generator<@Nullable JsonValue> fallback;
|
||||
private @Nullable Function<@NotNull JsonQueryException, @NotNull Generator<@Nullable JsonValue>> fallbackSupplier;
|
||||
|
||||
private JsonValue value;
|
||||
private boolean hasValue;
|
||||
|
||||
private QuietIterator(
|
||||
@NotNull Generator<@Nullable JsonValue> delegate,
|
||||
@Nullable Function<@NotNull JsonQueryException, @NotNull Generator<@Nullable JsonValue>> fallback
|
||||
) {
|
||||
this.delegate = delegate;
|
||||
this.fallbackSupplier = fallback;
|
||||
}
|
||||
|
||||
private boolean advance() {
|
||||
if (hasValue) return true;
|
||||
assert fallback == null && delegate != null;
|
||||
try {
|
||||
hasValue = delegate.hasNext();
|
||||
value = hasValue ? delegate.next() : null;
|
||||
return hasValue;
|
||||
} catch (JsonQueryHaltException ex) {
|
||||
throw ex;
|
||||
} catch (JsonQueryException ex) {
|
||||
delegate = null;
|
||||
if (fallbackSupplier != null) {
|
||||
fallback = fallbackSupplier.apply(ex);
|
||||
hasValue = false;
|
||||
value = null;
|
||||
return fallback.hasNext();
|
||||
} else {
|
||||
hasValue = false;
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (fallback != null) return fallback.hasNext();
|
||||
if (delegate == null) return false;
|
||||
return advance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue next() {
|
||||
if (fallback != null) return fallback.next();
|
||||
if (delegate == null) throw new NoSuchElementException();
|
||||
if (!advance()) throw new NoSuchElementException();
|
||||
var out = value;
|
||||
hasValue = false;
|
||||
value = null;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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.Objects;
|
||||
|
||||
public record JQVariableExpression(@NotNull String name) implements JQExpression {
|
||||
public JQVariableExpression {
|
||||
Objects.requireNonNull(name);
|
||||
if (!name.startsWith("$")) throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return Generator.of(() -> context.variable(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<JQToken> {
|
||||
private final TrackingReader reader;
|
||||
|
||||
private final Queue<State> 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<JQToken> iterator() {
|
||||
return new JQTokenizerIterator();
|
||||
}
|
||||
|
||||
public @NotNull Stream<@NotNull JQToken> stream() {
|
||||
return StreamSupport.stream(this.spliterator(), false);
|
||||
}
|
||||
|
||||
private class JQTokenizerIterator implements Iterator<JQToken> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
65
query/src/main/java/eu/jonahbauer/json/query/util/Util.java
Normal file
65
query/src/main/java/eu/jonahbauer/json/query/util/Util.java
Normal file
@ -0,0 +1,65 @@
|
||||
package eu.jonahbauer.json.query.util;
|
||||
|
||||
import eu.jonahbauer.json.*;
|
||||
import eu.jonahbauer.json.query.impl.Generator;
|
||||
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 Generator<@NotNull List<JsonValue>> cross(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) {
|
||||
return cross(asSupplier(expressions, context)).map(Collections::unmodifiableList);
|
||||
}
|
||||
|
||||
public static <T> @NotNull Generator<@NotNull List<T>> cross(@NotNull List<@NotNull Supplier<@NotNull Generator<T>>> expressions) {
|
||||
if (expressions.isEmpty()) return Generator.of(new ArrayList<T>().reversed());
|
||||
|
||||
return expressions.getFirst().get()
|
||||
.flatMap(value -> cross(expressions.subList(1, expressions.size()))
|
||||
.map(list -> { list.addFirst(value); return list; })
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Generator<@NotNull List<JsonValue>> crossReversed(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) {
|
||||
return crossReversed(asSupplier(expressions, context)).map(Collections::unmodifiableList);
|
||||
}
|
||||
|
||||
public static <T> @NotNull Generator<@NotNull List<T>> crossReversed(@NotNull List<@NotNull Supplier<@NotNull Generator<T>>> expressions) {
|
||||
if (expressions.isEmpty()) return Generator.of(new ArrayList<>());
|
||||
|
||||
return expressions.getLast().get()
|
||||
.flatMap(value -> crossReversed(expressions.subList(0, expressions.size() - 1))
|
||||
.map(list -> { list.addLast(value); return list; })
|
||||
);
|
||||
}
|
||||
|
||||
private static @NotNull List<@NotNull Supplier<@NotNull Generator<JsonValue>>> asSupplier(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) {
|
||||
var list = new ArrayList<Supplier<Generator<JsonValue>>>(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);
|
||||
}
|
||||
}
|
186
query/src/test/java/eu/jonahbauer/json/query/JsonMathTest.java
Normal file
186
query/src/test/java/eu/jonahbauer/json/query/JsonMathTest.java
Normal file
@ -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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> 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<Arguments> orArguments() {
|
||||
return arguments(
|
||||
Arguments.of(false, "a string", true),
|
||||
Arguments.of(false, null, false),
|
||||
Arguments.of(1, false, true)
|
||||
);
|
||||
}
|
||||
|
||||
static Stream<Arguments> arguments(@NotNull Arguments @NotNull ... arguments) {
|
||||
return Stream.of(arguments).map(args -> Arguments.of(Arrays.stream(args.get()).map(JsonValue::valueOf).toArray()));
|
||||
}
|
||||
|
||||
}
|
1435
query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java
Normal file
1435
query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,3 +9,4 @@ dependencyResolutionManagement {
|
||||
|
||||
rootProject.name = "json"
|
||||
include("core")
|
||||
include("query")
|
||||
|
Loading…
x
Reference in New Issue
Block a user