add support for StringTemplates
This commit is contained in:
parent
97892ff273
commit
4d9a3ef4ab
25
README.md
25
README.md
@ -1,2 +1,27 @@
|
|||||||
# json
|
# json
|
||||||
|
|
||||||
|
A simple JSON library with support for `StringTemplate`s which were available in JDK 21 and 22 as a preview
|
||||||
|
feature and removed again in JDK 23. The library allows placeholders to be used as keys, values and inside strings.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```java
|
||||||
|
var parameter = "numbers";
|
||||||
|
var value = List.of(1, 2, 3, 4);
|
||||||
|
var name = "World";
|
||||||
|
|
||||||
|
var json = JSON_OBJECT."""
|
||||||
|
{
|
||||||
|
\{parameter}: \{value},
|
||||||
|
"string": "Hello \{name}!"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
assertEquals(3, json.getArray("numbers").getNumber(2));
|
||||||
|
assertEquals(JsonString.valueOf("Hello World!"), json.get("string"));
|
||||||
|
|
||||||
|
System.out.println(json.toPrettyJsonString());
|
||||||
|
// {
|
||||||
|
// "numbers": [1, 2, 3, 4],
|
||||||
|
// "string": "Hello World!"
|
||||||
|
// }
|
||||||
|
```
|
@ -0,0 +1,37 @@
|
|||||||
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.exceptions.JsonConversionException;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public final class JsonTemplateProcessor<T extends JsonValue> implements StringTemplate.Processor<T, RuntimeException> {
|
||||||
|
/**
|
||||||
|
* Parses the string template as a JSON using {@link JsonValue#parse(StringTemplate)}.
|
||||||
|
*/
|
||||||
|
public static final @NotNull JsonTemplateProcessor<JsonValue> JSON = new JsonTemplateProcessor<>(JsonValue.class, true);
|
||||||
|
/**
|
||||||
|
* Parses the string template as a JSON using {@link JsonValue#parse(StringTemplate)}. Throws a
|
||||||
|
* {@link JsonConversionException} if the result is not a JSON object.
|
||||||
|
*/
|
||||||
|
public static final @NotNull JsonTemplateProcessor<@NotNull JsonObject> JSON_OBJECT = new JsonTemplateProcessor<>(JsonObject.class, false);
|
||||||
|
/**
|
||||||
|
* Parses the string template as a JSON using {@link JsonValue#parse(StringTemplate)}. Throws a
|
||||||
|
* {@link JsonConversionException} if the result is not a JSON array.
|
||||||
|
*/
|
||||||
|
public static final @NotNull JsonTemplateProcessor<@NotNull JsonArray> JSON_ARRAY = new JsonTemplateProcessor<>(JsonArray.class, false);
|
||||||
|
|
||||||
|
private final @NotNull Class<T> clazz;
|
||||||
|
private final boolean allowNull;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T process(@NotNull StringTemplate template) throws RuntimeException {
|
||||||
|
var result = JsonValue.parse(template);
|
||||||
|
if (result == null && !allowNull || result != null && !clazz.isInstance(result)) {
|
||||||
|
throw new JsonConversionException("The template does not represent a " + clazz.getSimpleName());
|
||||||
|
}
|
||||||
|
return clazz.cast(result);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public sealed interface JsonValue extends Serializable permits JsonObject, JsonArray, JsonBoolean, JsonNumber, JsonString {
|
public sealed interface JsonValue extends Serializable permits JsonObject, JsonArray, JsonBoolean, JsonNumber, JsonString {
|
||||||
|
|
||||||
@ -71,6 +72,38 @@ public sealed interface JsonValue extends Serializable permits JsonObject, JsonA
|
|||||||
return parser.parse();
|
return parser.parse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a string template as a JSON. The string template must adhere to the
|
||||||
|
* <a href="https://www.json.org">standard JSON grammar</a> with the following additional rules:
|
||||||
|
* <pre>{@code
|
||||||
|
* value
|
||||||
|
* placeholder
|
||||||
|
*
|
||||||
|
* member
|
||||||
|
* ws placeholder ws ':' element
|
||||||
|
*
|
||||||
|
* characters
|
||||||
|
* placeholder characters
|
||||||
|
* }</pre>
|
||||||
|
* where <code>placeholder</code> represents an object embedded in the string template.
|
||||||
|
* <p>
|
||||||
|
* A placeholder will be converted to JSON depending on the context it appears in.
|
||||||
|
* When used as a value, {@link JsonValue#valueOf(Object)} will be used.
|
||||||
|
* When used as an object key, only instances or {@link CharSequence} are allowed, which will be converted using
|
||||||
|
* {@link CharSequence#toString()}.
|
||||||
|
* When used in a string, {@link Objects#toString(Object)} will be used.
|
||||||
|
*
|
||||||
|
* @param json a string template
|
||||||
|
* @return the result of parsing the string template as a JSON
|
||||||
|
* @throws JsonParserException if the string template cannot be parsed as JSON
|
||||||
|
* @throws JsonConversionException if a placeholder cannot be converted to JSON
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
static @Nullable JsonValue parse(@NotNull StringTemplate json) {
|
||||||
|
var parser = new JsonParser(json);
|
||||||
|
return parser.parse();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an object to a JSON value:
|
* Converts an object to a JSON value:
|
||||||
* <ul>
|
* <ul>
|
||||||
|
@ -4,11 +4,11 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
public class JsonParserException extends JsonReaderException {
|
public class JsonParserException extends JsonReaderException {
|
||||||
|
|
||||||
public JsonParserException(int line, int column, @NotNull String message) {
|
public JsonParserException(int fragment, int line, int column, @NotNull String message) {
|
||||||
super(line, column, message);
|
super(fragment, line, column, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonParserException(int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
public JsonParserException(int fragment, int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
||||||
super(line, column, message, cause);
|
super(fragment, line, column, message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,23 +5,30 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
abstract class JsonReaderException extends JsonProcessingException {
|
abstract class JsonReaderException extends JsonProcessingException {
|
||||||
|
private final int fragment;
|
||||||
private final int lineNumber;
|
private final int lineNumber;
|
||||||
private final int columnNumber;
|
private final int columnNumber;
|
||||||
|
|
||||||
public JsonReaderException(int line, int column, @NotNull String message) {
|
public JsonReaderException(int fragment, int line, int column, @NotNull String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
this.fragment = fragment;
|
||||||
this.lineNumber = line;
|
this.lineNumber = line;
|
||||||
this.columnNumber = column;
|
this.columnNumber = column;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonReaderException(int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
public JsonReaderException(int fragment, int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
|
this.fragment = fragment;
|
||||||
this.lineNumber = line;
|
this.lineNumber = line;
|
||||||
this.columnNumber = column;
|
this.columnNumber = column;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull String getMessage() {
|
public @NotNull String getMessage() {
|
||||||
return super.getMessage() + " at line " + getLineNumber() + ", column " + getColumnNumber();
|
if (getFragment() == 0) {
|
||||||
|
return super.getMessage() + " at line " + getLineNumber() + ", column " + getColumnNumber();
|
||||||
|
} else {
|
||||||
|
return super.getMessage() + " at fragment " + getFragment() + ", line " + getLineNumber() + ", column " + getColumnNumber();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,11 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
public class JsonTokenizerException extends JsonReaderException {
|
public class JsonTokenizerException extends JsonReaderException {
|
||||||
|
|
||||||
public JsonTokenizerException(int line, int column, @NotNull String message) {
|
public JsonTokenizerException(int fragment, int line, int column, @NotNull String message) {
|
||||||
super(line, column, message);
|
super(fragment, line, column, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonTokenizerException(int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
public JsonTokenizerException(int fragment, int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
||||||
super(line, column, message, cause);
|
super(fragment, line, column, message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
package eu.jonahbauer.json.parser;
|
package eu.jonahbauer.json.parser;
|
||||||
|
|
||||||
import eu.jonahbauer.json.*;
|
import eu.jonahbauer.json.*;
|
||||||
|
import eu.jonahbauer.json.exceptions.JsonConversionException;
|
||||||
import eu.jonahbauer.json.exceptions.JsonParserException;
|
import eu.jonahbauer.json.exceptions.JsonParserException;
|
||||||
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
|
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
|
||||||
import eu.jonahbauer.json.tokenizer.JsonTokenizer;
|
import eu.jonahbauer.json.tokenizer.JsonTokenizer;
|
||||||
import eu.jonahbauer.json.tokenizer.JsonTokenizerImpl;
|
import eu.jonahbauer.json.tokenizer.JsonTokenizerImpl;
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonNull;
|
import eu.jonahbauer.json.tokenizer.token.*;
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonPunctuation;
|
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonToken;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@ -17,8 +16,18 @@ import java.io.UncheckedIOException;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public final class JsonParser {
|
public final class JsonParser {
|
||||||
|
private static final String TT_JSON_STRING = "JSON_STRING";
|
||||||
|
private static final String TT_JSON_NUMBER = "JSON_NUMBER";
|
||||||
|
private static final String TT_JSON_STRING_TEMPLATE = "JSON_STRING_TEMPLATE";
|
||||||
|
private static final String TT_JSON_PLACEHOLDER = "JSON_PLACEHOLDER";
|
||||||
|
|
||||||
private final @NotNull JsonTokenizer tokenizer;
|
private final @NotNull JsonTokenizer tokenizer;
|
||||||
|
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
public JsonParser(@NotNull StringTemplate json) {
|
||||||
|
this(new JsonTokenizerImpl(json));
|
||||||
|
}
|
||||||
|
|
||||||
public JsonParser(@NotNull String json) {
|
public JsonParser(@NotNull String json) {
|
||||||
this(new JsonTokenizerImpl(json));
|
this(new JsonTokenizerImpl(json));
|
||||||
}
|
}
|
||||||
@ -41,13 +50,13 @@ public final class JsonParser {
|
|||||||
try {
|
try {
|
||||||
var out = parse0();
|
var out = parse0();
|
||||||
if (tokenizer.next() != null) {
|
if (tokenizer.next() != null) {
|
||||||
throw new JsonParserException(tokenizer.getLineNumber(), tokenizer.getColumnNumber(), "unexpected trailing entries");
|
throw new JsonParserException(tokenizer.getFragment(), tokenizer.getLineNumber(), tokenizer.getColumnNumber(), "unexpected trailing entries");
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
throw new UncheckedIOException(ex);
|
throw new UncheckedIOException(ex);
|
||||||
} catch (JsonTokenizerException ex) {
|
} catch (JsonTokenizerException ex) {
|
||||||
throw new JsonParserException(ex.getLineNumber(), ex.getColumnNumber(), ex.getMessage(), ex);
|
throw new JsonParserException(ex.getFragment(), ex.getLineNumber(), ex.getColumnNumber(), ex.getMessage(), ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +102,8 @@ public final class JsonParser {
|
|||||||
case JsonPunctuation.BEGIN_ARRAY, JsonPunctuation.BEGIN_OBJECT -> throw new AssertionError();
|
case JsonPunctuation.BEGIN_ARRAY, JsonPunctuation.BEGIN_OBJECT -> throw new AssertionError();
|
||||||
case JsonNull.NULL -> null;
|
case JsonNull.NULL -> null;
|
||||||
case JsonValue v -> v;
|
case JsonValue v -> v;
|
||||||
|
case JsonStringTemplate template -> template.asString();
|
||||||
|
case JsonPlaceholder(var object) -> JsonValue.valueOf(object);
|
||||||
case JsonPunctuation _ -> throw unexpectedStartOfValue(token);
|
case JsonPunctuation _ -> throw unexpectedStartOfValue(token);
|
||||||
case null -> throw unexpectedEndOfFile();
|
case null -> throw unexpectedEndOfFile();
|
||||||
};
|
};
|
||||||
@ -110,42 +121,62 @@ public final class JsonParser {
|
|||||||
private @NotNull String parseJsonObjectKey(@NotNull ObjectContext context, @Nullable JsonToken token) {
|
private @NotNull String parseJsonObjectKey(@NotNull ObjectContext context, @Nullable JsonToken token) {
|
||||||
return switch (token) {
|
return switch (token) {
|
||||||
case JsonString string -> string.value();
|
case JsonString string -> string.value();
|
||||||
|
case JsonStringTemplate template -> template.asString().value();
|
||||||
|
case JsonPlaceholder(var object) -> switch (object) {
|
||||||
|
case CharSequence chars -> chars.toString();
|
||||||
|
case null -> throw new JsonConversionException("cannot use null as json object key");
|
||||||
|
default -> throw new JsonConversionException("cannot convert object of type " + object.getClass() + " to json object key");
|
||||||
|
};
|
||||||
case null -> throw unexpectedEndOfFile();
|
case null -> throw unexpectedEndOfFile();
|
||||||
default -> throw unexpectedStartOfKey(context, token);
|
default -> throw unexpectedStartOfKey(context, token);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonParserException unexpectedStartOfKey(@NotNull ObjectContext context, @Nullable JsonToken token) {
|
private @NotNull JsonParserException unexpectedStartOfKey(@NotNull ObjectContext context, @Nullable JsonToken token) {
|
||||||
|
var expected = new ArrayList<>(4);
|
||||||
|
expected.add(TT_JSON_STRING);
|
||||||
|
|
||||||
if (context.isEmpty()) {
|
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
|
// if the context is not empty, we have already consumed a VALUE_SEPARATOR(,) and standard JSON does not
|
||||||
// allow trailing commas
|
// allow trailing commas
|
||||||
throw unexpectedToken(token, "JSON_STRING");
|
expected.add(JsonPunctuation.END_OBJECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tokenizer.getFragment() != 0) {
|
||||||
|
// when the input is a StringTemplate, additional tokens are allowed
|
||||||
|
expected.add(TT_JSON_STRING_TEMPLATE);
|
||||||
|
expected.add(TT_JSON_PLACEHOLDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unexpectedToken(token, expected.toArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonParserException unexpectedStartOfValue(@Nullable JsonToken token) {
|
private @NotNull JsonParserException unexpectedStartOfValue(@Nullable JsonToken token) {
|
||||||
var expected = new Object[] {
|
var expected = new ArrayList<>(9);
|
||||||
|
expected.addAll(Arrays.asList(
|
||||||
JsonPunctuation.BEGIN_OBJECT, JsonPunctuation.BEGIN_ARRAY, JsonBoolean.TRUE, JsonBoolean.FALSE,
|
JsonPunctuation.BEGIN_OBJECT, JsonPunctuation.BEGIN_ARRAY, JsonBoolean.TRUE, JsonBoolean.FALSE,
|
||||||
JsonNull.NULL, "JSON_STRING", "JSON_NUMBER"
|
JsonNull.NULL, TT_JSON_STRING, TT_JSON_NUMBER
|
||||||
};
|
));
|
||||||
|
if (tokenizer.getFragment() != 0) {
|
||||||
|
expected.add(TT_JSON_STRING_TEMPLATE);
|
||||||
|
expected.add(TT_JSON_PLACEHOLDER);
|
||||||
|
}
|
||||||
throw unexpectedToken(token, expected);
|
throw unexpectedToken(token, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonParserException unexpectedEndOfFile() {
|
private @NotNull JsonParserException unexpectedEndOfFile() {
|
||||||
return new JsonParserException(tokenizer.getLineNumber(), tokenizer.getColumnNumber(), "unexpected end of file");
|
return new JsonParserException(tokenizer.getFragment(), tokenizer.getLineNumber(), tokenizer.getColumnNumber(), "unexpected end of file");
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonParserException unexpectedToken(@Nullable JsonToken token, @NotNull Object @NotNull... expected) {
|
private @NotNull JsonParserException unexpectedToken(@Nullable JsonToken token, @NotNull Object @NotNull... expected) {
|
||||||
if (expected.length == 1) {
|
if (expected.length == 1) {
|
||||||
throw new JsonParserException(
|
throw new JsonParserException(
|
||||||
tokenizer.getLineNumber(), tokenizer.getColumnNumber(),
|
tokenizer.getFragment(), tokenizer.getLineNumber(), tokenizer.getColumnNumber(),
|
||||||
"unexpected token: " + token + " (expected " + expected[0] + ")"
|
"unexpected token: " + token + " (expected " + expected[0] + ")"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new JsonParserException(
|
throw new JsonParserException(
|
||||||
tokenizer.getLineNumber(), tokenizer.getColumnNumber(),
|
tokenizer.getFragment(), tokenizer.getLineNumber(), tokenizer.getColumnNumber(),
|
||||||
"unexpected token: " + token + " (expected one of " + Arrays.toString(expected) + ")"
|
"unexpected token: " + token + " (expected one of " + Arrays.toString(expected) + ")"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package eu.jonahbauer.json.tokenizer;
|
|||||||
import eu.jonahbauer.json.tokenizer.token.JsonToken;
|
import eu.jonahbauer.json.tokenizer.token.JsonToken;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.jetbrains.annotations.Range;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
@ -19,15 +20,20 @@ public interface JsonTokenizer extends Iterable<JsonToken> {
|
|||||||
*/
|
*/
|
||||||
@Nullable JsonToken next() throws IOException;
|
@Nullable JsonToken next() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the fragment at which the previously read token appeared}
|
||||||
|
*/
|
||||||
|
@Range(from = 0, to = Integer.MAX_VALUE) int getFragment();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@return the line at which the previously read token appeared}
|
* {@return the line at which the previously read token appeared}
|
||||||
*/
|
*/
|
||||||
int getLineNumber();
|
@Range(from = 1, to = Integer.MAX_VALUE) int getLineNumber();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@return the column at which the previously read token appeared}
|
* {@return the column at which the previously read token appeared}
|
||||||
*/
|
*/
|
||||||
int getColumnNumber();
|
@Range(from = 1, to = Integer.MAX_VALUE) int getColumnNumber();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@return an iterator over the tokens} The {@link Iterator#next()} and {@link Iterator#hasNext()} methods
|
* {@return an iterator over the tokens} The {@link Iterator#next()} and {@link Iterator#hasNext()} methods
|
||||||
|
@ -3,9 +3,7 @@ package eu.jonahbauer.json.tokenizer;
|
|||||||
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
|
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
|
||||||
import eu.jonahbauer.json.tokenizer.reader.PushbackReader;
|
import eu.jonahbauer.json.tokenizer.reader.PushbackReader;
|
||||||
import eu.jonahbauer.json.tokenizer.reader.PushbackReaderImpl;
|
import eu.jonahbauer.json.tokenizer.reader.PushbackReaderImpl;
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonNull;
|
import eu.jonahbauer.json.tokenizer.token.*;
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonPunctuation;
|
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonToken;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import eu.jonahbauer.json.JsonBoolean;
|
import eu.jonahbauer.json.JsonBoolean;
|
||||||
import eu.jonahbauer.json.JsonNumber;
|
import eu.jonahbauer.json.JsonNumber;
|
||||||
@ -14,6 +12,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public final class JsonTokenizerImpl implements JsonTokenizer {
|
public final class JsonTokenizerImpl implements JsonTokenizer {
|
||||||
@ -30,6 +29,8 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
private final @NotNull PushbackReader reader;
|
private final @NotNull PushbackReader reader;
|
||||||
private final int[] buffer = new int[4];
|
private final int[] buffer = new int[4];
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private int fragment;
|
||||||
@Getter
|
@Getter
|
||||||
private int lineNumber;
|
private int lineNumber;
|
||||||
@Getter
|
@Getter
|
||||||
@ -39,6 +40,11 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
this(new StringReader(json));
|
this(new StringReader(json));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
public JsonTokenizerImpl(@NotNull StringTemplate template) {
|
||||||
|
this.reader = new PushbackReaderImpl(template);
|
||||||
|
}
|
||||||
|
|
||||||
public JsonTokenizerImpl(@NotNull Reader reader) {
|
public JsonTokenizerImpl(@NotNull Reader reader) {
|
||||||
this.reader = new PushbackReaderImpl(reader);
|
this.reader = new PushbackReaderImpl(reader);
|
||||||
}
|
}
|
||||||
@ -51,10 +57,12 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
chr = reader.read();
|
chr = reader.read();
|
||||||
} while (isWhitespace(chr));
|
} while (isWhitespace(chr));
|
||||||
|
|
||||||
|
fragment = reader.getFragment();
|
||||||
lineNumber = reader.getLineNumber();
|
lineNumber = reader.getLineNumber();
|
||||||
columnNumber = reader.getColumnNumber();
|
columnNumber = reader.getColumnNumber();
|
||||||
|
|
||||||
if (chr == PushbackReader.EOF) return null;
|
if (chr == PushbackReader.EOF) return null;
|
||||||
|
if (chr == PushbackReader.PLACEHOLDER) return new JsonPlaceholder(reader.getObject());
|
||||||
if (chr < 128 && PUNCTUATION[chr] != null) return PUNCTUATION[chr];
|
if (chr < 128 && PUNCTUATION[chr] != null) return PUNCTUATION[chr];
|
||||||
if (chr == '"') return nextString();
|
if (chr == '"') return nextString();
|
||||||
|
|
||||||
@ -102,12 +110,22 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonString nextString() throws IOException {
|
private @NotNull JsonToken nextString() throws IOException {
|
||||||
|
var fragments = new ArrayList<String>();
|
||||||
|
var values = new ArrayList<>();
|
||||||
|
|
||||||
var current = new StringBuilder();
|
var current = new StringBuilder();
|
||||||
while (true) {
|
while (true) {
|
||||||
int chr = reader.read();
|
int chr = reader.read();
|
||||||
if (chr == '"') {
|
if (chr == '"') {
|
||||||
return new JsonString(current.toString());
|
if (fragments.isEmpty()) {
|
||||||
|
return new JsonString(current.toString());
|
||||||
|
} else {
|
||||||
|
fragments.add(current.toString());
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
var result = new JsonStringTemplate(StringTemplate.of(fragments, values));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
} else if (chr == '\\') {
|
} else if (chr == '\\') {
|
||||||
chr = reader.read();
|
chr = reader.read();
|
||||||
switch (chr) {
|
switch (chr) {
|
||||||
@ -151,6 +169,10 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
|
|
||||||
} else if (chr == PushbackReader.EOF) {
|
} else if (chr == PushbackReader.EOF) {
|
||||||
throw error("unclosed string literal");
|
throw error("unclosed string literal");
|
||||||
|
} else if (chr == PushbackReader.PLACEHOLDER) {
|
||||||
|
fragments.add(current.toString());
|
||||||
|
current.setLength(0);
|
||||||
|
values.add(reader.getObject());
|
||||||
} else if (chr < 32) {
|
} else if (chr < 32) {
|
||||||
throw error("unescaped control character in string literal");
|
throw error("unescaped control character in string literal");
|
||||||
} else {
|
} else {
|
||||||
@ -160,11 +182,11 @@ public final class JsonTokenizerImpl implements JsonTokenizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonTokenizerException error(@NotNull String message) {
|
private @NotNull JsonTokenizerException error(@NotNull String message) {
|
||||||
return new JsonTokenizerException(getLineNumber(), getColumnNumber(), message);
|
return new JsonTokenizerException(getFragment(), getLineNumber(), getColumnNumber(), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull JsonTokenizerException error(@NotNull String message, @NotNull Throwable cause) {
|
private @NotNull JsonTokenizerException error(@NotNull String message, @NotNull Throwable cause) {
|
||||||
return new JsonTokenizerException(getLineNumber(), getColumnNumber(), message, cause);
|
return new JsonTokenizerException(getFragment(), getLineNumber(), getColumnNumber(), message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NotNull String toString(int chr) {
|
private static @NotNull String toString(int chr) {
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
package eu.jonahbauer.json.tokenizer.reader;
|
package eu.jonahbauer.json.tokenizer.reader;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.jetbrains.annotations.Range;
|
import org.jetbrains.annotations.Range;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public interface PushbackReader extends AutoCloseable {
|
public interface PushbackReader extends AutoCloseable {
|
||||||
int EOF = -1;
|
int EOF = -1;
|
||||||
|
int PLACEHOLDER = -2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the next character.
|
* Reads the next character.
|
||||||
* @return the next character or {@link #EOF} if the end of the stream has been reached
|
* @return the next character or {@link #EOF} if the end of the stream has been reached
|
||||||
* @throws IOException if an I/O error occurs
|
* @throws IOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
@Range(from = -1, to = Character.MAX_VALUE) int read() throws IOException;
|
@Range(from = -2, to = Character.MAX_VALUE) int read() throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pushes the reader back, making {@link #read()} return the same character as before.
|
* Pushes the reader back, making {@link #read()} return the same character as before.
|
||||||
@ -20,6 +22,13 @@ public interface PushbackReader extends AutoCloseable {
|
|||||||
*/
|
*/
|
||||||
void pushback();
|
void pushback();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the index of the fragment the current character belongs to} Returns 0 unless a {@code StringTemplate} is
|
||||||
|
* being read.
|
||||||
|
* @throws IllegalStateException when called before the first read
|
||||||
|
*/
|
||||||
|
@Range(from = 0, to = Integer.MAX_VALUE) int getFragment();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@return the line number of the current character}
|
* {@return the line number of the current character}
|
||||||
* @throws IllegalStateException when called before the first read
|
* @throws IllegalStateException when called before the first read
|
||||||
@ -32,6 +41,13 @@ public interface PushbackReader extends AutoCloseable {
|
|||||||
*/
|
*/
|
||||||
@Range(from = 1, to = Integer.MAX_VALUE) int getColumnNumber();
|
@Range(from = 1, to = Integer.MAX_VALUE) int getColumnNumber();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the object that has been read after {@link #read()} returned {@link #PLACEHOLDER}.
|
||||||
|
* @throws IllegalStateException the previous call to read did not return {@link #PLACEHOLDER}
|
||||||
|
* @return the object that has been read
|
||||||
|
*/
|
||||||
|
@Nullable Object getObject();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void close() throws IOException;
|
void close() throws IOException;
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,46 @@
|
|||||||
package eu.jonahbauer.json.tokenizer.reader;
|
package eu.jonahbauer.json.tokenizer.reader;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.util.Objects;
|
import java.io.StringReader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
public class PushbackReaderImpl implements PushbackReader {
|
public class PushbackReaderImpl implements PushbackReader {
|
||||||
private final @NotNull Reader reader;
|
private @Nullable Input input;
|
||||||
|
private @Nullable Reader reader;
|
||||||
|
|
||||||
private int current = Integer.MIN_VALUE;
|
private int current = Integer.MIN_VALUE;
|
||||||
private int lineNumber = 1;
|
private Object object;
|
||||||
private int columnNumber = 0; // column number will be incremented when a character is read
|
|
||||||
|
private int fragment;
|
||||||
|
private int lineNumber;
|
||||||
|
private int columnNumber;
|
||||||
|
|
||||||
private boolean pushback;
|
private boolean pushback;
|
||||||
|
|
||||||
public PushbackReaderImpl(@NotNull Reader reader) {
|
public PushbackReaderImpl(@NotNull Reader reader) {
|
||||||
this.reader = Objects.requireNonNull(reader, "reader");
|
this.input = new Input(List.of(reader).iterator(), Collections.emptyIterator());
|
||||||
|
this.fragment = -1; // fragment will be incremented to 0 when the first character is read
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
public PushbackReaderImpl(@NotNull StringTemplate template) {
|
||||||
|
this.input = new Input(
|
||||||
|
template.fragments().stream().<Reader>map(StringReader::new).iterator(),
|
||||||
|
template.values().iterator()
|
||||||
|
);
|
||||||
|
this.fragment = 0; // fragment will be incremented to 1 when the first character is read
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getFragment() {
|
||||||
|
if (current == Integer.MIN_VALUE) {
|
||||||
|
throw new IllegalStateException("No character has been read so far.");
|
||||||
|
}
|
||||||
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -52,6 +76,21 @@ public class PushbackReaderImpl implements PushbackReader {
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var input = this.input;
|
||||||
|
if (input == null) {
|
||||||
|
throw new IOException("closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader = this.reader;
|
||||||
|
if (reader == null && input.readers().hasNext()) {
|
||||||
|
this.reader = reader = input.readers().next();
|
||||||
|
fragment++;
|
||||||
|
lineNumber = 1;
|
||||||
|
columnNumber = 0;
|
||||||
|
} else if (reader == null) {
|
||||||
|
return EOF;
|
||||||
|
}
|
||||||
|
|
||||||
int result = reader.read();
|
int result = reader.read();
|
||||||
|
|
||||||
if (current == '\n' && result != '\n' || current == '\r' && result != '\n' && result != '\r') {
|
if (current == '\n' && result != '\n' || current == '\r' && result != '\n' && result != '\r') {
|
||||||
@ -61,12 +100,59 @@ public class PushbackReaderImpl implements PushbackReader {
|
|||||||
columnNumber++;
|
columnNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
current = result;
|
if (result == EOF) {
|
||||||
return result;
|
reader.close();
|
||||||
|
this.reader = null;
|
||||||
|
|
||||||
|
if (input.values().hasNext()) {
|
||||||
|
this.object = input.values().next();
|
||||||
|
return this.current = PLACEHOLDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.object = null;
|
||||||
|
return this.current = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Object getObject() {
|
||||||
|
if (current != PLACEHOLDER) {
|
||||||
|
throw new IllegalStateException("Currently not at a placeholder.");
|
||||||
|
}
|
||||||
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
reader.close();
|
var input = this.input;
|
||||||
|
var reader = this.reader;
|
||||||
|
this.input = null;
|
||||||
|
this.reader = null;
|
||||||
|
close(Arrays.asList(input, reader).iterator());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void close(@NotNull Iterator<? extends @Nullable AutoCloseable> closeables) throws ClosingException {
|
||||||
|
ClosingException exception = null;
|
||||||
|
while (closeables.hasNext()) {
|
||||||
|
var closeable = closeables.next();
|
||||||
|
if (closeable == null) continue;
|
||||||
|
try {
|
||||||
|
closeable.close();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (exception == null) exception = new ClosingException();
|
||||||
|
exception.addSuppressed(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exception != null) throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Input(@NotNull Iterator<@NotNull Reader> readers, @NotNull Iterator<Object> values) implements AutoCloseable {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws ClosingException {
|
||||||
|
PushbackReaderImpl.close(readers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ClosingException extends IOException { }
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
package eu.jonahbauer.json.tokenizer.token;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
public record JsonPlaceholder(@Nullable Object object) implements JsonToken {
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package eu.jonahbauer.json.tokenizer.token;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.JsonString;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
public record JsonStringTemplate(@NotNull StringTemplate template) implements JsonToken {
|
||||||
|
public JsonStringTemplate {
|
||||||
|
Objects.requireNonNull(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NotNull JsonString asString() {
|
||||||
|
return new JsonString(STR.process(template));
|
||||||
|
}
|
||||||
|
}
|
@ -7,4 +7,4 @@ import eu.jonahbauer.json.JsonString;
|
|||||||
/**
|
/**
|
||||||
* Represents a JSON token.
|
* Represents a JSON token.
|
||||||
*/
|
*/
|
||||||
public sealed interface JsonToken permits JsonBoolean, JsonNull, JsonNumber, JsonString, JsonPunctuation { }
|
public sealed interface JsonToken permits JsonBoolean, JsonNumber, JsonString, JsonNull, JsonPlaceholder, JsonPunctuation, JsonStringTemplate { }
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
package eu.jonahbauer.json;
|
||||||
|
|
||||||
|
import eu.jonahbauer.json.exceptions.JsonParserException;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
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.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static eu.jonahbauer.json.JsonTemplateProcessor.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
class JsonTemplateProcessorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processWithoutPlaceholders() {
|
||||||
|
JsonValue value = JSON."""
|
||||||
|
{
|
||||||
|
"string": "string",
|
||||||
|
"number": 1337,
|
||||||
|
"true": true,
|
||||||
|
"false": false,
|
||||||
|
"null": null,
|
||||||
|
"array": [1, "2", true, false, null]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
assertInstanceOf(JsonObject.class, value);
|
||||||
|
JsonObject object = (JsonObject) value;
|
||||||
|
|
||||||
|
assertEquals(JsonValue.valueOf("string"), object.get("string"));
|
||||||
|
assertEquals(JsonValue.valueOf(1337), object.get("number"));
|
||||||
|
assertEquals(JsonValue.valueOf(true), object.get("true"));
|
||||||
|
assertEquals(JsonValue.valueOf(false), object.get("false"));
|
||||||
|
assertEquals(JsonValue.valueOf(null), object.get("null"));
|
||||||
|
assertEquals(JsonValue.valueOf(Arrays.asList(1, "2", true, false, null)), object.get("array"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processWithValuePlaceholder() {
|
||||||
|
JsonValue value = JSON."""
|
||||||
|
{
|
||||||
|
"string": \{"string"},
|
||||||
|
"number": \{1337},
|
||||||
|
"true": \{true},
|
||||||
|
"false": \{false},
|
||||||
|
"null": \{null},
|
||||||
|
"array": \{Arrays.asList(1, "2", true, false, null)}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
assertInstanceOf(JsonObject.class, value);
|
||||||
|
JsonObject object = (JsonObject) value;
|
||||||
|
|
||||||
|
assertEquals(JsonValue.valueOf("string"), object.get("string"));
|
||||||
|
assertEquals(JsonValue.valueOf(1337), object.get("number"));
|
||||||
|
assertEquals(JsonValue.valueOf(true), object.get("true"));
|
||||||
|
assertEquals(JsonValue.valueOf(false), object.get("false"));
|
||||||
|
assertEquals(JsonValue.valueOf(null), object.get("null"));
|
||||||
|
assertEquals(JsonValue.valueOf(Arrays.asList(1, "2", true, false, null)), object.get("array"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processWithKeyPlaceholder() {
|
||||||
|
String key = "foo";
|
||||||
|
JsonValue value = JSON."""
|
||||||
|
{
|
||||||
|
\{key}: "value"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
assertInstanceOf(JsonObject.class, value);
|
||||||
|
JsonObject object = (JsonObject) value;
|
||||||
|
|
||||||
|
assertEquals(JsonValue.valueOf("value"), object.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processWithPlaceholderInInvalidPosition() {
|
||||||
|
assertThrows(JsonParserException.class, () -> {
|
||||||
|
var _ = JSON."{\"key\" \{"value"}}";
|
||||||
|
}).printStackTrace(System.out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processWithPlaceholderInStringLiteral() {
|
||||||
|
Object object = new Object();
|
||||||
|
JsonValue value = JSON."{\"key\": \"Hello \{object}!\"}";
|
||||||
|
assertEquals(JsonValue.valueOf(Map.of(
|
||||||
|
"key", "Hello " + object + "!"
|
||||||
|
)), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processWithPlaceholderInKeyStringLiteral() {
|
||||||
|
Object object = new Object();
|
||||||
|
JsonValue value = JSON."{\"key-\{object}\": \"value\"}";
|
||||||
|
assertEquals(JsonValue.valueOf(Map.of(
|
||||||
|
"key-" + object, "value"
|
||||||
|
)), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0}")
|
||||||
|
@MethodSource("parameters")
|
||||||
|
void suite(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(JsonTemplateProcessor.class.getResourceAsStream(path))) {
|
||||||
|
String input = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if (expected == Boolean.FALSE) {
|
||||||
|
assertThrows(JsonParserException.class, () -> JSON.process(StringTemplate.of(input))).printStackTrace(System.out);
|
||||||
|
} else if (expected == Boolean.TRUE) {
|
||||||
|
Assertions.assertDoesNotThrow(() -> JSON.process(StringTemplate.of(input)));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
JSON.process(StringTemplate.of(input));
|
||||||
|
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(JsonTemplateProcessorTest.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();
|
||||||
|
}
|
||||||
|
}
|
@ -4,14 +4,13 @@ import eu.jonahbauer.json.JsonBoolean;
|
|||||||
import eu.jonahbauer.json.JsonNumber;
|
import eu.jonahbauer.json.JsonNumber;
|
||||||
import eu.jonahbauer.json.JsonString;
|
import eu.jonahbauer.json.JsonString;
|
||||||
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
|
import eu.jonahbauer.json.exceptions.JsonTokenizerException;
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonNull;
|
import eu.jonahbauer.json.tokenizer.token.*;
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonPunctuation;
|
|
||||||
import eu.jonahbauer.json.tokenizer.token.JsonToken;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static java.lang.StringTemplate.RAW;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
@ -115,9 +114,30 @@ class JsonTokenizerImplTest {
|
|||||||
test("1,234", new JsonNumber(1), JsonPunctuation.VALUE_SEPARATOR, new JsonNumber(234));
|
test("1,234", new JsonNumber(1), JsonPunctuation.VALUE_SEPARATOR, new JsonNumber(234));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
void stringTemplate() {
|
||||||
|
var name = "World";
|
||||||
|
test(RAW."\"Hello, \{name}!\"", new JsonStringTemplate(RAW."Hello, \{name}!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
void placeholder() {
|
||||||
|
var name = "World";
|
||||||
|
test(RAW."\{name}", new JsonPlaceholder(name));
|
||||||
|
}
|
||||||
|
|
||||||
private void test(@NotNull String json, @NotNull JsonToken @NotNull... expected) {
|
private void test(@NotNull String json, @NotNull JsonToken @NotNull... expected) {
|
||||||
var tokenizer = new JsonTokenizerImpl(json);
|
var tokenizer = new JsonTokenizerImpl(json);
|
||||||
var actual = tokenizer.stream().toList();
|
var actual = tokenizer.stream().toList();
|
||||||
assertEquals(List.of(expected), actual);
|
assertEquals(List.of(expected), actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("preview")
|
||||||
|
private void test(@NotNull StringTemplate json, @NotNull JsonToken @NotNull... expected) {
|
||||||
|
var tokenizer = new JsonTokenizerImpl(json);
|
||||||
|
var actual = tokenizer.stream().toList();
|
||||||
|
assertEquals(List.of(expected), actual);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user