This commit is contained in:
jbb01 2024-05-10 23:58:08 +02:00
parent 4d9a3ef4ab
commit dc5791815e
No known key found for this signature in database
GPG Key ID: 83C72CB6D5442CF1
56 changed files with 6482 additions and 0 deletions

View File

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

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,34 @@
package eu.jonahbauer.json.query;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.parser.JQParser;
import eu.jonahbauer.json.query.parser.ast.JQExpression;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.stream.Stream;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class JsonQuery {
private final @NotNull JQExpression expression;
public static @NotNull JsonQuery parse(@NotNull String query) {
try {
var parser = new JQParser(query);
var programm = parser.parseTopLevel();
if (programm.expression() == null) throw new IllegalArgumentException();
return new JsonQuery(programm.expression());
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public @NotNull Stream<@NotNull JsonValue> run(@Nullable JsonValue value) {
var context = new JQExpression.Context(value);
return expression.evaluate(context);
}
}

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

View File

@ -0,0 +1,28 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
final class ConcatGenerator<T> implements Generator<T> {
private final @NotNull Queue<@NotNull Generator<? extends T>> generators;
public ConcatGenerator(@NotNull List<@NotNull Generator<? extends T>> generators) {
this.generators = new LinkedList<>(generators);
}
@Override
public T next() throws EndOfStreamException {
while (!generators.isEmpty()) {
try {
var current = generators.peek();
return current.next();
} catch (EndOfStreamException ex) {
generators.remove();
}
}
throw new EndOfStreamException();
}
}

View File

@ -0,0 +1,10 @@
package eu.jonahbauer.json.query.impl;
enum EmptyGenerator implements Generator<Object> {
INSTANCE;
@Override
public Object next() throws EndOfStreamException {
throw new EndOfStreamException();
}
}

View File

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

View File

@ -0,0 +1,33 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.function.Function;
public interface Generator<T> {
T next() throws EndOfStreamException;
default <S> @NotNull Generator<S> map(@NotNull Function<? super T, ? extends S> function) {
return new MappingGenerator<>(this, function);
}
default <S> @NotNull Generator<S> flatMap(@NotNull Function<? super T, ? extends Generator<? extends S>> function) {
return new FlatMappingGenerator<>(this, function);
}
@SuppressWarnings("unchecked")
static <T> @NotNull Generator<T> empty() {
return (Generator<T>) EmptyGenerator.INSTANCE;
}
static <T> @NotNull Generator<T> concat(@NotNull Generator<? extends T> first, @NotNull Generator<? extends T> second) {
return new ConcatGenerator<>(List.of(first, second));
}
static <T> @NotNull Generator<T> of(T value) {
return new SingletonGenerator<>(value);
}
class EndOfStreamException extends RuntimeException {}
}

View File

@ -0,0 +1,12 @@
package eu.jonahbauer.json.query.impl;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
record MappingGenerator<T, S>(@NotNull Generator<T> delegate, @NotNull Function<? super T, ? extends S> function) implements Generator<S> {
@Override
public S next() throws EndOfStreamException {
return function.apply(delegate.next());
}
}

View File

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

View File

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

View File

@ -0,0 +1,80 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonMath;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Supplier;
import java.util.stream.Gatherer;
import java.util.stream.Stream;
@SuppressWarnings("preview")
public record JQAlternativeExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
public JQAlternativeExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
}
@Override
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
return first.evaluate(context).gather(new AlternativeGatherer(second, context));
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return first + " // " + second;
}
@NoArgsConstructor
@AllArgsConstructor
private static class State {
private boolean empty = true;
}
private record AlternativeGatherer(
@NotNull JQExpression expression,
@NotNull Context context
) implements Gatherer<JsonValue, State, JsonValue> {
@Override
public @NotNull Supplier<State> initializer() {
return State::new;
}
@Override
public @NotNull Integrator<State, JsonValue, JsonValue> integrator() {
return Integrator.ofGreedy((state, element, downstream) -> {
if (JsonMath.isTruthy(element)) {
state.empty = false;
return downstream.push(element);
}
return true;
});
}
@Override
public @NotNull BinaryOperator<State> combiner() {
return (state1, state2) -> new State(state1.empty && state2.empty);
}
@Override
public @NotNull BiConsumer<State, Downstream<? super JsonValue>> finisher() {
return (state, downstream) -> {
if (!state.empty) return;
var it = expression.evaluate(context).iterator();
while (it.hasNext() && downstream.push(it.next()));
};
}
}
}

View File

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

View File

@ -0,0 +1,173 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.*;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.util.Util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public record JQAsExpression(
@NotNull JQExpression variable, @NotNull List<@NotNull Pattern> patterns, @NotNull JQExpression expression
) implements JQExpression {
public JQAsExpression {
Objects.requireNonNull(variable);
Objects.requireNonNull(expression);
patterns = List.copyOf(patterns);
}
@Override
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
var variables = new HashMap<String, JsonValue>();
patterns.stream().map(Pattern::variables).flatMap(Set::stream).forEach(key -> variables.put(key, null));
return variable.evaluate(context)
.flatMap(value -> {
Stream<Map<String, JsonValue>> result = null;
// find first pattern that does not throw
var it = patterns.iterator();
while (it.hasNext()) {
try {
result = it.next().bind(context, value);
} catch (JsonQueryException ex) {
if (!it.hasNext()) throw ex;
}
}
// execute expression for all possible pattern matches
assert result != null;
return result
.map(vars -> {
var out = new HashMap<>(variables);
out.putAll(vars);
return context.withVariables(out);
})
.flatMap(expression::evaluate);
});
}
@Override
public boolean isConstant() {
return variable.isConstant() && expression.isConstant();
}
@Override
public @NotNull String toString() {
return variable
+ " as " + patterns.stream().map(Objects::toString).collect(Collectors.joining(" ?// "))
+ " | " + expression;
}
public sealed interface Pattern {
@NotNull Set<@NotNull String> variables();
@NotNull Stream<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value);
record ValuePattern(@NotNull String name) implements Pattern {
public ValuePattern {
Objects.requireNonNull(name);
if (!name.startsWith("$")) throw new IllegalArgumentException();
}
@Override
public @NotNull Set<@NotNull String> variables() {
return Set.of(name);
}
@Override
public @NotNull Stream<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
var map = new HashMap<String, JsonValue>();
map.put(name, value);
return Stream.of(Collections.unmodifiableMap(map));
}
@Override
public @NotNull String toString() {
return name;
}
}
record ArrayPattern(@NotNull List<@NotNull Pattern> patterns) implements Pattern {
public ArrayPattern {
patterns = List.copyOf(patterns);
if (patterns.isEmpty()) throw new IllegalArgumentException();
}
@Override
public @NotNull Set<@NotNull String> variables() {
var out = new HashSet<String>();
patterns.forEach(p -> out.addAll(p.variables()));
return Collections.unmodifiableSet(out);
}
@Override
public @NotNull Stream<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
var streams = new ArrayList<Supplier<Stream<Map<String, JsonValue>>>>();
for (int i = 0; i < patterns.size(); i++) {
var k = new JsonNumber(i);
var v = JsonMath.index(value, k);
var pattern = patterns.get(i);
streams.add(() -> pattern.bind(context, v));
}
return Util.crossReversed(streams).map(list -> {
var map = new HashMap<String, JsonValue>();
list.forEach(map::putAll);
return map;
});
}
@Override
public @NotNull String toString() {
return patterns.stream().map(Objects::toString).collect(Collectors.joining(", ", "[", "]"));
}
}
record ObjectPattern(@NotNull SequencedMap<@NotNull JQExpression, @NotNull Pattern> patterns) implements Pattern {
public ObjectPattern {
patterns = Collections.unmodifiableSequencedMap(new LinkedHashMap<>(patterns));
if (patterns.isEmpty()) throw new IllegalArgumentException();
}
@Override
public @NotNull Set<@NotNull String> variables() {
var out = new HashSet<String>();
patterns.values().forEach(p -> out.addAll(p.variables()));
return Collections.unmodifiableSet(out);
}
@Override
public @NotNull Stream<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
var streams = new ArrayList<Supplier<Stream<Map<String, JsonValue>>>>();
this.patterns.reversed().forEach((keyExpression, pattern) -> streams.add(() -> {
var keyStream = keyExpression.evaluate(context);
return keyStream.flatMap(key -> pattern.bind(context, JsonMath.index(value, key)));
}));
return Util.cross(streams).map(list -> {
var map = new HashMap<String, JsonValue>();
list.reversed().forEach(map::putAll);
return map;
});
}
@Override
public @NotNull String toString() {
var out = new StringJoiner(", ", "{", "}");
patterns.forEach((key, pattern) -> out.add(key + ": " + pattern));
return out.toString();
}
}
}
}

View File

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

View File

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

View File

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

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

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 org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.stream.Stream;
public record JQBooleanAndExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
public JQBooleanAndExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
}
@Override
public @NotNull Stream<JsonValue> evaluate(@NotNull Context context) {
return first.evaluate(context)
.flatMap(value -> JsonMath.isFalsy(value)
? Stream.of(JsonBoolean.FALSE)
: second.evaluate(context).map(JsonMath::isTruthy).map(JsonBoolean::valueOf)
);
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return first + " and " + second;
}
}

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 org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.stream.Stream;
public record JQBooleanOrExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
public JQBooleanOrExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
}
@Override
public @NotNull Stream<JsonValue> evaluate(@NotNull Context context) {
return first.evaluate(context)
.flatMap(value -> JsonMath.isTruthy(value)
? Stream.of(JsonBoolean.TRUE)
: second.evaluate(context).map(JsonMath::isTruthy).map(JsonBoolean::valueOf)
);
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return first + " or " + second;
}
}

View File

@ -0,0 +1,276 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.*;
import eu.jonahbauer.json.query.JsonMath;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.util.Util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Gatherer;
import java.util.stream.Stream;
public enum JQBuiltIn implements JQInvocable {
ABS(0, (context, _) -> context.stream().map(JsonMath::abs)),
ADD(0, (context, _) -> context.stream().map(value -> JsonMath.values(value).reduce(null, JsonMath::add))),
NOT(0, (context, _) -> context.stream().map(JsonMath::not)),
// error handling
ERROR$0(0, (context, _) -> context.stream().flatMap(JsonMath::error)),
ERROR$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(JsonMath::error)),
HALT(0, (_, _) -> Stream.of((JsonValue) null).flatMap(_ -> JsonMath.halt())),
HALT_ERROR$0(0, (context, _) -> context.stream().flatMap(JsonMath::halt)),
HALT_ERROR$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(value -> JsonMath.halt(context.root(), value))),
// stream operations
EMPTY(0, (_, _) -> Stream.empty()),
RANGE$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(JsonMath::range)),
RANGE$2(2, (context, args) -> Util.cross(args, context).flatMap(bounds -> JsonMath.range(bounds.get(0), bounds.get(1)))),
RANGE$3(3, (context, args) -> Util.cross(args, context).flatMap(bounds -> JsonMath.range(bounds.get(0), bounds.get(1), bounds.get(2)))),
LIMIT(2, (context, args) -> args.getFirst().evaluate(context).flatMap(limit -> JsonMath.limit(args.getLast().evaluate(context), limit))),
FIRST$1(1, (context, args) -> JsonMath.first(args.getFirst().evaluate(context))),
LAST$1(1, (context, args) -> JsonMath.last(args.getFirst().evaluate(context))),
NTH$2(2, (context, args) -> args.getFirst().evaluate(context).flatMap(n -> JsonMath.nth(args.getLast().evaluate(context), n))),
// iterable operations
MAP(1, (context, args) -> {
var filter = args.getFirst().bind(context);
return context.stream().map(value -> JsonMath.map(value, filter));
}),
MAP_VALUES(1, (context, args) -> {
var filter = args.getFirst().bind(context);
return context.stream().map(value -> JsonMath.mapValues(value, filter));
}),
KEYS(0, (context, _) -> context.stream().map(JsonMath::keys)),
KEYS_UNSORTED(0, (context, _) -> context.stream().map(JsonMath::keysUnsorted)),
HAS(1, (context, args) -> args.getFirst().evaluate(context).map(index -> JsonMath.has(context.root(), index))),
IN(1, (context, args) -> args.getFirst().evaluate(context).map(value -> JsonMath.in(context.root(), value))),
FIRST$0(0, (context, _) -> context.stream().map(value -> JsonMath.index(value, JsonNumber.ZERO))),
LAST$0(0, (context, _) -> context.stream().map(value -> JsonMath.index(value, new JsonNumber(-1)))),
NTH$1(1, (context, args) -> args.getFirst().evaluate(context).map(index -> JsonMath.index(context.root(), index))),
ANY$0(0, (context, _) -> context.stream().map(JsonMath::any)),
ANY$1(1, (context, args) -> {
var filter = args.getFirst().bind(context);
return context.stream().map(value -> JsonMath.any(value, filter));
}),
ANY$2(2, (context, args) -> {
var filter = args.getFirst().bind(context);
return Util.lazy(() -> JsonMath.any(args.getFirst().evaluate(context), filter));
}),
ALL$0(0, (context, _) -> context.stream().map(JsonMath::all)),
ALL$1(1, (context, args) -> {
var filter = args.getFirst().bind(context);
return context.stream().map(value -> JsonMath.all(value, filter));
}),
ALL$2(2, (context, args) -> {
var filter = args.getFirst().bind(context);
return Util.lazy(() -> JsonMath.all(args.getFirst().evaluate(context), filter));
}),
FLATTEN$0(0, (context, _) -> context.stream().map(JsonMath::flatten)),
FLATTEN$1(1, (context, args) -> context.stream().flatMap(value -> args.getFirst().evaluate(context).map(depth -> JsonMath.flatten(value, depth)))),
SORT(0, (context, _) -> context.stream().map(JsonMath::sort)),
SORT_BY(1, (context, args) -> {
var filter = args.getFirst().bind(context);
return context.stream().map(value -> JsonMath.sort(value, filter));
}),
MIN(0, (context, _) -> context.stream().map(JsonMath::min)),
MIN_BY(1, (context, args) -> {
var filter = args.getFirst().bind(context);
return context.stream().map(value -> JsonMath.min(value, filter));
}),
MAX(0, (context, _) -> context.stream().map(JsonMath::max)),
MAX_BY(1, (context, args) -> {
var filter = args.getFirst().bind(context);
return context.stream().map(value -> JsonMath.max(value, filter));
}),
UNIQUE(0, (context, _) -> context.stream().map(JsonMath::unique)),
UNIQUE_BY(1, (context, args) -> {
var filter = args.getFirst().bind(context);
return context.stream().map(value -> JsonMath.unique(value, filter));
}),
GROUP_BY(1, (context, args) -> {
var filter = args.getFirst().bind(context);
return context.stream().map(value -> JsonMath.group(value, filter));
}),
REVERSE(0, (context, _) -> context.stream().map(JsonMath::reverse)),
CONTAINS(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.contains(context.root(), content))),
INDICES(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.indices(context.root(), content))),
INDEX(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.firstindex(context.root(), content))),
RINDEX(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.lastindex(context.root(), content))),
INSIDE(1, (context, args) -> args.getFirst().evaluate(context).map(content -> JsonMath.inside(context.root(), content))),
COMBINATIONS$0(0, (context, _) -> context.stream().flatMap(JsonMath::combinations)),
COMBINATIONS$1(1, (context, args) -> args.getFirst().evaluate(context).flatMap(n -> JsonMath.combinations(context.root(), n))),
BSEARCH(1, (context, args) -> args.getFirst().evaluate(context).map(value -> JsonMath.bsearch(context.root(), value))),
TRANSPOSE(0, (context, _) -> context.stream().map(JsonMath::transpose)),
// filters
ARRAYS(0, (context, _) -> context.stream().filter(JsonMath::isArray)),
OBJECTS(0, (context, _) -> context.stream().filter(JsonMath::isObject)),
ITERABLES(0, (context, _) -> context.stream().filter(JsonMath::isIterable)),
BOOLEANS(0, (context, _) -> context.stream().filter(JsonMath::isBoolean)),
NUMBERS(0, (context, _) -> context.stream().filter(JsonMath::isNumber)),
NORMALS(0, (context, _) -> context.stream().filter(JsonMath::isNormal)),
FINITES(0, (context, _) -> context.stream().filter(JsonMath::isFinite)),
STRINGS(0, (context, _) -> context.stream().filter(JsonMath::isString)),
NULLS(0, (context, _) -> context.stream().filter(JsonMath::isNull)),
VALUES(0, (context, _) -> context.stream().filter(JsonMath::isValue)),
SCALARS(0, (context, _) -> context.stream().filter(JsonMath::isScalar)),
// checks
ISINFINITE(0, (context, _) -> context.stream().map(JsonMath::isInfinite).map(JsonBoolean::valueOf)),
ISNAN(0, (context, _) -> context.stream().map(JsonMath::isNan).map(JsonBoolean::valueOf)),
ISFINITE(0, (context, _) -> context.stream().map(JsonMath::isFinite).map(JsonBoolean::valueOf)),
ISNORMAL(0, (context, _) -> context.stream().map(JsonMath::isNormal).map(JsonBoolean::valueOf)),
ISEMPTY(1, (context, args) -> Util.lazy(() -> JsonBoolean.valueOf(JsonMath.isEmpty(args.getFirst().evaluate(context))))),
// string operations
TRIM(0, (context, _) -> context.stream().map(JsonMath::trim)),
LTRIM(0, (context, _) -> context.stream().map(JsonMath::ltrim)),
RTRIM(0, (context, _) -> context.stream().map(JsonMath::rtrim)),
LTRIMSTR(1, (context, args) -> args.getFirst().evaluate(context).map(prefix -> JsonMath.ltrimstr(context.root(), prefix))),
RTRIMSTR(1, (context, args) -> args.getFirst().evaluate(context).map(suffix -> JsonMath.rtrimstr(context.root(), suffix))),
SPLIT$1(1, (context, args) -> args.getFirst().evaluate(context).map(separator -> JsonMath.split(context.root(), separator))),
JOIN(1, (context, args) -> args.getFirst().evaluate(context).map(separator -> JsonMath.join(context.root(), separator))),
IMPLODE(0, (context, _) -> context.stream().map(JsonMath::implode)),
EXPLODE(0, (context, _) -> context.stream().map(JsonMath::explode)),
ASCII_UPCASE(0, (context, _) -> context.stream().map(JsonMath::asciiUpcase)),
ASCII_DOWNCASE(0, (context, _) -> context.stream().map(JsonMath::asciiDowncase)),
UTF8BYTELENGTH(0, (context, _) -> context.stream().map(JsonMath::utf8ByteLength)),
STARTSWITH(1, (context, args) -> args.getFirst().evaluate(context).map(prefix -> JsonMath.startswith(context.root(), prefix))),
ENDSWITH(1, (context, args) -> args.getFirst().evaluate(context).map(prefix -> JsonMath.endswith(context.root(), prefix))),
// regex
TEST$1(1, JsonMath::test),
TEST$2(2, JsonMath::test),
MATCH$1(1, JsonMath::match),
MATCH$2(2, JsonMath::match),
CAPTURE$1(1, JsonMath::capture),
CAPTURE$2(2, JsonMath::capture),
SCAN$1(1, JsonMath::scan),
SCAN$2(2, JsonMath::scan),
SPLIT$2(2, JsonMath::split),
SPLITS$1(1, JsonMath::splits),
SPLITS$2(2, JsonMath::splits),
SUB$2(2, JsonMath::sub),
SUB$3(3, JsonMath::sub),
GSUB$2(2, JsonMath::gsub),
GSUB$3(3, JsonMath::gsub),
// conversions
TYPE(0, (context, _) -> context.stream().map(JsonMath::type)),
TOJSON(0, (context, _) -> context.stream().map(JsonMath::tojson)),
TOSTRING(0, (context, _) -> context.stream().map(JsonMath::tostring)),
TONUMBER(0, (context, _) -> context.stream().map(JsonMath::tonumber)),
FROMJSON(0, (context, _) -> context.stream().map(JsonMath::fromjson)),
// misc
LENGTH(0, (context, _) -> context.stream().map(JsonMath::length)),
REPEAT(1, (context, args) -> Stream.generate(() -> args.getFirst().evaluate(context)).flatMap(Function.identity())),
// math library
ACOS(0, (context, _) -> context.stream().map(JsonMath::acos)),
ACOSH(0, (context, _) -> context.stream().map(JsonMath::acosh)),
ASIN(0, (context, _) -> context.stream().map(JsonMath::asin)),
ASINH(0, (context, _) -> context.stream().map(JsonMath::asinh)),
ATAN(0, (context, _) -> context.stream().map(JsonMath::atan)),
ATANH(0, (context, _) -> context.stream().map(JsonMath::atanh)),
CBRT(0, (context, _) -> context.stream().map(JsonMath::cbrt)),
CEIL(0, (context, _) -> context.stream().map(JsonMath::ceil)),
COS(0, (context, _) -> context.stream().map(JsonMath::cos)),
COSH(0, (context, _) -> context.stream().map(JsonMath::cosh)),
ERF(0, (context, _) -> context.stream().map(JsonMath::erf)),
ERFC(0, (context, _) -> context.stream().map(JsonMath::erfc)),
EXP(0, (context, _) -> context.stream().map(JsonMath::exp)),
EXP10(0, (context, _) -> context.stream().map(JsonMath::exp10)),
EXP2(0, (context, _) -> context.stream().map(JsonMath::exp2)),
EXPM1(0, (context, _) -> context.stream().map(JsonMath::expm1)),
FABS(0, (context, _) -> context.stream().map(JsonMath::fabs)),
FLOOR(0, (context, _) -> context.stream().map(JsonMath::floor)),
GAMMA(0, (context, _) -> context.stream().map(JsonMath::gamma)),
J0(0, (context, _) -> context.stream().map(JsonMath::j0)),
J1(0, (context, _) -> context.stream().map(JsonMath::j1)),
LGAMMA(0, (context, _) -> context.stream().map(JsonMath::lgamma)),
LOG(0, (context, _) -> context.stream().map(JsonMath::log)),
LOG10(0, (context, _) -> context.stream().map(JsonMath::log10)),
LOG1P(0, (context, _) -> context.stream().map(JsonMath::log1p)),
LOG2(0, (context, _) -> context.stream().map(JsonMath::log2)),
LOGB(0, (context, _) -> context.stream().map(JsonMath::logb)),
NEARBYINT(0, (context, _) -> context.stream().map(JsonMath::nearbyint)),
RINT(0, (context, _) -> context.stream().map(JsonMath::rint)),
ROUND(0, (context, _) -> context.stream().map(JsonMath::round)),
SIGNIFICAND(0, (context, _) -> context.stream().map(JsonMath::significand)),
SIN(0, (context, _) -> context.stream().map(JsonMath::sin)),
SINH(0, (context, _) -> context.stream().map(JsonMath::sinh)),
SQRT(0, (context, _) -> context.stream().map(JsonMath::sqrt)),
TAN(0, (context, _) -> context.stream().map(JsonMath::tan)),
TANH(0, (context, _) -> context.stream().map(JsonMath::tanh)),
TGAMMA(0, (context, _) -> context.stream().map(JsonMath::tgamma)),
TRUNC(0, (context, _) -> context.stream().map(JsonMath::trunc)),
Y0(0, (context, _) -> context.stream().map(JsonMath::y0)),
Y1(0, (context, _) -> context.stream().map(JsonMath::y1)),
ATAN2(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.atan2(values.get(0), values.get(1)))),
COPYSIGN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.copysign(values.get(0), values.get(1)))),
DREM(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.drem(values.get(0), values.get(1)))),
FDIM(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fdim(values.get(0), values.get(1)))),
FMAX(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fmax(values.get(0), values.get(1)))),
FMIN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fmin(values.get(0), values.get(1)))),
FMOD(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fmod(values.get(0), values.get(1)))),
FREXP(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.frexp(values.get(0), values.get(1)))),
HYPOT(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.hypot(values.get(0), values.get(1)))),
JN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.jn(values.get(0), values.get(1)))),
LDEXP(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.ldexp(values.get(0), values.get(1)))),
MODF(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.modf(values.get(0), values.get(1)))),
NEXTAFTER(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.nextafter(values.get(0), values.get(1)))),
NEXTTOWARD(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.nexttoward(values.get(0), values.get(1)))),
POW(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.pow(values.get(0), values.get(1)))),
REMAINDER(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.remainder(values.get(0), values.get(1)))),
SCALB(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.scalb(values.get(0), values.get(1)))),
SCALBLN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.scalbln(values.get(0), values.get(1)))),
YN(2, (context, args) -> Util.cross(args, context).map(values -> JsonMath.yn(values.get(0), values.get(1)))),
FMA(3, (context, args) -> Util.cross(args, context).map(values -> JsonMath.fma(values.get(0), values.get(1), values.get(2))))
;
public static final @NotNull Map<@NotNull String, @NotNull JQInvocable> ALL_BUILTINS = Arrays.stream(JQBuiltIn.values())
.collect(Collectors.toMap(
JQInvocable::reference,
Function.identity()
));
private final @NotNull String identifier;
private final int arity;
private final @NotNull Implementation implementation;
JQBuiltIn(int arity, @NotNull Implementation implementation) {
var identifier = this.name().toLowerCase(Locale.ROOT);
var idx = identifier.lastIndexOf("$");
if (idx != -1) identifier = identifier.substring(0, idx);
this.identifier = identifier;
this.arity = arity;
this.implementation = implementation;
}
@Override
public @NotNull Stream<@Nullable JsonValue> invoke(JQExpression.@NotNull Context context, @NotNull List<@NotNull JQExpression> args) {
if (args.size() != arity) throw new JsonQueryException("invalid argument count");
return implementation.invoke(context, args);
}
@Override
public @NotNull String identifier() {
return identifier;
}
@Override
public int arity() {
return arity;
}
@FunctionalInterface
private interface Implementation {
@NotNull Stream<@Nullable JsonValue> invoke(@NotNull JQExpression.Context context, @NotNull List<@NotNull JQExpression> args);
}
}

View File

@ -0,0 +1,58 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public record JQCommaExpression(@NotNull JQExpression first, @NotNull JQExpression second) implements JQExpression {
public JQCommaExpression {
Objects.requireNonNull(first);
Objects.requireNonNull(second);
}
@Override
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
var it = new Iterator<JsonValue>() {
private Iterator<JsonValue> delegate;
private Queue<Supplier<Iterator<JsonValue>>> queue = new LinkedList<>(List.of(
() -> first.evaluate(context).iterator(),
() -> second.evaluate(context).iterator()
));
@Override
public boolean hasNext() {
if (delegate == null) delegate = queue.remove().get();
while (!delegate.hasNext() && !queue.isEmpty()) {
delegate = queue.remove().get();
}
return delegate.hasNext();
}
@Override
public JsonValue next() {
if (delegate == null) delegate = queue.remove().get();
while (!delegate.hasNext() && !queue.isEmpty()) {
delegate = queue.remove().get();
}
return delegate.next();
}
};
var spliterator = Spliterators.spliteratorUnknownSize(it, Spliterator.ORDERED);
return StreamSupport.stream(spliterator, false);
}
@Override
public boolean isConstant() {
return first.isConstant() && second.isConstant();
}
@Override
public @NotNull String toString() {
return first + ", " + second;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.stream.Stream;
public record JQIterateExpression(@NotNull JQExpression expression, boolean optional) implements JQExpression {
public JQIterateExpression {
Objects.requireNonNull(expression);
}
@Override
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
return expression.evaluate(context).flatMap(value -> JsonMath.values(value, optional));
}
@Override
public boolean isConstant() {
return expression.isConstant();
}
@Override
public @NotNull String toString() {
return expression + "[]" + (optional ? "?" : "");
}
}

View File

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

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
package eu.jonahbauer.json.query.parser.ast;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public record JQProgram(
@Nullable JQModule module,
@NotNull List<@NotNull JQImport> imports,
@NotNull List<@NotNull JQFunction> functions,
@Nullable JQExpression expression
) {
}

View File

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

View File

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

View File

@ -0,0 +1,67 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonArray;
import eu.jonahbauer.json.JsonNumber;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.util.Util;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public record JQSliceExpression(@NotNull JQExpression expression, @Nullable JQExpression start, @Nullable JQExpression end, boolean optional) implements JQExpression {
public JQSliceExpression {
Objects.requireNonNull(expression);
if (start == null && end == null) throw new IllegalArgumentException();
}
@Override
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
return expression.evaluate(context).flatMap(value -> switch (value) {
case JsonArray array -> slice(context, "an array slice", array.size(), array::subList);
case JsonString string -> slice(context, "a string slice", string.length(), string::subSequence);
case null -> Stream.of((JsonValue) null);
default -> {
if (optional) yield Stream.empty();
throw new JsonQueryException(STR."Cannot index \{Util.type(value)} with object.");
}
});
}
private @NotNull Stream<@Nullable JsonValue> slice(@NotNull Context context, @NotNull String type, int length, @NotNull BiFunction<Integer, Integer, JsonValue> slice) {
return getIndices(start, context, type, length, 0)
.mapToObj(start -> getIndices(end, context, type, length, length)
.mapToObj(end -> start > end ? slice.apply(0, 0) : slice.apply(start, end))
)
.flatMap(Function.identity());
}
private @NotNull IntStream getIndices(@Nullable JQExpression expression, @NotNull Context context, @NotNull String type, int length, int fallback) {
if (expression == null) return IntStream.of(fallback);
return expression.evaluate(context).mapToInt(value -> getIndex(value, type, length));
}
private int getIndex(@Nullable JsonValue value, @NotNull String type, int length) {
if (!(value instanceof JsonNumber(double d))) {
throw new JsonQueryException(STR."Start and end indices of \{type} must be numbers.");
}
var i = (int) Math.floor(d);
return i < 0 ? Math.max(0, length + i) : Math.min(length, i);
}
@Override
public boolean isConstant() {
return expression.isConstant() && (start == null || start.isConstant()) && (end == null || end.isConstant());
}
@Override
public @NotNull String toString() {
return expression + "[" + (start == null ? "" : start) + ":" + (end == null ? "" : end) + "]";
}
}

View File

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

View File

@ -0,0 +1,98 @@
package eu.jonahbauer.json.query.parser.ast;
import eu.jonahbauer.json.JsonString;
import eu.jonahbauer.json.JsonValue;
import eu.jonahbauer.json.query.JsonQueryException;
import eu.jonahbauer.json.query.JsonQueryHaltException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public record JQTryExpression(@NotNull JQExpression expression, @Nullable JQExpression fallback) implements JQExpression {
public JQTryExpression {
Objects.requireNonNull(expression);
}
public JQTryExpression(@NotNull JQExpression expression) {
this(expression, null);
}
@Override
public @NotNull Stream<@Nullable JsonValue> evaluate(@NotNull Context context) {
var iterator = new QuietIterator(
expression.evaluate(context).iterator(),
fallback == null ? null : ex -> fallback.evaluate(context.withRoot(new JsonString(ex.getMessage()))).iterator()
);
var spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED);
return StreamSupport.stream(spliterator, false);
}
@Override
public boolean isConstant() {
return expression.isConstant() && (fallback == null || fallback.isConstant());
}
@Override
public @NotNull String toString() {
return "try " + expression + (fallback == null ? "" : " catch " + fallback);
}
private static class QuietIterator implements Iterator<@Nullable JsonValue> {
private @Nullable Iterator<@Nullable JsonValue> delegate;
private @Nullable Function<@NotNull JsonQueryException, @NotNull Iterator<@Nullable JsonValue>> fallback;
private @Nullable JsonValue next;
private boolean hasNext;
private boolean valid = false;
private QuietIterator(
@NotNull Iterator<@Nullable JsonValue> delegate,
@Nullable Function<@NotNull JsonQueryException, @NotNull Iterator<@Nullable JsonValue>> fallback
) {
this.delegate = delegate;
this.fallback = fallback;
}
private void ensureValid() {
if (valid) return;
while (true) {
try {
if (delegate != null && delegate.hasNext()) { // still have values in current stream
next = delegate.next();
hasNext = true;
} else { // end of stream
next = null;
hasNext = false;
}
break;
} catch (JsonQueryHaltException ex) {
throw ex;
} catch (JsonQueryException ex) { // switch to fallback
delegate = fallback != null ? fallback.apply(ex) : null;
fallback = null;
}
}
valid = true;
}
@Override
public boolean hasNext() {
ensureValid();
return hasNext;
}
@Override
public @Nullable JsonValue next() {
ensureValid();
if (!hasNext) throw new NoSuchElementException();
valid = false;
return next;
}
}
}

View File

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

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

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")