Compare commits

...

4 Commits

Author SHA1 Message Date
87ae709f6d
fixup query 2025-04-13 02:29:38 +02:00
ef88d89ce1
fixup query 2025-04-12 20:22:02 +02:00
bdb505fce7
fixup query 2025-04-12 20:21:56 +02:00
dc5791815e
jq wip 2025-04-12 20:18:57 +02:00
67 changed files with 7509 additions and 3 deletions

View File

@ -10,9 +10,8 @@ import org.jetbrains.annotations.Nullable;
* numbers and therefore all numbers are stored as {@code double}.
*/
public record JsonNumber(double value) implements JsonValue, JsonToken {
public JsonNumber {
if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite");
}
public static final @NotNull JsonNumber ZERO = new JsonNumber(0);
public static final @NotNull JsonNumber ONE = new JsonNumber(1);
/**
* Converts the given int to a JSON number.

35
query/build.gradle.kts Normal file
View 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")
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
package eu.jonahbauer.json.query;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import eu.jonahbauer.json.query.parser.JQParser;
import eu.jonahbauer.json.query.parser.ast.JQProgram;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.UncheckedIOException;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class JsonQuery {
private final @NotNull JQProgram programm;
public static @NotNull JsonQuery parse(@NotNull String query) {
try {
var parser = new JQParser(query);
var programm = parser.parseTopLevel();
if (programm.expression() == null) throw new IllegalArgumentException();
return new JsonQuery(programm);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public @NotNull Generator<@NotNull JsonValue> run(@Nullable JsonValue value) {
return programm.run(value);
}
}

View File

@ -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);
}
}

View File

@ -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);
};
}
}

View File

@ -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}";
}
}

View File

@ -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);
}
}

View File

@ -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();
};
}
}

View File

@ -0,0 +1,155 @@
package eu.jonahbauer.json.query;
import eu.jonahbauer.json.JsonValue;
import java.util.LinkedHashMap;
import java.util.SequencedMap;
import static eu.jonahbauer.json.JsonTemplateProcessor.JSON;
public class Main {
public static void main(String[] args) {
System.out.println();
System.out.println("Object Identifier-Index: .foo, .foo.bar");
JsonQuery.parse("{\"foo\": 1, \"bar\": 2} | reduce . as {(\"foo\", \"bar\"): $item} (0; . + $item, 2 * . * $item)")
.run(null)
.forEach(System.out::println);
JsonQuery.parse(".[] as {$a, $b, c: {$d, $e}} ?// {$a, $b, c: {$d, $e}} | {$a, $b, $d, $e}")
.run(JsonValue.parse("[{\"a\": 1, \"b\": 2, \"c\": {\"d\": 3, \"e\": 4}}, {\"a\": 1, \"b\": 2, \"c\": [{\"d\": 3, \"e\": 4}]}]"))
.forEach(System.out::println);
JsonQuery.parse(".foo")
.run(JSON."{\"foo\": 42, \"bar\": \"less interesting data\"}")
.forEach(System.out::println);
JsonQuery.parse(".foo")
.run(JSON."{\"notfoo\": true, \"alsonotfoo\": false}")
.forEach(System.out::println);
JsonQuery.parse(".[\"foo\"]")
.run(JSON."{\"foo\": 42}")
.forEach(System.out::println);
JsonQuery.parse(".foo?")
.run(JSON."{\"foo\": 42, \"bar\": \"less interesting data\"}")
.forEach(System.out::println);
System.out.println();
System.out.println("Optional Object Identifier-Index: .foo?");
JsonQuery.parse(".foo?")
.run(JSON."{\"notfoo\": true, \"alsonotfoo\": false}")
.forEach(System.out::println);
JsonQuery.parse(".[\"foo\"]?")
.run(JSON."{\"foo\": 42}")
.forEach(System.out::println);
JsonQuery.parse("[.foo?]")
.run(JSON."[1, 2]")
.forEach(System.out::println);
System.out.println();
System.out.println("Array Index: .[<number>]");
JsonQuery.parse(".[0]")
.run(JSON."[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]")
.forEach(System.out::println);
JsonQuery.parse(".[2]")
.run(JSON."[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]")
.forEach(System.out::println);
JsonQuery.parse(".[-2]")
.run(JSON."[1, 2, 3]")
.forEach(System.out::println);
System.out.println();
System.out.println("Array/String Slice: .[<number>:<number>]");
JsonQuery.parse(".[2:4]")
.run(JSON."[\"a\",\"b\",\"c\",\"d\",\"e\"]")
.forEach(System.out::println);
JsonQuery.parse(".[2:4]")
.run(JSON."\"abcdefghi\"")
.forEach(System.out::println);
JsonQuery.parse(".[:3]")
.run(JSON."[\"a\",\"b\",\"c\",\"d\",\"e\"]")
.forEach(System.out::println);
JsonQuery.parse(".[-2:]")
.run(JSON."[\"a\",\"b\",\"c\",\"d\",\"e\"]")
.forEach(System.out::println);
System.out.println();
System.out.println("Array/Object Value Iterator: .[]");
JsonQuery.parse(".[]")
.run(JSON."[{\"name\":\"JSON\", \"good\":true}, {\"name\":\"XML\", \"good\":false}]")
.forEach(System.out::println);
JsonQuery.parse(".[]")
.run(JSON."[]")
.forEach(System.out::println);
JsonQuery.parse(".foo[]")
.run(JSON."{\"foo\":[1,2,3]}")
.forEach(System.out::println);
JsonQuery.parse(".[]")
.run(JSON."{\"a\": 1, \"b\": 1}")
.forEach(System.out::println);
// var root = JsonValue.valueOf(List.of(
// List.of(
// List.of(10, 20),
// List.of(30, 40)
// ),
// List.of(
// List.of(1, 2),
// List.of(3, 4)
// )
// ));
//
// var key = new JQCommaExpression(new JQConstant(new JsonNumber(0)), new JQConstant(new JsonNumber(1)));
//
// var pattern = new ArrayPattern(List.of(
// new JQAsExpression.Pattern.ObjectPattern(of(
// key, new JQAsExpression.Pattern.ObjectPattern(of(
// key,
// new JQAsExpression.Pattern.ValuePattern("$x")
// ))
// )),
// new JQAsExpression.Pattern.ObjectPattern(of(
// key, new JQAsExpression.Pattern.ObjectPattern(of(
// key,
// new JQAsExpression.Pattern.ValuePattern("$y")
// ))
// ))
// ));
//
// var expression = new JQBinaryExpression(
// new JQVariableExpression("$x"),
// new JQVariableExpression("$y"),
// JQBinaryExpression.Operator.ADD
// );
//
// var as = new JQAsExpression(new JQConstant(root), List.of(pattern), expression);
// as.evaluate(new JQExpression.Context(null)).forEach(System.out::println);
//
// var parser = new JQParser("[{(0, 1): {(0,1): $x}}, {(0, 1): {(0, 1): $y}}]");
// var pattern2 = parser.parsePattern();
// System.out.println(pattern);
// System.out.println(pattern2);
}
private static <K, V> SequencedMap<K, V> of(K key1, V value1) {
var map = new LinkedHashMap<K, V>();
map.put(key1, value1);
return map;
}
}

View File

@ -0,0 +1,42 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Queue;
final class ConcatGenerator<T> implements Generator<T> {
private final @NotNull Queue<@NotNull Generator<? extends T>> generators;
public ConcatGenerator(@NotNull List<@NotNull Generator<? extends T>> generators) {
this.generators = new LinkedList<>(generators);
}
@Override
public boolean hasNext() {
while (!generators.isEmpty()) {
var current = generators.peek();
if (!current.hasNext()) {
generators.remove();
} else {
return true;
}
}
return false;
}
@Override
public T next() {
while (!generators.isEmpty()) {
var current = generators.peek();
if (!current.hasNext()) {
generators.remove();
} else {
return current.next();
}
}
throw new NoSuchElementException();
}
}

View File

@ -0,0 +1,17 @@
package eu.jonahbauer.json.query.impl;
import java.util.NoSuchElementException;
enum EmptyGenerator implements Generator<Object> {
INSTANCE;
@Override
public boolean hasNext() {
return false;
}
@Override
public Object next() {
throw new NoSuchElementException();
}
}

View File

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

View File

@ -0,0 +1,32 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
final class FlatMappingGenerator<T, S> implements Generator<S> {
private final @NotNull Generator<T> source;
private final @NotNull Function<? super T, ? extends Generator<? extends S>> function;
private @NotNull Generator<? extends S> current = Generator.empty();
public FlatMappingGenerator(@NotNull Generator<T> source, @NotNull Function<? super T, ? extends Generator<? extends S>> function) {
this.source = source;
this.function = function;
}
@Override
public boolean hasNext() {
while (!current.hasNext() && source.hasNext()) {
current = function.apply(source.next());
}
return current.hasNext();
}
@Override
public S next() {
while (!current.hasNext() && source.hasNext()) {
current = function.apply(source.next());
}
return current.next();
}
}

View File

@ -0,0 +1,96 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.function.*;
import java.util.stream.Stream;
public interface Generator<T> {
boolean hasNext();
T next() throws NoSuchElementException;
default void forEach(@NotNull Consumer<? super T> action) {
while (hasNext()) {
action.accept(next());
}
}
default T reduce(T seed, @NotNull BinaryOperator<T> operator) {
while (hasNext()) {
seed = operator.apply(seed, next());
}
return seed;
}
default boolean anyMatch(@NotNull Predicate<? super T> predicate) {
while (hasNext()) {
if (predicate.test(next())) return true;
}
return false;
}
default boolean allMatch(@NotNull Predicate<? super T> predicate) {
while (hasNext()) {
if (!predicate.test(next())) return false;
}
return true;
}
default @NotNull List<T> toList() {
var out = new ArrayList<T>();
while (hasNext()) {
out.add(next());
}
return Collections.unmodifiableList(out);
}
default @NotNull Generator<T> filter(@NotNull Predicate<? super T> predicate) {
return new FilterGenerator<>(this, predicate);
}
default @NotNull Generator<T> limit(int count) {
return new LimitGenerator<>(this, count);
}
default <S> @NotNull Generator<S> map(@NotNull Function<? super T, ? extends S> function) {
return new MappingGenerator<>(this, function);
}
default <S> @NotNull Generator<S> flatMap(@NotNull Function<? super T, ? extends Generator<? extends S>> function) {
return new FlatMappingGenerator<>(this, function);
}
default <S> @NotNull Generator<S> mapMulti(@NotNull BiConsumer<? super T, ? super Consumer<S>> function) {
return new MapMultiGenerator<>(this, function);
}
@SuppressWarnings("unchecked")
static <T> @NotNull Generator<T> empty() {
return (Generator<T>) EmptyGenerator.INSTANCE;
}
static <T> @NotNull Generator<T> concat(@NotNull Generator<? extends T> first, @NotNull Generator<? extends T> second) {
return new ConcatGenerator<>(List.of(first, second));
}
static <T> @NotNull Generator<T> of(T value) {
return new SingletonGenerator<>(value);
}
static <T> @NotNull Generator<T> of(@NotNull Supplier<T> value) {
return new SupplierGenerator<>(value);
}
static <T> @NotNull Generator<T> from(@NotNull Iterable<T> value) {
return new IteratorGenerator<>(value.iterator());
}
static <T> @NotNull Generator<T> from(@NotNull Iterator<T> value) {
return new IteratorGenerator<>(value);
}
static <T> @NotNull Generator<T> from(@NotNull Stream<T> value) {
return new IteratorGenerator<>(value.iterator());
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.function.Function;
final class MappingGenerator<T, S> implements Generator<S> {
private final @NotNull Generator<T> delegate;
private final @NotNull Function<? super T, ? extends S> function;
public MappingGenerator(@NotNull Generator<T> delegate, @NotNull Function<? super T, ? extends S> function) {
this.delegate = Objects.requireNonNull(delegate);
this.function = Objects.requireNonNull(function);
}
@Override
public boolean hasNext() {
return delegate.hasNext();
}
@Override
public S next() {
return function.apply(delegate.next());
}
}

View File

@ -0,0 +1,27 @@
package eu.jonahbauer.json.query.impl;
import java.util.NoSuchElementException;
final class SingletonGenerator<T> implements Generator<T> {
private boolean done;
private T value;
public SingletonGenerator(T value) {
this.value = value;
}
@Override
public boolean hasNext() {
return !done;
}
@Override
public T next() {
if (done) throw new NoSuchElementException();
var out = value;
done = true;
value = null;
return out;
}
}

View File

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

View File

@ -0,0 +1,668 @@
package eu.jonahbauer.json.query.parser;
import eu.jonahbauer.json.*;
import eu.jonahbauer.json.query.JsonQueryParserException;
import eu.jonahbauer.json.query.parser.ast.*;
import eu.jonahbauer.json.query.parser.tokenizer.JQToken;
import eu.jonahbauer.json.query.parser.tokenizer.JQTokenKind;
import eu.jonahbauer.json.query.parser.tokenizer.JQTokenizer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.Reader;
import java.util.*;
import java.util.function.BinaryOperator;
public class JQParser {
private final @NotNull JQTokenizer tokenizer;
private final @NotNull Queue<JQToken> pushback = new LinkedList<>();
public JQParser(@NotNull String string) {
this.tokenizer = new JQTokenizer(string);
}
public JQParser(@NotNull Reader reader) {
this.tokenizer = new JQTokenizer(reader);
}
public @NotNull JQProgram parseTopLevel() throws IOException {
var module = parseModule();
var imports = parseImports();
var functions = parseFuncDefs();
var expr = peek() == null ? null : parseExpression();
return new JQProgram(module, imports, functions, expr);
}
private @Nullable JQModule parseModule() throws IOException {
if (!tryConsume(JQTokenKind.MODULE)) return null;
var metadata = parseExpression();
consume(JQTokenKind.SEMICOLON);
return new JQModule(metadata);
}
private @NotNull List<@NotNull JQImport> parseImports() throws IOException {
var out = new ArrayList<JQImport>();
while (peek(JQTokenKind.IMPORT) || peek(JQTokenKind.INCLUDE)) {
out.add(parseImport());
}
return out;
}
private @NotNull JQImport parseImport() throws IOException {
var include = tryConsume(JQTokenKind.INCLUDE);
if (!include) consume(JQTokenKind.IMPORT);
var path = parseString();
String as;
if (include) {
as = null;
} else {
consume(JQTokenKind.AS);
var dollar = tryConsume(JQTokenKind.DOLLAR);
var ident = consume(JQTokenKind.IDENT);
as = (dollar ? "$" : "") + ident.text();
}
if (tryConsume(JQTokenKind.SEMICOLON)) {
return new JQImport(path, as, null);
} else {
var metadata = parseExpression();
consume(JQTokenKind.SEMICOLON);
return new JQImport(path, as, metadata);
}
}
private @NotNull List<@NotNull JQFunction> parseFuncDefs() throws IOException {
var out = new ArrayList<JQFunction>();
while (peek(JQTokenKind.DEF)) {
out.add(parseFuncDef());
}
return out;
}
private @NotNull JQFunction parseFuncDef() throws IOException {
consume(JQTokenKind.DEF);
var name = consume(JQTokenKind.IDENT).text();
var params = new ArrayList<String>();
if (tryConsume(JQTokenKind.LPAREN)) {
do {
var dollar = tryConsume(JQTokenKind.DOLLAR);
var param = consume(JQTokenKind.IDENT).text();
params.add((dollar ? "$" : "") + param);
} while (tryConsume(JQTokenKind.SEMICOLON));
consume(JQTokenKind.RPAREN);
}
consume(JQTokenKind.COLON);
var body = parseExpression();
consume(JQTokenKind.SEMICOLON);
return new JQFunction(name, params, body);
}
public @NotNull JQExpression parseExpression() throws IOException {
if (peek(JQTokenKind.DEF)) {
var function = parseFuncDef();
var expr = parsePipeExpression();
return new JQFunctionDefinition(function, expr);
} else if (peek(JQTokenKind.LABEL)) {
} else {
return parsePipeExpression();
}
throw new UnsupportedOperationException("not yet implemented");
}
private @NotNull JQExpression parseNonAssocBinaryExpression(
@NotNull Map<@NotNull JQTokenKind, @NotNull BinaryOperator<@NotNull JQExpression>> operations,
@NotNull Downstream next
) throws IOException {
var first = next.get();
var operator = peek();
var operation = operator == null ? null : operations.get(operator.kind());
if (operation != null) {
next();
var second = next.get();
first = operation.apply(first, second);
}
return first;
}
private @NotNull JQExpression parseLeftAssocBinaryExpression(
@NotNull Map<@NotNull JQTokenKind, @NotNull BinaryOperator<@NotNull JQExpression>> operations,
@NotNull Downstream next
) throws IOException {
var first = next.get();
while (true) {
var operator = peek();
var operation = operator == null ? null : operations.get(operator.kind());
if (operation == null) break;
next();
var second = next.get();
first = operation.apply(first, second);
}
return first;
}
private interface Downstream {
@NotNull
JQExpression get() throws IOException;
}
private @NotNull JQExpression parsePipeExpression() throws IOException {
return parseLeftAssocBinaryExpression(Map.of(
JQTokenKind.PIPE, JQPipeExpression::new
), this::parseCommaExpression);
}
private @NotNull JQExpression parseCommaExpression() throws IOException {
return parseLeftAssocBinaryExpression(Map.of(
JQTokenKind.COMMA, JQCommaExpression::new
), this::parseAlternativeExpression);
}
private @NotNull JQExpression parseAlternativeExpression() throws IOException {
return parseLeftAssocBinaryExpression(Map.of(
JQTokenKind.DEFINEDOR, JQAlternativeExpression::new
), this::parseAssignment);
}
private @NotNull JQExpression parseAssignment() throws IOException {
return parseNonAssocBinaryExpression(Map.of(
JQTokenKind.ASSIGN, JQAssignment::new,
JQTokenKind.SETPLUS, JQAssignment::add,
JQTokenKind.SETMINUS, JQAssignment::sub,
JQTokenKind.SETMULT, JQAssignment::mul,
JQTokenKind.SETDIV, JQAssignment::div,
JQTokenKind.SETMOD, JQAssignment::mod,
JQTokenKind.SETPIPE, JQAssignmentPipe::new,
JQTokenKind.SETDEFINEDOR, JQAssignmentCoerce::new
), this::parseBooleanOrExpression);
}
private @NotNull JQExpression parseBooleanOrExpression() throws IOException {
return parseLeftAssocBinaryExpression(Map.of(
JQTokenKind.OR, JQBooleanOrExpression::new
), this::parseBooleanAndExpression);
}
private @NotNull JQExpression parseBooleanAndExpression() throws IOException {
return parseLeftAssocBinaryExpression(Map.of(
JQTokenKind.AND, JQBooleanAndExpression::new
), this::parseComparisonExpression);
}
private @NotNull JQExpression parseComparisonExpression() throws IOException {
return parseNonAssocBinaryExpression(Map.of(
JQTokenKind.EQ, JQBinaryExpression::eq,
JQTokenKind.NEQ, JQBinaryExpression::neq,
JQTokenKind.LESS, JQBinaryExpression::lt,
JQTokenKind.GREATER, JQBinaryExpression::gt,
JQTokenKind.LESSEQ, JQBinaryExpression::leq,
JQTokenKind.GREATEREQ, JQBinaryExpression::geq
), this::parseAdditiveExpression);
}
private @NotNull JQExpression parseAdditiveExpression() throws IOException {
return parseLeftAssocBinaryExpression(Map.of(
JQTokenKind.PLUS, JQBinaryExpression::add,
JQTokenKind.MINUS, JQBinaryExpression::sub
), this::parseMultiplicativeExpression);
}
private @NotNull JQExpression parseMultiplicativeExpression() throws IOException {
return parseLeftAssocBinaryExpression(Map.of(
JQTokenKind.MULT, JQBinaryExpression::mul,
JQTokenKind.DIV, JQBinaryExpression::div,
JQTokenKind.MOD, JQBinaryExpression::mod
), this::parseTryCatch);
}
private @NotNull JQExpression parseTryCatch() throws IOException {
if (tryConsume(JQTokenKind.TRY)) {
var expr = parseNegation();
var fallback = tryConsume(JQTokenKind.CATCH) ? parseNegation() : null;
return new JQTryExpression(expr, fallback);
} else {
return parseNegation();
}
}
private @NotNull JQExpression parseNegation() throws IOException {
if (tryConsume(JQTokenKind.MINUS)) {
var expr = parseErrorSuppression();
return new JQNegation(expr);
} else {
return parseErrorSuppression();
}
}
private @NotNull JQExpression parseErrorSuppression() throws IOException {
var expression = parseControlFlow();
if (tryConsume(JQTokenKind.QUESTION_MARK)) {
return new JQTryExpression(expression);
} else {
return expression;
}
}
private @NotNull JQExpression parseControlFlow() throws IOException {
if (tryConsume(JQTokenKind.REDUCE)) {
var expr = parseTerm();
consume(JQTokenKind.AS);
var patterns = parsePatterns();
consume(JQTokenKind.LPAREN);
var init = parseExpression();
consume(JQTokenKind.SEMICOLON);
var update = parseExpression();
consume(JQTokenKind.RPAREN);
return new JQReduceExpression(expr, patterns, init, update);
} else if (tryConsume(JQTokenKind.FOREACH)) {
var expr = parseTerm();
consume(JQTokenKind.AS);
var patterns = parsePatterns();
consume(JQTokenKind.LPAREN);
var init = parseExpression();
consume(JQTokenKind.SEMICOLON);
var update = parseExpression();
var extract = tryConsume(JQTokenKind.SEMICOLON) ? parseExpression() : new JQRootExpression();
consume(JQTokenKind.RPAREN);
return new JQForEachExpression(expr, patterns, init, update, extract);
} else if (tryConsume(JQTokenKind.IF)) {;
var conds = new ArrayList<JQExpression>();
var thens = new ArrayList<JQExpression>();
do {
conds.add(parseExpression());
consume(JQTokenKind.THEN);
thens.add(parseExpression());
} while (tryConsume(JQTokenKind.ELSE_IF));
var otherwise = tryConsume(JQTokenKind.ELSE) ? parseExpression() : null;
consume(JQTokenKind.END);
var out = new JQIfExpression(conds.removeLast(), thens.removeLast(), otherwise);
while (!conds.isEmpty()) {
out = new JQIfExpression(conds.removeLast(), thens.removeLast(), out);
}
return out;
} else {
var term = parseTerm();
if (tryConsume(JQTokenKind.AS)) {
var patterns = parsePatterns();
consume(JQTokenKind.PIPE);
var expr = parseExpression();
return new JQAsExpression(term, patterns, expr);
}
return term;
}
}
private @NotNull JQPatterns parsePatterns() throws IOException {
var patterns = new ArrayList<JQPatterns.Pattern>();
patterns.add(parsePattern());
while (tryConsume(JQTokenKind.ALTERNATION)) {
patterns.add(parsePattern());
}
return new JQPatterns(patterns);
}
private @NotNull JQPatterns.Pattern parsePattern() throws IOException {
if (tryConsume(JQTokenKind.LBRACKET)) {
var patterns = new ArrayList<JQPatterns.Pattern>();
do {
patterns.add(parsePattern());
} while (tryConsume(JQTokenKind.COMMA));
consume(JQTokenKind.RBRACKET);
return new JQPatterns.Pattern.ArrayPattern(patterns);
} else if (tryConsume(JQTokenKind.LBRACE)) {
var patterns = new LinkedHashMap<JQExpression, JQPatterns.Pattern>();
do {
if (tryConsume(JQTokenKind.DOLLAR)) {
var ident = consume(JQTokenKind.IDENT).text();
var pattern = new JQPatterns.Pattern.ValuePattern("$" + ident);
patterns.put(new JQConstant(JsonString.valueOf(ident)), pattern);
} else if (peek(JQTokenKind.IDENT)) {
var ident = consume(JQTokenKind.IDENT).text();
consume(JQTokenKind.COLON);
var pattern = parsePattern();
patterns.put(new JQConstant(JsonString.valueOf(ident)), pattern);
} else if (tryConsume(JQTokenKind.LPAREN)) {
var expr = parseExpression();
consume(JQTokenKind.RPAREN);
consume(JQTokenKind.COLON);
var pattern = parsePattern();
patterns.put(new JQParenthesizedExpression(expr), pattern);
} else {
var key = parseString();
consume(JQTokenKind.COLON);
var pattern = parsePattern();
patterns.put(key, pattern);
}
} while (tryConsume(JQTokenKind.COMMA));
consume(JQTokenKind.RBRACE);
return new JQPatterns.Pattern.ObjectPattern(patterns);
} else {
consume(JQTokenKind.DOLLAR);
var ident = consume(JQTokenKind.IDENT).text();
return new JQPatterns.Pattern.ValuePattern("$" + ident);
}
}
private @NotNull JQExpression parseTerm() throws IOException {
var next = peek();
var term = switch (next == null ? null : next.kind()) {
case NUMBER -> new JQConstant(new JsonNumber(Objects.requireNonNull(next().nval())));
case LPAREN -> parseParenthesizedExpression();
case LBRACKET -> parseArrayConstructionExpression();
case LBRACE -> parseObjectConstructionExpression();
case QQSTRING_START -> parseString();
case FORMAT -> throw new UnsupportedOperationException("not yet implemented");
case BREAK -> {
consume(JQTokenKind.IDENT);
consume(JQTokenKind.DOLLAR);
var label = consume(JQTokenKind.IDENT).text();
throw new UnsupportedOperationException("not yet implemented");
}
case IDENT -> switch (Objects.requireNonNull(peek()).text()) {
case "null" -> {
consume(JQTokenKind.IDENT);
yield new JQConstant(null);
}
case "true" -> {
consume(JQTokenKind.IDENT);
yield new JQConstant(JsonBoolean.TRUE);
}
case "false" -> {
consume(JQTokenKind.IDENT);
yield new JQConstant(JsonBoolean.FALSE);
}
default -> parseFunctionInvocation();
};
case LOC -> {
var loc = consume(JQTokenKind.LOC);
yield new JQLocExpression("<top-level>", loc.line());
}
case DOLLAR -> parseVariableExpression();
case FIELD -> {
var name = consume(JQTokenKind.FIELD).text().substring(1);
var optional = tryConsume(JQTokenKind.QUESTION_MARK);
yield new JQIndexExpression(new JQRootExpression(), name, optional);
}
case DOT -> {
consume(JQTokenKind.DOT);
if (peek(JQTokenKind.QQSTRING_START)) {
var name = parseString();
var optional = tryConsume(JQTokenKind.QUESTION_MARK);
yield new JQIndexExpression(new JQRootExpression(), name, optional);
} else {
yield new JQRootExpression();
}
}
case REC -> {
consume(JQTokenKind.REC);
yield new JQRecursionExpression();
}
case null -> throw new JsonQueryParserException(-1, -1, "unexpected end of file");
default -> {
var token = Objects.requireNonNull(peek());
throw new JsonQueryParserException(token.line(), token.column(), "unexpected token " + token.kind());
}
};
while (peek(JQTokenKind.LBRACKET) || peek(JQTokenKind.DOT) || peek(JQTokenKind.FIELD)) {
term = parseIndexingExpression(term);
}
return term;
}
private @NotNull JQExpression parseFunctionInvocation() throws IOException {
var name = consume(JQTokenKind.IDENT).text();
var args = new ArrayList<JQExpression>();
if (tryConsume(JQTokenKind.LPAREN)) {
do {
args.add(parseExpression());
} while (tryConsume(JQTokenKind.SEMICOLON));
consume(JQTokenKind.RPAREN);
}
return new JQFunctionInvocation(name, args);
}
private @NotNull JQVariableExpression parseVariableExpression() throws IOException {
consume(JQTokenKind.DOLLAR);
var ident = consume(JQTokenKind.IDENT).text();
return new JQVariableExpression("$" + ident);
}
private @NotNull JQExpression parseParenthesizedExpression() throws IOException {
consume(JQTokenKind.LPAREN);
var expression = parseExpression();
consume(JQTokenKind.RPAREN);
return new JQParenthesizedExpression(expression);
}
private @NotNull JQExpression parseArrayConstructionExpression() throws IOException {
consume(JQTokenKind.LBRACKET);
if (tryConsume(JQTokenKind.RBRACKET)) {
return new JQConstant(JsonArray.EMPTY);
} else {
var expression = parseExpression();
consume(JQTokenKind.RBRACKET);
return new JQArrayConstructionExpression(expression);
}
}
private @NotNull JQExpression parseObjectConstructionExpression() throws IOException {
consume(JQTokenKind.LBRACE);
if (tryConsume(JQTokenKind.RBRACE)) {
return new JQConstant(JsonObject.EMPTY);
}
var entries = new ArrayList<JQObjectConstructionExpression.Entry>();
while (!tryConsume(JQTokenKind.RBRACE)) {
if (!entries.isEmpty()) {
consume(JQTokenKind.COMMA);
}
if (peek(JQTokenKind.LOC)) {
var loc = consume(JQTokenKind.LOC);
entries.add(new JQObjectConstructionExpression.Entry(
new JQConstant(JsonValue.valueOf("__loc__")),
new JQLocExpression("<top-level>", loc.line())
));
continue;
}
var next = peek();
JQExpression key;
JQExpression value = null;
switch (next == null ? null : next.kind()) {
case IDENT -> {
key = new JQConstant(JsonString.valueOf(consume(JQTokenKind.IDENT).text()));
value = new JQIndexExpression(new JQRootExpression(), key, false);
}
case QQSTRING_START -> {
key = parseString();
value = new JQIndexExpression(new JQRootExpression(), key, false);
}
case DOLLAR -> {
var var = parseVariableExpression();
key = new JQConstant(JsonString.valueOf(var.name().substring(1)));
value = var;
}
case LPAREN -> {
key = parseParenthesizedExpression();
}
case AS, IMPORT, INCLUDE, MODULE, DEF, IF, THEN, ELSE, ELSE_IF, AND, OR, END, REDUCE, FOREACH, TRY,
CATCH, LABEL, BREAK -> {
key = new JQConstant(JsonString.valueOf(consume(next.kind()).text()));
value = key;
}
case null -> throw new JsonQueryParserException(-1, -1, "unexpected end of file");
default -> {
var token = Objects.requireNonNull(next);
throw new JsonQueryParserException(token.line(),
token.column(),
"unexpected token " + token.kind()
);
}
}
if (value == null) {
consume(JQTokenKind.COLON);
} else if (!tryConsume(JQTokenKind.COLON)) {
entries.add(new JQObjectConstructionExpression.Entry(key, value));
continue;
}
value = parseLeftAssocBinaryExpression(Map.of(JQTokenKind.PIPE, JQPipeExpression::new), this::parseNegation);
entries.add(new JQObjectConstructionExpression.Entry(key, value));
}
return new JQObjectConstructionExpression(entries);
}
private @NotNull JQExpression parseIndexingExpression(@NotNull JQExpression root) throws IOException {
if (tryConsume(JQTokenKind.LBRACKET)) {
if (tryConsume(JQTokenKind.RBRACKET)) {
var optional = tryConsume(JQTokenKind.QUESTION_MARK);
return new JQIterateExpression(root, optional);
} else if (tryConsume(JQTokenKind.COLON)) {
var end = parseExpression();
consume(JQTokenKind.RBRACKET);
var optional = tryConsume(JQTokenKind.QUESTION_MARK);
return new JQSliceExpression(root, null, end, optional);
} else {
var expr = parseExpression();
if (tryConsume(JQTokenKind.COLON)) {
var end = peek(JQTokenKind.RBRACKET) ? null : parseExpression();
consume(JQTokenKind.RBRACKET);
var optional = tryConsume(JQTokenKind.QUESTION_MARK);
return new JQSliceExpression(root, expr, end, optional);
} else {
consume(JQTokenKind.RBRACKET);
var optional = tryConsume(JQTokenKind.QUESTION_MARK);
return new JQIndexExpression(root, expr, optional);
}
}
} else if (peek(JQTokenKind.FIELD)) {
var name = consume(JQTokenKind.FIELD).text().substring(1);
var optional = tryConsume(JQTokenKind.QUESTION_MARK);
return new JQIndexExpression(root, name, optional);
} else {
consume(JQTokenKind.DOT);
var name = parseString();
var optional = tryConsume(JQTokenKind.QUESTION_MARK);
return new JQIndexExpression(root, name, optional);
}
}
private @NotNull JQStringInterpolation parseString() throws IOException {
var format = peek(JQTokenKind.FORMAT) ? next().text() : null;
var current = new StringBuilder();
var fragments = new ArrayList<String>();
var values = new ArrayList<JQExpression>();
consume(JQTokenKind.QQSTRING_START);
current.setLength(0);
while (peek(JQTokenKind.QQSTRING_TEXT)) {
current.append(next().sval());
}
fragments.add(current.toString());
while (tryConsume(JQTokenKind.QQSTRING_INTERP_START)) {
values.add(parseExpression());
consume(JQTokenKind.QQSTRING_INTERP_END);
current.setLength(0);
while (peek(JQTokenKind.QQSTRING_TEXT)) {
current.append(next().sval());
}
fragments.add(current.toString());
}
consume(JQTokenKind.QQSTRING_END);
return new JQStringInterpolation(format, fragments, values);
}
private void consume(@NotNull String ident) throws IOException {
var token = next();
if (token.kind() != JQTokenKind.IDENT || !Objects.equals(ident, token.text())) {
throw new JsonQueryParserException(0, 0, "");
}
}
private @NotNull JQToken consume(@NotNull JQTokenKind kind) throws IOException {
var token = next();
if (token.kind() != kind) throw new JsonQueryParserException(token.line(), token.column(), "unexpected " + token.kind() + " expected " + kind);
return token;
}
private boolean tryConsume(@NotNull String ident) throws IOException {
var out = peek(ident);
if (out) next();
return out;
}
private boolean tryConsume(@NotNull JQTokenKind kind) throws IOException {
var out = peek(kind);
if (out) next();
return out;
}
private boolean peek(@NotNull String ident) throws IOException {
var token = peek();
return token != null && token.kind() == JQTokenKind.IDENT && Objects.equals(ident, token.text());
}
private boolean peek(@NotNull JQTokenKind kind) throws IOException {
var token = peek();
return token != null && token.kind() == kind;
}
private @Nullable JQToken peek() throws IOException {
if (pushback.isEmpty()) {
var next = tokenizer.next();
pushback(next);
return next;
} else {
return pushback.peek();
}
}
private @NotNull JQToken next() throws IOException {
if (pushback.isEmpty()) {
var next = tokenizer.next();
if (next == null) throw new JsonQueryParserException(0, 0, "unexpected $end");
return next;
} else {
return pushback.remove();
}
}
private void pushback(@Nullable JQToken token) {
pushback.add(token);
}
}

View File

@ -0,0 +1,73 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.NoSuchElementException;
import java.util.Objects;
@SuppressWarnings("preview")
public record JQAlternativeExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
public JQAlternativeExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
var first = this.first.evaluate(context);
var second = this.second.evaluate(context);
return new Generator<>() {
private boolean empty = true;
private JsonValue value;
private boolean hasValue;
private boolean advance() {
if (hasValue) return true;
while (first.hasNext()) {
var value = first.next();
if (JsonMath.isTruthy(value)) {
this.empty = false;
this.value = value;
this.hasValue = true;
return true;
}
}
if (empty && second.hasNext()) {
this.value = second.next();
this.hasValue = true;
return true;
}
return false;
}
@Override
public boolean hasNext() {
return advance();
}
@Override
public @Nullable JsonValue next() throws NoSuchElementException {
if (!advance()) throw new NoSuchElementException();
var out = value;
hasValue = false;
value = null;
return out;
}
};
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return first + " // " + second;
}
}

View File

@ -0,0 +1,30 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonArray;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public record JQArrayConstructionExpression(@NotNull JQExpression expression) implements JQExpression {
public JQArrayConstructionExpression {
Objects.requireNonNull(expression);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return Generator.of(() -> new JsonArray(expression.evaluate(context).toList()));
}
@Override
public boolean isConstant() {
return expression.isConstant();
}
@Override
public @NotNull String toString() {
return "[" + expression + "]";
}
}

View File

@ -0,0 +1,36 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public record JQAsExpression(
@NotNull JQExpression variable, @NotNull JQPatterns patterns, @NotNull JQExpression expression
) implements JQExpression {
public JQAsExpression {
Objects.requireNonNull(variable);
Objects.requireNonNull(expression);
Objects.requireNonNull(patterns);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return variable.evaluate(context).flatMap(value -> patterns.bind(context, value, expression::evaluate));
}
@Override
public boolean isConstant() {
return variable.isConstant() && expression.isConstant();
}
@Override
public @NotNull String toString() {
return variable + " as " + patterns + " | " + expression;
}
}

View File

@ -0,0 +1,73 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.impl.Generator;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.function.BinaryOperator;
import java.util.stream.Stream;
public record JQAssignment(@NotNull JQExpression target, @NotNull JQExpression value, @NotNull Operator operator) implements JQExpression {
public static @NotNull JQAssignment add(@NotNull JQExpression target, @NotNull JQExpression value) {
return new JQAssignment(target, value, Operator.ADD);
}
public static @NotNull JQAssignment sub(@NotNull JQExpression target, @NotNull JQExpression value) {
return new JQAssignment(target, value, Operator.SUB);
}
public static @NotNull JQAssignment mul(@NotNull JQExpression target, @NotNull JQExpression value) {
return new JQAssignment(target, value, Operator.MUL);
}
public static @NotNull JQAssignment div(@NotNull JQExpression target, @NotNull JQExpression value) {
return new JQAssignment(target, value, Operator.DIV);
}
public static @NotNull JQAssignment mod(@NotNull JQExpression target, @NotNull JQExpression value) {
return new JQAssignment(target, value, Operator.MOD);
}
public JQAssignment {
Objects.requireNonNull(target);
Objects.requireNonNull(value);
Objects.requireNonNull(operator);
}
public JQAssignment(@NotNull JQExpression target, @NotNull JQExpression value) {
this(target, value, Operator.ID);
}
@Override
public @NotNull Generator<JsonValue> evaluate(@NotNull Context context) {
throw new UnsupportedOperationException("not yet implemented");
}
@Override
public boolean isConstant() {
throw new UnsupportedOperationException("not yet implemented");
}
@Override
public @NotNull String toString() {
return STR."\{target} \{operator.symbol} \{value}";
}
@RequiredArgsConstructor
public enum Operator {
ID((_, value) -> value, "="),
ADD(JsonMath::add, "+="),
SUB(JsonMath::sub, "-="),
MUL(JsonMath::mul, "*="),
DIV(JsonMath::div, "/="),
MOD(JsonMath::mod, "%="),
;
private final @NotNull BinaryOperator<@Nullable JsonValue> operator;
private final @NotNull String symbol;
}
}

View File

@ -0,0 +1,31 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.stream.Stream;
public record JQAssignmentCoerce(@NotNull JQExpression target, @NotNull JQExpression value) implements JQExpression {
public JQAssignmentCoerce {
Objects.requireNonNull(target);
Objects.requireNonNull(value);
}
@Override
public @NotNull Generator<JsonValue> evaluate(@NotNull Context context) {
throw new UnsupportedOperationException("not yet implemented");
}
@Override
public boolean isConstant() {
throw new UnsupportedOperationException("not yet implemented");
}
@Override
public @NotNull String toString() {
return target + " //= " + value;
}
}

View File

@ -0,0 +1,30 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public record JQAssignmentPipe(@NotNull JQExpression target, @NotNull JQExpression value) implements JQExpression {
public JQAssignmentPipe {
Objects.requireNonNull(target);
Objects.requireNonNull(value);
}
@Override
public @NotNull Generator<JsonValue> evaluate(@NotNull Context context) {
throw new UnsupportedOperationException("not yet implemented");
}
@Override
public boolean isConstant() {
throw new UnsupportedOperationException("not yet implemented");
}
@Override
public @NotNull String toString() {
return target + " |= " + value;
}
}

View File

@ -0,0 +1,105 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.impl.Generator;
import eu.jonahbauer.json.query.util.Util;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.function.BinaryOperator;
public record JQBinaryExpression(@NotNull JQExpression first, @NotNull JQExpression second, @NotNull Operator operator) implements JQExpression {
public static @NotNull JQBinaryExpression add(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.ADD);
}
public static @NotNull JQBinaryExpression sub(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.SUB);
}
public static @NotNull JQBinaryExpression mul(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.MUL);
}
public static @NotNull JQBinaryExpression div(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.DIV);
}
public static @NotNull JQBinaryExpression mod(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.MOD);
}
public static @NotNull JQBinaryExpression eq(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.EQ);
}
public static @NotNull JQBinaryExpression neq(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.NEQ);
}
public static @NotNull JQBinaryExpression lt(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.LT);
}
public static @NotNull JQBinaryExpression gt(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.GT);
}
public static @NotNull JQBinaryExpression leq(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.LEQ);
}
public static @NotNull JQBinaryExpression geq(@NotNull JQExpression first, @NotNull JQExpression second) {
return new JQBinaryExpression(first, second, Operator.GEQ);
}
public JQBinaryExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
Objects.requireNonNull(operator);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return Util.crossReversed(List.of(first, second), context)
.map(values -> operator.apply(values.getFirst(), values.getLast()));
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return STR."\{first} \{operator.symbol} \{second}";
}
@RequiredArgsConstructor
public enum Operator implements BinaryOperator<@Nullable JsonValue> {
ADD(JsonMath::add, "+"),
SUB(JsonMath::sub, "-"),
MUL(JsonMath::mul, "*"),
DIV(JsonMath::div, "/"),
MOD(JsonMath::mod, "%"),
EQ(JsonMath::eq, "=="),
NEQ(JsonMath::neq, "!="),
LT(JsonMath::lt, "<"),
GT(JsonMath::gt, ">"),
LEQ(JsonMath::leq, "<="),
GEQ(JsonMath::geq, ">="),
;
private final @NotNull BinaryOperator<@Nullable JsonValue> operator;
private final @NotNull String symbol;
@Override
public @Nullable JsonValue apply(@Nullable JsonValue value, @Nullable JsonValue value2) {
return operator.apply(value, value2);
}
}
}

View File

@ -0,0 +1,36 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonBoolean;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public record JQBooleanAndExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
public JQBooleanAndExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
}
@Override
public @NotNull Generator<JsonValue> evaluate(@NotNull Context context) {
return first.evaluate(context)
.flatMap(value -> JsonMath.isFalsy(value)
? Generator.of(JsonBoolean.FALSE)
: second.evaluate(context).map(JsonMath::isTruthy).map(JsonBoolean::valueOf)
);
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return first + " and " + second;
}
}

View File

@ -0,0 +1,37 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonBoolean;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.stream.Stream;
public record JQBooleanOrExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
public JQBooleanOrExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
}
@Override
public @NotNull Generator<JsonValue> evaluate(@NotNull Context context) {
return first.evaluate(context)
.flatMap(value -> JsonMath.isTruthy(value)
? Generator.of(JsonBoolean.TRUE)
: second.evaluate(context).map(JsonMath::isTruthy).map(JsonBoolean::valueOf)
);
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return first + " or " + second;
}
}

View File

@ -0,0 +1,402 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.*;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.impl.Generator;
import eu.jonahbauer.json.query.parser.JQParser;
import eu.jonahbauer.json.query.parser.ast.JQExpression.Context;
import eu.jonahbauer.json.query.util.Util;
import lombok.SneakyThrows;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
public enum JQBuiltIn implements JQInvocable {
ABS(0, Implementation.map$V(JsonMath::abs)),
ADD(0, (context, _) -> context.stream().map(value -> JsonMath.values(value).reduce(null, JsonMath::add))),
NOT(0, Implementation.map$V(JsonMath::not)),
// error handling
ERROR$0(0, (context, _) -> context.stream().flatMap(JsonMath::error)),
ERROR$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(JsonMath::error)),
HALT(0, (_, _) -> Generator.of((JsonValue) null).flatMap(_ -> JsonMath.halt())),
HALT_ERROR$0(0, (context, _) -> context.stream().flatMap(JsonMath::halt)),
HALT_ERROR$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(value -> JsonMath.halt(context.root(), value))),
// stream operations
EMPTY(0, (_, _) -> Generator.empty()),
RANGE$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(JsonMath::range)),
RANGE$2(2, (context, args) -> Util.cross(args, context).flatMap(bounds -> JsonMath.range(bounds.get(0), bounds.get(1)))),
RANGE$3(3, (context, args) -> Util.cross(args, context).flatMap(bounds -> JsonMath.range(bounds.get(0), bounds.get(1), bounds.get(2)))),
LIMIT(2, (context, args) -> args.getFirst().evaluate(context).flatMap(limit -> JsonMath.limit(args.getLast().evaluate(context), limit))),
FIRST$1(1, (context, args) -> JsonMath.first(args.getFirst().evaluate(context))),
LAST$1(1, (context, args) -> JsonMath.last(args.getFirst().evaluate(context))),
NTH$2(2, (context, args) -> args.getFirst().evaluate(context).flatMap(n -> JsonMath.nth(args.getLast().evaluate(context), n))),
SELECT(1, Implementation.mapF$F(JsonMath::select)),
// loops
RECURSE$0(0, Implementation.map$F(JsonMath::recurse)),
RECURSE$1(1, Implementation.mapF$F(JsonMath::recurse)),
RECURSE$2(2, Implementation.mapFF$F(JsonMath::recurse)),
WALK$1(1, Implementation.mapF$F(JsonMath::walk)),
WHILE(2, Implementation.mapFF$F(JsonMath::while_)),
UNTIL(2, Implementation.mapFF$F(JsonMath::until)),
// iterable operations
MAP(1, Implementation.mapF$V(JsonMath::map)),
MAP_VALUES(1, Implementation.mapF$V(JsonMath::mapValues)),
KEYS(0, Implementation.map$V(JsonMath::keys)),
KEYS_UNSORTED(0, Implementation.map$V(JsonMath::keysUnsorted)),
HAS(1, Implementation.mapV$V(JsonMath::has)),
IN(1, Implementation.mapV$V(JsonMath::in)),
FIRST$0(0, Implementation.map$V(JsonMath::first)),
LAST$0(0, Implementation.map$V(JsonMath::last)),
NTH$1(1, Implementation.mapV$V(JsonMath::index)),
ANY$0(0, Implementation.map$V(JsonMath::any)),
ANY$1(1, Implementation.mapF$V(JsonMath::any)),
ANY$2(2, Implementation.mapFF$V((root, gen, filter) -> JsonMath.any(gen.apply(root), filter))),
ALL$0(0, Implementation.map$V(JsonMath::all)),
ALL$1(1, Implementation.mapF$V(JsonMath::all)),
ALL$2(2, Implementation.mapFF$V((root, gen, filter) -> JsonMath.all(gen.apply(root), filter))),
FLATTEN$0(0, Implementation.map$V(JsonMath::flatten)),
FLATTEN$1(1, Implementation.mapV$V(JsonMath::flatten)),
SORT(0, Implementation.map$V(JsonMath::sort)),
SORT_BY(1, Implementation.mapF$V(JsonMath::sort)),
MIN(0, Implementation.map$V(JsonMath::min)),
MIN_BY(1, Implementation.mapF$V(JsonMath::min)),
MAX(0, Implementation.map$V(JsonMath::max)),
MAX_BY(1, Implementation.mapF$V(JsonMath::max)),
UNIQUE(0, Implementation.map$V(JsonMath::unique)),
UNIQUE_BY(1, Implementation.mapF$V(JsonMath::unique)),
GROUP_BY(1, Implementation.mapF$V(JsonMath::group)),
REVERSE(0, Implementation.map$V(JsonMath::reverse)),
CONTAINS(1, Implementation.mapV$V(JsonMath::contains)),
INDICES(1, Implementation.mapV$V(JsonMath::indices)),
INDEX(1, Implementation.mapV$V(JsonMath::firstindex)),
RINDEX(1, Implementation.mapV$V(JsonMath::lastindex)),
INSIDE(1, Implementation.mapV$V(JsonMath::inside)),
COMBINATIONS$0(0, Implementation.map$F(JsonMath::combinations)),
COMBINATIONS$1(1, Implementation.mapV$F(JsonMath::combinations)),
BSEARCH(1, Implementation.mapV$V(JsonMath::bsearch)),
TRANSPOSE(0, Implementation.map$V(JsonMath::transpose)),
TO_ENTRIES(0, Implementation.map$V(JsonMath::toEntries)),
FROM_ENTRIES(0, Implementation.map$V(JsonMath::fromEntries)),
WITH_ENTRIES(1, Implementation.mapF$V(JsonMath::withEntries)),
// filters
ARRAYS(0, Implementation.filter(JsonMath::isArray0)),
OBJECTS(0, Implementation.filter(JsonMath::isObject0)),
ITERABLES(0, Implementation.filter(JsonMath::isIterable0)),
BOOLEANS(0, Implementation.filter(JsonMath::isBoolean0)),
NUMBERS(0, Implementation.filter(JsonMath::isNumber0)),
NORMALS(0, Implementation.filter(JsonMath::isNormal0)),
FINITES(0, Implementation.filter(JsonMath::isFinite0)),
STRINGS(0, Implementation.filter(JsonMath::isString0)),
NULLS(0, Implementation.filter(JsonMath::isNull0)),
VALUES(0, Implementation.filter(JsonMath::isValue0)),
SCALARS(0, Implementation.filter(JsonMath::isScalar0)),
// checks
ISINFINITE(0, Implementation.map$V(JsonMath::isInfinite)),
ISNAN(0, Implementation.map$V(JsonMath::isNan)),
ISFINITE(0, Implementation.map$V(JsonMath::isFinite)),
ISNORMAL(0, Implementation.map$V(JsonMath::isNormal)),
ISEMPTY(1, (context, args) -> Generator.of(() -> JsonMath.isEmpty(args.getFirst().evaluate(context)))),
// string operations
TRIM(0, Implementation.map$V(JsonMath::trim)),
LTRIM(0, Implementation.map$V(JsonMath::ltrim)),
RTRIM(0, Implementation.map$V(JsonMath::rtrim)),
LTRIMSTR(1, Implementation.mapV$V(JsonMath::ltrimstr)),
RTRIMSTR(1, Implementation.mapV$V(JsonMath::rtrimstr)),
SPLIT$1(1, Implementation.mapV$V(JsonMath::split)),
JOIN(1, Implementation.mapV$V(JsonMath::join)),
IMPLODE(0, Implementation.map$V(JsonMath::implode)),
EXPLODE(0, Implementation.map$V(JsonMath::explode)),
ASCII_UPCASE(0, Implementation.map$V(JsonMath::asciiUpcase)),
ASCII_DOWNCASE(0, Implementation.map$V(JsonMath::asciiDowncase)),
UTF8BYTELENGTH(0, Implementation.map$V(JsonMath::utf8ByteLength)),
STARTSWITH(1, Implementation.mapV$V(JsonMath::startswith)),
ENDSWITH(1, Implementation.mapV$V(JsonMath::endswith)),
// regex
TEST$1(1, JsonMath::test),
TEST$2(2, JsonMath::test),
MATCH$1(1, JsonMath::match),
MATCH$2(2, JsonMath::match),
CAPTURE$1(1, JsonMath::capture),
CAPTURE$2(2, JsonMath::capture),
SCAN$1(1, JsonMath::scan),
SCAN$2(2, JsonMath::scan),
SPLIT$2(2, JsonMath::split),
SPLITS$1(1, JsonMath::splits),
SPLITS$2(2, JsonMath::splits),
SUB$2(2, JsonMath::sub),
SUB$3(3, JsonMath::sub),
GSUB$2(2, JsonMath::gsub),
GSUB$3(3, JsonMath::gsub),
// conversions
TYPE(0, Implementation.map$V(JsonMath::type)),
TOJSON(0, Implementation.map$V(JsonMath::tojson)),
TOSTRING(0, Implementation.map$V(JsonMath::tostring)),
TONUMBER(0, Implementation.map$V(JsonMath::tonumber)),
FROMJSON(0, Implementation.map$V(JsonMath::fromjson)),
// time
FROMDATEISO8601(0, Implementation.map$V(JsonMath::fromdateiso8601)),
FROMDATE(0, Implementation.map$V(JsonMath::fromdate)),
TODATEISO8601(0, Implementation.map$V(JsonMath::todateiso8601)),
TODATE(0, Implementation.map$V(JsonMath::todate)),
NOW(0, Implementation.map$V(_ -> JsonNumber.valueOf(System.currentTimeMillis()))),
// paths
GETPATH(1, Implementation.mapV$V(JsonMath::getpath)),
PATHS$0(0, Implementation.map$F(JsonMath::paths)),
PATHS$1(1, Implementation.mapF$F(JsonMath::paths)),
// misc
ENV(0, Implementation.map$V(_ -> JsonMath.env())),
LENGTH(0, Implementation.map$V(JsonMath::length)),
REPEAT(1, (context, args) -> JsonMath.repeat(() -> args.getFirst().evaluate(context))),
// math library
INFINITE(0, Implementation.map$V(_ -> JsonNumber.valueOf(Double.POSITIVE_INFINITY))),
NAN(0, Implementation.map$V(_ -> JsonNumber.valueOf(Double.NaN))),
ACOS(0, Implementation.map$V(JsonMath::acos)),
ACOSH(0, Implementation.map$V(JsonMath::acosh)),
ASIN(0, Implementation.map$V(JsonMath::asin)),
ASINH(0, Implementation.map$V(JsonMath::asinh)),
ATAN(0, Implementation.map$V(JsonMath::atan)),
ATANH(0, Implementation.map$V(JsonMath::atanh)),
CBRT(0, Implementation.map$V(JsonMath::cbrt)),
CEIL(0, Implementation.map$V(JsonMath::ceil)),
COS(0, Implementation.map$V(JsonMath::cos)),
COSH(0, Implementation.map$V(JsonMath::cosh)),
ERF(0, Implementation.map$V(JsonMath::erf)),
ERFC(0, Implementation.map$V(JsonMath::erfc)),
EXP(0, Implementation.map$V(JsonMath::exp)),
EXP10(0, Implementation.map$V(JsonMath::exp10)),
EXP2(0, Implementation.map$V(JsonMath::exp2)),
EXPM1(0, Implementation.map$V(JsonMath::expm1)),
FABS(0, Implementation.map$V(JsonMath::fabs)),
FLOOR(0, Implementation.map$V(JsonMath::floor)),
GAMMA(0, Implementation.map$V(JsonMath::gamma)),
J0(0, Implementation.map$V(JsonMath::j0)),
J1(0, Implementation.map$V(JsonMath::j1)),
LGAMMA(0, Implementation.map$V(JsonMath::lgamma)),
LOG(0, Implementation.map$V(JsonMath::log)),
LOG10(0, Implementation.map$V(JsonMath::log10)),
LOG1P(0, Implementation.map$V(JsonMath::log1p)),
LOG2(0, Implementation.map$V(JsonMath::log2)),
LOGB(0, Implementation.map$V(JsonMath::logb)),
NEARBYINT(0, Implementation.map$V(JsonMath::nearbyint)),
RINT(0, Implementation.map$V(JsonMath::rint)),
ROUND(0, Implementation.map$V(JsonMath::round)),
SIGNIFICAND(0, Implementation.map$V(JsonMath::significand)),
SIN(0, Implementation.map$V(JsonMath::sin)),
SINH(0, Implementation.map$V(JsonMath::sinh)),
SQRT(0, Implementation.map$V(JsonMath::sqrt)),
TAN(0, Implementation.map$V(JsonMath::tan)),
TANH(0, Implementation.map$V(JsonMath::tanh)),
TGAMMA(0, Implementation.map$V(JsonMath::tgamma)),
TRUNC(0, Implementation.map$V(JsonMath::trunc)),
Y0(0, Implementation.map$V(JsonMath::y0)),
Y1(0, Implementation.map$V(JsonMath::y1)),
ATAN2(2, Implementation.mapVV$V((_, a, b) -> JsonMath.atan2(a, b))),
COPYSIGN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.copysign(a, b))),
DREM(2, Implementation.mapVV$V((_, a, b) -> JsonMath.drem(a, b))),
FDIM(2, Implementation.mapVV$V((_, a, b) -> JsonMath.fdim(a, b))),
FMAX(2, Implementation.mapVV$V((_, a, b) -> JsonMath.fmax(a, b))),
FMIN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.fmin(a, b))),
FMOD(2, Implementation.mapVV$V((_, a, b) -> JsonMath.fmod(a, b))),
FREXP(2, Implementation.mapVV$V((_, a, b) -> JsonMath.frexp(a, b))),
HYPOT(2, Implementation.mapVV$V((_, a, b) -> JsonMath.hypot(a, b))),
JN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.jn(a, b))),
LDEXP(2, Implementation.mapVV$V((_, a, b) -> JsonMath.ldexp(a, b))),
MODF(2, Implementation.mapVV$V((_, a, b) -> JsonMath.modf(a, b))),
NEXTAFTER(2, Implementation.mapVV$V((_, a, b) -> JsonMath.nextafter(a, b))),
NEXTTOWARD(2, Implementation.mapVV$V((_, a, b) -> JsonMath.nexttoward(a, b))),
POW(2, Implementation.mapVV$V((_, a, b) -> JsonMath.pow(a, b))),
REMAINDER(2, Implementation.mapVV$V((_, a, b) -> JsonMath.remainder(a, b))),
SCALB(2, Implementation.mapVV$V((_, a, b) -> JsonMath.scalb(a, b))),
SCALBLN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.scalbln(a, b))),
YN(2, Implementation.mapVV$V((_, a, b) -> JsonMath.yn(a, b))),
FMA(3, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fma(values.get(0), values.get(1), values.get(2))))
;
public static final @NotNull Map<@NotNull String, @NotNull JQInvocable> ALL_BUILTINS = Arrays.stream(JQBuiltIn.values())
.collect(Collectors.toMap(
JQInvocable::reference,
Function.identity()
));
private final @NotNull JQInvocable implementation;
JQBuiltIn(@NotNull String body) {
this(List.of(), parse(body));
}
JQBuiltIn(@NotNull String arg0, @NotNull String body) {
this(List.of(arg0), parse(body));
}
JQBuiltIn(@NotNull String arg0, @NotNull String arg1, @NotNull String body) {
this(List.of(arg0, arg1), parse(body));
}
JQBuiltIn(@NotNull String arg0, @NotNull String arg1, @NotNull String arg2, @NotNull String body) {
this(List.of(arg0, arg1, arg2), parse(body));
}
JQBuiltIn(@NotNull List<@NotNull String> params, @NotNull JQExpression body) {
this.implementation = new JQFunction(getIdentifier(name()), params, body);
}
JQBuiltIn(int arity, @NotNull Implementation implementation) {
this.implementation = new InvocableDelegate(getIdentifier(name()), arity, implementation);
}
@SneakyThrows(IOException.class)
private static @NotNull JQExpression parse(@NotNull String expression) {
return new JQParser(expression).parseExpression();
}
private static @NotNull String getIdentifier(@NotNull String name) {
var identifier = name.toLowerCase(Locale.ROOT);
var idx = identifier.lastIndexOf("$");
if (idx != -1) identifier = identifier.substring(0, idx);
return identifier;
}
@Override
public @NotNull Generator<@Nullable JsonValue> invoke(@NotNull Context context, @NotNull List<@NotNull JQExpression> args) {
return implementation.invoke(context, args);
}
@Override
public @NotNull String identifier() {
return implementation.identifier();
}
@Override
public int arity() {
return implementation.arity();
}
private record InvocableDelegate(
@NotNull String identifier, int arity,
@NotNull Implementation delegate
) implements JQInvocable {
@Override
public @NotNull Generator<@Nullable JsonValue> invoke(@NotNull Context context, @NotNull List<@NotNull JQExpression> args) {
if (args.size() != arity) throw new JsonQueryException("invalid argument count");
return delegate.invoke(context, args);
}
}
@FunctionalInterface
private interface Implementation {
@NotNull Generator<@Nullable JsonValue> invoke(@NotNull Context context, @NotNull List<@NotNull JQExpression> args);
// format: map<args>$<return>
// where <args> and <return> use V for value, F for filter
/**
* Produces a zero-arg implementation applying the function to the current value.
*/
static @NotNull Implementation map$F(@NotNull Function<? super @Nullable JsonValue, ? extends @NotNull Generator<? extends @Nullable JsonValue>> operator) {
return (context, _) -> context.stream().flatMap(operator);
}
/**
* Produces a zero-arg implementation applying the function to the current value.
*/
static @NotNull Implementation map$V(@NotNull Function<? super @Nullable JsonValue, ? extends @Nullable JsonValue> operator) {
return (context, _) -> context.stream().map(operator);
}
/**
* Produces a one-arg implementation applying the function to the current value and the first argument
* (passed by value).
*/
static @NotNull Implementation mapV$V(@NotNull BiFunction<? super @Nullable JsonValue, ? super @Nullable JsonValue, ? extends @Nullable JsonValue> function) {
return (context, args) -> {
var arg = args.getFirst();
return context.stream().flatMap(root -> arg.evaluate(context).map(value -> function.apply(root, value)));
};
}
/**
* Produces a one-arg implementation applying the function to the current value and the first argument
* (passed by value).
*/
static @NotNull Implementation mapV$F(@NotNull BiFunction<? super @Nullable JsonValue, ? super @Nullable JsonValue, ? extends @NotNull Generator<? extends @Nullable JsonValue>> function) {
return (context, args) -> {
var arg = args.getFirst();
return context.stream().flatMap(root -> arg.evaluate(context).flatMap(value -> function.apply(root, value)));
};
}
/**
* Produces a one-arg implementation applying the function to the current value and the first argument
* (passed as filter).
*/
static @NotNull Implementation mapF$F(@NotNull BiFunction<? super @Nullable JsonValue, ? super @NotNull JQFilter, ? extends @NotNull Generator<? extends @Nullable JsonValue>> function) {
return (context, args) -> {
var arg = args.getFirst().bind(context);
return context.stream().flatMap(root -> function.apply(root, arg));
};
}
/**
* Produces a one-arg implementation applying the function to the current value and the first argument
* (passed as filter).
*/
static @NotNull Implementation mapF$V(@NotNull BiFunction<? super @Nullable JsonValue, ? super @NotNull JQFilter, ? extends @Nullable JsonValue> function) {
return (context, args) -> {
var arg = args.getFirst().bind(context);
return context.stream().map(root -> function.apply(root, arg));
};
}
static @NotNull Implementation mapVV$V(@NotNull TriFunction<? super @Nullable JsonValue, ? super @Nullable JsonValue, ? super @Nullable JsonValue, ? extends @Nullable JsonValue> function) {
return (context, args) -> {
var arg0 = args.get(0);
var arg1 = args.get(1);
return context.stream().flatMap(root -> arg0.evaluate(context).flatMap(value0 -> arg1.evaluate(context).map(value1 -> function.apply(root, value0, value1))));
};
}
static @NotNull Implementation mapFF$V(@NotNull TriFunction<? super @Nullable JsonValue, ? super @NotNull JQFilter, ? super @NotNull JQFilter, ? extends @Nullable JsonValue> function) {
return (context, args) -> {
var arg0 = args.get(0).bind(context);
var arg1 = args.get(1).bind(context);
return context.stream().map(root -> function.apply(root, arg0, arg1));
};
}
static @NotNull Implementation mapFF$F(@NotNull TriFunction<? super @Nullable JsonValue, ? super @NotNull JQFilter, ? super @NotNull JQFilter, ? extends @NotNull Generator<? extends @Nullable JsonValue>> function) {
return (context, args) -> {
var arg0 = args.get(0).bind(context);
var arg1 = args.get(1).bind(context);
return context.stream().flatMap(root -> function.apply(root, arg0, arg1));
};
}
static @NotNull Implementation filter(@NotNull Predicate<@Nullable JsonValue> predicate) {
return (context, _) -> context.stream().filter(predicate);
}
}
@FunctionalInterface
private interface TriFunction<S, T, U, R> {
R apply(S s, T t, U u);
}
}

View File

@ -0,0 +1,30 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public record JQCommaExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
public JQCommaExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return Generator.concat(first.evaluate(context), second.evaluate(context));
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return first + ", " + second;
}
}

View File

@ -0,0 +1,24 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public record JQConstant(@Nullable JsonValue value) implements JQExpression {
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return Generator.of(value);
}
@Override
public boolean isConstant() {
return true;
}
@Override
public @NotNull String toString() {
return JsonValue.toJsonString(value);
}
}

View File

@ -0,0 +1,88 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public interface JQExpression {
@NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context);
boolean isConstant();
@NotNull String toString();
default @NotNull JQFilter bind(@NotNull Context context) {
return value -> evaluate(context.withRoot(value));
}
record Context(
@Nullable JsonValue root,
@NotNull Map<@NotNull String, @Nullable JsonValue> variables,
@NotNull Map<@NotNull String, @NotNull JQInvocable> functions
) {
public Context(@Nullable JsonValue root) {
this(root, Map.of("$ENV", JsonMath.env()), JQBuiltIn.ALL_BUILTINS);
}
public Context {
var map = new LinkedHashMap<String, @Nullable JsonValue>();
variables.forEach((key, value) -> {
Objects.requireNonNull(key);
if (!key.startsWith("$")) throw new IllegalArgumentException();
map.put(key, value);
});
variables = Collections.unmodifiableMap(map);
functions = Map.copyOf(functions);
}
public @NotNull JQInvocable function(@NotNull String name, int arity) {
var out = functions.get(name + "/" + arity);
if (out == null) throw new JsonQueryException(name + "/" + arity + " is not defined.");
return out;
}
public @Nullable JsonValue variable(@NotNull String name) {
if (!variables.containsKey(name)) throw new JsonQueryException(name + " is not defined.");
return variables.get(name);
}
public @NotNull Context withRoot(@Nullable JsonValue root) {
return new Context(root, variables, functions);
}
public @NotNull Context withFunction(@NotNull JQInvocable function) {
var f = new HashMap<>(functions);
f.put(function.reference(), function);
return new Context(root, variables, f);
}
public @NotNull Context withFunctions(@NotNull List<? extends @NotNull JQInvocable> functions) {
var f = new HashMap<>(this.functions);
functions.forEach(func -> f.put(func.reference(), func));
return new Context(root, variables, f);
}
public @NotNull Context withVariable(@NotNull String name, @Nullable JsonValue variable) {
var v = new HashMap<>(variables);
v.put(name, variable);
return new Context(root, v, functions);
}
public @NotNull Context withVariables(@NotNull Map<@NotNull String, @Nullable JsonValue> variables) {
var v = new HashMap<>(this.variables);
v.putAll(variables);
return new Context(root, v, functions);
}
public @NotNull Generator<@Nullable JsonValue> stream() {
return Generator.of(root());
}
}
}

View File

@ -0,0 +1,12 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
@FunctionalInterface
public interface JQFilter extends Function<@Nullable JsonValue, @NotNull Generator<@Nullable JsonValue>> {
}

View File

@ -0,0 +1,42 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.NoSuchElementException;
public record JQForEachExpression(
@NotNull JQExpression expression,
@NotNull JQPatterns patterns,
@NotNull JQExpression init,
@NotNull JQExpression update,
@NotNull JQExpression extract
) implements JQExpression {
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return init.evaluate(context).flatMap(initial -> {
var state = new Object() {
private JsonValue state = initial;
};
return expression.evaluate(context).flatMap(value -> patterns.bind(context, value, ctx -> {
return update.evaluate(ctx.withRoot(state.state)).map(v -> {
state.state = v;
return ctx.withRoot(v);
}).flatMap(extract::evaluate);
}));
});
}
@Override
public boolean isConstant() {
return expression.isConstant() && init.isConstant() && update.isConstant();
}
@Override
public @NotNull String toString() {
return "foreach " + expression + " as " + patterns + " (" + init + "; " + update + "; " + extract + ")";
}
}

View File

@ -0,0 +1,52 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
public record JQFunction(@NotNull String identifier, @NotNull List<@NotNull String> params, @NotNull JQExpression body) implements JQInvocable {
public JQFunction {
Objects.requireNonNull(identifier);
Objects.requireNonNull(body);
params = List.copyOf(params);
}
@Override
public @NotNull Generator<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> arguments) {
if (arguments.size() != params.size()) throw new JsonQueryException("invalid argument count");
var expression = body;
var functions = new ArrayList<JQFunction>();
for (int i = params.size() - 1; i >= 0; i--) {
String param = params.get(i);
if (param.startsWith("$")) {
expression = new JQAsExpression(
new JQFunctionInvocation(param.substring(1), List.of()),
new JQPatterns(List.of(new JQPatterns.Pattern.ValuePattern(param))),
expression
);
param = param.substring(1);
}
functions.add(new JQFunction(param, List.of(), arguments.get(i)));
}
return expression.evaluate(context.withFunctions(functions));
}
@Override
public int arity() {
return params().size();
}
@Override
public @NotNull String toString() {
return "def " + identifier + (params.isEmpty() ? "" : String.join("; ", params)) + ": " + body + ";";
}
}

View File

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

View File

@ -0,0 +1,36 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
public record JQFunctionInvocation(@NotNull String name, @NotNull List<@NotNull JQExpression> args) implements JQExpression {
public JQFunctionInvocation {
Objects.requireNonNull(name);
args = List.copyOf(args);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
var function = context.function(name, args.size());
return function.invoke(context, args);
}
@Override
public boolean isConstant() {
return false;
}
@Override
public @NotNull String toString() {
var out = new StringJoiner("; ", name + "(", ")");
out.setEmptyValue(name);
for (var arg : args) out.add(arg.toString());
return out.toString();
}
}

View File

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

View File

@ -0,0 +1,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
) {
}

View File

@ -0,0 +1,52 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonNumber;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public record JQIndexExpression(@NotNull JQExpression expression, @NotNull JQExpression index, boolean optional) implements JQExpression {
public JQIndexExpression {
Objects.requireNonNull(expression);
Objects.requireNonNull(index);
}
public JQIndexExpression(@NotNull JQExpression expression, @Nullable JsonValue index, boolean optional) {
this(expression, new JQConstant(index), optional);
}
public JQIndexExpression(@NotNull JQExpression expression, double index, boolean optional) {
this(expression, new JsonNumber(index), optional);
}
public JQIndexExpression(@NotNull JQExpression expression, @NotNull String index, boolean optional) {
this(expression, new JsonString(index), optional);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return expression.evaluate(context).flatMap(value -> index.evaluate(context).mapMulti((index, downstream) -> {
try {
downstream.accept(JsonMath.index(value, index));
} catch (JsonQueryException ex) {
if (!optional) throw ex;
}
}));
}
@Override
public boolean isConstant() {
return expression.isConstant() && index.isConstant();
}
@Override
public @NotNull String toString() {
return expression + "[" + index + "]" + (optional ? "?" : "");
}
}

View File

@ -0,0 +1,19 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public interface JQInvocable {
@NotNull
Generator<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args);
int arity();
@NotNull String identifier();
default @NotNull String reference() {
return identifier() + "/" + arity();
}
}

View File

@ -0,0 +1,30 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public record JQIterateExpression(@NotNull JQExpression expression, boolean optional) implements JQExpression {
public JQIterateExpression {
Objects.requireNonNull(expression);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return expression.evaluate(context).flatMap(value -> JsonMath.values(value, optional));
}
@Override
public boolean isConstant() {
return expression.isConstant();
}
@Override
public @NotNull String toString() {
return expression + "[]" + (optional ? "?" : "");
}
}

View File

@ -0,0 +1,29 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonNumber;
import eu.jonahbauer.json.JsonObject;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public record JQLocExpression(@NotNull String file, int line) implements JQExpression {
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return Generator.of(JsonObject.of(
"file", new JsonString(file),
"line", new JsonNumber(line)
));
}
@Override
public boolean isConstant() {
return true;
}
@Override
public @NotNull String toString() {
return "$__loc__";
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,31 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.stream.Stream;
public record JQNegation(@NotNull JQExpression expression) implements JQExpression {
public JQNegation {
Objects.requireNonNull(expression);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return expression.evaluate(context).map(JsonMath::neg);
}
@Override
public boolean isConstant() {
return expression.isConstant();
}
@Override
public @NotNull String toString() {
return "-" + expression;
}
}

View File

@ -0,0 +1,52 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonObject;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.impl.Generator;
import eu.jonahbauer.json.query.util.Util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.StringJoiner;
public record JQObjectConstructionExpression(@NotNull List<@NotNull Entry> entries) implements JQExpression {
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
var generator = Generator.of(new LinkedHashMap<@NotNull String, @Nullable JsonValue>());
for (var entry : entries) {
generator = generator.flatMap(map -> entry.key().evaluate(context)
.flatMap(key -> {
if (!(key instanceof JsonString(var string))) {
throw new JsonQueryException("Cannot use " + Util.type(key) + "(" + Util.value(key) + ") as object key.");
}
return entry.value().evaluate(context)
.map(value -> {
var out = new LinkedHashMap<>(map);
out.put(string, value);
return out;
});
})
);
}
return generator.map(JsonObject::new);
}
@Override
public boolean isConstant() {
return false;
}
@Override
public @NotNull String toString() {
var out = new StringJoiner(", ", "{", "}");
entries.forEach(entry -> out.add(entry.key() + ": " + entry.value()));
return out.toString();
}
public record Entry(@NotNull JQExpression key, @NotNull JQExpression value) { }
}

View File

@ -0,0 +1,30 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public record JQParenthesizedExpression(@NotNull JQExpression expression) implements JQExpression {
public JQParenthesizedExpression {
Objects.requireNonNull(expression);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return expression.evaluate(context);
}
@Override
public boolean isConstant() {
return expression.isConstant();
}
@Override
public @NotNull String toString() {
return "(" + expression + ")";
}
}

View File

@ -0,0 +1,206 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonNumber;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.impl.Generator;
import eu.jonahbauer.json.query.parser.ast.JQExpression.Context;
import eu.jonahbauer.json.query.util.Util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
public JQPatterns {
patterns = List.copyOf(patterns);
if (patterns.isEmpty()) throw new IllegalArgumentException();
}
public @NotNull Generator<@Nullable JsonValue> bind(@NotNull Context context, @Nullable JsonValue value, Function<@NotNull Context, @NotNull Generator<@Nullable JsonValue>> downstream) {
return new PatternGeneratorImpl(patterns, context, value, downstream);
}
private static final class PatternGeneratorImpl implements Generator<@Nullable JsonValue> {
// one generator per pattern
private final @NotNull Generator<@NotNull Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>>> patterns;
private final @NotNull Context context;
private final @NotNull Function<@NotNull Context, @NotNull Generator<@Nullable JsonValue>> downstream;
// the generator for the current pattern
private @Nullable Generator<@Nullable JsonValue> current;
public PatternGeneratorImpl(
@NotNull List<@NotNull Pattern> patterns, @NotNull Context context, @Nullable JsonValue value,
@NotNull Function<@NotNull Context, @NotNull Generator<@Nullable JsonValue>> downstream
) {
this.context = context;
this.downstream = downstream;
var variables = new HashMap<String, JsonValue>();
patterns.stream().map(Pattern::variables).flatMap(Collection::stream).forEach(key -> variables.put(key, null));
this.patterns = Generator.from(patterns).map(pattern -> pattern.bind(context, value).map(vars -> {
var out = new HashMap<>(variables);
out.putAll(vars);
return out;
}));
}
private <T> T advance(@NotNull Function<Generator<@Nullable JsonValue>, T> function) {
var ex = new JsonQueryException("no match");
while (true) {
if (current == null) {
// find the next matching pattern
while (patterns.hasNext()) {
try {
var match = patterns.next();
current = match.map(context::withVariables).flatMap(downstream);
break;
} catch (JsonQueryException e) {
ex = e;
}
}
// no matching pattern; propagate exception from last match attempt
if (current == null) {
throw ex;
}
}
try {
return function.apply(current);
} catch (JsonQueryException e) {
// exception during execution; try the next pattern
current = null;
ex = e;
}
}
}
@Override
public boolean hasNext() {
return advance(Generator::hasNext);
}
@Override
public @Nullable JsonValue next() throws NoSuchElementException {
return advance(Generator::next);
}
}
@Override
public @NotNull String toString() {
return patterns.stream().map(Objects::toString).collect(Collectors.joining(" ?// "));
}
public sealed interface Pattern {
@NotNull
Set<@NotNull String> variables();
@NotNull
Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value);
record ValuePattern(@NotNull String name) implements Pattern {
public ValuePattern {
Objects.requireNonNull(name);
if (!name.startsWith("$")) throw new IllegalArgumentException();
}
@Override
public @NotNull Set<@NotNull String> variables() {
return Set.of(name);
}
@Override
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
var map = new HashMap<String, JsonValue>();
map.put(name, value);
return Generator.of(Collections.unmodifiableMap(map));
}
@Override
public @NotNull String toString() {
return name;
}
}
record ArrayPattern(@NotNull List<@NotNull Pattern> patterns) implements Pattern {
public ArrayPattern {
patterns = List.copyOf(patterns);
if (patterns.isEmpty()) throw new IllegalArgumentException();
}
@Override
public @NotNull Set<@NotNull String> variables() {
var out = new HashSet<String>();
patterns.forEach(p -> out.addAll(p.variables()));
return Collections.unmodifiableSet(out);
}
@Override
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
var streams = new ArrayList<Supplier<Generator<Map<String, JsonValue>>>>();
for (int i = 0; i < patterns.size(); i++) {
var k = new JsonNumber(i);
var v = JsonMath.index(value, k);
var pattern = patterns.get(i);
streams.add(() -> pattern.bind(context, v));
}
return Util.crossReversed(streams).map(list -> {
var map = new HashMap<String, JsonValue>();
list.forEach(map::putAll);
return map;
});
}
@Override
public @NotNull String toString() {
return patterns.stream().map(Objects::toString).collect(Collectors.joining(", ", "[", "]"));
}
}
record ObjectPattern(@NotNull SequencedMap<@NotNull JQExpression, @NotNull Pattern> patterns) implements Pattern {
public ObjectPattern {
patterns = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(patterns));
if (patterns.isEmpty()) throw new IllegalArgumentException();
}
@Override
public @NotNull Set<@NotNull String> variables() {
var out = new HashSet<String>();
patterns.values().forEach(p -> out.addAll(p.variables()));
return Collections.unmodifiableSet(out);
}
@Override
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
var streams = new ArrayList<Supplier<Generator<Map<String, JsonValue>>>>();
this.patterns.reversed().forEach((keyExpression, pattern) -> streams.add(() -> {
var keyStream = keyExpression.evaluate(context);
return keyStream.flatMap(key -> pattern.bind(context, JsonMath.index(value, key)));
}));
return Util.cross(streams).map(list -> {
var map = new HashMap<String, JsonValue>();
list.reversed().forEach(map::putAll);
return map;
});
}
@Override
public @NotNull String toString() {
var out = new StringJoiner(", ", "{", "}");
patterns.forEach((key, pattern) -> out.add(key + ": " + pattern));
return out.toString();
}
}
}
}

View File

@ -0,0 +1,31 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.stream.Stream;
public record JQPipeExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
public JQPipeExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return first.evaluate(context).flatMap(value -> second.evaluate(context.withRoot(value)));
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return first + " | " + second;
}
}

View File

@ -0,0 +1,20 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public record JQProgram(
@Nullable JQModule module,
@NotNull List<@NotNull JQImport> imports,
@NotNull List<@NotNull JQFunction> functions,
@Nullable JQExpression expression
) {
public @NotNull Generator<@Nullable JsonValue> run(@Nullable JsonValue value) {
if (expression == null) return Generator.empty();
return expression.evaluate(new JQExpression.Context(value).withFunctions(functions));
}
}

View File

@ -0,0 +1,28 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonArray;
import eu.jonahbauer.json.JsonObject;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public record JQRecursionExpression() implements JQExpression {
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return recurse(context.root());
}
private @NotNull Generator<@Nullable JsonValue> recurse(@Nullable JsonValue value) {
return switch (value) {
case JsonArray array -> Generator.concat(Generator.of(array), Generator.from(array).flatMap(this::recurse));
case JsonObject object -> Generator.concat(Generator.of(object), Generator.from(object.values()).flatMap(this::recurse));
case null, default -> Generator.of(value);
};
}
@Override
public boolean isConstant() {
return false;
}
}

View File

@ -0,0 +1,43 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public record JQReduceExpression(
@NotNull JQExpression expression,
@NotNull JQPatterns patterns,
@NotNull JQExpression init,
@NotNull JQExpression update
) implements JQExpression {
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return init.evaluate(context).flatMap(initial -> {
var state = new Object() {
private JsonValue state = initial;
};
return Generator.concat(
expression.evaluate(context).flatMap(value -> patterns.bind(context, value, ctx -> {
var gen = update.evaluate(ctx.withRoot(state.state));
while (gen.hasNext()) {
state.state = gen.next();
}
return Generator.empty();
})),
Generator.of(() -> state.state)
);
});
}
@Override
public boolean isConstant() {
return expression.isConstant() && init.isConstant() && update.isConstant();
}
@Override
public @NotNull String toString() {
return "reduce " + expression + " as " + patterns + " (" + init + "; " + update + ")";
}
}

View File

@ -0,0 +1,23 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public record JQRootExpression() implements JQExpression {
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return context.stream();
}
@Override
public boolean isConstant() {
return false;
}
@Override
public @NotNull String toString() {
return ".";
}
}

View File

@ -0,0 +1,36 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public record JQSliceExpression(@NotNull JQExpression expression, @Nullable JQExpression start, @Nullable JQExpression end, boolean optional) implements JQExpression {
public JQSliceExpression {
Objects.requireNonNull(expression);
if (start == null && end == null) throw new IllegalArgumentException();
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return expression.evaluate(context)
.flatMap(value -> (start == null ? Generator.of((JsonValue) null) : start.evaluate(context))
.flatMap(start -> (end == null ? Generator.of((JsonValue) null) : end.evaluate(context))
.map(end -> JsonMath.slice(value, start, end))
)
);
}
@Override
public boolean isConstant() {
return expression.isConstant() && (start == null || start.isConstant()) && (end == null || end.isConstant());
}
@Override
public @NotNull String toString() {
return expression + "[" + (start == null ? "" : start) + ":" + (end == null ? "" : end) + "]";
}
}

View File

@ -0,0 +1,61 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import eu.jonahbauer.json.query.util.Util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.stream.Stream;
@SuppressWarnings("preview")
public record JQStringInterpolation(
@Nullable String format,
@NotNull List<@NotNull String> fragments,
@NotNull List<@NotNull JQExpression> values
) implements JQExpression {
public JQStringInterpolation {
fragments = List.copyOf(fragments);
values = List.copyOf(values);
if (fragments.size() != values.size() + 1) throw new IllegalArgumentException();
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return Util.crossReversed(values, context)
.map(values -> StringTemplate.of(fragments, values.stream().map(JQStringInterpolation::toString).toList()))
.map(STR::process)
.map(JsonString::valueOf);
}
private static @NotNull String toString(@Nullable JsonValue value) {
return value instanceof JsonString(var string) ? string : JsonValue.toJsonString(value);
}
@Override
public boolean isConstant() {
return format == null && values.stream().allMatch(JQExpression::isConstant);
}
@Override
public @NotNull String toString() {
var out = new StringBuilder();
if (format != null) out.append("@").append(format);
out.append("\"");
var it1 = fragments.iterator();
for (JQExpression value : values) {
var fragment = JsonString.quote(it1.next());
out.append(fragment, 1, fragment.length() - 1);
out.append("\\(");
out.append(value);
out.append(")");
}
var last = JsonString.quote(it1.next());
out.append(last, 1, last.length());
return out.toString();
}
}

View File

@ -0,0 +1,99 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.JsonQueryHaltException;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
public record JQTryExpression(@NotNull JQExpression expression, @Nullable JQExpression fallback) implements JQExpression {
public JQTryExpression {
Objects.requireNonNull(expression);
}
public JQTryExpression(@NotNull JQExpression expression) {
this(expression, null);
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return new QuietIterator(
expression.evaluate(context),
fallback == null ? null : ex -> fallback.evaluate(context.withRoot(new JsonString(ex.getMessage())))
);
}
@Override
public boolean isConstant() {
return expression.isConstant() && (fallback == null || fallback.isConstant());
}
@Override
public @NotNull String toString() {
return "try " + expression + (fallback == null ? "" : " catch " + fallback);
}
private static class QuietIterator implements Generator<@Nullable JsonValue> {
private @Nullable Generator<@Nullable JsonValue> delegate;
private @Nullable Generator<@Nullable JsonValue> fallback;
private @Nullable Function<@NotNull JsonQueryException, @NotNull Generator<@Nullable JsonValue>> fallbackSupplier;
private JsonValue value;
private boolean hasValue;
private QuietIterator(
@NotNull Generator<@Nullable JsonValue> delegate,
@Nullable Function<@NotNull JsonQueryException, @NotNull Generator<@Nullable JsonValue>> fallback
) {
this.delegate = delegate;
this.fallbackSupplier = fallback;
}
private boolean advance() {
if (hasValue) return true;
assert fallback == null && delegate != null;
try {
hasValue = delegate.hasNext();
value = hasValue ? delegate.next() : null;
return hasValue;
} catch (JsonQueryHaltException ex) {
throw ex;
} catch (JsonQueryException ex) {
delegate = null;
if (fallbackSupplier != null) {
fallback = fallbackSupplier.apply(ex);
hasValue = false;
value = null;
return fallback.hasNext();
} else {
hasValue = false;
value = null;
return false;
}
}
}
@Override
public boolean hasNext() {
if (fallback != null) return fallback.hasNext();
if (delegate == null) return false;
return advance();
}
@Override
public @Nullable JsonValue next() {
if (fallback != null) return fallback.next();
if (delegate == null) throw new NoSuchElementException();
if (!advance()) throw new NoSuchElementException();
var out = value;
hasValue = false;
value = null;
return out;
}
}
}

View File

@ -0,0 +1,30 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.impl.Generator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public record JQVariableExpression(@NotNull String name) implements JQExpression {
public JQVariableExpression {
Objects.requireNonNull(name);
if (!name.startsWith("$")) throw new IllegalArgumentException();
}
@Override
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
return Generator.of(() -> context.variable(name));
}
@Override
public boolean isConstant() {
return false;
}
@Override
public @NotNull String toString() {
return name;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,65 @@
package eu.jonahbauer.json.query.util;
import eu.jonahbauer.json.*;
import eu.jonahbauer.json.query.impl.Generator;
import eu.jonahbauer.json.query.parser.ast.JQExpression;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
@UtilityClass
public class Util {
public static @NotNull Generator<@NotNull List<JsonValue>> cross(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) {
return cross(asSupplier(expressions, context)).map(Collections::unmodifiableList);
}
public static <T> @NotNull Generator<@NotNull List<T>> cross(@NotNull List<@NotNull Supplier<@NotNull Generator<T>>> expressions) {
if (expressions.isEmpty()) return Generator.of(new ArrayList<T>().reversed());
return expressions.getFirst().get()
.flatMap(value -> cross(expressions.subList(1, expressions.size()))
.map(list -> { list.addFirst(value); return list; })
);
}
public static @NotNull Generator<@NotNull List<JsonValue>> crossReversed(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) {
return crossReversed(asSupplier(expressions, context)).map(Collections::unmodifiableList);
}
public static <T> @NotNull Generator<@NotNull List<T>> crossReversed(@NotNull List<@NotNull Supplier<@NotNull Generator<T>>> expressions) {
if (expressions.isEmpty()) return Generator.of(new ArrayList<>());
return expressions.getLast().get()
.flatMap(value -> crossReversed(expressions.subList(0, expressions.size() - 1))
.map(list -> { list.addLast(value); return list; })
);
}
private static @NotNull List<@NotNull Supplier<@NotNull Generator<JsonValue>>> asSupplier(@NotNull List<@NotNull JQExpression> expressions, @NotNull JQExpression.Context context) {
var list = new ArrayList<Supplier<Generator<JsonValue>>>(expressions.size());
expressions.forEach(expr -> list.add(() -> expr.evaluate(context)));
return list;
}
public static @NotNull String type(@Nullable JsonValue value) {
return switch (value) {
case JsonArray _ -> "array";
case JsonObject _ -> "object";
case JsonNumber _ -> "number";
case JsonString _ -> "string";
case JsonBoolean _ -> "boolean";
case null -> "null";
};
}
public static @NotNull String value(@Nullable JsonValue value) {
return JsonValue.toJsonString(value);
}
}

View 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()));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -9,3 +9,4 @@ dependencyResolutionManagement {
rootProject.name = "json"
include("core")
include("query")