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