jq wip
This commit is contained in:
parent
4d9a3ef4ab
commit
dc5791815e
@ -10,6 +10,9 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
* numbers and therefore all numbers are stored as {@code double}.
|
* numbers and therefore all numbers are stored as {@code double}.
|
||||||
*/
|
*/
|
||||||
public record JsonNumber(double value) implements JsonValue, JsonToken {
|
public record JsonNumber(double value) implements JsonValue, JsonToken {
|
||||||
|
public static final @NotNull JsonNumber ZERO = new JsonNumber(0);
|
||||||
|
public static final @NotNull JsonNumber ONE = new JsonNumber(1);
|
||||||
|
|
||||||
public JsonNumber {
|
public JsonNumber {
|
||||||
if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite");
|
if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite");
|
||||||
}
|
}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
1517
query/src/main/java/eu/jonahbauer/json/query/JsonMath.java
Normal file
1517
query/src/main/java/eu/jonahbauer/json/query/JsonMath.java
Normal file
File diff suppressed because it is too large
Load Diff
34
query/src/main/java/eu/jonahbauer/json/query/JsonQuery.java
Normal file
34
query/src/main/java/eu/jonahbauer/json/query/JsonQuery.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package eu.jonahbauer.json.query;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import eu.jonahbauer.json.query.parser.JQParser;
|
||||||
|
import eu.jonahbauer.json.query.parser.ast.JQExpression;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class JsonQuery {
|
||||||
|
private final @NotNull JQExpression expression;
|
||||||
|
|
||||||
|
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.expression());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new UncheckedIOException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull Stream<@NotNull JsonValue> run(@Nullable JsonValue value) {
|
||||||
|
var context = new JQExpression.Context(value);
|
||||||
|
return expression.evaluate(context);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
145
query/src/main/java/eu/jonahbauer/json/query/Main.java
Normal file
145
query/src/main/java/eu/jonahbauer/json/query/Main.java
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package eu.jonahbauer.json.query;
|
||||||
|
|
||||||
|
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")
|
||||||
|
.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,28 @@
|
|||||||
|
package eu.jonahbauer.json.query.impl;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
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 T next() throws EndOfStreamException {
|
||||||
|
while (!generators.isEmpty()) {
|
||||||
|
try {
|
||||||
|
var current = generators.peek();
|
||||||
|
return current.next();
|
||||||
|
} catch (EndOfStreamException ex) {
|
||||||
|
generators.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package eu.jonahbauer.json.query.impl;
|
||||||
|
|
||||||
|
enum EmptyGenerator implements Generator<Object> {
|
||||||
|
INSTANCE;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object next() throws EndOfStreamException {
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
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 S next() throws EndOfStreamException {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return current.next();
|
||||||
|
} catch (EndOfStreamException _) {
|
||||||
|
current = function.apply(source.next());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.jonahbauer.json.query.impl;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public interface Generator<T> {
|
||||||
|
T next() throws EndOfStreamException;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EndOfStreamException extends RuntimeException {}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.jonahbauer.json.query.impl;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
record MappingGenerator<T, S>(@NotNull Generator<T> delegate, @NotNull Function<? super T, ? extends S> function) implements Generator<S> {
|
||||||
|
@Override
|
||||||
|
public S next() throws EndOfStreamException {
|
||||||
|
return function.apply(delegate.next());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package eu.jonahbauer.json.query.impl;
|
||||||
|
|
||||||
|
final class SingletonGenerator<T> implements Generator<T> {
|
||||||
|
private boolean done;
|
||||||
|
private T value;
|
||||||
|
|
||||||
|
public SingletonGenerator(T value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T next() throws EndOfStreamException {
|
||||||
|
if (!done) {
|
||||||
|
var out = value;
|
||||||
|
done = true;
|
||||||
|
value = null;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
throw new EndOfStreamException();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,563 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull JQExpression parseExpression() throws IOException {
|
||||||
|
if (peek(JQTokenKind.DEF)) {
|
||||||
|
|
||||||
|
} else if (peek(JQTokenKind.REDUCE)) {
|
||||||
|
|
||||||
|
} else if (peek(JQTokenKind.FOREACH)) {
|
||||||
|
|
||||||
|
} else if (peek(JQTokenKind.IF)) {
|
||||||
|
|
||||||
|
} 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 = parseTermOrAs();
|
||||||
|
if (tryConsume(JQTokenKind.QUESTION_MARK)) {
|
||||||
|
return new JQTryExpression(expression);
|
||||||
|
} else {
|
||||||
|
return expression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull JQExpression parseTermOrAs() throws IOException {
|
||||||
|
var term = parseTerm();
|
||||||
|
if (tryConsume(JQTokenKind.AS)) {
|
||||||
|
var patterns = new ArrayList<JQAsExpression.Pattern>();
|
||||||
|
patterns.add(parsePattern());
|
||||||
|
while (tryConsume(JQTokenKind.ALTERNATION)) {
|
||||||
|
patterns.add(parsePattern());
|
||||||
|
}
|
||||||
|
consume(JQTokenKind.PIPE);
|
||||||
|
var expr = parseExpression();
|
||||||
|
return new JQAsExpression(term, patterns, expr);
|
||||||
|
}
|
||||||
|
return term;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull JQAsExpression.Pattern parsePattern() throws IOException {
|
||||||
|
if (tryConsume(JQTokenKind.LBRACKET)) {
|
||||||
|
var patterns = new ArrayList<JQAsExpression.Pattern>();
|
||||||
|
do {
|
||||||
|
patterns.add(parsePattern());
|
||||||
|
} while (tryConsume(JQTokenKind.COMMA));
|
||||||
|
consume(JQTokenKind.RBRACKET);
|
||||||
|
return new JQAsExpression.Pattern.ArrayPattern(patterns);
|
||||||
|
} else if (tryConsume(JQTokenKind.LBRACE)) {
|
||||||
|
var patterns = new LinkedHashMap<JQExpression, JQAsExpression.Pattern>();
|
||||||
|
do {
|
||||||
|
if (tryConsume(JQTokenKind.DOLLAR)) {
|
||||||
|
var ident = consume(JQTokenKind.IDENT).text();
|
||||||
|
var pattern = new JQAsExpression.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 JQAsExpression.Pattern.ObjectPattern(patterns);
|
||||||
|
} else {
|
||||||
|
consume(JQTokenKind.DOLLAR);
|
||||||
|
var ident = consume(JQTokenKind.IDENT).text();
|
||||||
|
return new JQAsExpression.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 JQExpression parseVariableExpression() throws IOException {
|
||||||
|
var dollar = 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);
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException("not yet implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,80 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import eu.jonahbauer.json.query.JsonMath;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.BinaryOperator;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Gatherer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
public record JQAlternativeExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
|
||||||
|
public JQAlternativeExpression {
|
||||||
|
Objects.requireNonNull(first);
|
||||||
|
Objects.requireNonNull(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return first.evaluate(context).gather(new AlternativeGatherer(second, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConstant() {
|
||||||
|
return first.isConstant() && second.isConstant();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return first + " // " + second;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
private static class State {
|
||||||
|
private boolean empty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AlternativeGatherer(
|
||||||
|
@NotNull JQExpression expression,
|
||||||
|
@NotNull Context context
|
||||||
|
) implements Gatherer<JsonValue, State, JsonValue> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Supplier<State> initializer() {
|
||||||
|
return State::new;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Integrator<State, JsonValue, JsonValue> integrator() {
|
||||||
|
return Integrator.ofGreedy((state, element, downstream) -> {
|
||||||
|
if (JsonMath.isTruthy(element)) {
|
||||||
|
state.empty = false;
|
||||||
|
return downstream.push(element);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull BinaryOperator<State> combiner() {
|
||||||
|
return (state1, state2) -> new State(state1.empty && state2.empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull BiConsumer<State, Downstream<? super JsonValue>> finisher() {
|
||||||
|
return (state, downstream) -> {
|
||||||
|
if (!state.empty) return;
|
||||||
|
var it = expression.evaluate(context).iterator();
|
||||||
|
while (it.hasNext() && downstream.push(it.next()));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonArray;
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import eu.jonahbauer.json.query.util.Util;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQArrayConstructionExpression(@NotNull JQExpression expression) implements JQExpression {
|
||||||
|
public JQArrayConstructionExpression {
|
||||||
|
Objects.requireNonNull(expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return Util.lazy(() -> new JsonArray(expression.evaluate(context).toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConstant() {
|
||||||
|
return expression.isConstant();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return "[" + expression + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
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.util.Util;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQAsExpression(
|
||||||
|
@NotNull JQExpression variable, @NotNull List<@NotNull Pattern> patterns, @NotNull JQExpression expression
|
||||||
|
) implements JQExpression {
|
||||||
|
|
||||||
|
public JQAsExpression {
|
||||||
|
Objects.requireNonNull(variable);
|
||||||
|
Objects.requireNonNull(expression);
|
||||||
|
|
||||||
|
patterns = List.copyOf(patterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
var variables = new HashMap<String, JsonValue>();
|
||||||
|
patterns.stream().map(Pattern::variables).flatMap(Set::stream).forEach(key -> variables.put(key, null));
|
||||||
|
|
||||||
|
return variable.evaluate(context)
|
||||||
|
.flatMap(value -> {
|
||||||
|
Stream<Map<String, JsonValue>> result = null;
|
||||||
|
|
||||||
|
// find first pattern that does not throw
|
||||||
|
var it = patterns.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
try {
|
||||||
|
result = it.next().bind(context, value);
|
||||||
|
} catch (JsonQueryException ex) {
|
||||||
|
if (!it.hasNext()) throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute expression for all possible pattern matches
|
||||||
|
assert result != null;
|
||||||
|
return result
|
||||||
|
.map(vars -> {
|
||||||
|
var out = new HashMap<>(variables);
|
||||||
|
out.putAll(vars);
|
||||||
|
return context.withVariables(out);
|
||||||
|
})
|
||||||
|
.flatMap(expression::evaluate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConstant() {
|
||||||
|
return variable.isConstant() && expression.isConstant();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return variable
|
||||||
|
+ " as " + patterns.stream().map(Objects::toString).collect(Collectors.joining(" ?// "))
|
||||||
|
+ " | " + expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed interface Pattern {
|
||||||
|
@NotNull Set<@NotNull String> variables();
|
||||||
|
@NotNull Stream<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 Stream<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
|
||||||
|
var map = new HashMap<String, JsonValue>();
|
||||||
|
map.put(name, value);
|
||||||
|
return Stream.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 Stream<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
|
||||||
|
var streams = new ArrayList<Supplier<Stream<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 Stream<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
|
||||||
|
var streams = new ArrayList<Supplier<Stream<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,72 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import eu.jonahbauer.json.query.JsonMath;
|
||||||
|
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 Stream<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,30 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
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 Stream<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 org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQAssignmentPipe(@NotNull JQExpression target, @NotNull JQExpression value) implements JQExpression {
|
||||||
|
|
||||||
|
public JQAssignmentPipe {
|
||||||
|
Objects.requireNonNull(target);
|
||||||
|
Objects.requireNonNull(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<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.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;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
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 Stream<@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 org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQBooleanAndExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
|
||||||
|
|
||||||
|
public JQBooleanAndExpression {
|
||||||
|
Objects.requireNonNull(first);
|
||||||
|
Objects.requireNonNull(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return first.evaluate(context)
|
||||||
|
.flatMap(value -> JsonMath.isFalsy(value)
|
||||||
|
? Stream.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,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 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 Stream<JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return first.evaluate(context)
|
||||||
|
.flatMap(value -> JsonMath.isTruthy(value)
|
||||||
|
? Stream.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,276 @@
|
|||||||
|
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.util.Util;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Gatherer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public enum JQBuiltIn implements JQInvocable {
|
||||||
|
ABS(0, (context, _) -> context.stream().map(JsonMath::abs)),
|
||||||
|
ADD(0, (context, _) -> context.stream().map(value -> JsonMath.values(value).reduce(null, JsonMath::add))),
|
||||||
|
NOT(0, (context, _) -> context.stream().map(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, (_, _) -> Stream.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, (_, _) -> Stream.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))),
|
||||||
|
|
||||||
|
// iterable operations
|
||||||
|
MAP(1, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return context.stream().map(value -> JsonMath.map(value, filter));
|
||||||
|
}),
|
||||||
|
MAP_VALUES(1, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return context.stream().map(value -> JsonMath.mapValues(value, filter));
|
||||||
|
}),
|
||||||
|
KEYS(0, (context, _) -> context.stream().map(JsonMath::keys)),
|
||||||
|
KEYS_UNSORTED(0, (context, _) -> context.stream().map(JsonMath::keysUnsorted)),
|
||||||
|
HAS(1, (context, args) -> args.getFirst().evaluate(context).map(index -> JsonMath.has(context.root(), index))),
|
||||||
|
IN(1, (context, args) -> args.getFirst().evaluate(context).map(value -> JsonMath.in(context.root(), value))),
|
||||||
|
FIRST$0(0, (context, _) -> context.stream().map(value -> JsonMath.index(value, JsonNumber.ZERO))),
|
||||||
|
LAST$0(0, (context, _) -> context.stream().map(value -> JsonMath.index(value, new JsonNumber(-1)))),
|
||||||
|
NTH$1(1, (context, args) -> args.getFirst().evaluate(context).map(index -> JsonMath.index(context.root(), index))),
|
||||||
|
ANY$0(0, (context, _) -> context.stream().map(JsonMath::any)),
|
||||||
|
ANY$1(1, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return context.stream().map(value -> JsonMath.any(value, filter));
|
||||||
|
}),
|
||||||
|
ANY$2(2, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return Util.lazy(() -> JsonMath.any(args.getFirst().evaluate(context), filter));
|
||||||
|
}),
|
||||||
|
ALL$0(0, (context, _) -> context.stream().map(JsonMath::all)),
|
||||||
|
ALL$1(1, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return context.stream().map(value -> JsonMath.all(value, filter));
|
||||||
|
}),
|
||||||
|
ALL$2(2, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return Util.lazy(() -> JsonMath.all(args.getFirst().evaluate(context), filter));
|
||||||
|
}),
|
||||||
|
FLATTEN$0(0, (context, _) -> context.stream().map(JsonMath::flatten)),
|
||||||
|
FLATTEN$1(1, (context, args) -> context.stream().flatMap(value -> args.getFirst().evaluate(context).map(depth -> JsonMath.flatten(value, depth)))),
|
||||||
|
SORT(0, (context, _) -> context.stream().map(JsonMath::sort)),
|
||||||
|
SORT_BY(1, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return context.stream().map(value -> JsonMath.sort(value, filter));
|
||||||
|
}),
|
||||||
|
MIN(0, (context, _) -> context.stream().map(JsonMath::min)),
|
||||||
|
MIN_BY(1, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return context.stream().map(value -> JsonMath.min(value, filter));
|
||||||
|
}),
|
||||||
|
MAX(0, (context, _) -> context.stream().map(JsonMath::max)),
|
||||||
|
MAX_BY(1, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return context.stream().map(value -> JsonMath.max(value, filter));
|
||||||
|
}),
|
||||||
|
UNIQUE(0, (context, _) -> context.stream().map(JsonMath::unique)),
|
||||||
|
UNIQUE_BY(1, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return context.stream().map(value -> JsonMath.unique(value, filter));
|
||||||
|
}),
|
||||||
|
GROUP_BY(1, (context, args) -> {
|
||||||
|
var filter = args.getFirst().bind(context);
|
||||||
|
return context.stream().map(value -> JsonMath.group(value, filter));
|
||||||
|
}),
|
||||||
|
REVERSE(0, (context, _) -> context.stream().map(JsonMath::reverse)),
|
||||||
|
CONTAINS(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.contains(context.root(), content))),
|
||||||
|
INDICES(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.indices(context.root(), content))),
|
||||||
|
INDEX(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.firstindex(context.root(), content))),
|
||||||
|
RINDEX(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.lastindex(context.root(), content))),
|
||||||
|
INSIDE(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.inside(context.root(), content))),
|
||||||
|
COMBINATIONS$0(0, (context, _) -> context.stream().flatMap(JsonMath::combinations)),
|
||||||
|
COMBINATIONS$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(n -> JsonMath.combinations(context.root(), n))),
|
||||||
|
BSEARCH(1, (context, args) -> args.getFirst().evaluate(context).map(value -> JsonMath.bsearch(context.root(), value))),
|
||||||
|
TRANSPOSE(0, (context, _) -> context.stream().map(JsonMath::transpose)),
|
||||||
|
|
||||||
|
// filters
|
||||||
|
ARRAYS(0, (context, _) -> context.stream().filter(JsonMath::isArray)),
|
||||||
|
OBJECTS(0, (context, _) -> context.stream().filter(JsonMath::isObject)),
|
||||||
|
ITERABLES(0, (context, _) -> context.stream().filter(JsonMath::isIterable)),
|
||||||
|
BOOLEANS(0, (context, _) -> context.stream().filter(JsonMath::isBoolean)),
|
||||||
|
NUMBERS(0, (context, _) -> context.stream().filter(JsonMath::isNumber)),
|
||||||
|
NORMALS(0, (context, _) -> context.stream().filter(JsonMath::isNormal)),
|
||||||
|
FINITES(0, (context, _) -> context.stream().filter(JsonMath::isFinite)),
|
||||||
|
STRINGS(0, (context, _) -> context.stream().filter(JsonMath::isString)),
|
||||||
|
NULLS(0, (context, _) -> context.stream().filter(JsonMath::isNull)),
|
||||||
|
VALUES(0, (context, _) -> context.stream().filter(JsonMath::isValue)),
|
||||||
|
SCALARS(0, (context, _) -> context.stream().filter(JsonMath::isScalar)),
|
||||||
|
|
||||||
|
// checks
|
||||||
|
ISINFINITE(0, (context, _) -> context.stream().map(JsonMath::isInfinite).map(JsonBoolean::valueOf)),
|
||||||
|
ISNAN(0, (context, _) -> context.stream().map(JsonMath::isNan).map(JsonBoolean::valueOf)),
|
||||||
|
ISFINITE(0, (context, _) -> context.stream().map(JsonMath::isFinite).map(JsonBoolean::valueOf)),
|
||||||
|
ISNORMAL(0, (context, _) -> context.stream().map(JsonMath::isNormal).map(JsonBoolean::valueOf)),
|
||||||
|
ISEMPTY(1, (context, args) -> Util.lazy(() -> JsonBoolean.valueOf(JsonMath.isEmpty(args.getFirst().evaluate(context))))),
|
||||||
|
|
||||||
|
// string operations
|
||||||
|
TRIM(0, (context, _) -> context.stream().map(JsonMath::trim)),
|
||||||
|
LTRIM(0, (context, _) -> context.stream().map(JsonMath::ltrim)),
|
||||||
|
RTRIM(0, (context, _) -> context.stream().map(JsonMath::rtrim)),
|
||||||
|
LTRIMSTR(1, (context, args) -> args.getFirst().evaluate(context).map(prefix -> JsonMath.ltrimstr(context.root(), prefix))),
|
||||||
|
RTRIMSTR(1, (context, args) -> args.getFirst().evaluate(context).map(suffix -> JsonMath.rtrimstr(context.root(), suffix))),
|
||||||
|
SPLIT$1(1, (context, args) -> args.getFirst().evaluate(context).map(separator -> JsonMath.split(context.root(), separator))),
|
||||||
|
JOIN(1, (context, args) -> args.getFirst().evaluate(context).map(separator -> JsonMath.join(context.root(), separator))),
|
||||||
|
IMPLODE(0, (context, _) -> context.stream().map(JsonMath::implode)),
|
||||||
|
EXPLODE(0, (context, _) -> context.stream().map(JsonMath::explode)),
|
||||||
|
ASCII_UPCASE(0, (context, _) -> context.stream().map(JsonMath::asciiUpcase)),
|
||||||
|
ASCII_DOWNCASE(0, (context, _) -> context.stream().map(JsonMath::asciiDowncase)),
|
||||||
|
UTF8BYTELENGTH(0, (context, _) -> context.stream().map(JsonMath::utf8ByteLength)),
|
||||||
|
STARTSWITH(1, (context, args) -> args.getFirst().evaluate(context).map(prefix -> JsonMath.startswith(context.root(), prefix))),
|
||||||
|
ENDSWITH(1, (context, args) -> args.getFirst().evaluate(context).map(prefix -> JsonMath.endswith(context.root(), prefix))),
|
||||||
|
|
||||||
|
// 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, (context, _) -> context.stream().map(JsonMath::type)),
|
||||||
|
TOJSON(0, (context, _) -> context.stream().map(JsonMath::tojson)),
|
||||||
|
TOSTRING(0, (context, _) -> context.stream().map(JsonMath::tostring)),
|
||||||
|
TONUMBER(0, (context, _) -> context.stream().map(JsonMath::tonumber)),
|
||||||
|
FROMJSON(0, (context, _) -> context.stream().map(JsonMath::fromjson)),
|
||||||
|
|
||||||
|
// misc
|
||||||
|
LENGTH(0, (context, _) -> context.stream().map(JsonMath::length)),
|
||||||
|
REPEAT(1, (context, args) -> Stream.generate(() -> args.getFirst().evaluate(context)).flatMap(Function.identity())),
|
||||||
|
|
||||||
|
// math library
|
||||||
|
ACOS(0, (context, _) -> context.stream().map(JsonMath::acos)),
|
||||||
|
ACOSH(0, (context, _) -> context.stream().map(JsonMath::acosh)),
|
||||||
|
ASIN(0, (context, _) -> context.stream().map(JsonMath::asin)),
|
||||||
|
ASINH(0, (context, _) -> context.stream().map(JsonMath::asinh)),
|
||||||
|
ATAN(0, (context, _) -> context.stream().map(JsonMath::atan)),
|
||||||
|
ATANH(0, (context, _) -> context.stream().map(JsonMath::atanh)),
|
||||||
|
CBRT(0, (context, _) -> context.stream().map(JsonMath::cbrt)),
|
||||||
|
CEIL(0, (context, _) -> context.stream().map(JsonMath::ceil)),
|
||||||
|
COS(0, (context, _) -> context.stream().map(JsonMath::cos)),
|
||||||
|
COSH(0, (context, _) -> context.stream().map(JsonMath::cosh)),
|
||||||
|
ERF(0, (context, _) -> context.stream().map(JsonMath::erf)),
|
||||||
|
ERFC(0, (context, _) -> context.stream().map(JsonMath::erfc)),
|
||||||
|
EXP(0, (context, _) -> context.stream().map(JsonMath::exp)),
|
||||||
|
EXP10(0, (context, _) -> context.stream().map(JsonMath::exp10)),
|
||||||
|
EXP2(0, (context, _) -> context.stream().map(JsonMath::exp2)),
|
||||||
|
EXPM1(0, (context, _) -> context.stream().map(JsonMath::expm1)),
|
||||||
|
FABS(0, (context, _) -> context.stream().map(JsonMath::fabs)),
|
||||||
|
FLOOR(0, (context, _) -> context.stream().map(JsonMath::floor)),
|
||||||
|
GAMMA(0, (context, _) -> context.stream().map(JsonMath::gamma)),
|
||||||
|
J0(0, (context, _) -> context.stream().map(JsonMath::j0)),
|
||||||
|
J1(0, (context, _) -> context.stream().map(JsonMath::j1)),
|
||||||
|
LGAMMA(0, (context, _) -> context.stream().map(JsonMath::lgamma)),
|
||||||
|
LOG(0, (context, _) -> context.stream().map(JsonMath::log)),
|
||||||
|
LOG10(0, (context, _) -> context.stream().map(JsonMath::log10)),
|
||||||
|
LOG1P(0, (context, _) -> context.stream().map(JsonMath::log1p)),
|
||||||
|
LOG2(0, (context, _) -> context.stream().map(JsonMath::log2)),
|
||||||
|
LOGB(0, (context, _) -> context.stream().map(JsonMath::logb)),
|
||||||
|
NEARBYINT(0, (context, _) -> context.stream().map(JsonMath::nearbyint)),
|
||||||
|
RINT(0, (context, _) -> context.stream().map(JsonMath::rint)),
|
||||||
|
ROUND(0, (context, _) -> context.stream().map(JsonMath::round)),
|
||||||
|
SIGNIFICAND(0, (context, _) -> context.stream().map(JsonMath::significand)),
|
||||||
|
SIN(0, (context, _) -> context.stream().map(JsonMath::sin)),
|
||||||
|
SINH(0, (context, _) -> context.stream().map(JsonMath::sinh)),
|
||||||
|
SQRT(0, (context, _) -> context.stream().map(JsonMath::sqrt)),
|
||||||
|
TAN(0, (context, _) -> context.stream().map(JsonMath::tan)),
|
||||||
|
TANH(0, (context, _) -> context.stream().map(JsonMath::tanh)),
|
||||||
|
TGAMMA(0, (context, _) -> context.stream().map(JsonMath::tgamma)),
|
||||||
|
TRUNC(0, (context, _) -> context.stream().map(JsonMath::trunc)),
|
||||||
|
Y0(0, (context, _) -> context.stream().map(JsonMath::y0)),
|
||||||
|
Y1(0, (context, _) -> context.stream().map(JsonMath::y1)),
|
||||||
|
ATAN2(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.atan2(values.get(0), values.get(1)))),
|
||||||
|
COPYSIGN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.copysign(values.get(0), values.get(1)))),
|
||||||
|
DREM(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.drem(values.get(0), values.get(1)))),
|
||||||
|
FDIM(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fdim(values.get(0), values.get(1)))),
|
||||||
|
FMAX(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fmax(values.get(0), values.get(1)))),
|
||||||
|
FMIN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fmin(values.get(0), values.get(1)))),
|
||||||
|
FMOD(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fmod(values.get(0), values.get(1)))),
|
||||||
|
FREXP(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.frexp(values.get(0), values.get(1)))),
|
||||||
|
HYPOT(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.hypot(values.get(0), values.get(1)))),
|
||||||
|
JN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.jn(values.get(0), values.get(1)))),
|
||||||
|
LDEXP(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.ldexp(values.get(0), values.get(1)))),
|
||||||
|
MODF(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.modf(values.get(0), values.get(1)))),
|
||||||
|
NEXTAFTER(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.nextafter(values.get(0), values.get(1)))),
|
||||||
|
NEXTTOWARD(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.nexttoward(values.get(0), values.get(1)))),
|
||||||
|
POW(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.pow(values.get(0), values.get(1)))),
|
||||||
|
REMAINDER(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.remainder(values.get(0), values.get(1)))),
|
||||||
|
SCALB(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.scalb(values.get(0), values.get(1)))),
|
||||||
|
SCALBLN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.scalbln(values.get(0), values.get(1)))),
|
||||||
|
YN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.yn(values.get(0), values.get(1)))),
|
||||||
|
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 String identifier;
|
||||||
|
private final int arity;
|
||||||
|
private final @NotNull Implementation implementation;
|
||||||
|
|
||||||
|
JQBuiltIn(int arity, @NotNull Implementation implementation) {
|
||||||
|
var identifier = this.name().toLowerCase(Locale.ROOT);
|
||||||
|
var idx = identifier.lastIndexOf("$");
|
||||||
|
if (idx != -1) identifier = identifier.substring(0, idx);
|
||||||
|
|
||||||
|
this.identifier = identifier;
|
||||||
|
this.arity = arity;
|
||||||
|
this.implementation = implementation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> invoke(JQExpression.@NotNull Context context, @NotNull List<@NotNull JQExpression> args) {
|
||||||
|
if (args.size() != arity) throw new JsonQueryException("invalid argument count");
|
||||||
|
return implementation.invoke(context, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String identifier() {
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int arity() {
|
||||||
|
return arity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface Implementation {
|
||||||
|
@NotNull Stream<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
public record JQCommaExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
|
||||||
|
public JQCommaExpression {
|
||||||
|
Objects.requireNonNull(first);
|
||||||
|
Objects.requireNonNull(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
var it = new Iterator<JsonValue>() {
|
||||||
|
private Iterator<JsonValue> delegate;
|
||||||
|
private Queue<Supplier<Iterator<JsonValue>>> queue = new LinkedList<>(List.of(
|
||||||
|
() -> first.evaluate(context).iterator(),
|
||||||
|
() -> second.evaluate(context).iterator()
|
||||||
|
));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
if (delegate == null) delegate = queue.remove().get();
|
||||||
|
while (!delegate.hasNext() && !queue.isEmpty()) {
|
||||||
|
delegate = queue.remove().get();
|
||||||
|
}
|
||||||
|
return delegate.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonValue next() {
|
||||||
|
if (delegate == null) delegate = queue.remove().get();
|
||||||
|
while (!delegate.hasNext() && !queue.isEmpty()) {
|
||||||
|
delegate = queue.remove().get();
|
||||||
|
}
|
||||||
|
return delegate.next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var spliterator = Spliterators.spliteratorUnknownSize(it, Spliterator.ORDERED);
|
||||||
|
return StreamSupport.stream(spliterator, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConstant() {
|
||||||
|
return first.isConstant() && second.isConstant();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return first + ", " + second;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQConstant(@Nullable JsonValue value) implements JQExpression {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return Stream.of(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConstant() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return JsonValue.toJsonString(value);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import eu.jonahbauer.json.query.JsonQueryException;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public interface JQExpression {
|
||||||
|
|
||||||
|
@NotNull Stream<@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(), JQBuiltIn.ALL_BUILTINS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Context {
|
||||||
|
variables = variables.entrySet().stream().collect(Collectors.toMap(
|
||||||
|
entry -> {
|
||||||
|
Objects.requireNonNull(entry.getKey());
|
||||||
|
if (!entry.getKey().startsWith("$")) throw new IllegalArgumentException();
|
||||||
|
return entry.getKey();
|
||||||
|
},
|
||||||
|
Map.Entry::getValue
|
||||||
|
));
|
||||||
|
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 Stream<@Nullable JsonValue> stream() {
|
||||||
|
return Stream.of(root());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface JQFilter extends Function<@Nullable JsonValue, @NotNull Stream<@Nullable JsonValue>> {
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
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 Stream<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> arguments) {
|
||||||
|
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()),
|
||||||
|
List.of(new JQAsExpression.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,36 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
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 Stream<@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,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,53 @@
|
|||||||
|
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 org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
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 Stream<@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,18 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public interface JQInvocable {
|
||||||
|
@NotNull Stream<@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 org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQIterateExpression(@NotNull JQExpression expression, boolean optional) implements JQExpression {
|
||||||
|
public JQIterateExpression {
|
||||||
|
Objects.requireNonNull(expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@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,30 @@
|
|||||||
|
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 org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQLocExpression(@NotNull String file, int line) implements JQExpression {
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return Stream.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,30 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import eu.jonahbauer.json.query.JsonMath;
|
||||||
|
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 Stream<@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,30 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQParenthesizedExpression(@NotNull JQExpression expression) implements JQExpression {
|
||||||
|
|
||||||
|
public JQParenthesizedExpression {
|
||||||
|
Objects.requireNonNull(expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@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,30 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
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 Stream<@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,14 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonArray;
|
||||||
|
import eu.jonahbauer.json.JsonObject;
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQRecursionExpression() implements JQExpression {
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return recurse(context.root());
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull Stream<@Nullable JsonValue> recurse(@Nullable JsonValue value) {
|
||||||
|
return switch (value) {
|
||||||
|
case JsonArray array -> Stream.concat(Stream.of(array), array.stream().flatMap(this::recurse));
|
||||||
|
case JsonObject object -> Stream.concat(Stream.of(object), object.values().stream().flatMap(this::recurse));
|
||||||
|
case null, default -> Stream.of(value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConstant() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQRootExpression() implements JQExpression {
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return context.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isConstant() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String toString() {
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonArray;
|
||||||
|
import eu.jonahbauer.json.JsonNumber;
|
||||||
|
import eu.jonahbauer.json.JsonString;
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import eu.jonahbauer.json.query.JsonQueryException;
|
||||||
|
import eu.jonahbauer.json.query.util.Util;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQSliceExpression(@NotNull JQExpression expression, @Nullable JQExpression start, @Nullable JQExpression end, boolean optional) implements JQExpression {
|
||||||
|
public JQSliceExpression {
|
||||||
|
Objects.requireNonNull(expression);
|
||||||
|
if (start == null && end == null) throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return expression.evaluate(context).flatMap(value -> switch (value) {
|
||||||
|
case JsonArray array -> slice(context, "an array slice", array.size(), array::subList);
|
||||||
|
case JsonString string -> slice(context, "a string slice", string.length(), string::subSequence);
|
||||||
|
case null -> Stream.of((JsonValue) null);
|
||||||
|
default -> {
|
||||||
|
if (optional) yield Stream.empty();
|
||||||
|
throw new JsonQueryException(STR."Cannot index \{Util.type(value)} with object.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull Stream<@Nullable JsonValue> slice(@NotNull Context context, @NotNull String type, int length, @NotNull BiFunction<Integer, Integer, JsonValue> slice) {
|
||||||
|
return getIndices(start, context, type, length, 0)
|
||||||
|
.mapToObj(start -> getIndices(end, context, type, length, length)
|
||||||
|
.mapToObj(end -> start > end ? slice.apply(0, 0) : slice.apply(start, end))
|
||||||
|
)
|
||||||
|
.flatMap(Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull IntStream getIndices(@Nullable JQExpression expression, @NotNull Context context, @NotNull String type, int length, int fallback) {
|
||||||
|
if (expression == null) return IntStream.of(fallback);
|
||||||
|
return expression.evaluate(context).mapToInt(value -> getIndex(value, type, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getIndex(@Nullable JsonValue value, @NotNull String type, int length) {
|
||||||
|
if (!(value instanceof JsonNumber(double d))) {
|
||||||
|
throw new JsonQueryException(STR."Start and end indices of \{type} must be numbers.");
|
||||||
|
}
|
||||||
|
var i = (int) Math.floor(d);
|
||||||
|
return i < 0 ? Math.max(0, length + i) : Math.min(length, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,60 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonString;
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
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 Stream<@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,98 @@
|
|||||||
|
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 org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import java.util.stream.StreamSupport;
|
||||||
|
|
||||||
|
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 Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
var iterator = new QuietIterator(
|
||||||
|
expression.evaluate(context).iterator(),
|
||||||
|
fallback == null ? null : ex -> fallback.evaluate(context.withRoot(new JsonString(ex.getMessage()))).iterator()
|
||||||
|
);
|
||||||
|
var spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED);
|
||||||
|
return StreamSupport.stream(spliterator, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 Iterator<@Nullable JsonValue> {
|
||||||
|
private @Nullable Iterator<@Nullable JsonValue> delegate;
|
||||||
|
private @Nullable Function<@NotNull JsonQueryException, @NotNull Iterator<@Nullable JsonValue>> fallback;
|
||||||
|
|
||||||
|
private @Nullable JsonValue next;
|
||||||
|
private boolean hasNext;
|
||||||
|
|
||||||
|
private boolean valid = false;
|
||||||
|
|
||||||
|
private QuietIterator(
|
||||||
|
@NotNull Iterator<@Nullable JsonValue> delegate,
|
||||||
|
@Nullable Function<@NotNull JsonQueryException, @NotNull Iterator<@Nullable JsonValue>> fallback
|
||||||
|
) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
this.fallback = fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureValid() {
|
||||||
|
if (valid) return;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
if (delegate != null && delegate.hasNext()) { // still have values in current stream
|
||||||
|
next = delegate.next();
|
||||||
|
hasNext = true;
|
||||||
|
} else { // end of stream
|
||||||
|
next = null;
|
||||||
|
hasNext = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} catch (JsonQueryHaltException ex) {
|
||||||
|
throw ex;
|
||||||
|
} catch (JsonQueryException ex) { // switch to fallback
|
||||||
|
delegate = fallback != null ? fallback.apply(ex) : null;
|
||||||
|
fallback = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
ensureValid();
|
||||||
|
return hasNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable JsonValue next() {
|
||||||
|
ensureValid();
|
||||||
|
if (!hasNext) throw new NoSuchElementException();
|
||||||
|
valid = false;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package eu.jonahbauer.json.query.parser.ast;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonValue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public record JQVariableExpression(@NotNull String name) implements JQExpression {
|
||||||
|
public JQVariableExpression {
|
||||||
|
Objects.requireNonNull(name);
|
||||||
|
if (!name.startsWith("$")) throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||||
|
return Stream.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
query/src/main/java/eu/jonahbauer/json/query/util/Util.java
Normal file
72
query/src/main/java/eu/jonahbauer/json/query/util/Util.java
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package eu.jonahbauer.json.query.util;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.*;
|
||||||
|
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 <T> @NotNull Stream<T> lazy(@NotNull Supplier<T> supplier) {
|
||||||
|
return Stream.of((Object) null).map(_ -> supplier.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> @NotNull Stream<T> lazyStream(@NotNull Supplier<Stream<T>> supplier) {
|
||||||
|
return Stream.of((Object) null).flatMap(_ -> supplier.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull Stream<@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 Stream<@NotNull List<T>> cross(@NotNull List<@NotNull Supplier<@NotNull Stream<T>>> expressions) {
|
||||||
|
if (expressions.isEmpty()) return Stream.of(new ArrayList<T>().reversed());
|
||||||
|
|
||||||
|
return expressions.getFirst().get()
|
||||||
|
.flatMap(value -> cross(expressions.subList(1, expressions.size()))
|
||||||
|
.peek(list -> list.addFirst(value))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NotNull Stream<@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 Stream<@NotNull List<T>> crossReversed(@NotNull List<@NotNull Supplier<@NotNull Stream<T>>> expressions) {
|
||||||
|
if (expressions.isEmpty()) return Stream.of(new ArrayList<>());
|
||||||
|
|
||||||
|
return expressions.getLast().get()
|
||||||
|
.flatMap(value -> crossReversed(expressions.subList(0, expressions.size() - 1))
|
||||||
|
.peek(list -> list.addLast(value))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull List<@NotNull Supplier<@NotNull Stream<JsonValue>>> asSupplier(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) {
|
||||||
|
var list = new ArrayList<Supplier<Stream<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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1433
query/src/test/java/eu/jonahbauer/json/query/JsonQueryTest.java
Normal file
1433
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"
|
rootProject.name = "json"
|
||||||
include("core")
|
include("core")
|
||||||
|
include("query")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user