fixup query
This commit is contained in:
parent
ef88d89ce1
commit
87ae709f6d
@ -13,10 +13,6 @@ 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given int to a JSON number.
|
||||
* @param i an int
|
||||
|
@ -348,6 +348,49 @@ public class JsonMath {
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="loops" defaultstate="collapsed">
|
||||
public static @NotNull Generator<@Nullable JsonValue> recurse(@Nullable JsonValue value) {
|
||||
return recurse(value, v -> values(v, true));
|
||||
}
|
||||
|
||||
public static @NotNull Generator<@Nullable JsonValue> recurse(@Nullable JsonValue value, @NotNull JQFilter f) {
|
||||
return recurse(value, f, v -> Generator.of(JsonBoolean.TRUE));
|
||||
}
|
||||
|
||||
public static @NotNull Generator<@Nullable JsonValue> recurse(@Nullable JsonValue value, @NotNull JQFilter f, @NotNull JQFilter condition) {
|
||||
return Generator.concat(Generator.of(value), f.apply(value)
|
||||
.flatMap(v -> condition.apply(v).filter(JsonMath::isTruthy).map(_ -> v))
|
||||
.flatMap(v -> recurse(v, f, condition))
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Generator<@Nullable JsonValue> walk(@Nullable JsonValue value, @NotNull JQFilter filter) {
|
||||
var result = switch (value) {
|
||||
case JsonObject object -> mapValues(object, v -> walk(v, filter));
|
||||
case JsonArray array -> map(array, v -> walk(v, filter));
|
||||
case null, default -> value;
|
||||
};
|
||||
return filter.apply(result);
|
||||
}
|
||||
|
||||
public static @NotNull Generator<@Nullable JsonValue> while_(@Nullable JsonValue value, @NotNull JQFilter condition, @NotNull JQFilter update) {
|
||||
return condition.apply(value)
|
||||
.filter(JsonMath::isTruthy)
|
||||
.flatMap(_ -> Generator.concat(
|
||||
Generator.of(value),
|
||||
update.apply(value).flatMap(v -> while_(v, condition, update)))
|
||||
);
|
||||
}
|
||||
|
||||
public static @NotNull Generator<@Nullable JsonValue> until(@Nullable JsonValue value, @NotNull JQFilter condition, @NotNull JQFilter next) {
|
||||
return condition.apply(value)
|
||||
.flatMap(bool -> JsonMath.isTruthy(bool)
|
||||
? Generator.of(value)
|
||||
: next.apply(value).flatMap(v -> until(v, condition, next))
|
||||
);
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="iterable operations" defaultstate="collapsed">
|
||||
public static @Nullable JsonValue index(@Nullable JsonValue value, @Nullable JsonValue index) {
|
||||
return switch (index) {
|
||||
@ -379,10 +422,48 @@ public class JsonMath {
|
||||
}
|
||||
case null, default -> throw indexError(value, index);
|
||||
};
|
||||
case JsonObject indexObject -> slice(value, indexObject.get("start"), indexObject.get("end"));
|
||||
case null, default -> throw indexError(value, index);
|
||||
};
|
||||
}
|
||||
|
||||
public static @Nullable JsonValue slice(@Nullable JsonValue value, @Nullable JsonValue start, @Nullable JsonValue end) {
|
||||
String type;
|
||||
int length;
|
||||
BiFunction<Integer, Integer, JsonValue> slice;
|
||||
if (value instanceof JsonString string) {
|
||||
type = "a string slice";
|
||||
length = string.length();
|
||||
slice = string::subSequence;
|
||||
} else if (value instanceof JsonArray array) {
|
||||
type = "an array slice";
|
||||
length = array.size();
|
||||
slice = array::subList;
|
||||
} else {
|
||||
throw indexError(value, JsonObject.EMPTY);
|
||||
}
|
||||
|
||||
if (start != null && !(start instanceof JsonNumber) || end != null && !(end instanceof JsonNumber)) {
|
||||
throw new JsonQueryException("Start and end indices of " + type + " must be numbers");
|
||||
}
|
||||
var lower = getIndex((JsonNumber) start, length, false);
|
||||
var upper = getIndex((JsonNumber) end, length, true);
|
||||
if (lower > upper) {
|
||||
return slice.apply(0, 0);
|
||||
} else {
|
||||
return slice.apply(lower, upper);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getIndex(@Nullable JsonNumber index, int length, boolean isUpper) {
|
||||
if (index == null) {
|
||||
return isUpper ? length : 0;
|
||||
}
|
||||
var d = index.value();
|
||||
if (d < 0) d = length + d;
|
||||
return isUpper ? (int) Math.ceil(d) : (int) Math.floor(d);
|
||||
}
|
||||
|
||||
private static boolean isInt(double value) {
|
||||
return ((int) value) == value;
|
||||
}
|
||||
@ -674,6 +755,12 @@ public class JsonMath {
|
||||
}
|
||||
}
|
||||
return JsonBoolean.TRUE;
|
||||
} else if (container instanceof JsonBoolean bcontainer && content instanceof JsonBoolean bcontent) {
|
||||
return JsonBoolean.valueOf(bcontainer == bcontent);
|
||||
} else if (container instanceof JsonNumber ncontainer && content instanceof JsonNumber ncontent) {
|
||||
return JsonBoolean.valueOf(ncontainer.equals(ncontent));
|
||||
} else if (container == null && content == null) {
|
||||
return JsonBoolean.TRUE;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -1324,7 +1411,72 @@ public class JsonMath {
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="paths" defaultstate="collapsed">
|
||||
public static @Nullable JsonValue getpath(@Nullable JsonValue value, @Nullable JsonValue path) {
|
||||
if (!(path instanceof JsonArray array)) {
|
||||
throw new JsonQueryException("Path must be specified as an array");
|
||||
}
|
||||
|
||||
var v = value;
|
||||
for (var node : array) {
|
||||
v = index(v, node);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// public static @Nullable JsonValue setpath(@Nullable JsonValue container, @Nullable JsonValue path, @Nullable JsonValue value) {
|
||||
// if (!(path instanceof JsonArray array)) {
|
||||
// throw new JsonQueryException("Path must be specified as an array");
|
||||
// }
|
||||
// if (array.isEmpty()) {
|
||||
// return value;
|
||||
// } else if (array.size() > 1) {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// if (array.size() > 1) {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
||||
public static @NotNull Generator<@NotNull JsonArray> paths(@Nullable JsonValue value) {
|
||||
return switch (value) {
|
||||
case JsonObject object -> Generator.from(object.keySet()).flatMap(key -> Generator.concat(
|
||||
Generator.of(JsonArray.valueOf(key)),
|
||||
paths(object.get(key)).map(path -> {
|
||||
var list = new ArrayList<>(path.size() + 1);
|
||||
list.add(JsonString.valueOf(key));
|
||||
list.addAll(path);
|
||||
return JsonArray.valueOf(list);
|
||||
})
|
||||
));
|
||||
case JsonArray array -> Generator.from(IntStream.range(0, array.size()).boxed()).flatMap(i -> Generator.concat(
|
||||
Generator.of(JsonArray.valueOf(JsonNumber.valueOf(i))),
|
||||
paths(array.get(i)).map(path -> {
|
||||
var list = new ArrayList<>(path.size() + 1);
|
||||
list.add(JsonNumber.valueOf(i));
|
||||
list.addAll(path);
|
||||
return JsonArray.valueOf(list);
|
||||
})
|
||||
));
|
||||
case null, default -> Generator.empty();
|
||||
};
|
||||
}
|
||||
|
||||
public static @NotNull Generator<@NotNull JsonArray> paths(@Nullable JsonValue value, @NotNull JQFilter filter) {
|
||||
return paths(value)
|
||||
.flatMap(path -> filter.apply(getpath(value, path))
|
||||
.filter(JsonMath::isTruthy)
|
||||
.map(v -> path)
|
||||
);
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="misc" defaultstate="collapsed">
|
||||
public static @NotNull JsonObject env() {
|
||||
return JsonObject.valueOf(System.getenv());
|
||||
}
|
||||
|
||||
public static @NotNull JsonNumber length(@Nullable JsonValue value) {
|
||||
return new JsonNumber(length0(value));
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package eu.jonahbauer.json.query;
|
||||
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.SequencedMap;
|
||||
|
||||
@ -10,6 +12,14 @@ public class Main {
|
||||
System.out.println();
|
||||
System.out.println("Object Identifier-Index: .foo, .foo.bar");
|
||||
|
||||
JsonQuery.parse("{\"foo\": 1, \"bar\": 2} | reduce . as {(\"foo\", \"bar\"): $item} (0; . + $item, 2 * . * $item)")
|
||||
.run(null)
|
||||
.forEach(System.out::println);
|
||||
|
||||
JsonQuery.parse(".[] as {$a, $b, c: {$d, $e}} ?// {$a, $b, c: {$d, $e}} | {$a, $b, $d, $e}")
|
||||
.run(JsonValue.parse("[{\"a\": 1, \"b\": 2, \"c\": {\"d\": 3, \"e\": 4}}, {\"a\": 1, \"b\": 2, \"c\": [{\"d\": 3, \"e\": 4}]}]"))
|
||||
.forEach(System.out::println);
|
||||
|
||||
JsonQuery.parse(".foo")
|
||||
.run(JSON."{\"foo\": 42, \"bar\": \"less interesting data\"}")
|
||||
.forEach(System.out::println);
|
||||
|
@ -442,7 +442,7 @@ public class JQParser {
|
||||
return new JQFunctionInvocation(name, args);
|
||||
}
|
||||
|
||||
private @NotNull JQExpression parseVariableExpression() throws IOException {
|
||||
private @NotNull JQVariableExpression parseVariableExpression() throws IOException {
|
||||
consume(JQTokenKind.DOLLAR);
|
||||
var ident = consume(JQTokenKind.IDENT).text();
|
||||
return new JQVariableExpression("$" + ident);
|
||||
@ -470,9 +470,71 @@ public class JQParser {
|
||||
consume(JQTokenKind.LBRACE);
|
||||
if (tryConsume(JQTokenKind.RBRACE)) {
|
||||
return new JQConstant(JsonObject.EMPTY);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("not yet implemented");
|
||||
}
|
||||
|
||||
var entries = new ArrayList<JQObjectConstructionExpression.Entry>();
|
||||
while (!tryConsume(JQTokenKind.RBRACE)) {
|
||||
if (!entries.isEmpty()) {
|
||||
consume(JQTokenKind.COMMA);
|
||||
}
|
||||
|
||||
if (peek(JQTokenKind.LOC)) {
|
||||
var loc = consume(JQTokenKind.LOC);
|
||||
entries.add(new JQObjectConstructionExpression.Entry(
|
||||
new JQConstant(JsonValue.valueOf("__loc__")),
|
||||
new JQLocExpression("<top-level>", loc.line())
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
var next = peek();
|
||||
JQExpression key;
|
||||
JQExpression value = null;
|
||||
|
||||
switch (next == null ? null : next.kind()) {
|
||||
case IDENT -> {
|
||||
key = new JQConstant(JsonString.valueOf(consume(JQTokenKind.IDENT).text()));
|
||||
value = new JQIndexExpression(new JQRootExpression(), key, false);
|
||||
}
|
||||
case QQSTRING_START -> {
|
||||
key = parseString();
|
||||
value = new JQIndexExpression(new JQRootExpression(), key, false);
|
||||
}
|
||||
case DOLLAR -> {
|
||||
var var = parseVariableExpression();
|
||||
key = new JQConstant(JsonString.valueOf(var.name().substring(1)));
|
||||
value = var;
|
||||
}
|
||||
case LPAREN -> {
|
||||
key = parseParenthesizedExpression();
|
||||
}
|
||||
case AS, IMPORT, INCLUDE, MODULE, DEF, IF, THEN, ELSE, ELSE_IF, AND, OR, END, REDUCE, FOREACH, TRY,
|
||||
CATCH, LABEL, BREAK -> {
|
||||
key = new JQConstant(JsonString.valueOf(consume(next.kind()).text()));
|
||||
value = key;
|
||||
}
|
||||
case null -> throw new JsonQueryParserException(-1, -1, "unexpected end of file");
|
||||
default -> {
|
||||
var token = Objects.requireNonNull(next);
|
||||
throw new JsonQueryParserException(token.line(),
|
||||
token.column(),
|
||||
"unexpected token " + token.kind()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
consume(JQTokenKind.COLON);
|
||||
} else if (!tryConsume(JQTokenKind.COLON)) {
|
||||
entries.add(new JQObjectConstructionExpression.Entry(key, value));
|
||||
continue;
|
||||
}
|
||||
|
||||
value = parseLeftAssocBinaryExpression(Map.of(JQTokenKind.PIPE, JQPipeExpression::new), this::parseNegation);
|
||||
entries.add(new JQObjectConstructionExpression.Entry(key, value));
|
||||
}
|
||||
|
||||
return new JQObjectConstructionExpression(entries);
|
||||
}
|
||||
|
||||
private @NotNull JQExpression parseIndexingExpression(@NotNull JQExpression root) throws IOException {
|
||||
|
@ -17,10 +17,10 @@ public record JQAsExpression(
|
||||
Objects.requireNonNull(patterns);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return variable.evaluate(context)
|
||||
.flatMap(value -> patterns.bind(context, value).map(context::withVariables).flatMap(expression::evaluate));
|
||||
return variable.evaluate(context).flatMap(value -> patterns.bind(context, value, expression::evaluate));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -39,6 +39,14 @@ public enum JQBuiltIn implements JQInvocable {
|
||||
NTH$2(2, (context, args) -> args.getFirst().evaluate(context).flatMap(n -> JsonMath.nth(args.getLast().evaluate(context), n))),
|
||||
SELECT(1, Implementation.mapF$F(JsonMath::select)),
|
||||
|
||||
// loops
|
||||
RECURSE$0(0, Implementation.map$F(JsonMath::recurse)),
|
||||
RECURSE$1(1, Implementation.mapF$F(JsonMath::recurse)),
|
||||
RECURSE$2(2, Implementation.mapFF$F(JsonMath::recurse)),
|
||||
WALK$1(1, Implementation.mapF$F(JsonMath::walk)),
|
||||
WHILE(2, Implementation.mapFF$F(JsonMath::while_)),
|
||||
UNTIL(2, Implementation.mapFF$F(JsonMath::until)),
|
||||
|
||||
// iterable operations
|
||||
MAP(1, Implementation.mapF$V(JsonMath::map)),
|
||||
MAP_VALUES(1, Implementation.mapF$V(JsonMath::mapValues)),
|
||||
@ -147,11 +155,19 @@ public enum JQBuiltIn implements JQInvocable {
|
||||
TODATE(0, Implementation.map$V(JsonMath::todate)),
|
||||
NOW(0, Implementation.map$V(_ -> JsonNumber.valueOf(System.currentTimeMillis()))),
|
||||
|
||||
// paths
|
||||
GETPATH(1, Implementation.mapV$V(JsonMath::getpath)),
|
||||
PATHS$0(0, Implementation.map$F(JsonMath::paths)),
|
||||
PATHS$1(1, Implementation.mapF$F(JsonMath::paths)),
|
||||
|
||||
// misc
|
||||
ENV(0, Implementation.map$V(_ -> JsonMath.env())),
|
||||
LENGTH(0, Implementation.map$V(JsonMath::length)),
|
||||
REPEAT(1, (context, args) -> JsonMath.repeat(() -> args.getFirst().evaluate(context))),
|
||||
|
||||
// math library
|
||||
INFINITE(0, Implementation.map$V(_ -> JsonNumber.valueOf(Double.POSITIVE_INFINITY))),
|
||||
NAN(0, Implementation.map$V(_ -> JsonNumber.valueOf(Double.NaN))),
|
||||
ACOS(0, Implementation.map$V(JsonMath::acos)),
|
||||
ACOSH(0, Implementation.map$V(JsonMath::acosh)),
|
||||
ASIN(0, Implementation.map$V(JsonMath::asin)),
|
||||
@ -366,6 +382,14 @@ public enum JQBuiltIn implements JQInvocable {
|
||||
};
|
||||
}
|
||||
|
||||
static @NotNull Implementation mapFF$F(@NotNull TriFunction<? super @Nullable JsonValue, ? super @NotNull JQFilter, ? super @NotNull JQFilter, ? extends @NotNull Generator<? extends @Nullable JsonValue>> function) {
|
||||
return (context, args) -> {
|
||||
var arg0 = args.get(0).bind(context);
|
||||
var arg1 = args.get(1).bind(context);
|
||||
return context.stream().flatMap(root -> function.apply(root, arg0, arg1));
|
||||
};
|
||||
}
|
||||
|
||||
static @NotNull Implementation filter(@NotNull Predicate<@Nullable JsonValue> predicate) {
|
||||
return (context, _) -> context.stream().filter(predicate);
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
package eu.jonahbauer.json.query.parser.ast;
|
||||
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
import eu.jonahbauer.json.query.JsonMath;
|
||||
import eu.jonahbauer.json.query.JsonQueryException;
|
||||
import eu.jonahbauer.json.query.impl.Generator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface JQExpression {
|
||||
|
||||
@ -28,18 +28,17 @@ public interface JQExpression {
|
||||
) {
|
||||
|
||||
public Context(@Nullable JsonValue root) {
|
||||
this(root, Map.of(), JQBuiltIn.ALL_BUILTINS);
|
||||
this(root, Map.of("$ENV", JsonMath.env()), 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
|
||||
));
|
||||
var map = new LinkedHashMap<String, @Nullable JsonValue>();
|
||||
variables.forEach((key, value) -> {
|
||||
Objects.requireNonNull(key);
|
||||
if (!key.startsWith("$")) throw new IllegalArgumentException();
|
||||
map.put(key, value);
|
||||
});
|
||||
variables = Collections.unmodifiableMap(map);
|
||||
functions = Map.copyOf(functions);
|
||||
}
|
||||
|
||||
|
@ -17,36 +17,17 @@ public record JQForEachExpression(
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return init.evaluate(context)
|
||||
.flatMap(s -> new Generator<>() {
|
||||
private final Generator<JsonValue> source = expression.evaluate(context);
|
||||
private @NotNull Generator<JsonValue> current = Generator.empty();
|
||||
private @Nullable JsonValue state = s;
|
||||
|
||||
private @NotNull Generator<JsonValue> advance() {
|
||||
var item = source.next();
|
||||
|
||||
return patterns.bind(context, item)
|
||||
.map(context::withVariables)
|
||||
.map(ctx -> ctx.withRoot(state))
|
||||
.flatMap(ctx -> update.evaluate(ctx).map(s -> {
|
||||
state = s;
|
||||
return ctx.withRoot(s);
|
||||
}).flatMap(extract::evaluate));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
while (!current.hasNext() && source.hasNext()) current = advance();
|
||||
return current.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue next() throws NoSuchElementException {
|
||||
while (!current.hasNext() && source.hasNext()) current = advance();
|
||||
return current.next();
|
||||
}
|
||||
});
|
||||
return init.evaluate(context).flatMap(initial -> {
|
||||
var state = new Object() {
|
||||
private JsonValue state = initial;
|
||||
};
|
||||
return expression.evaluate(context).flatMap(value -> patterns.bind(context, value, ctx -> {
|
||||
return update.evaluate(ctx.withRoot(state.state)).map(v -> {
|
||||
state.state = v;
|
||||
return ctx.withRoot(v);
|
||||
}).flatMap(extract::evaluate);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,52 @@
|
||||
package eu.jonahbauer.json.query.parser.ast;
|
||||
|
||||
import eu.jonahbauer.json.JsonObject;
|
||||
import eu.jonahbauer.json.JsonString;
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
import eu.jonahbauer.json.query.JsonQueryException;
|
||||
import eu.jonahbauer.json.query.impl.Generator;
|
||||
import eu.jonahbauer.json.query.util.Util;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public record JQObjectConstructionExpression(@NotNull List<@NotNull Entry> entries) implements JQExpression {
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
var generator = Generator.of(new LinkedHashMap<@NotNull String, @Nullable JsonValue>());
|
||||
for (var entry : entries) {
|
||||
generator = generator.flatMap(map -> entry.key().evaluate(context)
|
||||
.flatMap(key -> {
|
||||
if (!(key instanceof JsonString(var string))) {
|
||||
throw new JsonQueryException("Cannot use " + Util.type(key) + "(" + Util.value(key) + ") as object key.");
|
||||
}
|
||||
return entry.value().evaluate(context)
|
||||
.map(value -> {
|
||||
var out = new LinkedHashMap<>(map);
|
||||
out.put(string, value);
|
||||
return out;
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
return generator.map(JsonObject::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConstant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
var out = new StringJoiner(", ", "{", "}");
|
||||
entries.forEach(entry -> out.add(entry.key() + ": " + entry.value()));
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
public record Entry(@NotNull JQExpression key, @NotNull JQExpression value) { }
|
||||
}
|
@ -5,11 +5,13 @@ import eu.jonahbauer.json.JsonValue;
|
||||
import eu.jonahbauer.json.query.JsonMath;
|
||||
import eu.jonahbauer.json.query.JsonQueryException;
|
||||
import eu.jonahbauer.json.query.impl.Generator;
|
||||
import eu.jonahbauer.json.query.parser.ast.JQExpression.Context;
|
||||
import eu.jonahbauer.json.query.util.Util;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -20,60 +22,74 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
|
||||
if (patterns.isEmpty()) throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
public @NotNull Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.Context context, @Nullable JsonValue v) {
|
||||
var variables = new HashMap<String, JsonValue>();
|
||||
Generator.from(patterns).map(Pattern::variables).flatMap(Generator::from).forEach(key -> variables.put(key, null));
|
||||
public @NotNull Generator<@Nullable JsonValue> bind(@NotNull Context context, @Nullable JsonValue value, Function<@NotNull Context, @NotNull Generator<@Nullable JsonValue>> downstream) {
|
||||
return new PatternGeneratorImpl(patterns, context, value, downstream);
|
||||
}
|
||||
|
||||
var result = new Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>>() {
|
||||
private final Generator<@NotNull Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>>> source
|
||||
= Generator.from(patterns).map(pattern -> pattern.bind(context, v));
|
||||
private @NotNull Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>> current = Generator.of(() -> { throw new JsonQueryException("empty"); });
|
||||
private static final class PatternGeneratorImpl implements Generator<@Nullable JsonValue> {
|
||||
// one generator per pattern
|
||||
private final @NotNull Generator<@NotNull Generator<@NotNull Map<@NotNull String, @Nullable JsonValue>>> patterns;
|
||||
private final @NotNull Context context;
|
||||
private final @NotNull Function<@NotNull Context, @NotNull Generator<@Nullable JsonValue>> downstream;
|
||||
|
||||
private boolean hasValue;
|
||||
private @Nullable Map<@NotNull String, @Nullable JsonValue> value;
|
||||
// the generator for the current pattern
|
||||
private @Nullable Generator<@Nullable JsonValue> current;
|
||||
|
||||
private boolean advance() {
|
||||
if (hasValue) return true;
|
||||
while (true) {
|
||||
try {
|
||||
hasValue = current.hasNext();
|
||||
value = hasValue ? current.next() : null;
|
||||
return hasValue;
|
||||
} catch (JsonQueryException _) {
|
||||
// switch to next pattern on error
|
||||
if (source.hasNext()) {
|
||||
current = source.next();
|
||||
} else {
|
||||
value = null;
|
||||
hasValue = false;
|
||||
return false;
|
||||
public PatternGeneratorImpl(
|
||||
@NotNull List<@NotNull Pattern> patterns, @NotNull Context context, @Nullable JsonValue value,
|
||||
@NotNull Function<@NotNull Context, @NotNull Generator<@Nullable JsonValue>> downstream
|
||||
) {
|
||||
this.context = context;
|
||||
this.downstream = downstream;
|
||||
var variables = new HashMap<String, JsonValue>();
|
||||
patterns.stream().map(Pattern::variables).flatMap(Collection::stream).forEach(key -> variables.put(key, null));
|
||||
this.patterns = Generator.from(patterns).map(pattern -> pattern.bind(context, value).map(vars -> {
|
||||
var out = new HashMap<>(variables);
|
||||
out.putAll(vars);
|
||||
return out;
|
||||
}));
|
||||
}
|
||||
|
||||
private <T> T advance(@NotNull Function<Generator<@Nullable JsonValue>, T> function) {
|
||||
var ex = new JsonQueryException("no match");
|
||||
while (true) {
|
||||
if (current == null) {
|
||||
// find the next matching pattern
|
||||
while (patterns.hasNext()) {
|
||||
try {
|
||||
var match = patterns.next();
|
||||
current = match.map(context::withVariables).flatMap(downstream);
|
||||
break;
|
||||
} catch (JsonQueryException e) {
|
||||
ex = e;
|
||||
}
|
||||
}
|
||||
|
||||
// no matching pattern; propagate exception from last match attempt
|
||||
if (current == null) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return function.apply(current);
|
||||
} catch (JsonQueryException e) {
|
||||
// exception during execution; try the next pattern
|
||||
current = null;
|
||||
ex = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return advance();
|
||||
}
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return advance(Generator::hasNext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Map<@NotNull String, @Nullable JsonValue> next() {
|
||||
if (!advance()) throw new NoSuchElementException();
|
||||
assert value != null;
|
||||
|
||||
var out = value;
|
||||
value = null;
|
||||
hasValue = false;
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
return result.map(vars -> {
|
||||
var out = new HashMap<>(variables);
|
||||
out.putAll(vars);
|
||||
return out;
|
||||
});
|
||||
@Override
|
||||
public @Nullable JsonValue next() throws NoSuchElementException {
|
||||
return advance(Generator::next);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -85,7 +101,7 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
|
||||
@NotNull
|
||||
Set<@NotNull String> variables();
|
||||
@NotNull
|
||||
Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value);
|
||||
Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value);
|
||||
|
||||
record ValuePattern(@NotNull String name) implements Pattern {
|
||||
public ValuePattern {
|
||||
@ -99,7 +115,7 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) {
|
||||
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
|
||||
var map = new HashMap<String, JsonValue>();
|
||||
map.put(name, value);
|
||||
return Generator.of(Collections.unmodifiableMap(map));
|
||||
@ -125,7 +141,7 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) {
|
||||
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
|
||||
var streams = new ArrayList<Supplier<Generator<Map<String, JsonValue>>>>();
|
||||
|
||||
for (int i = 0; i < patterns.size(); i++) {
|
||||
@ -164,7 +180,7 @@ public record JQPatterns(@NotNull List<@NotNull Pattern> patterns) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull JQExpression.Context context, @Nullable JsonValue value) {
|
||||
public @NotNull Generator<Map<@NotNull String, @Nullable JsonValue>> bind(@NotNull Context context, @Nullable JsonValue value) {
|
||||
var streams = new ArrayList<Supplier<Generator<Map<String, JsonValue>>>>();
|
||||
|
||||
this.patterns.reversed().forEach((keyExpression, pattern) -> streams.add(() -> {
|
||||
|
@ -14,27 +14,21 @@ public record JQReduceExpression(
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@Nullable JsonValue> evaluate(@NotNull Context context) {
|
||||
return init.evaluate(context)
|
||||
.map(state -> {
|
||||
var it = expression.evaluate(context);
|
||||
while (it.hasNext()) {
|
||||
var item = it.next();
|
||||
|
||||
var bindings = patterns.bind(context, item);
|
||||
var binding = bindings.next();
|
||||
while (bindings.hasNext()) {
|
||||
binding = bindings.next();
|
||||
return init.evaluate(context).flatMap(initial -> {
|
||||
var state = new Object() {
|
||||
private JsonValue state = initial;
|
||||
};
|
||||
return Generator.concat(
|
||||
expression.evaluate(context).flatMap(value -> patterns.bind(context, value, ctx -> {
|
||||
var gen = update.evaluate(ctx.withRoot(state.state));
|
||||
while (gen.hasNext()) {
|
||||
state.state = gen.next();
|
||||
}
|
||||
|
||||
JsonValue nextState = null;
|
||||
var up = update.evaluate(context.withVariables(binding).withRoot(state));
|
||||
while (up.hasNext()) {
|
||||
nextState = up.next();
|
||||
}
|
||||
state = nextState;
|
||||
}
|
||||
return state;
|
||||
});
|
||||
return Generator.empty();
|
||||
})),
|
||||
Generator.of(() -> state.state)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,20 +1,12 @@
|
||||
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.JsonMath;
|
||||
import eu.jonahbauer.json.query.impl.Generator;
|
||||
import eu.jonahbauer.json.query.util.Util;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.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 {
|
||||
@ -24,36 +16,12 @@ public record JQSliceExpression(@NotNull JQExpression expression, @Nullable JQEx
|
||||
|
||||
@Override
|
||||
public @NotNull Generator<@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 -> Generator.of((JsonValue) null);
|
||||
default -> {
|
||||
if (optional) yield Generator.empty();
|
||||
throw new JsonQueryException(STR."Cannot index \{Util.type(value)} with object.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull Generator<@Nullable JsonValue> slice(@NotNull Context context, @NotNull String type, int length, @NotNull BiFunction<Integer, Integer, JsonValue> slice) {
|
||||
return getIndices(start, context, type, length, 0)
|
||||
.map(start -> getIndices(end, context, type, length, length)
|
||||
.map(end -> start > end ? slice.apply(0, 0) : slice.apply(start, end))
|
||||
)
|
||||
.flatMap(Function.identity());
|
||||
}
|
||||
|
||||
private @NotNull Generator<Integer> getIndices(@Nullable JQExpression expression, @NotNull Context context, @NotNull String type, int length, int fallback) {
|
||||
if (expression == null) return Generator.of(fallback);
|
||||
return expression.evaluate(context).map(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);
|
||||
return expression.evaluate(context)
|
||||
.flatMap(value -> (start == null ? Generator.of((JsonValue) null) : start.evaluate(context))
|
||||
.flatMap(start -> (end == null ? Generator.of((JsonValue) null) : end.evaluate(context))
|
||||
.map(end -> JsonMath.slice(value, start, end))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -21,14 +21,16 @@ class JsonQueryTest {
|
||||
@MethodSource("arguments")
|
||||
void examples(@NotNull String jq, @Nullable JsonValue input, @NotNull List<@Nullable JsonValue> output) {
|
||||
JsonQuery query;
|
||||
List<JsonValue> actual;
|
||||
try {
|
||||
query = JsonQuery.parse(jq);
|
||||
} catch (UnsupportedOperationException _) {
|
||||
throw Assumptions.<RuntimeException>abort("not yet implemented");
|
||||
actual = query.run(input).toList();
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
var trace = ex.getStackTrace()[0];
|
||||
throw Assumptions.<RuntimeException>abort("not yet implemented: " + trace.getClassName() + "." + trace.getMethodName());
|
||||
} catch (Throwable t) {
|
||||
throw new AssertionFailedError("Unexpected exception thrown: " + t.getMessage(), t);
|
||||
}
|
||||
var actual = query.run(input).toList();
|
||||
assertEquals(output, actual);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user