fixup query

This commit is contained in:
jbb01 2025-04-13 02:29:38 +02:00
parent ef88d89ce1
commit 87ae709f6d
No known key found for this signature in database
GPG Key ID: 83C72CB6D5442CF1
13 changed files with 416 additions and 160 deletions

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(() -> {

View File

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

View File

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

View File

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