jq path wip

main
jonah 1 year ago
parent 92a20c82f8
commit 5a5d1492a5

@ -0,0 +1,249 @@
package eu.jonahbauer.json;
import lombok.EqualsAndHashCode;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@EqualsAndHashCode
public final class JsonPath {
public static final JsonPath EMPTY = new JsonPath(Collections.emptyList());
private final @NotNull List<@NotNull Component> components;
public static @NotNull JsonPath parse(@NotNull String path) {
var components = new ArrayList<Component>();
var reader = new StringReader(path);
if (reader.hasNext()) {
if (reader.next() == '.' && !reader.hasNext()) return JsonPath.EMPTY;
reader.pushback();
}
do {
if (reader.next() != '.') throw unexpectedCharacter(reader.current(), "'.'");
components.add(switch (reader.next()) {
// object access with string
case '"' -> {
reader.pushback();
yield new Component.ObjectAccess(readString(reader));
}
case '[' -> {
var out = switch (reader.next()) {
// object access with string
case '"' -> {
reader.pushback();
yield new Component.ObjectAccess(readString(reader));
}
// array access with index
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
var negative = reader.current() == '-';
if (negative) reader.pushback();
var index = readInteger(reader);
yield new Component.ArrayAccess(index, negative && index != 0);
}
default -> throw unexpectedCharacter(reader.current(), "'\"' or '1'-'9'");
};
if (reader.next() != ']') throw unexpectedCharacter(reader.current(), "']'");
yield out;
}
// object access with identifier
default -> {
var chr = reader.current();
if (chr == '_' || 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z') {
yield new Component.ObjectAccess(readIdentifier(reader));
} else {
throw unexpectedCharacter(chr, "one of '\"', '[', '_', 'a'-'z' or 'A'-'Z'");
}
}
});
} while (reader.hasNext());
return new JsonPath(Collections.unmodifiableList(components));
}
private JsonPath(@NotNull List<@NotNull Component> components) {
this.components = components;
}
public @Nullable JsonValue select(@Nullable JsonValue value) {
for (Component component : components) {
value = component.select(value);
}
return value;
}
public @NotNull JsonPath resolve(@NotNull String key) {
var components = new ArrayList<Component>(this.components.size() + 1);
components.addAll(this.components);
components.add(new Component.ObjectAccess(key));
return new JsonPath(Collections.unmodifiableList(components));
}
public @NotNull JsonPath resolve(int index) {
var components = new ArrayList<Component>(this.components.size() + 1);
components.addAll(this.components);
components.add(new Component.ArrayAccess(Math.abs(index), index < 0));
return new JsonPath(Collections.unmodifiableList(components));
}
private sealed interface Component {
@Nullable JsonValue select(@Nullable JsonValue value);
record ArrayAccess(int index, boolean end) implements Component {
@Override
public @Nullable JsonValue select(@Nullable JsonValue value) {
if (value instanceof JsonArray array) {
if (end) return array.get(array.size() - index);
return array.get(index);
} else {
throw new JsonProcessingException("unexpected type");
}
}
@Override
public String toString() {
return "[" + index + "]";
}
}
record ObjectAccess(@NotNull String key) implements Component {
@Override
public @Nullable JsonValue select(@Nullable JsonValue value) {
if (value instanceof JsonObject object) {
return object.get(key);
} else {
throw new JsonProcessingException("unexpected type");
}
}
@Override
public String toString() {
return ".[\"" + JsonString.quote(key) + "\"]";
}
}
}
private static int readInteger(@NotNull StringReader reader) {
var i = reader.next() - '0';
while (reader.hasNext()) {
var chr = reader.next();
if (chr < '0' || chr > '9') {
reader.pushback();
break;
} else if (i == 0) {
throw unexpectedCharacter(chr, "']'");
} else {
i = Math.multiplyExact(i, 10);
i = Math.addExact(i, chr - '0');
}
}
return i;
}
private static @NotNull String readString(@NotNull StringReader reader) {
var out = new StringBuilder();
if (reader.next() != '"') throw new IllegalStateException();
while (true) {
char chr = reader.next();
if (chr == '"') {
return out.toString();
} else if (chr == '\\') {
out.append(switch (reader.next()) {
case '"' -> '"';
case '\\' -> '\\';
case '/' -> '/';
case 'b' -> '\b';
case 'f' -> '\f';
case 'n' -> '\n';
case 'r' -> '\r';
case 't' -> '\t';
case 'u' -> {
var u0 = toHex(reader.next());
var u1 = toHex(reader.next());
var u2 = toHex(reader.next());
var u3 = toHex(reader.next());
yield (char) ((u0 << 12) + (u1 << 8) + (u2 << 4) + u3);
}
default -> throw unexpectedCharacter(reader.current(), "one of '\"', '\\', '/', 'b', 'f', 'n', 'r', 't' or 'u'");
});
} else if (chr < 32) {
throw new JsonProcessingException("unescaped control character in string literal");
} else {
out.append(chr);
}
}
}
private static int toHex(char chr) {
if ('0' <= chr && chr <= '9') {
return chr - '0';
} else if ('a' <= chr && chr <= 'f') {
return 11 + chr - 'a';
} else if ('A' <= chr && chr <= 'F') {
return 11 + chr - 'A';
} else {
throw unexpectedCharacter(chr, "one of '0'-'9', 'a'-'f' or 'A'-'F'");
}
}
private static @NotNull String readIdentifier(@NotNull StringReader reader) {
var out = new StringBuilder();
out.append(reader.next());
while (reader.hasNext()) {
var chr = reader.next();
if (chr == '_' || 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z' || '0' <= chr && chr <= '9') {
out.append(chr);
} else {
throw unexpectedCharacter(chr, "one of '.', '_', 'a'-'z', 'A'-'Z' or '0'-'9'");
}
}
return out.toString();
}
private static JsonProcessingException unexpectedCharacter(char character, String expected) {
return new JsonProcessingException("unexpected character '" + character + "' (expected " + expected + ")");
}
private static class StringReader {
private final String string;
private final int length;
private int index;
public StringReader(String string) {
this.string = string;
this.length = string.length();
}
public void pushback() {
if (index == 0) throw new IllegalStateException();
index--;
}
public char next() {
if (index >= length) {
throw new JsonProcessingException("unexpected end of string");
}
return string.charAt(index++);
}
public char current() {
return string.charAt(index - 1);
}
public boolean hasNext() {
return index < length;
}
}
}

@ -0,0 +1,75 @@
package eu.jonahbauer.json.parser;
import eu.jonahbauer.json.*;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class JsonPathTest {
private final JsonValue array = new JsonArray(List.of(
new JsonNumber(1),
new JsonNumber(2),
new JsonObject(Map.of(
"bar", JsonBoolean.FALSE
))
));
private final JsonValue object = new JsonObject(Map.of(
"foo", array,
"bar", new JsonString("baz"),
"quack quack", new JsonString("foobar")
));;
@Test
void parseEmptyShouldFail() {
assertThrows(JsonProcessingException.class, () -> JsonPath.parse(""));
}
@Test
void parseIdentity() {
var path = JsonPath.parse(".");
assertEquals(object, path.select(object));
}
@Test
void parseArrayAccess() {
var path = JsonPath.parse(".[0]");
assertEquals(new JsonNumber(1), path.select(array));
}
@Test
void parseArrayAccessWithLeadingZero() {
assertThrows(JsonProcessingException.class, () -> JsonPath.parse(".[01]"));
}
@Test
void parseObjectAccessWithIdentifier() {
var path = JsonPath.parse(".bar");
assertEquals(new JsonString("baz"), path.select(object));
}
@Test
void parseObjectAccessWithString() {
var path = JsonPath.parse(".\"quack quack\"");
assertEquals(new JsonString("foobar"), path.select(object));
}
@Test
void parseObjectAccessWithArraySyntax() {
var path = JsonPath.parse(".[\"quack quack\"]");
assertEquals(new JsonString("foobar"), path.select(object));
assertThrows(JsonProcessingException.class, () -> JsonPath.parse(".[quack quack]"));
}
@Test
void parseChainedAccess() {
var path = JsonPath.parse(".foo[2].bar");
assertEquals(JsonBoolean.FALSE, path.select(object));
}
}
Loading…
Cancel
Save