add JsonParser

This commit is contained in:
jbb01 2025-04-12 17:49:48 +02:00
parent 7e44eda639
commit 1da002ce70
No known key found for this signature in database
GPG Key ID: 83C72CB6D5442CF1
323 changed files with 658 additions and 0 deletions

View File

@ -1,6 +1,8 @@
package eu.jonahbauer.json;
import eu.jonahbauer.json.exceptions.JsonConversionException;
import eu.jonahbauer.json.exceptions.JsonParserException;
import eu.jonahbauer.json.parser.JsonParser;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -41,6 +43,17 @@ public sealed interface JsonValue extends Serializable permits JsonObject, JsonA
return value == null ? "null" : value.toPrettyJsonString();
}
/**
* Parses a string as a JSON. The string must adhere to the <a href="https://www.json.org">JSON grammar</a>.
* @param json a string
* @return the result of parsing the string as a JSON
* @throws JsonParserException if the string cannot be parsed as JSON
*/
static @Nullable JsonValue parse(@NotNull String json) {
var parser = new JsonParser(json);
return parser.parse();
}
/**
* Converts an object to a JSON value:
* <ul>

View File

@ -0,0 +1,14 @@
package eu.jonahbauer.json.exceptions;
import org.jetbrains.annotations.NotNull;
public class JsonParserException extends JsonReaderException {
public JsonParserException(int line, int column, @NotNull String message) {
super(line, column, message);
}
public JsonParserException(int line, int column, @NotNull String message, @NotNull Throwable cause) {
super(line, column, message, cause);
}
}

View File

@ -0,0 +1,242 @@
package eu.jonahbauer.json.parser;
import eu.jonahbauer.json.*;
import eu.jonahbauer.json.exceptions.JsonParserException;
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
import eu.jonahbauer.json.tokenizer.JsonTokenizer;
import eu.jonahbauer.json.tokenizer.JsonTokenizerImpl;
import eu.jonahbauer.json.tokenizer.token.JsonNull;
import eu.jonahbauer.json.tokenizer.token.JsonPunctuation;
import eu.jonahbauer.json.tokenizer.token.JsonToken;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.util.*;
public final class JsonParser {
private final @NotNull JsonTokenizer tokenizer;
public JsonParser(@NotNull String json) {
this(new JsonTokenizerImpl(json));
}
public JsonParser(@NotNull Reader reader) {
this(new JsonTokenizerImpl(reader));
}
public JsonParser(@NotNull JsonTokenizer tokenizer) {
this.tokenizer = Objects.requireNonNull(tokenizer);
}
/**
* Parses the input as a JSON value.
* @return the JSON value
* @throws JsonParserException if the input could not be parsed as JSON
* @throws UncheckedIOException if an I/O error occurs
*/
public @Nullable JsonValue parse() throws JsonParserException {
try {
var out = parse0();
if (tokenizer.next() != null) {
throw new JsonParserException(tokenizer.getLineNumber(), tokenizer.getColumnNumber(), "unexpected trailing entries");
}
return out;
} catch (IOException ex) {
throw new UncheckedIOException(ex);
} catch (JsonTokenizerException ex) {
throw new JsonParserException(ex.getLineNumber(), ex.getColumnNumber(), ex.getMessage(), ex);
}
}
private @Nullable JsonValue parse0() throws IOException {
// Use a stack of Context objects to store the objects and arrays which are currently being parsed.
// This avoids the risk of stack overflow which would exist when having one nested calls to parse0.
var stack = new ArrayDeque<@NotNull Context>();
while (true) {
JsonValue value;
var token = tokenizer.next();
if (!stack.isEmpty() && token == stack.peek().end()) {
value = stack.pop().build();
} else {
// consume value separator
if (stack.peek() instanceof Context context && !context.isEmpty()) {
if (token != JsonPunctuation.VALUE_SEPARATOR) {
throw unexpectedToken(token, context.end(), JsonPunctuation.VALUE_SEPARATOR);
}
token = tokenizer.next();
}
// read object key and consume name separator
if (stack.peek() instanceof ObjectContext object) {
var key = parseJsonObjectKey(object, token);
if ((token = tokenizer.next()) != JsonPunctuation.NAME_SEPARATOR) {
throw unexpectedToken(token, JsonPunctuation.NAME_SEPARATOR);
}
token = tokenizer.next();
object.setKey(key);
}
if (token == JsonPunctuation.BEGIN_OBJECT) {
stack.push(new ObjectContext());
continue;
} else if (token == JsonPunctuation.BEGIN_ARRAY) {
stack.push(new ArrayContext());
continue;
} else {
// read json value
value = switch (token) {
case JsonPunctuation.BEGIN_ARRAY, JsonPunctuation.BEGIN_OBJECT -> throw new AssertionError();
case JsonNull.NULL -> null;
case JsonValue v -> v;
case JsonPunctuation _ -> throw unexpectedStartOfValue(token);
case null -> throw unexpectedEndOfFile();
};
}
}
if (stack.peek() instanceof Context context) {
context.add(value);
} else {
return value;
}
}
}
private @NotNull String parseJsonObjectKey(@NotNull ObjectContext context, @Nullable JsonToken token) {
return switch (token) {
case JsonString string -> string.value();
case null -> throw unexpectedEndOfFile();
default -> throw unexpectedStartOfKey(context, token);
};
}
private @NotNull JsonParserException unexpectedStartOfKey(@NotNull ObjectContext context, @Nullable JsonToken token) {
if (context.isEmpty()) {
throw unexpectedToken(token, "JSON_STRING", JsonPunctuation.END_OBJECT);
} else {
// if the context is not empty, we have already consumed a VALUE_SEPARATOR(,) and standard JSON does not
// allow trailing commas
throw unexpectedToken(token, "JSON_STRING");
}
}
private @NotNull JsonParserException unexpectedStartOfValue(@Nullable JsonToken token) {
var expected = new Object[] {
JsonPunctuation.BEGIN_OBJECT, JsonPunctuation.BEGIN_ARRAY, JsonBoolean.TRUE, JsonBoolean.FALSE,
JsonNull.NULL, "JSON_STRING", "JSON_NUMBER"
};
throw unexpectedToken(token, expected);
}
private @NotNull JsonParserException unexpectedEndOfFile() {
return new JsonParserException(tokenizer.getLineNumber(), tokenizer.getColumnNumber(), "unexpected end of file");
}
private @NotNull JsonParserException unexpectedToken(@Nullable JsonToken token, @NotNull Object @NotNull... expected) {
if (expected.length == 1) {
throw new JsonParserException(
tokenizer.getLineNumber(), tokenizer.getColumnNumber(),
"unexpected token: " + token + " (expected " + expected[0] + ")"
);
} else {
throw new JsonParserException(
tokenizer.getLineNumber(), tokenizer.getColumnNumber(),
"unexpected token: " + token + " (expected one of " + Arrays.toString(expected) + ")"
);
}
}
/**
* Represents an JSON object or array that is currently being parsed.
*/
private sealed interface Context {
boolean isEmpty();
/**
* Adds a nested JSON value to this context
* @param value the value
*/
void add(@Nullable JsonValue value);
/**
* Builds and returns the JSON value represented by this context.
* @return a JSON value
*/
@NotNull JsonValue build();
/**
* {@return the token that indicates the end of this context}
*/
@NotNull JsonPunctuation end();
}
/**
* Represents a JSON object that is currently being parsed.
*/
private static final class ObjectContext implements Context {
private final @NotNull SequencedMap<@NotNull String, @Nullable JsonValue> map = new LinkedHashMap<>();
private @Nullable String key;
public void setKey(@Nullable String key) {
if (this.key != null) throw new IllegalStateException();
this.key = key;
}
@Override
public void add(@Nullable JsonValue value) {
if (key == null) throw new IllegalStateException();
map.put(key, value);
key = null;
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public @NotNull JsonValue build() {
if (key != null) throw new IllegalStateException();
return new JsonObject(map);
}
@Override
public @NotNull JsonPunctuation end() {
return JsonPunctuation.END_OBJECT;
}
}
/**
* Represents a JSON array that is currently being parsed.
*/
private static final class ArrayContext implements Context {
private final @NotNull List<@Nullable JsonValue> list = new ArrayList<>();
@Override
public void add(@Nullable JsonValue value) {
list.add(value);
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public @NotNull JsonArray build() {
return new JsonArray(list);
}
@Override
public @NotNull JsonPunctuation end() {
return JsonPunctuation.END_ARRAY;
}
}
}

View File

@ -6,4 +6,5 @@ module eu.jonahbauer.json {
exports eu.jonahbauer.json.exceptions;
exports eu.jonahbauer.json.tokenizer;
exports eu.jonahbauer.json.tokenizer.token;
exports eu.jonahbauer.json.parser;
}

View File

@ -0,0 +1,70 @@
package eu.jonahbauer.json.parser;
import eu.jonahbauer.json.exceptions.JsonParserException;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Objects;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class JsonParserTest {
// https://github.com/nst/JSONTestSuite
@ParameterizedTest(name = "{0}")
@MethodSource("parameters")
void suite(@NotNull String name) throws IOException {
var path = "/nst/JsonTestSuite/" + name;
Boolean expected = switch (name.charAt(0)) {
case 'i' -> null;
case 'y' -> true;
case 'n' -> false;
default -> throw new IllegalArgumentException();
};
try (
var in = Objects.requireNonNull(JsonParserTest.class.getResourceAsStream(path));
var reader = new InputStreamReader(in)
) {
var parser = new JsonParser(reader);
if (expected == Boolean.FALSE) {
assertThrows(JsonParserException.class, parser::parse).printStackTrace(System.out);
} else if (expected == Boolean.TRUE) {
assertDoesNotThrow(parser::parse);
} else {
try {
parser.parse();
System.out.println("accepted");
} catch (JsonParserException ex) {
System.out.println("rejected");
ex.printStackTrace(System.out);
}
}
}
}
static @NotNull Stream<@NotNull Arguments> parameters() throws IOException {
var filenames = new ArrayList<Arguments>();
try (
InputStream in = Objects.requireNonNull(JsonParserTest.class.getResource("/nst/JsonTestSuite")).openStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in))
) {
String resource;
while ((resource = br.readLine()) != null) {
filenames.add(Arguments.of(resource));
}
}
return filenames.stream();
}
}

View File

@ -0,0 +1 @@
[123.456e-789]

View File

@ -0,0 +1 @@
[0.4e00669999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999969999999006]

View File

@ -0,0 +1 @@
[-1e+9999]

View File

@ -0,0 +1 @@
[1.5e+9999]

View File

@ -0,0 +1 @@
[-123123e100000]

View File

@ -0,0 +1 @@
[123123e100000]

View File

@ -0,0 +1 @@
[123e-10000000]

View File

@ -0,0 +1 @@
[-123123123123123123123123123123]

View File

@ -0,0 +1 @@
[100000000000000000000]

View File

@ -0,0 +1 @@
[-237462374673276894279832749832423479823246327846]

View File

@ -0,0 +1 @@
{"\uDFAA":0}

View File

@ -0,0 +1 @@
["日ш<E697A5>"]

View File

@ -0,0 +1 @@
["\ud800abc"]

View File

@ -0,0 +1 @@
["<22>"]

View File

@ -0,0 +1 @@
["\uDd1e\uD834"]

View File

@ -0,0 +1 @@
["<22>"]

View File

@ -0,0 +1 @@
["<22><><EFBFBD><EFBFBD>"]

View File

@ -0,0 +1 @@
["<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"]

View File

@ -0,0 +1 @@
["<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"]

View File

@ -0,0 +1 @@
["<22><>"]

View File

@ -0,0 +1 @@
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]

View File

@ -0,0 +1 @@
[a<EFBFBD>]

View File

@ -0,0 +1 @@
[1,,2]

View File

@ -0,0 +1 @@
["x"]]

View File

@ -0,0 +1 @@
["",]

View File

@ -0,0 +1 @@
["x"

View File

@ -0,0 +1 @@
[<EFBFBD>]

View File

@ -0,0 +1 @@
[,]

View File

@ -0,0 +1 @@
[-]

View File

@ -0,0 +1 @@
[ , ""]

View File

@ -0,0 +1,3 @@
["a",
4
,1,

View File

@ -0,0 +1 @@
[*]

View File

@ -0,0 +1 @@
[""

View File

@ -0,0 +1 @@
[fals]

View File

@ -0,0 +1 @@
[nul]

View File

@ -0,0 +1 @@
[tru]

View File

@ -0,0 +1 @@
[++1234]

View File

@ -0,0 +1 @@
[+1]

View File

@ -0,0 +1 @@
[+Inf]

View File

@ -0,0 +1 @@
[-01]

View File

@ -0,0 +1 @@
[-1.0.]

View File

@ -0,0 +1 @@
[-2.]

View File

@ -0,0 +1 @@
[-NaN]

View File

@ -0,0 +1 @@
[.-1]

View File

@ -0,0 +1 @@
[.2e-3]

View File

@ -0,0 +1 @@
[0.1.2]

View File

@ -0,0 +1 @@
[0.3e+]

View File

@ -0,0 +1 @@
[0.3e]

View File

@ -0,0 +1 @@
[0.e1]

View File

@ -0,0 +1 @@
[0E+]

View File

@ -0,0 +1 @@
[0E]

View File

@ -0,0 +1 @@
[0e+]

View File

@ -0,0 +1 @@
[0e]

View File

@ -0,0 +1 @@
[1.0e+]

View File

@ -0,0 +1 @@
[1.0e-]

View File

@ -0,0 +1 @@
[1.0e]

View File

@ -0,0 +1 @@
[1 000.0]

View File

@ -0,0 +1 @@
[1eE2]

View File

@ -0,0 +1 @@
[2.e+3]

View File

@ -0,0 +1 @@
[2.e-3]

View File

@ -0,0 +1 @@
[2.e3]

View File

@ -0,0 +1 @@
[9.e+]

View File

@ -0,0 +1 @@
[Inf]

View File

@ -0,0 +1 @@
[NaN]

View File

@ -0,0 +1 @@
[1+2]

Some files were not shown because too many files have changed in this diff Show More