jq path wip
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…
Reference in New Issue