initial commit
parent
375382f04e
commit
92a20c82f8
@ -0,0 +1,37 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
}
|
||||
|
||||
group = "eu.jonahbauer"
|
||||
version = "1.0-SNAPSHOT"
|
||||
description = "json"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
version = 21
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(libs.annotations)
|
||||
compileOnly(libs.lombok)
|
||||
annotationProcessor(libs.lombok)
|
||||
testImplementation(libs.bundles.junit)
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
options.compilerArgs.add("--enable-preview")
|
||||
}
|
||||
|
||||
withType<Test> {
|
||||
useJUnitPlatform()
|
||||
jvmArgs("--enable-preview")
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
[versions]
|
||||
annotations = "24.1.0"
|
||||
junit = "5.10.1"
|
||||
lombok = "1.18.30"
|
||||
|
||||
[libraries]
|
||||
annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
|
||||
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
|
||||
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
|
||||
|
||||
[bundles]
|
||||
junit = ["junit-jupiter", "junit-jupiter-api", "junit-jupiter-params"]
|
Binary file not shown.
@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
@ -0,0 +1,249 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
@ -0,0 +1,92 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
@ -0,0 +1,5 @@
|
||||
/*
|
||||
* This file was generated by the Gradle 'init' task.
|
||||
*/
|
||||
|
||||
rootProject.name = "json"
|
@ -0,0 +1,274 @@
|
||||
package eu.jonahbauer.json;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Java representation of a JSON array.
|
||||
* @param elements the array's elements
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public record JsonArray(
|
||||
@NotNull List<@Nullable JsonValue> elements
|
||||
) implements JsonValue, List<@Nullable JsonValue> {
|
||||
|
||||
public JsonArray {
|
||||
Objects.requireNonNull(elements, "elements");
|
||||
if (!Util.isImmutable(elements)) {
|
||||
elements = elements.stream().toList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonArray} from the given list, using {@link JsonValue#valueOf(Object)} to convert the objects to
|
||||
* {@link JsonValue}s.
|
||||
* @param list a list of objects
|
||||
* @return a {@link JsonArray}
|
||||
*/
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static @Nullable JsonArray valueOf(@Nullable List<?> list) {
|
||||
if (list == null) return null;
|
||||
if (list instanceof JsonArray json) return json;
|
||||
return new JsonArray(list.stream().map(JsonValue::valueOf).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonArray} from the given array, using {@link JsonValue#valueOf(Object)} to convert the objects to
|
||||
* {@link JsonValue}s.
|
||||
* @param array an array of objects
|
||||
* @return a {@link JsonArray}
|
||||
*/
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static @Nullable JsonArray valueOf(@Nullable Object @Nullable... array) {
|
||||
return array == null ? null : new JsonArray(Arrays.stream(array).map(JsonValue::valueOf).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonArray} from the given array, using {@link JsonNumber#valueOf(int)} to convert the
|
||||
* {@code int}s to {@link JsonNumber}s.
|
||||
* @param array an array of {@code int}s
|
||||
* @return a {@link JsonArray}
|
||||
*/
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static @Nullable JsonArray valueOf(int @Nullable... array) {
|
||||
return array == null ? null : new JsonArray(Arrays.stream(array).<JsonValue>mapToObj(JsonNumber::valueOf).toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonArray} from the given array, using {@link JsonNumber#valueOf(double)} to convert the
|
||||
* {@code double}s to {@link JsonNumber}s.
|
||||
* @param array an array of {@code double}s
|
||||
* @return a {@link JsonArray}
|
||||
*/
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static @Nullable JsonArray valueOf(double @Nullable... array) {
|
||||
return array == null ? null : new JsonArray(Arrays.stream(array).<JsonValue>mapToObj(JsonNumber::valueOf).toList());
|
||||
}
|
||||
|
||||
//<editor-fold desc="List" defaultstate="collapsed">
|
||||
@Override
|
||||
public int size() {
|
||||
return elements.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return elements.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object o) {
|
||||
return elements.contains(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Iterator<@Nullable JsonValue> iterator() {
|
||||
return elements.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object @NotNull[] toArray() {
|
||||
return elements.toArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T @NotNull[] toArray(T @NotNull[] a) {
|
||||
return elements.toArray(a);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(@Nullable JsonValue value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object o) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("SlowListContainsAll")
|
||||
public boolean containsAll(@NotNull Collection<?> c) {
|
||||
return elements.containsAll(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(@NotNull Collection<? extends @Nullable JsonValue> c) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(int index, @NotNull Collection<? extends @Nullable JsonValue> c) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(@NotNull Collection<?> c) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(@NotNull Collection<?> c) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue get(int index) {
|
||||
return elements.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue set(int index, @Nullable JsonValue element) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int index, @Nullable JsonValue element) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue remove(int index) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int indexOf(Object o) {
|
||||
return elements.indexOf(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int lastIndexOf(Object o) {
|
||||
return elements.lastIndexOf(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ListIterator<@Nullable JsonValue> listIterator() {
|
||||
return elements.listIterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ListIterator<@Nullable JsonValue> listIterator(int index) {
|
||||
return elements.listIterator(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<@Nullable JsonValue> subList(int fromIndex, int toIndex) {
|
||||
return elements.subList(fromIndex, toIndex);
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
/**
|
||||
* {@return the element at the specified position in this array}
|
||||
* @param index index of the element to return
|
||||
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonString}.
|
||||
*/
|
||||
public @Nullable JsonString getString(int index) {
|
||||
return get(index, JsonString.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the element at the specified position in this array}
|
||||
* @param index index of the element to return
|
||||
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonNumber}.
|
||||
*/
|
||||
public @Nullable JsonNumber getNumber(int index) {
|
||||
return get(index, JsonNumber.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the element at the specified position in this array}
|
||||
* @param index index of the element to return
|
||||
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonBoolean}.
|
||||
*/
|
||||
public @Nullable JsonBoolean getBoolean(int index) {
|
||||
return get(index, JsonBoolean.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the element at the specified position in this array}
|
||||
* @param index index of the element to return
|
||||
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonArray}.
|
||||
*/
|
||||
public @Nullable JsonArray getArray(int index) {
|
||||
return get(index, JsonArray.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the element at the specified position in this array}
|
||||
* @param index index of the element to return
|
||||
* @throws ClassCastException if the element at the specified position is neither {@code null} nor an instance of {@link JsonObject}.
|
||||
*/
|
||||
public @Nullable JsonObject getObject(int index) {
|
||||
return get(index, JsonObject.class);
|
||||
}
|
||||
|
||||
private <T extends JsonValue> @Nullable T get(int index, @NotNull Class<T> type) {
|
||||
return type.cast(elements().get(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return elements.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toJsonString() {
|
||||
var joiner = new StringJoiner(", ", "[", "]");
|
||||
for (JsonValue element : elements) {
|
||||
joiner.add(JsonValue.toJsonString(element));
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toPrettyJsonString() {
|
||||
if (size() < 2) return toJsonString();
|
||||
|
||||
var out = new StringJoiner(",\n", "[\n", "\n]");
|
||||
elements().forEach(e -> out.add(indent(JsonValue.toPrettyJsonString(e))));
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static @NotNull String indent(@NotNull String string) {
|
||||
if (string.isEmpty()) return "";
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
Iterator<String> it = string.lines().iterator();
|
||||
while (it.hasNext()) {
|
||||
out.append(" ").append(it.next());
|
||||
if (it.hasNext()) out.append('\n');
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package eu.jonahbauer.json;
|
||||
|
||||
import eu.jonahbauer.json.token.JsonTokenKind;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import eu.jonahbauer.json.token.JsonToken;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Java representation of a JSON boolean.
|
||||
*/
|
||||
@Getter
|
||||
@Accessors(fluent = true)
|
||||
@RequiredArgsConstructor
|
||||
public enum JsonBoolean implements JsonValue, JsonToken, JsonTokenKind {
|
||||
TRUE(true),
|
||||
FALSE(false),
|
||||
;
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonBoolean} wrapping the given {@code boolean} value.
|
||||
* @param bool a {@code boolean} value
|
||||
* @return a {@link JsonBoolean}
|
||||
*/
|
||||
public static @NotNull JsonBoolean valueOf(boolean bool) {
|
||||
return bool ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonBoolean} wrapping the given {@link Boolean} value.
|
||||
* @param bool a {@link Boolean} value
|
||||
* @return a {@link JsonBoolean}
|
||||
*/
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static @Nullable JsonBoolean valueOf(@Nullable Boolean bool) {
|
||||
return bool == null ? null : valueOf((boolean) bool);
|
||||
}
|
||||
|
||||
private final boolean value;
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JsonBoolean getKind() {
|
||||
return this;
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package eu.jonahbauer.json;
|
||||
|
||||
import eu.jonahbauer.json.token.JsonTokenKind;
|
||||
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||
import eu.jonahbauer.json.token.JsonToken;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Java representation of a JSON boolean. Note, that JSON does not distinguish between integers and floating point
|
||||
* numbers and therefore all numbers are stored as {@code double}.
|
||||
*/
|
||||
public record JsonNumber(double value) implements JsonValue, JsonToken {
|
||||
public static final JsonTokenKind KIND = ComplexTokenKind.NUMBER;
|
||||
private static final long LONG_BIT_MASK = (~0L << Double.PRECISION);
|
||||
|
||||
public JsonNumber {
|
||||
if (!Double.isFinite(value)) throw new IllegalArgumentException("value must be finite");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonNumber} wrapping the given {@code int} value.
|
||||
* @param i an {@code int} value
|
||||
* @return a {@link JsonNumber}
|
||||
*/
|
||||
public static @NotNull JsonNumber valueOf(int i) {
|
||||
return new JsonNumber(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonNumber} wrapping the given {@code long} value.
|
||||
* @param l a {@code long} value
|
||||
* @return a {@link JsonNumber}
|
||||
* @throws IllegalArgumentException if conversion from {@code long} to {@code double} is lossy
|
||||
*/
|
||||
public static @NotNull JsonNumber valueOf(long l) {
|
||||
return new JsonNumber(checkLongRange(l));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonNumber} wrapping the given {@code double} value.
|
||||
* @param d an {@code double} value
|
||||
* @return a {@link JsonNumber}
|
||||
* @throws IllegalArgumentException if {@code d} is not {@linkplain Double#isFinite(double) finite}
|
||||
*/
|
||||
public static @NotNull JsonNumber valueOf(double d) {
|
||||
return new JsonNumber(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonNumber} wrapping {@code number}. {@code number} is converted to a {@code double} using
|
||||
* {@link Number#doubleValue()}.
|
||||
* @param number a number
|
||||
* @return a {@link JsonNumber}
|
||||
*/
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static @Nullable JsonNumber valueOf(@Nullable Number number) {
|
||||
return number == null ? null : new JsonNumber(number.doubleValue());
|
||||
}
|
||||
|
||||
private static long checkLongRange(long value) {
|
||||
if (((value >= 0 ? value : -value) & LONG_BIT_MASK) != 0) {
|
||||
throw new IllegalArgumentException("lossy conversion from long to double for value " + value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return Double.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JsonTokenKind getKind() {
|
||||
return KIND;
|
||||
}
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
package eu.jonahbauer.json;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Java representation of a JSON object.
|
||||
* @param entries the object's entries
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public record JsonObject(@NotNull Map<@NotNull String, @Nullable JsonValue> entries) implements JsonValue, Map<@NotNull String, @Nullable JsonValue> {
|
||||
|
||||
public JsonObject {
|
||||
Objects.requireNonNull(entries, "entries");
|
||||
if (!Util.isImmutable(entries)) {
|
||||
if (entries instanceof SequencedMap<?, ?>) {
|
||||
var map = new LinkedHashMap<String, JsonValue>();
|
||||
entries.forEach((key, value) -> map.put(Objects.requireNonNull(key, "key"), value));
|
||||
entries = Collections.unmodifiableSequencedMap(map);
|
||||
} else {
|
||||
var map = new HashMap<String, JsonValue>();
|
||||
entries.forEach((key, value) -> map.put(Objects.requireNonNull(key, "key"), value));
|
||||
entries = Collections.unmodifiableMap(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link JsonObject} from the given map, using {@link JsonValue#valueOf(Object)} to convert the values to
|
||||
* {@link JsonValue}s. The keys are expected to be instances of {@link CharSequence}.
|
||||
* @param map a map
|
||||
* @return a {@link JsonObject}
|
||||
*/
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static @Nullable JsonObject valueOf(@Nullable Map<?, ?> map) {
|
||||
if (map == null) return null;
|
||||
if (map instanceof JsonObject json) return json;
|
||||
|
||||
var out = new LinkedHashMap<String, JsonValue>();
|
||||
map.forEach((key, value) -> out.put(keyOf(key), JsonValue.valueOf(value)));
|
||||
return new JsonObject(out);
|
||||
}
|
||||
|
||||
private static @NotNull String keyOf(@Nullable Object object) {
|
||||
return switch (object) {
|
||||
case CharSequence chars -> chars.toString();
|
||||
case null -> throw new JsonProcessingException("cannot convert null to json key");
|
||||
default -> throw new JsonProcessingException(STR."cannot convert object of type \{object.getClass()} to json key");
|
||||
};
|
||||
}
|
||||
|
||||
//<editor-fold desc="Map" defaultstate="collapsed">
|
||||
@Override
|
||||
public int size() {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return entries.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
Objects.requireNonNull(key, "key");
|
||||
return entries.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
return entries.containsValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue get(Object key) {
|
||||
Objects.requireNonNull(key, "key");
|
||||
return entries.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue put(@NotNull String key, @Nullable JsonValue value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue remove(Object key) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(@NotNull Map<? extends @NotNull String, ? extends @Nullable JsonValue> m) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<@NotNull String> keySet() {
|
||||
return entries.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<@Nullable JsonValue> values() {
|
||||
return entries.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<Entry<@NotNull String, @Nullable JsonValue>> entrySet() {
|
||||
return entries.entrySet();
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="typed get(...)" defaultstate="collapsed">
|
||||
/**
|
||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonString}
|
||||
*/
|
||||
public @Nullable JsonString getString(@NotNull String key) {
|
||||
return get(key, JsonString.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonNumber}
|
||||
*/
|
||||
public @Nullable JsonNumber getNumber(@NotNull String key) {
|
||||
return get(key, JsonNumber.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonBoolean}
|
||||
*/
|
||||
public @Nullable JsonBoolean getBoolean(@NotNull String key) {
|
||||
return get(key, JsonBoolean.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonArray}
|
||||
*/
|
||||
public @Nullable JsonArray getArray(@NotNull String key) {
|
||||
return get(key, JsonArray.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the value to which the specified key is mapped, or <code>null</code> if this object contains no mapping for the key}
|
||||
* @throws ClassCastException if the value is neither {@code null} nor an instance of {@link JsonObject}
|
||||
*/
|
||||
public @Nullable JsonObject getObject(@NotNull String key) {
|
||||
return get(key, JsonObject.class);
|
||||
}
|
||||
|
||||
private <T extends JsonValue> @Nullable T get(@NotNull String key, @NotNull Class<T> type) {
|
||||
return type.cast(entries().get(key));
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
//<editor-fold desc="typed getOrDefault(...)" defaultstate="collapsed">
|
||||
// getStringOrDefault(String, JsonString) omitted because JsonString implements CharSequence
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped, or
|
||||
* {@link JsonString#valueOf(CharSequence) JsonString.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||
* instance of {@link JsonString}.
|
||||
* @return the value to which the specified key is mapped, or
|
||||
* {@link JsonString#valueOf(CharSequence) JsonString.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||
* nor an instance of {@link JsonString}
|
||||
*/
|
||||
public @Nullable JsonString getStringOrDefault(@NotNull String key, @Nullable CharSequence defaultValue) {
|
||||
return getOrDefault(key, defaultValue, JsonString.class, JsonString::valueOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
||||
* for the key or key is mapped to an object that is neither {@code null} nor an instance of {@link JsonNumber}.
|
||||
* @return the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
||||
* for the key or key is mapped to an object that is neither {@code null} nor an instance of
|
||||
* {@link JsonNumber}
|
||||
*/
|
||||
public @Nullable JsonNumber getNumberOrDefault(@NotNull String key, @Nullable JsonNumber defaultValue) {
|
||||
return getOrDefault(key, defaultValue, JsonNumber.class, Function.identity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped, or
|
||||
* {@link JsonNumber#valueOf(Number) JsonNumber.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||
* instance of {@link JsonNumber}.
|
||||
* @return the value to which the specified key is mapped, or
|
||||
* {@link JsonNumber#valueOf(Number) JsonNumber.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||
* nor an instance of {@link JsonNumber}
|
||||
*/
|
||||
public @Nullable JsonNumber getNumberOrDefault(@NotNull String key, @Nullable Number defaultValue) {
|
||||
return getOrDefault(key, defaultValue, JsonNumber.class, JsonNumber::valueOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
||||
* for the key or key is mapped to an object that is neither {@code null} nor an instance of {@link JsonBoolean}.
|
||||
* @return the value to which the specified key is mapped, or {@code defaultValue} if this object contains no mapping
|
||||
* for the key or key is mapped to an object that is neither {@code null} nor an instance of
|
||||
* {@link JsonBoolean}
|
||||
*/
|
||||
public @Nullable JsonBoolean getBooleanOrDefault(@NotNull String key, @Nullable JsonBoolean defaultValue) {
|
||||
return getOrDefault(key, defaultValue, JsonBoolean.class, Function.identity());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped, or
|
||||
* {@link JsonBoolean#valueOf(Boolean) JsonBoolean.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||
* instance of {@link JsonBoolean}.
|
||||
* @return the value to which the specified key is mapped, or
|
||||
* {@link JsonBoolean#valueOf(Boolean) JsonBoolean.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||
* nor an instance of {@link JsonBoolean}
|
||||
*/
|
||||
public @Nullable JsonBoolean getBooleanOrDefault(@NotNull String key, @Nullable Boolean defaultValue) {
|
||||
return getOrDefault(key, defaultValue, JsonBoolean.class, JsonBoolean::valueOf);
|
||||
}
|
||||
|
||||
// getArrayOrDefault(String, JsonArray) omitted because JsonArray implements List<?>
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped, or
|
||||
* {@link JsonArray#valueOf(List) JsonArray.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||
* instance of {@link JsonArray}.
|
||||
* @return the value to which the specified key is mapped, or
|
||||
* {@link JsonArray#valueOf(List) JsonArray.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||
* nor an instance of {@link JsonArray}
|
||||
*/
|
||||
public @Nullable JsonArray getArrayOrDefault(@NotNull String key, @Nullable List<?> defaultValue) {
|
||||
return getOrDefault(key, defaultValue, JsonArray.class, JsonArray::valueOf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped, or
|
||||
* {@link JsonArray#valueOf(Object...) JsonArray.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||
* instance of {@link JsonArray}.
|
||||
* @return the value to which the specified key is mapped, or
|
||||
* {@link JsonArray#valueOf(Object...) JsonArray.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||
* nor an instance of {@link JsonArray}
|
||||
*/
|
||||
public @Nullable JsonArray getArrayOrDefault(@NotNull String key, @Nullable Object @Nullable... defaultValue) {
|
||||
return getOrDefault(key, defaultValue, JsonArray.class, JsonArray::valueOf);
|
||||
}
|
||||
|
||||
// getObjectOrDefault(String, JsonObject) omitted because JsonObject implements Map<?, ?>
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped, or
|
||||
* {@link JsonObject#valueOf(Map) JsonObject.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null} nor an
|
||||
* instance of {@link JsonObject}.
|
||||
* @return the value to which the specified key is mapped, or
|
||||
* {@link JsonObject#valueOf(Map) JsonObject.valueOf}{@code (defaultValue)}
|
||||
* if this object contains no mapping for the key or key is mapped to an object that is neither {@code null}
|
||||
* nor an instance of {@link JsonObject}
|
||||
*/
|
||||
public @Nullable JsonObject getObjectOrDefault(@NotNull String key, @Nullable Map<?, ?> defaultValue) {
|
||||
return getOrDefault(key, defaultValue, JsonObject.class, JsonObject::valueOf);
|
||||
}
|
||||
|
||||
private <T extends JsonValue, S> @Nullable T getOrDefault(@NotNull String key, @Nullable S defaultValue, @NotNull Class<T> type, @NotNull Function<S, T> converter) {
|
||||
JsonValue value = entries.get(key);
|
||||
if (type.isInstance(value)) {
|
||||
return type.cast(value);
|
||||
} else if (value == null && containsKey(key)) {
|
||||
return null;
|
||||
} else {
|
||||
return converter.apply(defaultValue);
|
||||
}
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
StringJoiner out = new StringJoiner(",", "{", "}");
|
||||
entries.forEach((key, value) -> out.add(JsonString.quote(key) + ":" + value));
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toPrettyJsonString() {
|
||||
StringJoiner out = new StringJoiner(",\n ", "{\n ", "\n}");
|
||||
entries.forEach((key, value) -> out.add(JsonString.quote(key) + ": " + indent(JsonValue.toPrettyJsonString(value))));
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static @NotNull String indent(@NotNull String string) {
|
||||
if (string.isEmpty()) return "";
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
Iterator<String> it = string.lines().iterator();
|
||||
out.append(it.next());
|
||||
|
||||
while (it.hasNext()) {
|
||||
String next = it.next();
|
||||
out.append('\n').append(it.hasNext() ? " " : " ").append(next);
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package eu.jonahbauer.json;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@Getter
|
||||
public class JsonProcessingException extends RuntimeException {
|
||||
private final @Nullable Position position;
|
||||
|
||||
public JsonProcessingException(int fragment, int line, int column, @NotNull String message) {
|
||||
super(message);
|
||||
this.position = new Position(fragment, line, column);
|
||||
}
|
||||
|
||||
public JsonProcessingException(int fragment, int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
this.position = new Position(fragment, line, column);
|
||||
}
|
||||
|
||||
protected JsonProcessingException(@NotNull String message) {
|
||||
super(message);
|
||||
this.position = null;
|
||||
}
|
||||
|
||||
protected JsonProcessingException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
this.position = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
if (position != null) {
|
||||
return super.getMessage() + " at " + position;
|
||||
} else {
|
||||
return super.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public record Position(int fragment, int line, int column) {
|
||||
public Position {
|
||||
if (fragment < -1) throw new IllegalArgumentException();
|
||||
if (line < 0) throw new IllegalArgumentException();
|
||||
if (column < 0) throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (fragment == -1) {
|
||||
return STR."line \{line}, column \{column}";
|
||||
} else {
|
||||
return STR."fragment \{fragment}, line \{line}, column \{column}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
package eu.jonahbauer.json;
|
||||
|
||||
import eu.jonahbauer.json.token.JsonTokenKind;
|
||||
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||
import eu.jonahbauer.json.token.JsonToken;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public record JsonString(@NotNull String value) implements JsonValue, JsonToken, CharSequence {
|
||||
public static final JsonTokenKind KIND = ComplexTokenKind.STRING;
|
||||
|
||||
public JsonString {
|
||||
Objects.requireNonNull(value, "value");
|
||||
}
|
||||
|
||||
@Contract("null -> null; !null -> !null")
|
||||
public static @Nullable JsonString valueOf(@Nullable CharSequence chars) {
|
||||
return switch (chars) {
|
||||
case JsonString json -> json;
|
||||
case null -> null;
|
||||
default -> {
|
||||
@SuppressWarnings("java:S2259")
|
||||
var result = new JsonString(chars.toString());
|
||||
yield result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes the given {@link String} and encodes all invalid characters as an escape sequence.
|
||||
* @param value a string value
|
||||
* @return a JSON representation of the given {@code value}
|
||||
*/
|
||||
public static @NotNull String quote(@NotNull String value) {
|
||||
StringBuilder out = new StringBuilder(value.length() + 2);
|
||||
out.append('"');
|
||||
for (int i = 0, length = value.length(); i < length; i++) {
|
||||
char chr = value.charAt(i);
|
||||
switch (chr) {
|
||||
case '"' -> out.append("\\\"");
|
||||
case '\\' -> out.append("\\\\");
|
||||
case '\b' -> out.append("\\b");
|
||||
case '\f' -> out.append("\\f");
|
||||
case '\n' -> out.append("\\n");
|
||||
case '\r' -> out.append("\\r");
|
||||
case '\t' -> out.append("\\t");
|
||||
default -> {
|
||||
if (chr < 32) {
|
||||
out.append("\\u%04x".formatted((int) chr));
|
||||
} else {
|
||||
out.append(chr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.append('"');
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
//<editor-fold desc="CharSequence" defaultstate="collapsed">
|
||||
@Override
|
||||
public int length() {
|
||||
return value.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
return value.charAt(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JsonString subSequence(int start, int end) {
|
||||
return new JsonString(value.substring(start, end));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return value.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull IntStream chars() {
|
||||
return value.chars();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull IntStream codePoints() {
|
||||
return value.codePoints();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return value;
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
@Override
|
||||
public @NotNull String toJsonString() {
|
||||
return quote(value());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toPrettyJsonString() {
|
||||
return toJsonString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JsonTokenKind getKind() {
|
||||
return KIND;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package eu.jonahbauer.json;
|
||||
|
||||
import eu.jonahbauer.json.parser.JsonConversionException;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public sealed interface JsonValue extends Serializable permits JsonObject, JsonArray, JsonBoolean, JsonNumber, JsonString {
|
||||
|
||||
/**
|
||||
* {@return the JSON representation of this value}
|
||||
*/
|
||||
default @NotNull String toJsonString() {
|
||||
return toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return a prettified JSON representation of this value}
|
||||
*/
|
||||
default @NotNull String toPrettyJsonString() {
|
||||
return toJsonString();
|
||||
}
|
||||
|
||||
default @Nullable JsonValue select(@NotNull JsonPath path) {
|
||||
return path.select(this);
|
||||
}
|
||||
|
||||
default @Nullable JsonValue select(@NotNull String path) {
|
||||
return JsonPath.parse(path).select(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the JSON representation of the given value}
|
||||
* @param value a {@link JsonValue} (possibly {@code null})
|
||||
*/
|
||||
static @NotNull String toJsonString(@Nullable JsonValue value) {
|
||||
return value == null ? "null" : value.toJsonString();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return the prettified JSON representation of the given value}
|
||||
* @param value a {@link JsonValue} (possibly {@code null})
|
||||
*/
|
||||
static @NotNull String toPrettyJsonString(@Nullable JsonValue value) {
|
||||
return value == null ? "null" : value.toPrettyJsonString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to convert {@code object} to a {@link JsonValue}:
|
||||
* <ul>
|
||||
* <li>{@link JsonValue}s and {@code null} are simple returned</li>
|
||||
* <li>{@link Number}s are converted with {@link JsonNumber#valueOf(Number)}</li>
|
||||
* <li>{@link CharSequence}s are converted with {@link JsonString#valueOf(CharSequence)}</li>
|
||||
* <li>{@link Boolean}s are converted with {@link JsonBoolean#valueOf(Boolean)}</li>
|
||||
* <li>{@link List}s are converted with {@link JsonArray#valueOf(List)}</li>
|
||||
* <li>
|
||||
* {@code Object[]}, {@code int[]} and {@code double[]} are converted with
|
||||
* {@link JsonArray#valueOf(Object...)}, {@link JsonArray#valueOf(int...)} and
|
||||
* {@link JsonArray#valueOf(double...)} resp.
|
||||
* </li>
|
||||
* <li>{@link Map}s are converted with {@link JsonObject#valueOf(Map)}</li>
|
||||
* </ul>
|
||||
* @param object an object
|
||||
* @return a {@link JsonValue} representing the object
|
||||
* @throws JsonConversionException if the object cannot be converted to a {@link JsonValue}
|
||||
*/
|
||||
@Contract("null -> null; !null -> !null")
|
||||
static @Nullable JsonValue valueOf(@Nullable Object object) {
|
||||
return switch (object) {
|
||||
case JsonValue json -> json;
|
||||
case Number number -> JsonNumber.valueOf(number);
|
||||
case CharSequence chars -> JsonString.valueOf(chars);
|
||||
case Boolean bool -> JsonBoolean.valueOf(bool);
|
||||
case List<?> list -> JsonArray.valueOf(list);
|
||||
case Object[] array -> JsonArray.valueOf(array);
|
||||
case int[] array -> JsonArray.valueOf(array);
|
||||
case double[] array -> JsonArray.valueOf(array);
|
||||
case Map<?, ?> map -> JsonObject.valueOf(map);
|
||||
case null -> null;
|
||||
default -> throw new JsonConversionException(STR."cannot convert object of type \{object.getClass()} to json value");
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package eu.jonahbauer.json;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
class Util {
|
||||
private static final @NotNull Class<?> LIST_1_2 = List.of(1).getClass();
|
||||
private static final @NotNull Class<?> LIST_N = List.of(1, 2, 3).getClass();
|
||||
private static final @NotNull Class<?> MAP_1 = Map.of(1, 2).getClass();
|
||||
private static final @NotNull Class<?> MAP_N = Map.of(1, 2, 3, 4).getClass();
|
||||
|
||||
public static boolean isImmutable(@NotNull List<?> list) {
|
||||
return LIST_1_2.isInstance(list) || LIST_N.isInstance(list);
|
||||
}
|
||||
|
||||
public static boolean isImmutable(@NotNull Map<?, ?> map) {
|
||||
return MAP_1.isInstance(map) || MAP_N.isInstance(map);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package eu.jonahbauer.json.parser;
|
||||
|
||||
import eu.jonahbauer.json.JsonProcessingException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class JsonConversionException extends JsonProcessingException {
|
||||
public JsonConversionException(@NotNull String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public JsonConversionException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
package eu.jonahbauer.json.parser;
|
||||
|
||||
import eu.jonahbauer.json.*;
|
||||
import eu.jonahbauer.json.parser.tokenizer.JsonTokenizer;
|
||||
import eu.jonahbauer.json.parser.tokenizer.JsonTokenizerImpl;
|
||||
import eu.jonahbauer.json.token.*;
|
||||
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 StringTemplate json) {
|
||||
this(new JsonTokenizerImpl(json));
|
||||
}
|
||||
|
||||
JsonParser(@NotNull JsonTokenizer tokenizer) {
|
||||
this.tokenizer = Objects.requireNonNull(tokenizer, "tokenizer");
|
||||
}
|
||||
|
||||
public @Nullable JsonValue parse() throws JsonProcessingException {
|
||||
try {
|
||||
JsonValue out = readJsonValue();
|
||||
if (tokenizer.next() != null) throw new JsonProcessingException(
|
||||
tokenizer.getFragment(), tokenizer.getLine(), tokenizer.getColumn(),
|
||||
"unexpected trailing entries"
|
||||
);
|
||||
return out;
|
||||
} catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable JsonValue readJsonValue() throws IOException {
|
||||
Deque<Context> stack = new ArrayDeque<>();
|
||||
Context context = null;
|
||||
|
||||
while (true) {
|
||||
JsonValue value;
|
||||
|
||||
JsonToken token = tokenizer.next();
|
||||
if (context != null && token == context.end()) {
|
||||
stack.pop();
|
||||
value = context.build();
|
||||
context = stack.peek();
|
||||
} else {
|
||||
// consume value separator
|
||||
if (context != null && !context.isEmpty()) {
|
||||
if (token != JsonPunctuation.VALUE_SEPARATOR) {
|
||||
throw unexpectedToken(token, context.end(), JsonPunctuation.VALUE_SEPARATOR);
|
||||
}
|
||||
token = tokenizer.next();
|
||||
}
|
||||
|
||||
// read object key and consume value separator
|
||||
if (context instanceof Context.ObjectContext obj) {
|
||||
String key = readJsonObjectKey(obj, token);
|
||||
if ((token = tokenizer.next()) != JsonPunctuation.NAME_SEPARATOR) {
|
||||
throw unexpectedToken(token, JsonPunctuation.NAME_SEPARATOR);
|
||||
}
|
||||
token = tokenizer.next();
|
||||
obj.setKey(key);
|
||||
}
|
||||
|
||||
if (token == JsonPunctuation.BEGIN_OBJECT) {
|
||||
stack.push(context = new Context.ObjectContext());
|
||||
continue;
|
||||
} else if (token == JsonPunctuation.BEGIN_ARRAY) {
|
||||
stack.push(context = new Context.ArrayContext());
|
||||
continue;
|
||||
} else {
|
||||
// read value
|
||||
value = switch (token) {
|
||||
case JsonPunctuation.BEGIN_OBJECT, JsonPunctuation.BEGIN_ARRAY -> throw new AssertionError();
|
||||
case JsonNull.NULL -> null;
|
||||
case JsonValue v -> v;
|
||||
case JsonStringTemplate template -> template.asString();
|
||||
case JsonPlaceholder(var object) -> JsonValue.valueOf(object);
|
||||
case JsonPunctuation _ -> throw unexpectedStartOfValue(token);
|
||||
case null -> throw unexpectedEndOfFile();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
return value;
|
||||
} else {
|
||||
context.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull String readJsonObjectKey(@NotNull Context.ObjectContext context, @Nullable JsonToken token) {
|
||||
return switch (token) {
|
||||
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 convert null to json key");
|
||||
default -> throw new JsonConversionException(STR."cannot convert object of type \{object.getClass()} to json key");
|
||||
};
|
||||
case null -> throw unexpectedEndOfFile();
|
||||
default -> throw unexpectedStartOfKey(context, token);
|
||||
};
|
||||
}
|
||||
|
||||
private @NotNull JsonProcessingException unexpectedStartOfKey(@NotNull Context.ObjectContext context, @Nullable JsonToken token) {
|
||||
var expected = new ArrayList<JsonTokenKind>();
|
||||
expected.add(JsonString.KIND);
|
||||
if (context.isEmpty()) {
|
||||
expected.add(JsonPunctuation.END_OBJECT);
|
||||
}
|
||||
if (tokenizer.hasTemplateSupport()) {
|
||||
expected.add(JsonStringTemplate.KIND);
|
||||
expected.add(JsonPlaceholder.KIND);
|
||||
}
|
||||
return unexpectedToken(token, expected.toArray(JsonTokenKind[]::new));
|
||||
}
|
||||
|
||||
private @NotNull JsonProcessingException unexpectedStartOfValue(@Nullable JsonToken token) {
|
||||
var expected = new ArrayList<JsonTokenKind>();
|
||||
expected.add(JsonPunctuation.BEGIN_OBJECT);
|
||||
expected.add(JsonPunctuation.BEGIN_ARRAY);
|
||||
expected.add(JsonBoolean.TRUE);
|
||||
expected.add(JsonBoolean.FALSE);
|
||||
expected.add(JsonNull.NULL);
|
||||
expected.add(JsonString.KIND);
|
||||
expected.add(JsonNumber.KIND);
|
||||
if (tokenizer.hasTemplateSupport()) {
|
||||
expected.add(JsonStringTemplate.KIND);
|
||||
expected.add(JsonPlaceholder.KIND);
|
||||
}
|
||||
return unexpectedToken(token, expected.toArray(JsonTokenKind[]::new));
|
||||
}
|
||||
|
||||
private @NotNull JsonProcessingException unexpectedEndOfFile() {
|
||||
return new JsonProcessingException(tokenizer.getFragment(), tokenizer.getLine(), tokenizer.getColumn(), "unexpected end of file");
|
||||
}
|
||||
|
||||
private @NotNull JsonProcessingException unexpectedToken(@Nullable JsonToken token, @NotNull JsonTokenKind @NotNull... expected) {
|
||||
if (expected.length == 1) {
|
||||
throw new JsonProcessingException(
|
||||
tokenizer.getFragment(), tokenizer.getLine(), tokenizer.getColumn(),
|
||||
STR."unexpected token: \{token} (expected \{expected[0]})"
|
||||
);
|
||||
} else {
|
||||
throw new JsonProcessingException(
|
||||
tokenizer.getFragment(), tokenizer.getLine(), tokenizer.getColumn(),
|
||||
STR."unexpected token: \{token} (expected one of \{Arrays.toString(expected)})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private interface Context {
|
||||
boolean isEmpty();
|
||||
void add(@Nullable JsonValue value);
|
||||
@NotNull JsonValue build();
|
||||
@NotNull JsonPunctuation end();
|
||||
|
||||
class ObjectContext implements Context {
|
||||
private final SequencedMap<String, JsonValue> map = new LinkedHashMap<>();
|
||||
private String key;
|
||||
|
||||
public void setKey(@NotNull 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 JsonObject build() {
|
||||
if (this.key != null) throw new IllegalStateException();
|
||||
return new JsonObject(map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JsonPunctuation end() {
|
||||
return JsonPunctuation.END_OBJECT;
|
||||
}
|
||||
}
|
||||
|
||||
class ArrayContext implements Context {
|
||||
private final List<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package eu.jonahbauer.json.parser;
|
||||
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@SuppressWarnings("java:S6548")
|
||||
public enum JsonProcessor implements StringTemplate.Processor<@Nullable JsonValue, RuntimeException> {
|
||||
JSON;
|
||||
|
||||
@Override
|
||||
public @Nullable JsonValue process(@NotNull StringTemplate template) {
|
||||
return new JsonParser(template).parse();
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package eu.jonahbauer.json.parser;
|
||||
|
||||
import eu.jonahbauer.json.JsonProcessingException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class JsonTokenizerException extends JsonProcessingException {
|
||||
public JsonTokenizerException(int fragment, int line, int column, @NotNull String message) {
|
||||
super(fragment, line, column, message);
|
||||
}
|
||||
|
||||
public JsonTokenizerException(int fragment, int line, int column, @NotNull String message, @NotNull Throwable cause) {
|
||||
super(fragment, line, column, message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package eu.jonahbauer.json.parser.tokenizer;
|
||||
|
||||
import eu.jonahbauer.json.token.JsonToken;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
public interface JsonTokenizer extends Iterable<JsonToken> {
|
||||
|
||||
@Nullable JsonToken next() throws IOException;
|
||||
|
||||
int getFragment();
|
||||
int getLine();
|
||||
int getColumn();
|
||||
|
||||
boolean hasTemplateSupport();
|
||||
|
||||
|
||||
@Override
|
||||
default @NotNull Iterator<@NotNull JsonToken> iterator() {
|
||||
class JsonTokenizerIterator implements Iterator<@NotNull JsonToken> {
|
||||
private JsonToken next;
|
||||
private boolean valid = false;
|
||||
|
||||
|
||||
private void ensureValid() {
|
||||
try {
|
||||
if (!valid) {
|
||||
next = JsonTokenizer.this.next();
|
||||
valid = true;
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
ensureValid();
|
||||
return next != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JsonToken next() {
|
||||
ensureValid();
|
||||
if (next == null) {
|
||||
throw new NoSuchElementException();
|
||||
} else {
|
||||
valid = false;
|
||||
return next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonTokenizerIterator();
|
||||
}
|
||||
|
||||
default @NotNull Stream<@NotNull JsonToken> stream() {
|
||||
return StreamSupport.stream(this.spliterator(), false);
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
package eu.jonahbauer.json.parser.tokenizer;
|
||||
|
||||
import eu.jonahbauer.json.token.*;
|
||||
import lombok.Getter;
|
||||
import eu.jonahbauer.json.JsonBoolean;
|
||||
import eu.jonahbauer.json.JsonNumber;
|
||||
import eu.jonahbauer.json.JsonString;
|
||||
import eu.jonahbauer.json.parser.JsonTokenizerException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class JsonTokenizerImpl implements JsonTokenizer {
|
||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9]\\d*)(?:\\.\\d+)?([eE][+-]?\\d+)?");
|
||||
private final @NotNull TemplateReader reader;
|
||||
private final int[] buffer = new int[4];
|
||||
|
||||
@Getter
|
||||
private int fragment;
|
||||
@Getter
|
||||
private int line;
|
||||
@Getter
|
||||
private int column;
|
||||
|
||||
public JsonTokenizerImpl(@NotNull String json) {
|
||||
this.reader = new TemplateReader(json);
|
||||
}
|
||||
|
||||
public JsonTokenizerImpl(@NotNull Reader reader) {
|
||||
this.reader = new TemplateReader(reader);
|
||||
}
|
||||
|
||||
public JsonTokenizerImpl(@NotNull StringTemplate json) {
|
||||
this.reader = new TemplateReader(json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable JsonToken next() throws IOException {
|
||||
int chr;
|
||||
|
||||
do {
|
||||
chr = reader.read();
|
||||
} while (isWhitespace(chr));
|
||||
|
||||
fragment = reader.getFragment();
|
||||
line = reader.getLine();
|
||||
column = reader.getColumn();
|
||||
|
||||
return switch (chr) {
|
||||
case TemplateReader.EOF -> null;
|
||||
case TemplateReader.PLACEHOLDER -> new JsonPlaceholder(reader.getObject());
|
||||
case '{' -> JsonPunctuation.BEGIN_OBJECT;
|
||||
case '}' -> JsonPunctuation.END_OBJECT;
|
||||
case '[' -> JsonPunctuation.BEGIN_ARRAY;
|
||||
case ']' -> JsonPunctuation.END_ARRAY;
|
||||
case ',' -> JsonPunctuation.VALUE_SEPARATOR;
|
||||
case ':' -> JsonPunctuation.NAME_SEPARATOR;
|
||||
case '"' -> {
|
||||
StringTemplate string = readString();
|
||||
if (string.fragments().size() == 1) {
|
||||
yield new JsonString(string.fragments().getFirst());
|
||||
} else {
|
||||
yield new JsonStringTemplate(string);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
if (chr < 0) throw new AssertionError();
|
||||
|
||||
reader.pushback();
|
||||
String token = nextToken();
|
||||
yield switch (token) {
|
||||
case "true" -> JsonBoolean.TRUE;
|
||||
case "false" -> JsonBoolean.FALSE;
|
||||
case "null" -> JsonNull.NULL;
|
||||
case String number when isNumberLiteral(number) -> {
|
||||
try {
|
||||
yield new JsonNumber(Double.parseDouble(number));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw error("invalid number literal: " + number, ex);
|
||||
}
|
||||
}
|
||||
default -> throw error("invalid token: " + token);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTemplateSupport() {
|
||||
return reader.isHasTemplateSupport();
|
||||
}
|
||||
|
||||
protected boolean isWhitespace(int chr) {
|
||||
return chr == ' ' || chr == '\t' || chr == '\r' || chr == '\n';
|
||||
}
|
||||
|
||||
protected boolean isPunctuation(int chr) {
|
||||
return chr == '{' || chr == '}' || chr == '[' || chr == ']' || chr == ',' || chr == ':' || chr == '"';
|
||||
}
|
||||
|
||||
protected boolean isNumberLiteral(@NotNull String token) {
|
||||
return NUMBER_PATTERN.matcher(token).matches();
|
||||
}
|
||||
|
||||
private @NotNull String nextToken() throws IOException {
|
||||
StringBuilder out = new StringBuilder();
|
||||
while (true) {
|
||||
int chr = reader.read();
|
||||
if (isWhitespace(chr) || isPunctuation(chr)) {
|
||||
reader.pushback();
|
||||
return out.toString();
|
||||
} else if (chr == -1) {
|
||||
return out.toString();
|
||||
} else {
|
||||
out.append((char) chr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull StringTemplate readString() throws IOException {
|
||||
List<String> fragments = new ArrayList<>();
|
||||
List<Object> values = new ArrayList<>();
|
||||
|
||||
StringBuilder current = new StringBuilder();
|
||||
while (true) {
|
||||
int chr = reader.read();
|
||||
if (chr == '"') {
|
||||
fragments.add(current.toString());
|
||||
return StringTemplate.of(fragments, values);
|
||||
} else if (chr == '\\') {
|
||||
chr = reader.read();
|
||||
switch (chr) {
|
||||
case TemplateReader.EOF -> throw error("incomplete escape sequence in string literal");
|
||||
case '"' -> current.append('"');
|
||||
case '\\' -> current.append('\\');
|
||||
case '/' -> current.append('/');
|
||||
case 'b' -> current.append('\b');
|
||||
case 'f' -> current.append('\f');
|
||||
case 'n' -> current.append('\n');
|
||||
case 'r' -> current.append('\r');
|
||||
case 't' -> current.append('\t');
|
||||
case 'u' -> {
|
||||
buffer[0] = reader.read();
|
||||
buffer[1] = reader.read();
|
||||
buffer[2] = reader.read();
|
||||
buffer[3] = reader.read();
|
||||
|
||||
if (buffer[0] < 0 || buffer[1] < 0 || buffer[2] < 0 || buffer[3] < 0) {
|
||||
throw error("incomplete escape sequence in string literal");
|
||||
}
|
||||
|
||||
int code = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
code *= 16;
|
||||
char esc = (char) buffer[i];
|
||||
if ('0' <= esc && esc <= '9') {
|
||||
code += esc - '0';
|
||||
} else if ('a' <= esc && esc <= 'f') {
|
||||
code += 11 + (esc - 'a');
|
||||
} else if ('A' <= esc && esc <= 'F') {
|
||||
code += 11 + (esc - 'A');
|
||||
} else {
|
||||
throw error(STR."invalid character \{toString(buffer[i])} in escape sequence");
|
||||
}
|
||||
}
|
||||
current.append((char) code);
|
||||
}
|
||||
default -> throw error(STR."invalid character \{toString(chr)} in escape sequence");
|
||||
}
|
||||
|
||||
} else if (chr == TemplateReader.EOF) {
|
||||
throw error("unclosed string literal");
|
||||
} else if (chr == TemplateReader.PLACEHOLDER) {
|
||||
fragments.add(current.toString());
|
||||
current.setLength(0);
|
||||
values.add(reader.getObject());
|
||||
} else if (chr < 32) {
|
||||
throw error("unescaped control character in string literal");
|
||||
} else {
|
||||
current.append((char) chr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull JsonTokenizerException error(@NotNull String message) {
|
||||
return new JsonTokenizerException(getFragment(), getLine(), getColumn(), message);
|
||||
}
|
||||
|
||||
private @NotNull JsonTokenizerException error(@NotNull String message, @NotNull Throwable cause) {
|
||||
return new JsonTokenizerException(getFragment(), getLine(), getColumn(), message, cause);
|
||||
}
|
||||
|
||||
private static @NotNull String toString(int chr) {
|
||||
return switch (chr) {
|
||||
case TemplateReader.EOF -> "EOF";
|
||||
case TemplateReader.PLACEHOLDER -> "PLACEHOLDER";
|
||||
case '\t' -> "'\\t'";
|
||||
case '\r' -> "'\\r'";
|
||||
case '\n' -> "'\\n'";
|
||||
case '\b' -> "'\\b'";
|
||||
case '\f' -> "'\\f'";
|
||||
default -> {
|
||||
if (Character.isISOControl((char) chr)) {
|
||||
yield STR."0x\{Integer.toHexString(chr)}";
|
||||
} else {
|
||||
yield STR."'\{(char) chr}'";
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
package eu.jonahbauer.json.parser.tokenizer;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
@VisibleForTesting
|
||||
class TemplateReader {
|
||||
public static final int EOF = -1;
|
||||
public static final int PLACEHOLDER = -2;
|
||||
|
||||
|
||||
private final IntFunction<Reader> readers;
|
||||
private final List<Object> values;
|
||||
private final int length;
|
||||
|
||||
@Getter
|
||||
private final boolean hasTemplateSupport;
|
||||
|
||||
private Reader reader;
|
||||
|
||||
private int fragment;
|
||||
@Getter
|
||||
private int line = 1;
|
||||
@Getter
|
||||
private int column;
|
||||
|
||||
|
||||
private int current;
|
||||
private Object currentObject;
|
||||
|
||||
private boolean pushback;
|
||||
|
||||
|
||||
public TemplateReader(@NotNull String string) {
|
||||
this(new StringReader(string));
|
||||
}
|
||||
|
||||
public TemplateReader(@NotNull Reader reader) {
|
||||
Objects.requireNonNull(reader, "reader");
|
||||
this.readers = _ -> reader;
|
||||
this.values = List.of();
|
||||
this.length = 1;
|
||||
this.hasTemplateSupport = false;
|
||||
}
|
||||
|
||||
public TemplateReader(@NotNull StringTemplate template) {
|
||||
Objects.requireNonNull(template, "template");
|
||||
this.readers = i -> new StringReader(template.fragments().get(i));
|
||||
this.values = template.values();
|
||||
this.length = template.fragments().size();
|
||||
this.hasTemplateSupport = true;
|
||||
}
|
||||
|
||||
public int read() throws IOException {
|
||||
if (pushback) {
|
||||
pushback = false;
|
||||
return current;
|
||||
}
|
||||
|
||||
if (!ensureReader()) return EOF;
|
||||
|
||||
int result = reader.read();
|
||||
|
||||
// track position
|
||||
if (current != '\r' && result == '\n' || result == '\r') {
|
||||
line++;
|
||||
column = 0;
|
||||
} else if (result != '\n') {
|
||||
column++;
|
||||
}
|
||||
|
||||
if (result == EOF) {
|
||||
reader = null;
|
||||
if (fragment < length) {
|
||||
currentObject = values.get(fragment - 1);
|
||||
current = PLACEHOLDER;
|
||||
} else {
|
||||
currentObject = null;
|
||||
current = result;
|
||||
}
|
||||
} else {
|
||||
currentObject = null;
|
||||
current = result;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private boolean ensureReader() {
|
||||
if (reader != null) {
|
||||
return true;
|
||||
} else if (fragment < length) {
|
||||
reader = readers.apply(fragment++);
|
||||
line = 1;
|
||||
column = 0;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Object getObject() {
|
||||
if (current != PLACEHOLDER) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return currentObject;
|
||||
}
|
||||
|
||||
public void pushback() {
|
||||
if (pushback) throw new IllegalStateException();
|
||||
pushback = true;
|
||||
}
|
||||
|
||||
public int getFragment() {
|
||||
return hasTemplateSupport ? fragment : -1;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package eu.jonahbauer.json.token;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("java:S6548")
|
||||
public enum JsonNull implements JsonToken, JsonTokenKind {
|
||||
NULL;
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return "null";
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JsonNull getKind() {
|
||||
return this;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package eu.jonahbauer.json.token;
|
||||
|
||||
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public record JsonPlaceholder(Object value) implements JsonToken {
|
||||
public static final JsonTokenKind KIND = ComplexTokenKind.PLACEHOLDER;
|
||||
|
||||
@Override
|
||||
public @NotNull JsonTokenKind getKind() {
|
||||
return KIND;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package eu.jonahbauer.json.token;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
@Accessors(fluent = true)
|
||||
@RequiredArgsConstructor
|
||||
public enum JsonPunctuation implements JsonToken, JsonTokenKind {
|
||||
BEGIN_OBJECT('{'),
|
||||
END_OBJECT('}'),
|
||||
BEGIN_ARRAY('['),
|
||||
END_ARRAY(']'),
|
||||
VALUE_SEPARATOR(','),
|
||||
NAME_SEPARATOR(':'),
|
||||
;
|
||||
|
||||
private final char value;
|
||||
|
||||
@Override
|
||||
public @NotNull JsonTokenKind getKind() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return name() + "(" + value + ")";
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.jonahbauer.json.token;
|
||||
|
||||
import eu.jonahbauer.json.JsonString;
|
||||
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record JsonStringTemplate(@NotNull StringTemplate template) implements JsonToken {
|
||||
public static final JsonTokenKind KIND = ComplexTokenKind.STRING_TEMPLATE;
|
||||
|
||||
public JsonStringTemplate {
|
||||
Objects.requireNonNull(template, "template");
|
||||
}
|
||||
|
||||
public @NotNull JsonString asString() {
|
||||
return new JsonString(STR.process(template));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull JsonTokenKind getKind() {
|
||||
return KIND;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package eu.jonahbauer.json.token;
|
||||
|
||||
import eu.jonahbauer.json.JsonBoolean;
|
||||
import eu.jonahbauer.json.JsonNumber;
|
||||
import eu.jonahbauer.json.JsonString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public sealed interface JsonToken permits JsonBoolean, JsonNull, JsonNumber, JsonString, JsonPlaceholder, JsonPunctuation, JsonStringTemplate {
|
||||
|
||||
@NotNull JsonTokenKind getKind();
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package eu.jonahbauer.json.token;
|
||||
|
||||
import eu.jonahbauer.json.token.impl.ComplexTokenKind;
|
||||
import eu.jonahbauer.json.JsonBoolean;
|
||||
|
||||
public sealed interface JsonTokenKind permits JsonBoolean, JsonNull, JsonPunctuation, ComplexTokenKind {
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package eu.jonahbauer.json.token.impl;
|
||||
|
||||
import eu.jonahbauer.json.token.JsonTokenKind;
|
||||
|
||||
public enum ComplexTokenKind implements JsonTokenKind {
|
||||
STRING, NUMBER, STRING_TEMPLATE, PLACEHOLDER
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
module eu.jonahbauer.json {
|
||||
requires static lombok;
|
||||
requires static org.jetbrains.annotations;
|
||||
|
||||
exports eu.jonahbauer.json;
|
||||
exports eu.jonahbauer.json.parser;
|
||||
exports eu.jonahbauer.json.token;
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package eu.jonahbauer.json.parser;
|
||||
|
||||
import eu.jonahbauer.json.JsonProcessingException;
|
||||
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.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class JsonParserTest {
|
||||
|
||||
@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(JsonParserTest.class.getResourceAsStream(path));
|
||||
var reader = new InputStreamReader(in)
|
||||
) {
|
||||
var parser = new JsonParser(reader);
|
||||
|
||||
if (expected == Boolean.FALSE) {
|
||||
assertThrows(JsonProcessingException.class, parser::parse).printStackTrace(System.out);
|
||||
} else if (expected == Boolean.TRUE) {
|
||||
assertDoesNotThrow(parser::parse);
|
||||
} else {
|
||||
try {
|
||||
parser.parse();
|
||||
System.out.println("accepted");
|
||||
} catch (JsonProcessingException ex) {
|
||||
System.out.println("rejected");
|
||||
ex.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Stream<Arguments> parameters() throws IOException {
|
||||
List<Arguments> filenames = new ArrayList<>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
package eu.jonahbauer.json.parser;
|
||||
|
||||
import eu.jonahbauer.json.JsonObject;
|
||||
import eu.jonahbauer.json.JsonProcessingException;
|
||||
import eu.jonahbauer.json.JsonValue;
|
||||
import org.example.json.*;
|
||||
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 org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class JsonProcessorTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("StringOperationCanBeSimplified")
|
||||
void processWithoutPlaceholders() {
|
||||
JsonValue value = JsonProcessor.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
|
||||
@SuppressWarnings("TrailingWhitespacesInTextBlock")
|
||||
void processWithValuePlaceholder() {
|
||||
JsonValue value = JsonProcessor.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 = JsonProcessor.JSON."""
|
||||
{
|
||||
\{key}: "value"
|
||||
}
|
||||
""";
|
||||
|
||||
assertInstanceOf(JsonObject.class, value);
|
||||
JsonObject object = (JsonObject) value;
|
||||
|
||||
assertEquals(JsonValue.valueOf("value"), object.get(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
void processWithPlaceholderInInvalidPosition() {
|
||||
assertThrows(JsonProcessingException.class, () -> {
|
||||
var _ = JsonProcessor.JSON."{\"key\" \{"value"}}";
|
||||
}).printStackTrace(System.out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void processWithPlaceholderInStringLiteral() {
|
||||
Object object = new Object();
|
||||
JsonValue value = JsonProcessor.JSON."{\"key\": \"Hello \{object}!\"}";
|
||||
assertEquals(JsonValue.valueOf(Map.of(
|
||||
"key", "Hello " + object + "!"
|
||||
)), value);
|
||||
}
|
||||
|
||||
@Test
|
||||
void processWithPlaceholderInKeyStringLiteral() {
|
||||
Object object = new Object();
|
||||
JsonValue value = JsonProcessor.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(JsonProcessorTest.class.getResourceAsStream(path))) {
|
||||
String input = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
if (expected == Boolean.FALSE) {
|
||||
assertThrows(JsonProcessingException.class, () -> JsonProcessor.JSON.process(StringTemplate.of(input))).printStackTrace(System.out);
|
||||
} else if (expected == Boolean.TRUE) {
|
||||
Assertions.assertDoesNotThrow(() -> JsonProcessor.JSON.process(StringTemplate.of(input)));
|
||||
} else {
|
||||
try {
|
||||
JsonProcessor.JSON.process(StringTemplate.of(input));
|
||||
System.out.println("accepted");
|
||||
} catch (JsonProcessingException ex) {
|
||||
System.out.println("rejected");
|
||||
ex.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Stream<Arguments> parameters() throws IOException {
|
||||
List<Arguments> filenames = new ArrayList<>();
|
||||
|
||||
try (
|
||||
InputStream in = Objects.requireNonNull(JsonProcessorTest.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();
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package eu.jonahbauer.json.parser.tokenizer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static java.lang.StringTemplate.RAW;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class JsonTokenizerImpl$TemplateReaderTest {
|
||||
|
||||
@Test
|
||||
void readString() throws IOException {
|
||||
var tokenizer = new TemplateReader("Hello World!");
|
||||
assertEquals('H', tokenizer.read());
|
||||
assertEquals('e', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('o', tokenizer.read());
|
||||
assertEquals(' ', tokenizer.read());
|
||||
assertEquals('W', tokenizer.read());
|
||||
assertEquals('o', tokenizer.read());
|
||||
assertEquals('r', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('d', tokenizer.read());
|
||||
assertEquals('!', tokenizer.read());
|
||||
assertEquals(-1, tokenizer.read());
|
||||
assertEquals(-1, tokenizer.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readStringTemplate() throws IOException {
|
||||
String name = "World";
|
||||
var tokenizer = new TemplateReader(RAW."Hello \{name}!");
|
||||
assertEquals('H', tokenizer.read());
|
||||
assertEquals('e', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('o', tokenizer.read());
|
||||
assertEquals(' ', tokenizer.read());
|
||||
assertEquals(-2, tokenizer.read());
|
||||
assertEquals(name, tokenizer.getObject());
|
||||
assertEquals('!', tokenizer.read());
|
||||
assertThrows(IllegalStateException.class, tokenizer::getObject);
|
||||
assertEquals(-1, tokenizer.read());
|
||||
assertEquals(-1, tokenizer.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushbackString() throws IOException {
|
||||
var tokenizer = new TemplateReader("Hello World!");
|
||||
assertEquals('H', tokenizer.read());
|
||||
assertEquals('e', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('o', tokenizer.read());
|
||||
assertEquals(' ', tokenizer.read());
|
||||
tokenizer.pushback();
|
||||
assertEquals(' ', tokenizer.read());
|
||||
assertEquals('W', tokenizer.read());
|
||||
assertEquals('o', tokenizer.read());
|
||||
assertEquals('r', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
tokenizer.pushback();
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('d', tokenizer.read());
|
||||
assertEquals('!', tokenizer.read());
|
||||
assertEquals(-1, tokenizer.read());
|
||||
assertEquals(-1, tokenizer.read());
|
||||
tokenizer.pushback();
|
||||
assertEquals(-1, tokenizer.read());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushbackPlaceholder() throws IOException {
|
||||
String name = "World";
|
||||
var tokenizer = new TemplateReader(RAW."Hello \{name}!");
|
||||
assertEquals('H', tokenizer.read());
|
||||
assertEquals('e', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('l', tokenizer.read());
|
||||
assertEquals('o', tokenizer.read());
|
||||
assertEquals(' ', tokenizer.read());
|
||||
assertEquals(-2, tokenizer.read());
|
||||
assertEquals(name, tokenizer.getObject());
|
||||
tokenizer.pushback();
|
||||
assertEquals(-2, tokenizer.read());
|
||||
assertEquals(name, tokenizer.getObject());
|
||||
assertEquals('!', tokenizer.read());
|
||||
assertThrows(IllegalStateException.class, tokenizer::getObject);
|
||||
assertEquals(-1, tokenizer.read());
|
||||
assertEquals(-1, tokenizer.read());
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
package eu.jonahbauer.json.parser.tokenizer;
|
||||
|
||||
import eu.jonahbauer.json.token.JsonPunctuation;
|
||||
import eu.jonahbauer.json.token.JsonStringTemplate;
|
||||
import eu.jonahbauer.json.JsonBoolean;
|
||||
import eu.jonahbauer.json.token.JsonNull;
|
||||
import eu.jonahbauer.json.JsonNumber;
|
||||
import eu.jonahbauer.json.JsonString;
|
||||
import eu.jonahbauer.json.parser.JsonTokenizerException;
|
||||
import eu.jonahbauer.json.token.JsonToken;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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.assertThrows;
|
||||
|
||||
class JsonTokenizerImplTest {
|
||||
|
||||
@Test
|
||||
void simpleTokens() {
|
||||
test("{[]},:", JsonPunctuation.BEGIN_OBJECT, JsonPunctuation.BEGIN_ARRAY, JsonPunctuation.END_ARRAY, JsonPunctuation.END_OBJECT, JsonPunctuation.VALUE_SEPARATOR, JsonPunctuation.NAME_SEPARATOR);
|
||||
}
|
||||
|
||||
@Test
|
||||
void literals() {
|
||||
test("true", JsonBoolean.TRUE);
|
||||
test("false", JsonBoolean.FALSE);
|
||||
test("null", JsonNull.NULL);
|
||||
test("null true false", JsonNull.NULL, JsonBoolean.TRUE, JsonBoolean.FALSE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void misspelledLiterals() {
|
||||
assertThrows(JsonTokenizerException.class, () -> test("tru"));
|
||||
assertThrows(JsonTokenizerException.class, () -> test("fal"));
|
||||
assertThrows(JsonTokenizerException.class, () -> test("nu"));
|
||||
assertThrows(JsonTokenizerException.class, () -> test("TRUE"));
|
||||
assertThrows(JsonTokenizerException.class, () -> test("FALSE"));
|
||||
assertThrows(JsonTokenizerException.class, () -> test("NULL"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringWithoutEscapes() {
|
||||
test("\"foobar\"\"baz\"", new JsonString("foobar"), new JsonString("baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringWithSimpleEscapes() {
|
||||
test("\"\\b\\t\\f\\r\\n\\/\\\\\\\"\"", new JsonString("\b\t\f\r\n/\\\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringWithUnicodeEscapes() {
|
||||
test("\"\\u0041\\u0042\\u0043\"", new JsonString("ABC"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringWithInvalidEscapeSequence() {
|
||||
assertThrows(JsonTokenizerException.class, () -> test("\"\\x\""));
|
||||
assertThrows(JsonTokenizerException.class, () -> test("\"\\uxxxx\""));
|
||||
assertThrows(JsonTokenizerException.class, () -> test("\"\\uaa\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringWithControlCharacters() {
|
||||
assertThrows(JsonTokenizerException.class, () -> test("\"\u0010\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringWithoutTrailingQuotes() {
|
||||
assertThrows(JsonTokenizerException.class, () -> test("\"hello world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void number() {
|
||||
test("-0", new JsonNumber(-0.0));
|
||||
test("0", new JsonNumber(0.0));
|
||||
test("123", new JsonNumber(123.0));
|
||||
test("123.456", new JsonNumber(123.456));
|
||||
test("-123", new JsonNumber(-123.0));
|
||||
test("-123.456", new JsonNumber(-123.456));
|
||||
test("1E10", new JsonNumber(1E10));
|
||||
test("1E+10", new JsonNumber(1E+10));
|
||||
test("1E-10", new JsonNumber(1E-10));
|
||||
test("123.456E10", new JsonNumber(123.456E10));
|
||||
test("123.456E+10", new JsonNumber(123.456E+10));
|
||||
test("123.456E-10", new JsonNumber(123.456E-10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberWithLeadingZero() {
|
||||
assertThrows(JsonTokenizerException.class, () -> test("00"));
|
||||
assertThrows(JsonTokenizerException.class, () -> test("-00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberWithEmptyIntegralPart() {
|
||||
assertThrows(JsonTokenizerException.class, () -> test(".0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberWithEmptyFractionPart() {
|
||||
assertThrows(JsonTokenizerException.class, () -> test("0."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberWithEmptyExponentPart() {
|
||||
assertThrows(JsonTokenizerException.class, () -> test("0.0E"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void numberWithGrouping() {
|
||||
assertThrows(JsonTokenizerException.class, () -> test("1_234"));
|
||||
test("1,234", new JsonNumber(1), JsonPunctuation.VALUE_SEPARATOR, new JsonNumber(234));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringTemplate() {
|
||||
var name = "World";
|
||||
test(RAW."\"Hello, \{name}!\"", new JsonStringTemplate(RAW."Hello, \{name}!"));
|
||||
}
|
||||
|
||||
private void test(@NotNull String json, @NotNull JsonToken @NotNull... expected) {
|
||||
var tokenizer = new JsonTokenizerImpl(json);
|
||||
var actual = tokenizer.stream().toList();
|
||||
assertEquals(List.of(expected), actual);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
[123.456e-789]
|
@ -0,0 +1 @@
|
||||
[0.4e00669999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999969999999006]
|
@ -0,0 +1 @@
|
||||
[-1e+9999]
|
@ -0,0 +1 @@
|
||||
[1.5e+9999]
|
@ -0,0 +1 @@
|
||||
[-123123e100000]
|
@ -0,0 +1 @@
|
||||
[123123e100000]
|
@ -0,0 +1 @@
|
||||
[123e-10000000]
|
@ -0,0 +1 @@
|
||||
[-123123123123123123123123123123]
|
@ -0,0 +1 @@
|
||||
[100000000000000000000]
|
@ -0,0 +1 @@
|
||||
[-237462374673276894279832749832423479823246327846]
|
@ -0,0 +1 @@
|
||||
{"\uDFAA":0}
|
@ -0,0 +1 @@
|
||||
["\uDADA"]
|
@ -0,0 +1 @@
|
||||
["\uD888\u1234"]
|
Binary file not shown.
@ -0,0 +1 @@
|
||||
["譌・ム淫"]
|
@ -0,0 +1 @@
|
||||
["\uD800\n"]
|
@ -0,0 +1 @@
|
||||
["\uDd1ea"]
|
@ -0,0 +1 @@
|
||||
["\uD800\uD800\n"]
|
@ -0,0 +1 @@
|
||||
["\ud800"]
|
@ -0,0 +1 @@
|
||||
["\ud800abc"]
|
@ -0,0 +1 @@
|
||||
["\uDd1e\uD834"]
|
@ -0,0 +1 @@
|
||||
["И"]
|
@ -0,0 +1 @@
|
||||
["\uDFAA"]
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
||||
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
|
@ -0,0 +1 @@
|
||||
[1 true]
|
@ -0,0 +1 @@
|
||||
[a蘊
|
@ -0,0 +1 @@
|
||||
["": 1]
|
@ -0,0 +1 @@
|
||||
[""],
|
@ -0,0 +1 @@
|
||||
[,1]
|
@ -0,0 +1 @@
|
||||
[1,,2]
|
@ -0,0 +1 @@
|
||||
["x",,]
|
@ -0,0 +1 @@
|
||||
["x"]]
|
@ -0,0 +1 @@
|
||||
["",]
|
@ -0,0 +1 @@
|
||||
["x"
|
@ -0,0 +1 @@
|
||||
[x
|
@ -0,0 +1 @@
|
||||
[3[4]]
|
@ -0,0 +1 @@
|
||||
[1:2]
|
@ -0,0 +1 @@
|
||||
[,]
|
@ -0,0 +1 @@
|
||||
[-]
|
@ -0,0 +1 @@
|
||||
[ , ""]
|
@ -0,0 +1,3 @@
|
||||
["a",
|
||||
4
|
||||
,1,
|
@ -0,0 +1 @@
|
||||
[1,]
|
@ -0,0 +1 @@
|
||||
[1,,]
|
@ -0,0 +1 @@
|
||||
[*]
|
@ -0,0 +1 @@
|
||||
[""
|
@ -0,0 +1 @@
|
||||
[1,
|
@ -0,0 +1,3 @@
|
||||
[1,
|
||||
1
|
||||
,1
|
@ -0,0 +1 @@
|
||||
[{}
|
@ -0,0 +1 @@
|
||||
[fals]
|
@ -0,0 +1 @@
|
||||
[nul]
|
@ -0,0 +1 @@
|
||||
[tru]
|
Binary file not shown.
@ -0,0 +1 @@
|
||||
[++1234]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue