diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..ae54d75 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,25 @@ +name: Java CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Build with Gradle + run: ./gradlew build diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml deleted file mode 100644 index b068918..0000000 --- a/.github/workflows/maven.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Java CI with Maven - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt' - - name: Build with Maven - run: mvn -B package --file pom.xml diff --git a/.gitignore b/.gitignore index 36c7b05..f2bb7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,26 @@ + +### Intellij ### +.idea/ +out/ +# File-based project format +*.iws +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# mpeltonen/sbt-idea plugin +.idea_modules/ + + +# Created by https://www.toptal.com/developers/gitignore/api/windows,macos,linux,visualstudiocode,gradle,kotlin +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,linux,visualstudiocode,gradle,kotlin + +### Kotlin ### # Compiled class file *.class @@ -21,11 +44,119 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -/bin/ -# eclipse -.settings/ -.classpath +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +!.vscode/*.code-snippets + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Gradle ### +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +# Eclipse Gradle plugin generated files +# Eclipse Core .project +# JDT-specific (Eclipse Java Development Tools) +.classpath -target/ \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,intellij,visualstudiocode,gradle,kotlin diff --git a/LICENSE b/LICENSE index e38e6fb..0193993 100644 --- a/LICENSE +++ b/LICENSE @@ -14,7 +14,7 @@ copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/README.md b/README.md index b24d938..da10cad 100644 --- a/README.md +++ b/README.md @@ -1,181 +1,117 @@ -# json5 [![javadoc](https://img.shields.io/endpoint?label=javadoc&url=https%3A%2F%2Fjavadoc.syntaxerror.at%2Fjson5%2F%3Fbadge%3Dtrue%26version%3Dlatest)](https://javadoc.syntaxerror.at/json5/latest) ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/Synt4xErr0r4/json5/Java%20CI%20with%20Maven) +# WORK IN PROGRESS -A JSON5 Library for Java (11+) +I'm hacking around with this at the moment - almost nothing is tested. -## Overview - -The [JSON5 Standard](https://json5.org/) tries to make JSON more human-readable - -This is a reference implementation, capable of parsing JSON5 data according to the [specification](https://spec.json5.org/). - -## Getting started - -In order to use the code, you can either [download the jar](https://github.com/Synt4xErr0r4/json5/releases/download/1.2.0/json5-1.2.0.jar), or use the Maven dependency: -```xml - +The intention is to integrate with Kotlinx Serialization. - - syntaxerror.at - https://maven.syntaxerror.at - +# json5 Kotlin - +A JSON5 Library for Kotlin 1.6, Java 11 - - at.syntaxerror - json5 - 1.2.0 - -``` +## Overview -The library itself is located in the module `json5`. +The [JSON5 Standard](https://json5.org/) tries to make JSON more human-readable -## Usage +This is a reference implementation, capable of parsing JSON5 data according to +the [specification](https://spec.json5.org/). -### Deserializing (Parsing) +## Getting started -To parse a JSON object (`{ ... }`), all you need to do is: -```java -import at.syntaxerror.json5.JSONObject; +Gradle (Kotlin): -//... +```kotlin +repositories { + maven("https://jitpack.io") +} -JSONObject jsonObject = new JSONObject("{ ... }"); +dependencies { + implementation("adamko-dev:json5-kotlin:${version}") +} ``` -Or if you want to read directly from a `Reader` or `InputStream`: -```java -import java.io.InputStream; -import at.syntaxerror.json5.JSONObject; +Maven: -//... - -try(InputStream stream = ...) { - JSONObject jsonObject = new JSONObject(new JSONParser(stream)); - // ... -} catch (Exception e) { - //... -} +```xml + + + jitpack.io + https://jitpack.io + + ``` -Just replace `JSONObject` with `JSONArray` to read list-like data (`[ ... ]`). +```xml + + + adamko-dev + json5-kotlin + ${json5-kotlin.version} + + +``` -### Serializing (Stringifying) +### Usage -Both the `JSONObject` and `JSONArray` class contain two methods for serialization: -- `toString()` and -- `toString(int indentFactor)` +```kotlin +import at.syntaxerror.json5.Json5Module +import kotlinx.serialization.json.JsonObject -The normal `toString()` method will return the compact string representation, without any optional whitespaces. -The `indentFactor` of the `toString(int indentFactor)` method will enable pretty-printing if `> 0`. -Any value `< 1` will disable pretty-printing. The indent factor indicates the number of spaces before each key-value pair/ value: +// create and configure the Json5Module +val j5 = Json5Module { + allowInfinity = true + indentFactor = 4u +} -`indentFactor = 2` -```json +val json5 = """ + { + // comments + unquoted: 'and you can quote me on that', + singleQuotes: 'I can use "double quotes" here', + lineBreaks: "Look, Mom! \ + No \\n's!", + hexadecimal: 0xdecaf, + leadingDecimalPoint: .8675309, + andTrailing: 8675309., + positiveSign: +1, + trailingComma: 'in objects', + andIn: [ + 'arrays', + ], + "backwardsCompatible": "with JSON", + } + """.trimIndent() + +// Parse a JSON5 String to a Kotlinx Serialization JsonObject +val jsonObject: JsonObject = j5.decodeObject(json5) + +// encode the JsonObject to a Json5 String +val jsonString = j5.encodeToString(jsonObject) + +println(jsonString) +/* { - "key0": "value0", - "key1": { - "nested": 123 - }, - "key2": false + "unquoted": "and you can quote me on that", + "singleQuotes": "I can use \"double quotes\" here", + "lineBreaks": "Look, Mom! No \\n's!", + "hexadecimal": 912559, + "leadingDecimalPoint": 0.8675309, + "andTrailing": 8675309.0, + "positiveSign": 1, + "trailingComma": "in objects", + "andIn": [ + "arrays" + ], + "backwardsCompatible": "with JSON" } - -[ - "value", - { - "nested": 123 - }, - false -] +*/ ``` -`indentFactor = 0` -```json -{"key0":"value0","key1":{"nested":123},"key2":false} - -["value",{"nested":123},false] -``` - -Calling `json.toString(indentFactor)` is the same as `JSONStringify.toString(json, indentFactor)`. - -### Working with JSONObjects and JSONArrays - -The `getXXX` methods are used to read values from the JSON object/ array. -The `set` methods are used to override or set values in the JSON object/ array. -The `add` and `insert` methods are used to add values to a JSON array. - -Supported data types are: -- `boolean` -- `byte` -- `short` -- `int` -- `float` -- `double` -- `Number` (any sub-class) -- `String` -- `JSONObject` -- `JSONArray` -- `Instants` (since `1.1.0`, see below) - -The normal `getXXX(String key)` and `getXXX(int index)` methods will throw an exception if the specified key or index does not exist, but the -`getXXX(String key, XXX defaults)` and `getXXX(int index, XXX defaults)` methods will return the default value (parameter `defaults`) instead. - -The `set(int index, Object value)` method will also throw an exception if the index does not exist. You can use `add(Object value)` instead to append a value to the list. - -The getter-methods for numbers always return a rounded or truncated result. -If the actual number is too large to fit into the requested type, the upper bits are truncated (e.g. `int` to `byte` truncates the upper 24 bits). -If the actual number is a decimal number (e.g. `123.456`), and the requested type is not (e.g. `long`), the decimal places are discarded. -To check if a number can fit into a type, you can use the `getXXXExact` methods, which will throw an exception if the conversion is not possible without altering the result. - -Numbers are internally always stored as either a `java.math.BigInteger`, `java.math.BigDecimal`, or `double` (`double` is used for `Infinity` and `NaN` only). Therefore, any method -returning raw `java.lang.Object`s will return numbers as one of those types. The same behaviour applies to the `getNumber` methods. - -## Changelog -### v1.1.0 - -Instants (date and time) are now supported. This option can be toggled via the options listed below. - -The `JSONOptions` class allows you to customize the behaviour of the parser and stringifyer. It can be created via the builder subclass. -You can also set the default options used if the supplied options are `null`, by using the method `setDefaultOptions(JSONOptions)`. The default options must not be `null`. - -The following options are currently implemented: - -- `parseInstants`: (default `true`, *Parser-only*) ([proposed here](https://github.com/json5/json5-spec/issues/4)) - Whether or not instants should be parsed as such. - If this is `false`, `parseStringInstants` and `parseUnixInstants` are ignored -- `parseStringInstants`: (default `true`, *Parser-only*) ([proposed here](https://github.com/json5/json5-spec/issues/4)) - Whether or not string instants (according to [RFC 3339, Section 5.6](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6)) should be parsed as such. - Ignored if `parseInstants` is `false` -- `parseUnixInstants`: (default `true`, *Parser-only*) ([proposed here](https://github.com/json5/json5-spec/issues/4)) - Whether or not unix instants (integers) should be parsed as such. - Ignored if `parseInstants` is `false` -- `stringifyUnixInstants`: (default `false`, *Stringify-only*) ([proposed here](https://github.com/json5/json5-spec/issues/4)) - Whether or not instants should be stringifyed as unix timestamps (integers). - If this is `false`, instants will be stringifyed as strings (according to [RFC 3339, Section 5.6](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6)) -- `allowNaN`: (default `true`, *Parser-only*) ([proposed here](https://github.com/json5/json5-spec/issues/24)) - Whether or not `NaN` should be allowed as a number -- `allowInfinity`: (default `true`, *Parser-only*) ([proposed here](https://github.com/json5/json5-spec/issues/24)) - Whether or not `Infinity` should be allowed as a number. This applies to both `+Infinity` and `-Infinity` -- `allowInvalidSurrogates`: (default `true`, *Parser-only*) ([proposed here](https://github.com/json5/json5-spec/issues/12)) - Whether or not invalid unicode surrogate pairs should be allowed -- `quoteSingle`: (default `false`, *Stringify-only*) - Whether or not string should be single-quoted (`'`) instead of double-quoted (`"`). This also includes a JSONObject's member names - -### v1.2.0 - -- added `clear()` method - removes all values from an object/array -- added `remove(String key)` and `remove(int index)` methods - remove a certain key/index from an object/array - -## Documentation - -The JavaDoc for the latest version can be found [here](https://javadoc.syntaxerror.at/json5/latest). - ## Credits -This project is partly based on stleary's [JSON-Java](https://github.com/stleary/JSON-java) library. +This project is entirely based on [Synt4xErr0r4/json5](https://github.com/Synt4xErr0r4/json5/), +which was partly based on stleary's [JSON-Java](https://github.com/stleary/JSON-java) library. ## License -This project is licensed under the [MIT License](https://github.com/Synt4xErr0r4/json5/blob/main/LICENSE) +This project is licensed under +the [MIT License](https://github.com/Synt4xErr0r4/json5/blob/main/LICENSE) diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..055b1b1 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,92 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + val kotlinVersion = "1.6.10" + kotlin("jvm") version kotlinVersion + jacoco +} + +dependencies { + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + + val kotlinxSerializationVersion = "1.3.1" + implementation(project.dependencies.enforcedPlatform("org.jetbrains.kotlinx:kotlinx-serialization-bom:$kotlinxSerializationVersion")) + api("org.jetbrains.kotlinx:kotlinx-serialization-core") + api("org.jetbrains.kotlinx:kotlinx-serialization-json") + + val junitVersion = "5.8.2" + testImplementation(enforcedPlatform("org.junit:junit-bom:$junitVersion")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") { + because("Only needed to run tests in a version of IntelliJ IDEA that bundles older versions") + } + + val kotestVersion = "5.0.2" + testImplementation(enforcedPlatform("io.kotest:kotest-bom:$kotestVersion")) + testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") + testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") + testImplementation("io.kotest:kotest-property:$kotestVersion") + testImplementation("io.kotest:kotest-assertions-json:$kotestVersion") + + testImplementation("io.mockk:mockk:1.12.1") +} + +group = "at.syntaxerror" +version = "2.0.0" +description = "JSON5 for Kotlin" + +java { + withSourcesJar() +} + +kotlin { + jvmToolchain { + (this as JavaToolchainSpec).languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +tasks.withType().configureEach { + + kotlinOptions { + jvmTarget = "11" + apiVersion = "1.6" + languageVersion = "1.6" + } + + kotlinOptions.freeCompilerArgs += listOf( + "-Xopt-in=kotlin.RequiresOptIn", + "-Xopt-in=kotlin.ExperimentalStdlibApi", + "-Xopt-in=kotlin.time.ExperimentalTime", + "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi", + ) +} + +tasks.withType { + useJUnitPlatform() + // report is always generated after tests run + finalizedBy(tasks.withType()) +} + +jacoco { + toolVersion = "0.8.7" +} + +tasks.withType { + dependsOn(tasks.withType()) + + reports { + xml.required.set(true) + html.required.set(true) + csv.required.set(false) + } + + doLast { + val htmlReportLocation = reports.html.outputLocation.locationOnly + .map { it.asFile.resolve("index.html").invariantSeparatorsPath } + + logger.lifecycle("Jacoco report for ${project.name}: ${htmlReportLocation.get()}") + } +} +tasks.withType { + options.encoding = "UTF-8" +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..84d1f85 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..c53aefa --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/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/master/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 + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# 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"' + +# 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 + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + 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 + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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=. +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%" == "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%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 0000000..a5396c1 --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,5 @@ +# https://jitpack.io/docs/BUILDING/ + +# https://jitpack.io/docs/BUILDING/#java-version +jdk: + - openjdk11 diff --git a/pom.xml b/pom.xml deleted file mode 100755 index f764f36..0000000 --- a/pom.xml +++ /dev/null @@ -1,126 +0,0 @@ - - 4.0.0 - at.syntaxerror - json5 - 1.2.0 - jar - - JSON5 for Java - A JSON5 Library for Java - - - - The MIT License - https://github.com/Synt4xErr0r4/json5/blob/main/LICENSE - repo - - - - - - syntaxerror404 - SyntaxError404 - thomas@syntaxerror.at - https://syntaxerror.at - Europe/Vienna - - - - - scm:git:git://github.com/Synt4xErr0r4/json5.git - http://github.com/Synt4xErr0r4/json5 - - - - GitHub Issues - https://github.com/Synt4xErr0r4/json5/issues - - - - src/main/java - - - - org.projectlombok - lombok-maven-plugin - 1.18.20.0 - - UTF-8 - src/main/java - target/delombok - - - - maven-compiler-plugin - 3.8.1 - - target/delombok - 11 - - - - org.projectlombok - lombok - 1.18.20 - - - - - - maven-javadoc-plugin - 3.3.0 - - - - aggregate-jar - - - - - target/delombok - - - - maven-source-plugin - 3.2.0 - - - - jar - - - - - - - - org.apache.maven.wagon - wagon-ftp - 3.2.0 - - - - - - - org.junit.jupiter - junit-jupiter-api - 5.7.2 - test - - - - org.projectlombok - lombok - 1.18.20 - provided - - - - - - syntaxerror.at - ftp://syntaxerror404.lima-ftp.de/ - - - \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..3874f21 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,21 @@ +rootProject.name = "json5-kotlin" + +@Suppress("UnstableApiUsage") +dependencyResolutionManagement { + + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + + repositories { + mavenCentral() + gradlePluginPortal() + maven("https://jitpack.io") + } + + pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + maven("https://jitpack.io") + } + } +} diff --git a/src/main/java/at/syntaxerror/json5/JSONArray.java b/src/main/java/at/syntaxerror/json5/JSONArray.java deleted file mode 100755 index a89ec6d..0000000 --- a/src/main/java/at/syntaxerror/json5/JSONArray.java +++ /dev/null @@ -1,855 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2021 SyntaxError404 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package at.syntaxerror.json5; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.function.Function; -import java.util.function.Predicate; - -/** - * A JSONArray is an array structure capable of holding multiple values, - * including other JSONArrays and {@link JSONObject JSONObjects} - * - * @author SyntaxError404 - * - */ -public class JSONArray implements Iterable { - - private List values; - - /** - * Constructs a new JSONArray - */ - public JSONArray() { - values = new ArrayList<>(); - } - - /** - * Constructs a new JSONArray from a string - * - * @param source a string - */ - public JSONArray(String source) { - this(new JSONParser(source)); - } - - /** - * Constructs a new JSONArray from a JSONParser - * - * @param parser a JSONParser - */ - public JSONArray(JSONParser parser) { - this(); - - char c; - - if(parser.nextClean() != '[') - throw parser.syntaxError("A JSONArray must begin with '['"); - - while(true) { - c = parser.nextClean(); - - switch(c) { - case 0: - throw parser.syntaxError("A JSONArray must end with ']'"); - case ']': - return; - default: - parser.back(); - } - - Object value = parser.nextValue(); - - values.add((Object) value); - - c = parser.nextClean(); - - if(c == ']') - return; - - if(c != ',') - throw parser.syntaxError("Expected ',' or ']' after value, got '" + c + "' instead"); - } - } - - // - - /** - * Converts the JSONArray into a list. All JSONObjects and JSONArrays - * contained within this JSONArray will be converted into their - * Map or List form as well - * - * @return a list of the values of this array - */ - public List toList() { - List list = new ArrayList<>(); - - for(Object value : this) { - if(value instanceof JSONObject) - value = ((JSONObject) value).toMap(); - - else if(value instanceof JSONArray) - value = ((JSONArray) value).toList(); - - list.add((Object) value); - } - - return list; - } - - /** - * Returns a collection of values of the JSONArray. - * Modifying the collection will modify the JSONArray - * - * Use with caution. - * - * @return a set of entries - */ - public Collection entrySet() { - return values; - } - - @Override - public Iterator iterator() { - return values.iterator(); - } - - /** - * Returns the number of values in the JSONArray - * - * @return the number of values - */ - public int length() { - return values.size(); - } - - /** - * Removes all values from this JSONArray - * - * @since 1.2.0 - */ - public void clear() { - values.clear(); - } - - /** - * Removes the value at an index from a JSONArray - * - * @param index the index to be removed - * @since 1.2.0 - * - * @throws JSONException if the index does not exist - */ - public void remove(int index) { - checkIndex(index); - values.remove(index); - } - - // -- CHECK -- - - /** - * Checks if the value with the specified index is {@code null} - * - * @param index the index - * @return whether or not the value is {@code null} - * - * @throws JSONException if the index does not exist - */ - public boolean isNull(int index) { - return checkIndex(index) == null; - } - - /** - * Checks if the value with the specified index is a boolean - * - * @param index the index - * @return whether or not the value is a boolean - * - * @throws JSONException if the index does not exist - */ - public boolean isBoolean(int index) { - return checkIndex(index) instanceof Boolean; - } - - /** - * Checks if the value with the specified index is a string - * - * @param index the index - * @return whether or not the value is a string - * - * @throws JSONException if the index does not exist - */ - public boolean isString(int index) { - Object value = checkIndex(index); - return value instanceof String || value instanceof Instant; - } - - /** - * Checks if the value with the specified index is a number - * - * @param index the index - * @return whether or not the value is a number - * - * @throws JSONException if the index does not exist - */ - public boolean isNumber(int index) { - Object value = checkIndex(index); - return value instanceof Number || value instanceof Instant; - } - - /** - * Checks if the value with the specified index is a JSONObject - * - * @param index the index - * @return whether or not the value is a JSONObject - * - * @throws JSONException if the index does not exist - */ - public boolean isObject(int index) { - return checkIndex(index) instanceof JSONObject; - } - - /** - * Checks if the value with the specified index is a JSONArray - * - * @param index the index - * @return whether or not the value is a JSONArray - * - * @throws JSONException if the index does not exist - */ - public boolean isArray(int index) { - return checkIndex(index) instanceof JSONArray; - } - - /** - * Checks if the value with the specified index is an Instant - * - * @param index the index - * @return whether or not the value is an Instant - * @since 1.1.0 - * - * @throws JSONException if the index does not exist - */ - public boolean isInstant(int index) { - return checkIndex(index) instanceof Instant; - } - - // -- GET -- - - /** - * Returns the value for a given index - * - * @param index the index - * @return the value - * - * @throws JSONException if the index does not exist - */ - public Object get(int index) { - checkIndex(index); - return values.get(index); - } - - /** - * Returns the value as a boolean for a given index - * - * @param index the index - * @return the boolean - * - * @throws JSONException if the index does not exist, or if the value is not a boolean - */ - public boolean getBoolean(int index) { - return checkType(this::isBoolean, index, "boolean"); - } - - /** - * Returns the value as a string for a given index - * - * @param index the index - * @return the string - * - * @throws JSONException if the index does not exist, or if the value is not a string - */ - public String getString(int index) { - return checkType(this::isString, index, "string"); - } - - /** - * Returns the value as a number for a given index - * - * @param index the index - * @return the number - * - * @throws JSONException if the index does not exist, or if the value is not a number - */ - public Number getNumber(int index) { - return checkType(this::isNumber, index, "number"); - } - - /** - * Returns the value as a byte for a given index - * - * @param index the index - * @return the byte - * - * @throws JSONException if the index does not exist, or if the value is not a byte - */ - public byte getByte(int index) { - return getNumber(index).byteValue(); - } - /** - * Returns the value as a short for a given index - * - * @param index the index - * @return the short - * - * @throws JSONException if the index does not exist, or if the value is not a short - */ - public short getShort(int index) { - return getNumber(index).shortValue(); - } - /** - * Returns the value as an int for a given index - * - * @param index the index - * @return the int - * - * @throws JSONException if the index does not exist, or if the value is not an int - */ - public int getInt(int index) { - return getNumber(index).intValue(); - } - /** - * Returns the value as a long for a given index - * - * @param index the index - * @return the long - * - * @throws JSONException if the index does not exist, or if the value is not a long - */ - public long getLong(int index) { - return getNumber(index).longValue(); - } - - /** - * Returns the value as a float for a given index - * - * @param index the index - * @return the float - * - * @throws JSONException if the index does not exist, or if the value is not a float - */ - public float getFloat(int index) { - return getNumber(index).floatValue(); - } - /** - * Returns the value as a double for a given index - * - * @param index the index - * @return the double - * - * @throws JSONException if the index does not exist, or if the value is not a double - */ - public double getDouble(int index) { - return getNumber(index).doubleValue(); - } - - private T getNumberExact(int index, String type, Function bigint, Function bigdec) { - Number number = getNumber(index); - - try { - - if(number instanceof BigInteger) - return bigint.apply((BigInteger) number); - - if(number instanceof BigDecimal) - return bigdec.apply((BigDecimal) number); - - } catch (Exception e) { } - - throw mismatch(index, type); - } - - /** - * Returns the exact value as a byte for a given index. - * This fails if the value does not fit into a byte - * - * @param index the index - * @return the byte - * - * @throws JSONException if the index does not exist, the value is not a byte, or if the value does not fit into a byte - */ - public byte getByteExact(int index) { - return getNumberExact(index, "byte", BigInteger::byteValueExact, BigDecimal::byteValueExact); - } - /** - * Returns the exact value as a short for a given index. - * This fails if the value does not fit into a short - * - * @param index the index - * @return the short - * - * @throws JSONException if the index does not exist, the value is not a short, or if the value does not fit into a short - */ - public short getShortExact(int index) { - return getNumberExact(index, "short", BigInteger::shortValueExact, BigDecimal::shortValueExact); - } - /** - * Returns the exact value as an int for a given index. - * This fails if the value does not fit into an int - * - * @param index the index - * @return the int - * - * @throws JSONException if the index does not exist, the value is not an int, or if the value does not fit into an int - */ - public int getIntExact(int index) { - return getNumberExact(index, "int", BigInteger::intValueExact, BigDecimal::intValueExact); - } - /** - * Returns the exact value as a long for a given index. - * This fails if the value does not fit into a long - * - * @param index the index - * @return the long - * - * @throws JSONException if the index does not exist, the value is not a long, or if the value does not fit into a long - */ - public long getLongExact(int index) { - return getNumberExact(index, "long", BigInteger::longValueExact, BigDecimal::longValueExact); - } - - /** - * Returns the exact value as a float for a given index. - * This fails if the value does not fit into a float - * - * @param index the index - * @return the float - * - * @throws JSONException if the index does not exist, the value is not a float, or if the value does not fit into a float - */ - public float getFloatExact(int index) { - Number num = getNumber(index); - - if(num instanceof Double) // NaN and Infinity - return ((Double) num).floatValue(); - - float f = num.floatValue(); - - if(!Float.isFinite(f)) - throw mismatch(index, "float"); - - return f; - } - /** - * Returns the exact value as a double for a given index. - * This fails if the value does not fit into a double - * - * @param index the index - * @return the double - * - * @throws JSONException if the index does not exist, the value is not a double, or if the value does not fit into a double - */ - public double getDoubleExact(int index) { - Number num = getNumber(index); - - if(num instanceof Double) // NaN and Infinity - return (Double) num; - - double d = num.doubleValue(); - - if(!Double.isFinite(d)) - throw mismatch(index, "double"); - - return d; - } - - /** - * Returns the value as a JSONObject for a given index - * - * @param index the index - * @return the JSONObject - * - * @throws JSONException if the index does not exist, or if the value is not a JSONObject - */ - public JSONObject getObject(int index) { - return checkType(this::isObject, index, "object"); - } - - /** - * Returns the value as a JSONArray for a given index - * - * @param index the index - * @return the JSONArray - * - * @throws JSONException if the index does not exist, or if the value is not a JSONArray - */ - public JSONArray getArray(int index) { - return checkType(this::isArray, index, "array"); - } - - /** - * Returns the value as an Instant for a given index - * - * @param index the index - * @return the Instant - * @since 1.1.0 - * - * @throws JSONException if the index does not exist, or if the value is not an Instant - */ - public Instant getInstant(int index) { - return checkType(this::isInstant, index, "instant"); - } - - // -- OPTIONAL -- - - private T getOpt(int index, Function supplier, T defaults) { - try { - return supplier.apply(index); - } catch (Exception e) { - return defaults; - } - } - - /** - * Returns the value for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the value - */ - public Object get(int index, Object defaults) { - return getOpt(index, this::get, defaults); - } - - /** - * Returns the value as a boolean for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the boolean - */ - public boolean getBoolean(int index, boolean defaults) { - return getOpt(index, this::getBoolean, defaults); - } - - /** - * Returns the value as a string for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the string - */ - public String getString(int index, String defaults) { - return getOpt(index, this::getString, defaults); - } - - /** - * Returns the value as a number for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the number - */ - public Number getNumber(int index, Number defaults) { - return getOpt(index, this::getNumber, defaults); - } - - /** - * Returns the value as a byte for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the byte - */ - public byte getByte(int index, byte defaults) { - return getOpt(index, this::getByte, defaults); - } - /** - * Returns the value as a short for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the short - */ - public short getShort(int index, short defaults) { - return getOpt(index, this::getShort, defaults); - } - /** - * Returns the value as an int for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the int - */ - public int getInt(int index, int defaults) { - return getOpt(index, this::getInt, defaults); - } - /** - * Returns the value as a long for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the long - */ - public long getLong(int index, long defaults) { - return getOpt(index, this::getLong, defaults); - } - - /** - * Returns the value as a float for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the float - */ - public float getFloat(int index, float defaults) { - return getOpt(index, this::getFloat, defaults); - } - /** - * Returns the value as a double for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the double - */ - public double getDouble(int index, double defaults) { - return getOpt(index, this::getDouble, defaults); - } - - /** - * Returns the exact value as a byte for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the byte - */ - public byte getByteExact(int index, byte defaults) { - return getOpt(index, this::getByteExact, defaults); - } - /** - * Returns the exact value as a short for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the short - */ - public short getShortExact(int index, short defaults) { - return getOpt(index, this::getShortExact, defaults); - } - /** - * Returns the exact value as an int for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the int - */ - public int getIntExact(int index, int defaults) { - return getOpt(index, this::getIntExact, defaults); - } - /** - * Returns the exact value as a long for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the long - */ - public long getLongExact(int index, long defaults) { - return getOpt(index, this::getLongExact, defaults); - } - - /** - * Returns the exact value as a float for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the float - */ - public float getFloatExact(int index, float defaults) { - return getOpt(index, this::getFloatExact, defaults); - } - /** - * Returns the exact value as a double for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the double - */ - public double getDoubleExact(int index, double defaults) { - return getOpt(index, this::getDoubleExact, defaults); - } - - /** - * Returns the value as a JSONObject for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the JSONObject - */ - public JSONObject getObject(int index, JSONObject defaults) { - return getOpt(index, this::getObject, defaults); - } - - /** - * Returns the value as a JSONArray for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the JSONArray - */ - public JSONArray getArray(int index, JSONArray defaults) { - return getOpt(index, this::getArray, defaults); - } - - /** - * Returns the value as an Instant for a given index, or the default value if the operation is not possible - * - * @param index the index - * @param defaults the default value - * @return the Instant - * @since 1.1.0 - */ - public Instant getInstant(int index, Instant defaults) { - return getOpt(index, this::getInstant, defaults); - } - - // -- ADD -- - - /** - * Adds a value to the JSONArray - * - * @param value the new value - * @return this JSONArray - */ - public JSONArray add(Object value) { - values.add(JSONObject.sanitize(value)); - return this; - } - - // -- INSERT -- - - /** - * Inserts a value to the JSONArray at a given index - * - * @param index the index - * @param value the new value - * @return this JSONArray - * @since 1.1.0 - */ - public JSONArray insert(int index, Object value) { - if(index < 0 || index > length()) - throw new JSONException("JSONArray[" + index + "] is out of bounds"); - - values.add(index, JSONObject.sanitize(value)); - return this; - } - - // -- SET -- - - /** - * Sets the value at a given index - * - * @param index the index - * @param value the new value - * @return this JSONArray - */ - public JSONArray set(int index, Object value) { - checkIndex(index); - values.set(index, JSONObject.sanitize(value)); - return this; - } - - // -- STRINGIFY -- - - /** - * Converts the JSONArray into its string representation. - * The indentation factor enables pretty-printing and defines - * how many spaces (' ') should be placed before each value. - * A factor of {@code < 1} disables pretty-printing and discards - * any optional whitespace characters. - *

- * {@code indentFactor = 2}: - *

-	 * [
-	 *   "value",
-	 *   {
-	 *     "nested": 123
-	 *   },
-	 *   false
-	 * ]
-	 * 
- *

- * {@code indentFactor = 0}: - *

-	 * ["value",{"nested":123},false]
-	 * 
- * - * @param indentFactor the indentation factor - * @return the string representation - * - * @see JSONStringify#toString(JSONArray, int) - */ - public String toString(int indentFactor) { - return JSONStringify.toString(this, indentFactor); - } - - /** - * Converts the JSONArray into its compact string representation. - * - * @return the compact string representation - */ - @Override - public String toString() { - return toString(0); - } - - // -- MISCELLANEOUS -- - - private Object checkIndex(int index) { - if(index < 0 || index >= length()) - throw new JSONException("JSONArray[" + index + "] does not exist"); - - return values.get(index); - } - - @SuppressWarnings("unchecked") - private T checkType(Predicate predicate, int index, String type) { - if(!predicate.test(index)) - throw mismatch(index, type); - - return (T) values.get(index); - } - - // - - private static JSONException mismatch(int index, String type) { - return new JSONException("JSONArray[" + index +"] is not of type " + type); - } - -} diff --git a/src/main/java/at/syntaxerror/json5/JSONException.java b/src/main/java/at/syntaxerror/json5/JSONException.java deleted file mode 100755 index c76ab7e..0000000 --- a/src/main/java/at/syntaxerror/json5/JSONException.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2021 SyntaxError404 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package at.syntaxerror.json5; - -/** - * An exception used by the JSON5 for Java Library if something went wrong - * - * @author SyntaxError404 - * @version 1.0.0 - */ -@SuppressWarnings("serial") -public class JSONException extends RuntimeException { - - /** - * Constructs a new JSONException - */ - public JSONException() { - super(); - } - - /** - * Constructs a new JSONException with a detail message - * - * @param message the detail message - */ - public JSONException(String message) { - super(message); - } - - /** - * Constructs a new JSONException with a causing exception - * - * @param cause the causing exception - */ - public JSONException(Throwable cause) { - super(cause); - } - - /** - * Constructs a new JSONException with a detail message and a causing exception - * - * @param message the detail message - * @param cause the causing exception - */ - public JSONException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/main/java/at/syntaxerror/json5/JSONObject.java b/src/main/java/at/syntaxerror/json5/JSONObject.java deleted file mode 100755 index 1a1e27e..0000000 --- a/src/main/java/at/syntaxerror/json5/JSONObject.java +++ /dev/null @@ -1,919 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2021 SyntaxError404 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package at.syntaxerror.json5; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.time.Instant; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; - -/** - * A JSONObject is a map (key-value) structure capable of holding multiple values, - * including other {@link JSONArray JSONArrays} and JSONObjects - * - * @author SyntaxError404 - * - */ -public class JSONObject implements Iterable> { - - private Map values; - - /** - * Constructs a new JSONObject - */ - public JSONObject() { - values = new HashMap<>(); - } - - /** - * Constructs a new JSONObject from a string - * - * @param source a string - */ - public JSONObject(String source) { - this(new JSONParser(source)); - } - - /** - * Constructs a new JSONObject from a JSONParser - * - * @param parser a JSONParser - */ - public JSONObject(JSONParser parser) { - this(); - - char c; - String key; - - if(parser.nextClean() != '{') - throw parser.syntaxError("A JSONObject must begin with '{'"); - - while(true) { - c = parser.nextClean(); - - switch(c) { - case 0: - throw parser.syntaxError("A JSONObject must end with '}'"); - case '}': - return; - default: - parser.back(); - key = parser.nextMemberName(); - } - - if(has(key)) - throw new JSONException("Duplicate key " + JSONStringify.quote(key)); - - c = parser.nextClean(); - - if(c != ':') - throw parser.syntaxError("Expected ':' after a key, got '" + c + "' instead"); - - Object value = parser.nextValue(); - - values.put(key, value); - - c = parser.nextClean(); - - if(c == '}') - return; - - if(c != ',') - throw parser.syntaxError("Expected ',' or '}' after value, got '" + c + "' instead"); - } - } - - // - - /** - * Converts the JSONObject into a map. All JSONObjects and JSONArrays - * contained within this JSONObject will be converted into their - * Map or List form as well - * - * @return a map of the entries of this object - */ - public Map toMap() { - Map map = new HashMap<>(); - - for(Entry entry : this) { - Object value = entry.getValue(); - - if(value instanceof JSONObject) - value = ((JSONObject) value).toMap(); - - else if(value instanceof JSONArray) - value = ((JSONArray) value).toList(); - - map.put(entry.getKey(), value); - } - - return map; - } - - /** - * Returns a set of keys of the JSONObject - * - * @return a set of keys - * - * @see Map#keySet() - */ - public Set keySet() { - return values.keySet(); - } - - /** - * Returns a set of entries of the JSONObject. Modifying the set - * or an entry will modify the JSONObject - * - * Use with caution. - * - * @return a set of entries - * - * @see Map#entrySet() - */ - public Set> entrySet() { - return values.entrySet(); - } - - @Override - public Iterator> iterator() { - return values.entrySet().iterator(); - } - - /** - * Returns the number of entries in the JSONObject - * - * @return the number of entries - */ - public int length() { - return values.size(); - } - - /** - * Removes all values from this JSONObject - * - * @since 1.2.0 - */ - public void clear() { - values.clear(); - } - - /** - * Removes a key from a JSONObject - * - * @param key the key to be removed - * @since 1.2.0 - * - * @throws JSONException if the key does not exist - */ - public void remove(String key) { - checkKey(key); - values.remove(key); - } - - // -- CHECK -- - - /** - * Checks if a key exists within the JSONObject - * - * @param key the key - * @return whether or not the key exists - */ - public boolean has(String key) { - return values.containsKey(key); - } - - /** - * Checks if the value with the specified key is {@code null} - * - * @param key the key - * @return whether or not the value is {@code null} - * - * @throws JSONException if the key does not exist - */ - public boolean isNull(String key) { - return checkKey(key) == null; - } - - /** - * Checks if the value with the specified key is a boolean - * - * @param key the key - * @return whether or not the value is a boolean - * - * @throws JSONException if the key does not exist - */ - public boolean isBoolean(String key) { - return checkKey(key) instanceof Boolean; - } - - /** - * Checks if the value with the specified key is a string - * - * @param key the key - * @return whether or not the value is a string - * - * @throws JSONException if the key does not exist - */ - public boolean isString(String key) { - Object value = checkKey(key); - return value instanceof String || value instanceof Instant; - } - - /** - * Checks if the value with the specified key is a number - * - * @param key the key - * @return whether or not the value is a number - * - * @throws JSONException if the key does not exist - */ - public boolean isNumber(String key) { - Object value = checkKey(key); - return value instanceof Number || value instanceof Instant; - } - - /** - * Checks if the value with the specified key is a JSONObject - * - * @param key the key - * @return whether or not the value is a JSONObject - * - * @throws JSONException if the key does not exist - */ - public boolean isObject(String key) { - return checkKey(key) instanceof JSONObject; - } - - /** - * Checks if the value with the specified key is a JSONArray - * - * @param key the key - * @return whether or not the value is a JSONArray - * - * @throws JSONException if the key does not exist - */ - public boolean isArray(String key) { - return checkKey(key) instanceof JSONArray; - } - - /** - * Checks if the value with the specified key is an Instant - * - * @param key the key - * @return whether or not the value is an Instant - * @since 1.1.0 - * - * @throws JSONException if the key does not exist - */ - public boolean isInstant(String key) { - return checkKey(key) instanceof Instant; - } - - // -- GET -- - - /** - * Returns the value for a given key - * - * @param key the key - * @return the value - * - * @throws JSONException if the key does not exist - */ - public Object get(String key) { - checkKey(key); - return values.get(key); - } - - /** - * Returns the value as a boolean for a given key - * - * @param key the key - * @return the boolean - * - * @throws JSONException if the key does not exist, or if the value is not a boolean - */ - public boolean getBoolean(String key) { - return checkType(this::isBoolean, key, "boolean"); - } - - /** - * Returns the value as a string for a given key - * - * @param key the key - * @return the string - * - * @throws JSONException if the key does not exist, or if the value is not a string - */ - public String getString(String key) { - if(isInstant(key)) - return getInstant(key).toString(); - - return checkType(this::isString, key, "string"); - } - - /** - * Returns the value as a number for a given key - * - * @param key the key - * @return the number - * - * @throws JSONException if the key does not exist, or if the value is not a number - */ - public Number getNumber(String key) { - if(isInstant(key)) - return getInstant(key).getEpochSecond(); - - return checkType(this::isNumber, key, "number"); - } - - /** - * Returns the value as a byte for a given key - * - * @param key the key - * @return the byte - * - * @throws JSONException if the key does not exist, or if the value is not a byte - */ - public byte getByte(String key) { - return getNumber(key).byteValue(); - } - /** - * Returns the value as a short for a given key - * - * @param key the key - * @return the short - * - * @throws JSONException if the key does not exist, or if the value is not a short - */ - public short getShort(String key) { - return getNumber(key).shortValue(); - } - /** - * Returns the value as an int for a given key - * - * @param key the key - * @return the int - * - * @throws JSONException if the key does not exist, or if the value is not an int - */ - public int getInt(String key) { - return getNumber(key).intValue(); - } - /** - * Returns the value as a long for a given key - * - * @param key the key - * @return the long - * - * @throws JSONException if the key does not exist, or if the value is not a long - */ - public long getLong(String key) { - return getNumber(key).longValue(); - } - - /** - * Returns the value as a float for a given key - * - * @param key the key - * @return the float - * - * @throws JSONException if the key does not exist, or if the value is not a float - */ - public float getFloat(String key) { - return getNumber(key).floatValue(); - } - /** - * Returns the value as a double for a given key - * - * @param key the key - * @return the double - * - * @throws JSONException if the key does not exist, or if the value is not a double - */ - public double getDouble(String key) { - return getNumber(key).doubleValue(); - } - - private T getNumberExact(String key, String type, Function bigint, Function bigdec) { - Number number = getNumber(key); - - try { - if(number instanceof BigInteger) - return bigint.apply((BigInteger) number); - - if(number instanceof BigDecimal) - return bigdec.apply((BigDecimal) number); - - } catch (Exception e) { } - - throw mismatch(key, type); - } - - /** - * Returns the exact value as a byte for a given key. - * This fails if the value does not fit into a byte - * - * @param key the key - * @return the byte - * - * @throws JSONException if the key does not exist, the value is not a byte, or if the value does not fit into a byte - */ - public byte getByteExact(String key) { - return getNumberExact(key, "byte", BigInteger::byteValueExact, BigDecimal::byteValueExact); - } - /** - * Returns the exact value as a short for a given key. - * This fails if the value does not fit into a short - * - * @param key the key - * @return the short - * - * @throws JSONException if the key does not exist, the value is not a short, or if the value does not fit into a short - */ - public short getShortExact(String key) { - return getNumberExact(key, "short", BigInteger::shortValueExact, BigDecimal::shortValueExact); - } - /** - * Returns the exact value as an int for a given key. - * This fails if the value does not fit into an int - * - * @param key the key - * @return the int - * - * @throws JSONException if the key does not exist, the value is not an int, or if the value does not fit into an int - */ - public int getIntExact(String key) { - return getNumberExact(key, "int", BigInteger::intValueExact, BigDecimal::intValueExact); - } - /** - * Returns the exact value as a long for a given key. - * This fails if the value does not fit into a long - * - * @param key the key - * @return the long - * - * @throws JSONException if the key does not exist, the value is not a long, or if the value does not fit into a long - */ - public long getLongExact(String key) { - return getNumberExact(key, "long", BigInteger::longValueExact, BigDecimal::longValueExact); - } - - /** - * Returns the exact value as a float for a given key. - * This fails if the value does not fit into a float - * - * @param key the key - * @return the float - * - * @throws JSONException if the key does not exist, the value is not a float, or if the value does not fit into a float - */ - public float getFloatExact(String key) { - Number num = getNumber(key); - - if(num instanceof Double) // NaN and Infinity - return ((Double) num).floatValue(); - - float f = num.floatValue(); - - if(!Float.isFinite(f)) - throw mismatch(key, "float"); - - return f; - } - /** - * Returns the exact value as a double for a given key. - * This fails if the value does not fit into a double - * - * @param key the key - * @return the double - * - * @throws JSONException if the key does not exist, the value is not a double, or if the value does not fit into a double - */ - public double getDoubleExact(String key) { - Number num = getNumber(key); - - if(num instanceof Double) // NaN and Infinity - return (Double) num; - - double d = num.doubleValue(); - - if(!Double.isFinite(d)) - throw mismatch(key, "double"); - - return d; - } - - /** - * Returns the value as a JSONObject for a given key - * - * @param key the key - * @return the JSONObject - * - * @throws JSONException if the key does not exist, or if the value is not a JSONObject - */ - public JSONObject getObject(String key) { - return checkType(this::isObject, key, "object"); - } - - /** - * Returns the value as a JSONArray for a given key - * - * @param key the key - * @return the JSONArray - * - * @throws JSONException if the key does not exist, or if the value is not a JSONArray - */ - public JSONArray getArray(String key) { - return checkType(this::isArray, key, "array"); - } - - /** - * Returns the value as an Instant for a given key - * - * @param key the key - * @return the Instant - * @since 1.1.0 - * - * @throws JSONException if the key does not exist, or if the value is not an Instant - */ - public Instant getInstant(String key) { - return checkType(this::isInstant, key, "instant"); - } - - // -- OPTIONAL -- - - private T getOpt(String key, Function supplier, T defaults) { - try { - return supplier.apply(key); - } catch (Exception e) { - return defaults; - } - } - - /** - * Returns the value for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the value - */ - public Object get(String key, Object defaults) { - return getOpt(key, this::get, defaults); - } - - /** - * Returns the value as a boolean for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the boolean - */ - public boolean getBoolean(String key, boolean defaults) { - return getOpt(key, this::getBoolean, defaults); - } - - /** - * Returns the value as a string for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the string - */ - public String getString(String key, String defaults) { - return getOpt(key, this::getString, defaults); - } - - /** - * Returns the value as a number for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the number - */ - public Number getNumber(String key, Number defaults) { - return getOpt(key, this::getNumber, defaults); - } - - /** - * Returns the value as a byte for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the byte - */ - public byte getByte(String key, byte defaults) { - return getOpt(key, this::getByte, defaults); - } - /** - * Returns the value as a short for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the short - */ - public short getShort(String key, short defaults) { - return getOpt(key, this::getShort, defaults); - } - /** - * Returns the value as an int for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the int - */ - public int getInt(String key, int defaults) { - return getOpt(key, this::getInt, defaults); - } - /** - * Returns the value as a long for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the long - */ - public long getLong(String key, long defaults) { - return getOpt(key, this::getLong, defaults); - } - - /** - * Returns the value as a float for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the float - */ - public float getFloat(String key, float defaults) { - return getOpt(key, this::getFloat, defaults); - } - /** - * Returns the value as a double for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the double - */ - public double getDouble(String key, double defaults) { - return getOpt(key, this::getDouble, defaults); - } - - /** - * Returns the exact value as a byte for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the byte - */ - public byte getByteExact(String key, byte defaults) { - return getOpt(key, this::getByteExact, defaults); - } - /** - * Returns the exact value as a short for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the short - */ - public short getShortExact(String key, short defaults) { - return getOpt(key, this::getShortExact, defaults); - } - /** - * Returns the exact value as an int for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the int - */ - public int getIntExact(String key, int defaults) { - return getOpt(key, this::getIntExact, defaults); - } - /** - * Returns the exact value as a long for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the long - */ - public long getLongExact(String key, long defaults) { - return getOpt(key, this::getLongExact, defaults); - } - - /** - * Returns the exact value as a float for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the float - */ - public float getFloatExact(String key, float defaults) { - return getOpt(key, this::getFloatExact, defaults); - } - /** - * Returns the exact value as a double for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the double - */ - public double getDoubleExact(String key, double defaults) { - return getOpt(key, this::getDoubleExact, defaults); - } - - /** - * Returns the value as a JSONObject for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the JSONObject - */ - public JSONObject getObject(String key, JSONObject defaults) { - return getOpt(key, this::getObject, defaults); - } - - /** - * Returns the value as a JSONArray for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the JSONArray - */ - public JSONArray getArray(String key, JSONArray defaults) { - return getOpt(key, this::getArray, defaults); - } - - /** - * Returns the value as an Instant for a given key, or the default value if the operation is not possible - * - * @param key the key - * @param defaults the default value - * @return the Instant - * @since 1.1.0 - */ - public Instant getInstant(String key, Instant defaults) { - return getOpt(key, this::getInstant, defaults); - } - - // -- SET -- - - /** - * Sets the value at a given key - * - * @param key the key - * @param value the new value - * @return this JSONObject - */ - public JSONObject set(String key, Object value) { - values.put(key, sanitize(value)); - return this; - } - - // -- STRINGIFY -- - - /** - * Converts the JSONObject into its string representation. - * The indentation factor enables pretty-printing and defines - * how many spaces (' ') should be placed before each key/value pair. - * A factor of {@code < 1} disables pretty-printing and discards - * any optional whitespace characters. - *

- * {@code indentFactor = 2}: - *

-	 * {
-	 *   "key0": "value0",
-	 *   "key1": {
-	 *     "nested": 123
-	 *   },
-	 *   "key2": false
-	 * }
-	 * 
- *

- * {@code indentFactor = 0}: - *

-	 * {"key0":"value0","key1":{"nested":123},"key2":false}
-	 * 
- * - * @param indentFactor the indentation factor - * @return the string representation - * - * @see JSONStringify#toString(JSONObject, int) - */ - public String toString(int indentFactor) { - return JSONStringify.toString(this, indentFactor); - } - - /** - * Converts the JSONObject into its compact string representation. - * - * @return the compact string representation - */ - @Override - public String toString() { - return toString(0); - } - - // -- MISCELLANEOUS -- - - private Object checkKey(String key) { - if(!values.containsKey(key)) - throw new JSONException("JSONObject[" + JSONStringify.quote(key) + "] does not exist"); - - return values.get(key); - } - - @SuppressWarnings("unchecked") - private T checkType(Predicate predicate, String key, String type) { - if(!predicate.test(key)) - throw mismatch(key, type); - - return (T) values.get(key); - } - - // - - private static JSONException mismatch(String key, String type) { - return new JSONException("JSONObject[" + JSONStringify.quote(key) +"] is not of type " + type); - } - - /** - * Sanitizes an input value - * - * @param value the value - * @return the sanitized value - * - * @throws JSONException if the value is illegal - */ - static Object sanitize(Object value) { - if(value == null) - return null; - - if(value instanceof Boolean || - value instanceof String || - value instanceof JSONObject || - value instanceof JSONArray || - value instanceof Instant) - return value; - - else if(value instanceof Number) { - Number num = (Number) value; - - if(value instanceof Double) { - double d = (Double) num; - - if(Double.isFinite(d)) - return BigDecimal.valueOf(d); - } - - else if(value instanceof Float) { - float f = (Float) num; - - if(Float.isFinite(f)) - return BigDecimal.valueOf(f); - - // NaN and Infinity - return num.doubleValue(); - } - - else if(value instanceof Byte || - value instanceof Short || - value instanceof Integer || - value instanceof Long) - return BigInteger.valueOf(num.longValue()); - - else if(!(value instanceof BigDecimal || - value instanceof BigInteger)) - return BigDecimal.valueOf(num.doubleValue()); - - return num; - } - - else throw new JSONException("Illegal type '" + value.getClass() + "'"); - } - -} diff --git a/src/main/java/at/syntaxerror/json5/JSONOptions.java b/src/main/java/at/syntaxerror/json5/JSONOptions.java deleted file mode 100755 index 3c2e59c..0000000 --- a/src/main/java/at/syntaxerror/json5/JSONOptions.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2021 SyntaxError404 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package at.syntaxerror.json5; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; -import lombok.experimental.FieldDefaults; - -/** - * This class used is used to customize the behaviour of {@link JSONParser parsing} and {@link JSONStringify stringifying} - * - * @author SyntaxError404 - * @since 1.1.0 - */ -@AllArgsConstructor(access = AccessLevel.PRIVATE) -@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) -@Getter -@Builder(toBuilder = true) -public class JSONOptions { - - /** - * -- GETTER -- - * Returns the default options for parsing and stringifying - * - * @return the default options - * @since 1.1.0 - * - * -- SETTER -- - * Sets the default options for parsing and stringifying. - * Must not be {@code null} - * - * @param defaultOptions the new default options - * @since 1.1.0 - */ - @Getter - @Setter - @NonNull - private static JSONOptions defaultOptions = builder().build(); - - /** - * Whether or not instants should be parsed as such. - * If this is {@code false}, {@link #isParseStringInstants()} and {@link #isParseUnixInstants()} - * are ignored - *

- * Default: {@code true} - *

- * This is a {@link JSONParser Parser}-only option - * - * @param parseInstants a boolean - * - * @return whether or not instants should be parsed - * @since 1.1.0 - */ - @Builder.Default - boolean parseInstants = true; - /** - * Whether or not string instants (according to - * RFC 3339, Section 5.6) - * should be parsed as such. - * Ignored if {@link #isParseInstants()} is {@code false} - *

- * Default: {@code true} - *

- * This is a {@link JSONParser Parser}-only option - * - * @param parseStringInstants a boolean - * - * @return whether or not string instants should be parsed - * @since 1.1.0 - */ - @Builder.Default - boolean parseStringInstants = true; - /** - * Whether or not unix instants (integers) should be parsed as such. - * Ignored if {@link #isParseInstants()} is {@code false} - *

- * Default: {@code true} - *

- * This is a {@link JSONParser Parser}-only option - * - * @param parseUnixInstants a boolean - * - * @return whether or not unix instants should be parsed - * @since 1.1.0 - */ - @Builder.Default - boolean parseUnixInstants = true; - - /** - * Whether or not instants should be stringifyed as unix timestamps. - * If this is {@code false}, instants will be stringifyed as strings - * (according to RFC 3339, Section 5.6). - *

- * Default: {@code false} - *

- * This is a {@link JSONStringify Stringify}-only option - * - * @param stringifyUnixInstants a boolean - * - * @return whether or not instants should be stringifyed as unix timestamps - * @since 1.1.0 - */ - @Builder.Default - boolean stringifyUnixInstants = false; - - /** - * Whether or not {@code NaN} should be allowed as a number - *

- * Default: {@code true} - * - * @param allowNaN a boolean - * - * @return whether or not {@code NaN} should be allowed - * @since 1.1.0 - */ - @Builder.Default - boolean allowNaN = true; - - /** - * Whether or not {@code Infinity} should be allowed as a number. - * This applies to both {@code +Infinity} and {@code -Infinity} - *

- * Default: {@code true} - * - * @param allowInfinity a boolean - * - * @return whether or not {@code Infinity} should be allowed - * @since 1.1.0 - */ - @Builder.Default - boolean allowInfinity = true; - - /** - * Whether or not invalid unicode surrogate pairs should be allowed - *

- * Default: {@code true} - *

- * This is a {@link JSONParser Parser}-only option - * - * @param allowInvalidSurrogates a boolean - * - * @return whether or not invalid unicode surrogate pairs should be allowed - * @since 1.1.0 - */ - @Builder.Default - boolean allowInvalidSurrogates = true; - - /** - * Whether or not string should be single-quoted ({@code '}) instead of double-quoted ({@code "}). - * This also includes a {@link JSONObject JSONObject's} member names - *

- * Default: {@code false} - *

- * This is a {@link JSONStringify Stringify}-only option - * - * @param quoteSingle a boolean - * - * @return whether or not string should be single-quoted - * @since 1.1.0 - */ - @Builder.Default - boolean quoteSingle = false; - -} diff --git a/src/main/java/at/syntaxerror/json5/JSONParser.java b/src/main/java/at/syntaxerror/json5/JSONParser.java deleted file mode 100755 index 6c4a6e8..0000000 --- a/src/main/java/at/syntaxerror/json5/JSONParser.java +++ /dev/null @@ -1,730 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2021 SyntaxError404 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package at.syntaxerror.json5; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.StringReader; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.time.Instant; -import java.util.regex.Pattern; - -/** - * A JSONParser is used to convert a source string into tokens, which then are used to - * construct {@link JSONObject JSONObjects} and {@link JSONArray JSONArrays} - * - * @author SyntaxError404 - */ -public class JSONParser { - - private static final Pattern PATTERN_BOOLEAN = Pattern.compile( - "true|false" - ); - - private static final Pattern PATTERN_NUMBER_FLOAT = Pattern.compile( - "[+-]?((0|[1-9]\\d*)(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?" - ); - private static final Pattern PATTERN_NUMBER_INTEGER = Pattern.compile( - "[+-]?(0|[1-9]\\d*)" - ); - private static final Pattern PATTERN_NUMBER_HEX = Pattern.compile( - "[+-]?0[xX][0-9a-fA-F]+" - ); - private static final Pattern PATTERN_NUMBER_SPECIAL = Pattern.compile( - "[+-]?(Infinity|NaN)" - ); - - private final Reader reader; - private final JSONOptions options; - - /** whether or not the end of the file has been reached */ - private boolean eof; - - /** whether or not the current character should be re-read */ - private boolean back; - - /** the absolute position in the string */ - private long index; - /** the relative position in the line */ - private long character; - /** the line number */ - private long line; - - /** the previous character */ - private char previous; - /** the current character */ - private char current; - - /** - * Constructs a new JSONParser from a Reader. The reader is not {@link Reader#close() closed} - * - * @param reader a reader - * @param options the options for parsing - * @since 1.1.0 - */ - public JSONParser(Reader reader, JSONOptions options) { - this.reader = reader.markSupported() ? - reader : new BufferedReader(reader); - - this.options = options == null ? - JSONOptions.getDefaultOptions() : options; - - eof = false; - back = false; - - index = -1; - character = 0; - line = 1; - - previous = 0; - current = 0; - } - - /** - * Constructs a new JSONParser from a string - * - * @param source a string - * @param options the options for parsing - * @since 1.1.0 - */ - public JSONParser(String source, JSONOptions options) { - this(new StringReader(source), options); - } - - /** - * Constructs a new JSONParser from an InputStream. The stream is not {@link InputStream#close() closed} - * - * @param stream a stream - * @param options the options for parsing - * @since 1.1.0 - */ - public JSONParser(InputStream stream, JSONOptions options) { - this(new InputStreamReader(stream), options); - } - - /** - * Constructs a new JSONParser from a Reader. The reader is not {@link Reader#close() closed}. - * This uses the {@link JSONOptions#getDefaultOptions() default options} - * - * @param reader a reader - */ - public JSONParser(Reader reader) { - this(reader, null); - } - - /** - * Constructs a new JSONParser from a string. - * This uses the {@link JSONOptions#getDefaultOptions() default options} - * - * @param source a string - */ - public JSONParser(String source) { - this(source, null); - } - - /** - * Constructs a new JSONParser from an InputStream. The stream is not {@link InputStream#close() closed}. - * This uses the {@link JSONOptions#getDefaultOptions() default options} - * - * @param stream a stream - */ - public JSONParser(InputStream stream) { - this(stream, null); - } - - private boolean more() { - if(back || eof) - return back && !eof; - - return peek() > 0; - } - - /** - * Forces the parser to re-read the last character - */ - public void back() { - back = true; - } - - private char peek() { - if(eof) - return 0; - - int c; - - try { - reader.mark(1); - - c = reader.read(); - - reader.reset(); - } catch(Exception e) { - throw syntaxError("Could not peek from source", e); - } - - return c == -1 ? 0 : (char) c; - } - - private char next() { - if(back) { - back = false; - return current; - } - - int c; - - try { - c = reader.read(); - } catch(Exception e) { - throw syntaxError("Could not read from source", e); - } - - if(c < 0) { - eof = true; - return 0; - } - - previous = current; - current = (char) c; - - index++; - - if(isLineTerminator(current) && (current != '\n' || (current == '\n' && previous != '\r'))) { - line++; - character = 0; - } - else character++; - - return current; - } - - // https://262.ecma-international.org/5.1/#sec-7.3 - private boolean isLineTerminator(char c) { - switch(c) { - case '\n': - case '\r': - case 0x2028: - case 0x2029: - return true; - default: - return false; - } - } - - // https://spec.json5.org/#white-space - private boolean isWhitespace(char c) { - switch(c) { - case '\t': - case '\n': - case 0x0B: // Vertical Tab - case '\f': - case '\r': - case ' ': - case 0xA0: // No-break space - case 0x2028: // Line separator - case 0x2029: // Paragraph separator - case 0xFEFF: // Byte Order Mark - return true; - default: - // Unicode category "Zs" (space separators) - if(Character.getType(c) == Character.SPACE_SEPARATOR) - return true; - - return false; - } - } - - // https://262.ecma-international.org/5.1/#sec-9.3.1 - private boolean isDecimalDigit(char c) { - return c >= '0' && c <= '9'; - } - - private void nextMultiLineComment() { - while(true) { - char n = next(); - - if(n == '*' && peek() == '/') { - next(); - return; - } - } - } - - private void nextSingleLineComment() { - while(true) { - char n = next(); - - if(isLineTerminator(n) || n == 0) - return; - } - } - - /** - * Reads until encountering a character that is not a whitespace according to the - * JSON5 Specification - * - * @return a non-whitespace character, or {@code 0} if the end of the stream has been reached - */ - public char nextClean() { - while(true) { - if(!more()) - throw syntaxError("Unexpected end of data"); - - char n = next(); - - if(n == '/') { - char p = peek(); - - if(p == '*') { - next(); - nextMultiLineComment(); - } - - else if(p == '/') { - next(); - nextSingleLineComment(); - } - - else return n; - } - - else if(!isWhitespace(n)) - return n; - } - } - - private String nextCleanTo(String delimiters) { - StringBuilder result = new StringBuilder(); - - while(true) { - if(!more()) - throw syntaxError("Unexpected end of data"); - - char n = nextClean(); - - if(delimiters.indexOf(n) > -1 || isWhitespace(n)) { - back(); - break; - } - - result.append(n); - } - - return result.toString(); - } - - private int dehex(char c) { - if(c >= '0' && c <= '9') - return c - '0'; - - if(c >= 'a' && c <= 'f') - return c - 'a' + 0xA; - - if(c >= 'A' && c <= 'F') - return c - 'A' + 0xA; - - return -1; - } - - private char unicodeEscape(boolean member, boolean part) { - String where = member ? "key" : "string"; - - String value = ""; - int codepoint = 0; - - for(int i = 0; i < 4; ++i) { - char n = next(); - value += n; - - int hex = dehex(n); - - if(hex == -1) - throw syntaxError("Illegal unicode escape sequence '\\u" + value + "' in " + where); - - codepoint |= hex << ((3 - i) << 2); - } - - if(member && !isMemberNameChar((char) codepoint, part)) - throw syntaxError("Illegal unicode escape sequence '\\u" + value + "' in key"); - - return (char) codepoint; - } - - private void checkSurrogate(char hi, char lo) { - if(options.isAllowInvalidSurrogates()) - return; - - if(!Character.isHighSurrogate(hi) || !Character.isLowSurrogate(lo)) - return; - - if(!Character.isSurrogatePair(hi, lo)) - throw syntaxError(String.format( - "Invalid surrogate pair: U+%04X and U+%04X", - hi, lo - )); - } - - // https://spec.json5.org/#prod-JSON5String - private String nextString(char quote) { - StringBuilder result = new StringBuilder(); - - String value; - int codepoint; - - char n = 0; - char prev; - - while(true) { - if(!more()) - throw syntaxError("Unexpected end of data"); - - prev = n; - n = next(); - - if(n == quote) - break; - - if(isLineTerminator(n) && n != 0x2028 && n != 0x2029) - throw syntaxError("Unescaped line terminator in string"); - - if(n == '\\') { - n = next(); - - if(isLineTerminator(n)) { - if(n == '\r' && peek() == '\n') - next(); - - // escaped line terminator/ line continuation - continue; - } - - else switch(n) { - case '\'': - case '"': - case '\\': - result.append(n); - continue; - case 'b': - result.append('\b'); - continue; - case 'f': - result.append('\f'); - continue; - case 'n': - result.append('\n'); - continue; - case 'r': - result.append('\r'); - continue; - case 't': - result.append('\t'); - continue; - case 'v': // Vertical Tab - result.append((char) 0x0B); - continue; - - case '0': // NUL - char p = peek(); - - if(isDecimalDigit(p)) - throw syntaxError("Illegal escape sequence '\\0" + p + "'"); - - result.append((char) 0); - continue; - - case 'x': // Hex escape sequence - value = ""; - codepoint = 0; - - for(int i = 0; i < 2; ++i) { - n = next(); - value += n; - - int hex = dehex(n); - - if(hex == -1) - throw syntaxError("Illegal hex escape sequence '\\x" + value + "' in string"); - - codepoint |= hex << ((1 - i) << 2); - } - - n = (char) codepoint; - break; - - case 'u': // Unicode escape sequence - n = unicodeEscape(false, false); - break; - - default: - if(isDecimalDigit(n)) - throw syntaxError("Illegal escape sequence '\\" + n + "'"); - - break; - } - } - - checkSurrogate(prev, n); - - result.append(n); - } - - return result.toString(); - } - - private boolean isMemberNameChar(char n, boolean part) { - if(n == '$' || n == '_' || n == 0x200C || n == 0x200D) - return true; - - int type = Character.getType(n); - - switch(type) { - case Character.UPPERCASE_LETTER: - case Character.LOWERCASE_LETTER: - case Character.TITLECASE_LETTER: - case Character.MODIFIER_LETTER: - case Character.OTHER_LETTER: - case Character.LETTER_NUMBER: - return true; - - case Character.NON_SPACING_MARK: - case Character.COMBINING_SPACING_MARK: - case Character.DECIMAL_DIGIT_NUMBER: - case Character.CONNECTOR_PUNCTUATION: - if(part) - return true; - break; - } - - return false; - } - - /** - * Reads a member name from the source according to the - * JSON5 Specification - * - * @return an member name - */ - public String nextMemberName() { - StringBuilder result = new StringBuilder(); - - char prev; - char n = next(); - - if(n == '"' || n == '\'') - return nextString(n); - - back(); - n = 0; - - while(true) { - if(!more()) - throw syntaxError("Unexpected end of data"); - - boolean part = result.length() > 0; - - prev = n; - n = next(); - - if(n == '\\') { // unicode escape sequence - n = next(); - - if(n != 'u') - throw syntaxError("Illegal escape sequence '\\" + n + "' in key"); - - n = unicodeEscape(true, part); - } - else if(!isMemberNameChar(n, part)) { - back(); - break; - } - - checkSurrogate(prev, n); - - result.append(n); - } - - if(result.length() == 0) - throw syntaxError("Empty key"); - - return result.toString(); - } - - /** - * Reads a value from the source according to the - * JSON5 Specification - * - * @return an member name - */ - public Object nextValue() { - char n = nextClean(); - - switch(n) { - case '"': - case '\'': - String string = nextString(n); - - if(options.isParseInstants() && options.isParseStringInstants()) - try { - return Instant.parse(string); - } catch (Exception e) { } - - return string; - case '{': - back(); - return new JSONObject(this); - case '[': - back(); - return new JSONArray(this); - } - - back(); - - String string = nextCleanTo(",]}"); - - if(string.equals("null")) - return null; - - if(PATTERN_BOOLEAN.matcher(string).matches()) - return string.equals("true"); - - if(PATTERN_NUMBER_INTEGER.matcher(string).matches()) { - BigInteger bigint = new BigInteger(string); - - if(options.isParseInstants() && options.isParseUnixInstants()) - try { - long unix = bigint.longValueExact(); - - return Instant.ofEpochSecond(unix); - } catch (Exception e) { } - - return bigint; - } - - if(PATTERN_NUMBER_FLOAT.matcher(string).matches()) - return new BigDecimal(string); - - if(PATTERN_NUMBER_SPECIAL.matcher(string).matches()) { - String special; - - int factor; - double d = 0; - - switch(string.charAt(0)) { // +, -, or 0 - case '+': - special = string.substring(1); // + - factor = 1; - break; - - case '-': - special = string.substring(1); // - - factor = -1; - break; - - default: - special = string; - factor = 1; - break; - } - - switch(special) { - case "NaN": - if(!options.isAllowNaN()) - throw syntaxError("NaN is not allowed"); - - d = Double.NaN; - break; - case "Infinity": - if(!options.isAllowInfinity()) - throw syntaxError("Infinity is not allowed"); - - d = Double.POSITIVE_INFINITY; - break; - } - - return factor * d; - } - - if(PATTERN_NUMBER_HEX.matcher(string).matches()) { - String hex; - - int factor; - - switch(string.charAt(0)) { // +, -, or 0 - case '+': - hex = string.substring(3); // +0x - factor = 1; - break; - - case '-': - hex = string.substring(3); // -0x - factor = -1; - break; - - default: - hex = string.substring(2); // 0x - factor = 1; - break; - } - - BigInteger bigint = new BigInteger(hex, 16); - - if(factor == -1) - return bigint.negate(); - - return bigint; - } - - throw new JSONException("Illegal value '" + string + "'"); - } - - /** - * Constructs a new JSONException with a detail message and a causing exception - * - * @param message the detail message - * @param cause the causing exception - * @return a JSONException - */ - public JSONException syntaxError(String message, Throwable cause) { - return new JSONException(message + this, cause); - } - - /** - * Constructs a new JSONException with a detail message - * - * @param message the detail message - * @return a JSONException - */ - public JSONException syntaxError(String message) { - return new JSONException(message + this); - } - - @Override - public String toString() { - return " at index " + index + " [character " + character + " in line " + line + "]"; - } - -} diff --git a/src/main/java/at/syntaxerror/json5/JSONStringify.java b/src/main/java/at/syntaxerror/json5/JSONStringify.java deleted file mode 100755 index fb820d2..0000000 --- a/src/main/java/at/syntaxerror/json5/JSONStringify.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2021 SyntaxError404 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package at.syntaxerror.json5; - -import java.time.Instant; -import java.util.Map; - -/** - * A utility class for serializing {@link JSONObject JSONObjects} and - * {@link JSONArray JSONArrays} into their string representations - * - * @author SyntaxError404 - * - */ -public class JSONStringify { - - private JSONStringify() { - throw new UnsupportedOperationException("Utility class"); - } - - /** - * Converts a JSONObject into its string representation. - * The indentation factor enables pretty-printing and defines - * how many spaces (' ') should be placed before each key/value pair. - * A factor of {@code < 1} disables pretty-printing and discards - * any optional whitespace characters. - *

- * {@code indentFactor = 2}: - *

-	 * {
-	 *   "key0": "value0",
-	 *   "key1": {
-	 *     "nested": 123
-	 *   },
-	 *   "key2": false
-	 * }
-	 * 
- *

- * {@code indentFactor = 0}: - *

-	 * {"key0":"value0","key1":{"nested":123},"key2":false}
-	 * 
- * - * @param object the JSONObject - * @param indentFactor the indentation factor - * @param options the options for stringifying - * @return the string representation - * @since 1.1.0 - */ - public static String toString(JSONObject object, int indentFactor, JSONOptions options) { - return toString( - object, - "", - Math.max(0, indentFactor), - options == null ? - JSONOptions.getDefaultOptions() : options - ); - } - - /** - * Converts a JSONArray into its string representation. - * The indentation factor enables pretty-printing and defines - * how many spaces (' ') should be placed before each value. - * A factor of {@code < 1} disables pretty-printing and discards - * any optional whitespace characters. - *

- * {@code indentFactor = 2}: - *

-	 * [
-	 *   "value",
-	 *   {
-	 *     "nested": 123
-	 *   },
-	 *   false
-	 * ]
-	 * 
- *

- * {@code indentFactor = 0}: - *

-	 * ["value",{"nested":123},false]
-	 * 
- * - * @param array the JSONArray - * @param indentFactor the indentation factor - * @param options the options for stringifying - * @return the string representation - * @since 1.1.0 - */ - public static String toString(JSONArray array, int indentFactor, JSONOptions options) { - return toString( - array, - "", - Math.max(0, indentFactor), - options == null ? - JSONOptions.getDefaultOptions() : options - ); - } - - /** - * Converts a JSONObject into its string representation. - * The indentation factor enables pretty-printing and defines - * how many spaces (' ') should be placed before each key/value pair. - * A factor of {@code < 1} disables pretty-printing and discards - * any optional whitespace characters. - *

- * {@code indentFactor = 2}: - *

-	 * {
-	 *   "key0": "value0",
-	 *   "key1": {
-	 *     "nested": 123
-	 *   },
-	 *   "key2": false
-	 * }
-	 * 
- *

- * {@code indentFactor = 0}: - *

-	 * {"key0":"value0","key1":{"nested":123},"key2":false}
-	 * 
- * This uses the {@link JSONOptions#getDefaultOptions() default options} - * - * @param object the JSONObject - * @param indentFactor the indentation factor - * @return the string representation - */ - public static String toString(JSONObject object, int indentFactor) { - return toString(object, indentFactor, null); - } - - /** - * Converts a JSONArray into its string representation. - * The indentation factor enables pretty-printing and defines - * how many spaces (' ') should be placed before each value. - * A factor of {@code < 1} disables pretty-printing and discards - * any optional whitespace characters. - *

- * {@code indentFactor = 2}: - *

-	 * [
-	 *   "value",
-	 *   {
-	 *     "nested": 123
-	 *   },
-	 *   false
-	 * ]
-	 * 
- *

- * {@code indentFactor = 0}: - *

-	 * ["value",{"nested":123},false]
-	 * 
- * This uses the {@link JSONOptions#getDefaultOptions() default options} - * - * @param array the JSONArray - * @param indentFactor the indentation factor - * @return the string representation - */ - public static String toString(JSONArray array, int indentFactor) { - return toString(array, indentFactor, null); - } - - private static String toString(JSONObject object, String indent, int indentFactor, JSONOptions options) { - StringBuilder sb = new StringBuilder(); - - String childIndent = indent + " ".repeat(indentFactor); - - sb.append('{'); - - for(Map.Entry entry : object) { - if(sb.length() != 1) - sb.append(','); - - if(indentFactor > 0) - sb.append('\n').append(childIndent); - - sb.append(quote(entry.getKey(), options)) - .append(':'); - - if(indentFactor > 0) - sb.append(' '); - - sb.append(toString(entry.getValue(), childIndent, indentFactor, options)); - } - - if(indentFactor > 0) - sb.append('\n').append(indent); - - sb.append('}'); - - return sb.toString(); - } - private static String toString(JSONArray array, String indent, int indentFactor, JSONOptions options) { - StringBuilder sb = new StringBuilder(); - - String childIndent = indent + " ".repeat(indentFactor); - - sb.append('['); - - for(Object value : array) { - if(sb.length() != 1) - sb.append(','); - - if(indentFactor > 0) - sb.append('\n').append(childIndent); - - sb.append(toString(value, childIndent, indentFactor, options)); - } - - if(indentFactor > 0) - sb.append('\n').append(indent); - - sb.append(']'); - - return sb.toString(); - } - - private static String toString(Object value, String indent, int indentFactor, JSONOptions options) { - if(value == null) - return "null"; - - if(value instanceof JSONObject) - return toString((JSONObject) value, indent, indentFactor, options); - - if(value instanceof JSONArray) - return toString((JSONArray) value, indent, indentFactor, options); - - if(value instanceof String) - return quote((String) value, options); - - if(value instanceof Instant) { - Instant instant = (Instant) value; - - if(options.isStringifyUnixInstants()) - return String.valueOf(instant.getEpochSecond()); - - return quote(instant.toString(), options); - } - - if(value instanceof Double) { - double d = (Double) value; - - if(!options.isAllowNaN() && Double.isNaN(d)) - throw new JSONException("Illegal NaN in JSON"); - - if(!options.isAllowInfinity() && Double.isInfinite(d)) - throw new JSONException("Illegal Infinity in JSON"); - } - - return String.valueOf(value); - } - - static String quote(String string) { - return quote(string, null); - } - - private static String quote(String string, JSONOptions options) { - options = options == null ? - JSONOptions.getDefaultOptions() : options; - - if(string == null || string.isEmpty()) - return options.isQuoteSingle() ? "''" : "\"\""; - - final char qt = options.isQuoteSingle() ? '\'' : '"'; - - StringBuilder quoted = new StringBuilder(string.length() + 2); - - quoted.append(qt); - - for(char c : string.toCharArray()) { - if(c == qt) { - quoted.append('\\'); - quoted.append(c); - continue; - } - - switch(c) { - case '\\': - quoted.append("\\\\"); - break; - case '\b': - quoted.append("\\b"); - break; - case '\f': - quoted.append("\\f"); - break; - case '\n': - quoted.append("\\n"); - break; - case '\r': - quoted.append("\\r"); - break; - case '\t': - quoted.append("\\t"); - break; - case 0x0B: // Vertical Tab - quoted.append("\\v"); - break; - default: - // escape non-graphical characters (https://www.unicode.org/versions/Unicode13.0.0/ch02.pdf#G286941) - switch(Character.getType(c)) { - case Character.FORMAT: - case Character.LINE_SEPARATOR: - case Character.PARAGRAPH_SEPARATOR: - case Character.CONTROL: - case Character.PRIVATE_USE: - case Character.SURROGATE: - case Character.UNASSIGNED: - quoted.append("\\u"); - quoted.append(String.format("%04X", c)); - break; - default: - quoted.append(c); - break; - } - } - } - - quoted.append(qt); - - return quoted.toString(); - } - -} diff --git a/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Array.kt b/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Array.kt new file mode 100644 index 0000000..74e0b1c --- /dev/null +++ b/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Array.kt @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2021 SyntaxError404 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package at.syntaxerror.json5 + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement + + +/** + * A JSONArray is an array structure capable of holding multiple values, including other JSONArrays + * and [JSONObjects][DecodeJson5Object] + * + * @author SyntaxError404 + */ +class DecodeJson5Array { + + fun decode(parser: JSONParser): JsonArray { + val content: MutableList = mutableListOf() + + if (parser.nextClean() != '[') { + throw parser.createSyntaxException("A JSONArray must begin with '['") + } + while (true) { + var c: Char = parser.nextClean() + when (c) { + Char.MIN_VALUE -> throw parser.createSyntaxException("A JSONArray must end with ']'") + ']' -> break // finish parsing this array + else -> parser.back() + } + val value = parser.nextValue() + content.add(value) + c = parser.nextClean() + when { + c == ']' -> break // finish parsing this array + c != ',' -> throw parser.createSyntaxException("Expected ',' or ']' after value, got '$c' instead") + } + } + + return JsonArray(content) + } + +} diff --git a/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Object.kt b/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Object.kt new file mode 100644 index 0000000..b53c23e --- /dev/null +++ b/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Object.kt @@ -0,0 +1,76 @@ +/* + * MIT License + * + * Copyright (c) 2021 SyntaxError404 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package at.syntaxerror.json5 + +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +/** + * A JSONObject is a map (key-value) structure capable of holding multiple values, including other + * [JSONArrays][DecodeJson5Array] and JSONObjects + * + * @author SyntaxError404 + */ +class DecodeJson5Object( + private val j5: Json5Module, +) { + + fun decode(parser: JSONParser): JsonObject { + + val content: MutableMap = mutableMapOf() + + var c: Char + var key: String + if (parser.nextClean() != '{') { + throw parser.createSyntaxException("A JSONObject must begin with '{'") + } + while (true) { + c = parser.nextClean() + key = when (c) { + Char.MIN_VALUE -> throw parser.createSyntaxException("A JSONObject must end with '}'") + '}' -> break // end of object + else -> { + parser.back() + parser.nextMemberName() + } + } + if (content.containsKey(key)) { + throw JSONException("Duplicate key ${j5.stringify.escapeString(key)}") + } + c = parser.nextClean() + if (c != ':') { + throw parser.createSyntaxException("Expected ':' after a key, got '$c' instead") + } + val value = parser.nextValue() + content[key] = value + c = parser.nextClean() + when { + c == '}' -> break // end of object + c != ',' -> throw parser.createSyntaxException("Expected ',' or '}' after value, got '$c' instead") + } + } + + return JsonObject(content) + } +} diff --git a/src/main/java/module-info.java b/src/main/kotlin/at/syntaxerror/json5/JSONException.kt old mode 100755 new mode 100644 similarity index 68% rename from src/main/java/module-info.java rename to src/main/kotlin/at/syntaxerror/json5/JSONException.kt index f3587fb..14d8836 --- a/src/main/java/module-info.java +++ b/src/main/kotlin/at/syntaxerror/json5/JSONException.kt @@ -1,30 +1,42 @@ -/** - *
+/*
  * MIT License
- * 
+ *
  * Copyright (c) 2021 SyntaxError404
- * 
+ *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
  * in the Software without restriction, including without limitation the rights
  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  * copies of the Software, and to permit persons to whom the Software is
  * furnished to do so, subject to the following conditions:
- * 
+ *
  * The above copyright notice and this permission notice shall be included in all
  * copies or substantial portions of the Software.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  * SOFTWARE.
- * 
*/ -module json5 { - requires lombok; - - exports at.syntaxerror.json5; -} \ No newline at end of file +package at.syntaxerror.json5 + +/** + * An exception used by the JSON5 for Java Library if something went wrong + * + * @author SyntaxError404 + * @version 1.0.0 + */ +open class JSONException( + message: String, + cause: Throwable? = null, +) : RuntimeException(message, cause) { + + class SyntaxError( + message: String, + cause: Throwable? = null, + ) : JSONException(message, cause) + +} diff --git a/src/main/kotlin/at/syntaxerror/json5/JSONOptions.kt b/src/main/kotlin/at/syntaxerror/json5/JSONOptions.kt new file mode 100644 index 0000000..11b48e9 --- /dev/null +++ b/src/main/kotlin/at/syntaxerror/json5/JSONOptions.kt @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2021 SyntaxError404 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package at.syntaxerror.json5 + +/** + * This class used is used to customize the behaviour of [parsing][JSONParser] and [stringifying][JSONStringify] + * + * @author SyntaxError404 + * @since 1.1.0 + */ +data class JSONOptions( + /** + * Whether `NaN` should be allowed as a number + * + * Default: `true` + */ + var allowNaN: Boolean = true, + + /** + * Whether `Infinity` should be allowed as a number. + * This applies to both `+Infinity` and `-Infinity` + * + * Default: `true` + */ + var allowInfinity: Boolean = true, + + /** + * Whether invalid unicode surrogate pairs should be allowed + * + * Default: `true` + * + * *This is a [Parser][JSONParser]-only option* + */ + var allowInvalidSurrogates: Boolean = true, + + /** + * Whether string should be single-quoted (`'`) instead of double-quoted (`"`). + * This also includes a [JSONObject's][DecodeJson5Object] member names + * + * Default: `false` + * + * *This is a [Stringify][JSONStringify]-only option* + */ + var quoteSingle: Boolean = false, + + var indentFactor: UInt = 2u +) diff --git a/src/main/kotlin/at/syntaxerror/json5/JSONParser.kt b/src/main/kotlin/at/syntaxerror/json5/JSONParser.kt new file mode 100644 index 0000000..3eee827 --- /dev/null +++ b/src/main/kotlin/at/syntaxerror/json5/JSONParser.kt @@ -0,0 +1,457 @@ +/* + * MIT License + * + * Copyright (c) 2021 SyntaxError404 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package at.syntaxerror.json5 + +import at.syntaxerror.json5.JSONException.SyntaxError +import java.io.BufferedReader +import java.io.Reader +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive + +/** + * A JSONParser is used to convert a source string into tokens, which then are used to construct + * [JSONObjects][DecodeJson5Object] and [JSONArrays][DecodeJson5Array] + * + * The reader is not [closed][Reader.close] + * + * @author SyntaxError404 + */ +class JSONParser( + reader: Reader, + private val j5: Json5Module, +) { + + private val reader: Reader = if (reader.markSupported()) reader else BufferedReader(reader) + /** whether the end of the file has been reached */ + private var eof: Boolean = false + /** whether the current character should be re-read */ + private var back: Boolean = false + /** the absolute position in the string */ + private var index: Long = -1 + /** the relative position in the line */ + private var character: Long = 0 + /** the line number */ + private var line: Long = 1 + /** the previous character */ + private var previous: Char = Char.MIN_VALUE + /** the current character */ + private var current: Char = Char.MIN_VALUE + + private val nextCleanToDelimiters: String = ",]}" + + private fun more(): Boolean { + return if (back || eof) { + back && !eof + } else peek().code > 0 + } + + /** Forces the parser to re-read the last character */ + fun back() { + back = true + } + + private fun peek(): Char { + if (eof) { + return Char.MIN_VALUE + } + val c: Int + try { + reader.mark(1) + c = reader.read() + reader.reset() + } catch (e: Exception) { + throw createSyntaxException("Could not peek from source", e) + } + return if (c == -1) Char.MIN_VALUE else c.toChar() + } + + private operator fun next(): Char { + if (back) { + back = false + return current + } + val c: Int = try { + reader.read() + } catch (e: Exception) { + throw createSyntaxException("Could not read from source", e) + } + if (c < 0) { + eof = true + return Char.MIN_VALUE + } + previous = current + current = c.toChar() + index++ + if (isLineTerminator(current) && (current != '\n' || previous != '\r')) { + line++ + character = 0 + } else { + character++ + } + return current + } + + // https://262.ecma-international.org/5.1/#sec-7.3 + private fun isLineTerminator(c: Char): Boolean { + return when (c) { + '\n', '\r', '\u2028', '\u2029' -> true + else -> false + } + } + + // https://spec.json5.org/#white-space + private fun isWhitespace(c: Char): Boolean { + return when (c) { + '\t', '\n', '\u000B', Json5EscapeSequence.FormFeed.char, + '\r', ' ', '\u00A0', '\u2028', '\u2029', '\uFEFF' -> true + else -> + // Unicode category "Zs" (space separators) + Character.getType(c) == Character.SPACE_SEPARATOR.toInt() + } + } + + private fun nextMultiLineComment() { + while (true) { + val n = next() + if (n == '*' && peek() == '/') { + next() + return + } + } + } + + private fun nextSingleLineComment() { + while (true) { + val n = next() + if (isLineTerminator(n) || n.code == 0) { + return + } + } + } + /** + * Reads until encountering a character that is not a whitespace according to the + * [JSON5 Specification](https://spec.json5.org/#white-space) + * + * @return a non-whitespace character, or `0` if the end of the stream has been reached + */ + fun nextClean(): Char { + while (true) { + if (!more()) { + throw createSyntaxException("Unexpected end of data") + } + val n = next() + if (n == '/') { + when (peek()) { + '*' -> { + next() + nextMultiLineComment() + } + '/' -> { + next() + nextSingleLineComment() + } + else -> { + return n + } + } + } else if (!isWhitespace(n)) { + return n + } + } + } + + private fun nextCleanTo(delimiters: String = nextCleanToDelimiters): String { + val result = StringBuilder() + while (true) { + if (!more()) { + throw createSyntaxException("Unexpected end of data") + } + val n = nextClean() + if (delimiters.indexOf(n) > -1 || isWhitespace(n)) { + back() + break + } + result.append(n) + } + return result.toString() + } + + private fun deHex(c: Char): Int? { + return when (c) { + in '0'..'9' -> c - '0' + in 'a'..'f' -> c - 'a' + 0xA + in 'A'..'F' -> c - 'A' + 0xA + else -> null + } + } + + private fun unicodeEscape(member: Boolean, part: Boolean): Char { + var value = "" + var codepoint = 0 + for (i in 0..3) { + val n = next() + value += n + val hex = deHex(n) + ?: throw createSyntaxException("Illegal unicode escape sequence '\\u$value' in ${if (member) "key" else "string"}") + codepoint = codepoint or (hex shl (3 - i shl 2)) + } + if (member && !isMemberNameChar(codepoint.toChar(), part)) { + throw createSyntaxException("Illegal unicode escape sequence '\\u$value' in key") + } + return codepoint.toChar() + } + + private fun checkSurrogate(hi: Char, lo: Char) { + if (j5.options.allowInvalidSurrogates) { + return + } + if (!Character.isHighSurrogate(hi) || !Character.isLowSurrogate(lo)) { + return + } + if (!Character.isSurrogatePair(hi, lo)) { + throw createSyntaxException( + String.format( + "Invalid surrogate pair: U+%04X and U+%04X", + hi, lo + ) + ) + } + } + + // https://spec.json5.org/#prod-JSON5String + private fun nextString(quote: Char): String { + val result = StringBuilder() + var value: String + var codepoint: Int + var n = 0.toChar() + var prev: Char + while (true) { + if (!more()) { + throw createSyntaxException("Unexpected end of data") + } + prev = n + n = next() + if (n == quote) { + break + } + if (isLineTerminator(n) && n.code != 0x2028 && n.code != 0x2029) { + throw createSyntaxException("Unescaped line terminator in string") + } + if (n == '\\') { + n = next() + if (isLineTerminator(n)) { + if (n == '\r' && peek() == '\n') { + next() + } + // escaped line terminator/ line continuation + continue + } else { + when (n) { + '\'', '"', '\\' -> { + result.append(n) + continue + } + 'b' -> { + result.append('\b') + continue + } + 'f' -> { + result.append(Json5EscapeSequence.FormFeed.char) + continue + } + 'n' -> { + result.append('\n') + continue + } + 'r' -> { + result.append('\r') + continue + } + 't' -> { + result.append('\t') + continue + } + 'v' -> { + result.append(0x0B.toChar()) + continue + } + '0' -> { + val p = peek() + if (p.isDigit()) { + throw createSyntaxException("Illegal escape sequence '\\0$p'") + } + result.append(0.toChar()) + continue + } + 'x' -> { + value = "" + codepoint = 0 + var i = 0 + while (i < 2) { + n = next() + value += n + val hex = deHex(n) + ?: throw createSyntaxException("Illegal hex escape sequence '\\x$value' in string") + codepoint = codepoint or (hex shl (1 - i shl 2)) + ++i + } + n = codepoint.toChar() + } + 'u' -> n = unicodeEscape(member = false, part = false) + else -> if (n.isDigit()) { + throw SyntaxError("Illegal escape sequence '\\$n'") + } + } + } + } + checkSurrogate(prev, n) + result.append(n) + } + return result.toString() + } + + private fun isMemberNameChar(n: Char, isNotEmpty: Boolean): Boolean { + if (n == '$' || n == '_' || n.code == 0x200C || n.code == 0x200D) { + return true + } + + return when (n.category) { + + CharCategory.UPPERCASE_LETTER, + CharCategory.LOWERCASE_LETTER, + CharCategory.TITLECASE_LETTER, + CharCategory.MODIFIER_LETTER, + CharCategory.OTHER_LETTER, + CharCategory.LETTER_NUMBER -> return true + + CharCategory.NON_SPACING_MARK, + CharCategory.COMBINING_SPACING_MARK, + CharCategory.DECIMAL_DIGIT_NUMBER, + CharCategory.CONNECTOR_PUNCTUATION -> isNotEmpty + + else -> return false + } + } + + /** + * Reads a member name from the source according to the + * [JSON5 Specification](https://spec.json5.org/#prod-JSON5MemberName) + */ + fun nextMemberName(): String { + val result = StringBuilder() + var prev: Char + var n = next() + if (n == '"' || n == '\'') { + return nextString(n) + } + back() + n = 0.toChar() + while (true) { + if (!more()) { + throw createSyntaxException("Unexpected end of data") + } + val isNotEmpty = result.isNotEmpty() + prev = n + n = next() + if (n == '\\') { // unicode escape sequence + n = next() + if (n != 'u') { + throw createSyntaxException("Illegal escape sequence '\\$n' in key") + } + n = unicodeEscape(true, isNotEmpty) + } else if (!isMemberNameChar(n, isNotEmpty)) { + back() + break + } + checkSurrogate(prev, n) + result.append(n) + } + if (result.isEmpty()) { + throw createSyntaxException("Empty key") + } + return result.toString() + } + + /** + * Reads a value from the source according to the + * [JSON5 Specification](https://spec.json5.org/#prod-JSON5Value) + */ + fun nextValue(): JsonElement { + when (val n = nextClean()) { + '"', '\'' -> { + val string = nextString(n) + return JsonPrimitive(string) + } + '{' -> { + back() + return j5.objectDecoder.decode(this) + } + '[' -> { + back() + return j5.arrayDecoder.decode(this) + } + } + back() + val string = nextCleanTo() + return when { + string == "null" -> JsonNull + PATTERN_BOOLEAN.matches(string) -> JsonPrimitive(string == "true") + + // val bigint = BigInteger(string) + // return bigint + PATTERN_NUMBER_INTEGER.matches(string) -> JsonPrimitive(string.toLong()) + + PATTERN_NUMBER_FLOAT.matches(string) + || PATTERN_NUMBER_NON_FINITE.matches(string) -> { + try { + JsonPrimitive(string.toDouble()) + } catch (e: NumberFormatException) { + throw createSyntaxException("could not parse number '$string'") + } + } + PATTERN_NUMBER_HEX.matches(string) -> { + val hex = string.uppercase().split("0X").joinToString("") + JsonPrimitive(hex.toLong(16)) + } + else -> throw JSONException("Illegal value '$string'") + } + } + + fun createSyntaxException(message: String, cause: Throwable? = null): SyntaxError = + SyntaxError("$message, at index $index, character $character, line $line]", cause) + + companion object { + private val PATTERN_BOOLEAN = + Regex("true|false") + private val PATTERN_NUMBER_FLOAT = + Regex("[+-]?((0|[1-9]\\d*)(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?") + private val PATTERN_NUMBER_INTEGER = + Regex("[+-]?(0|[1-9]\\d*)") + private val PATTERN_NUMBER_HEX = + Regex("[+-]?0[xX][0-9a-fA-F]+") + private val PATTERN_NUMBER_NON_FINITE = + Regex("[+-]?(Infinity|NaN)") + } +} diff --git a/src/main/kotlin/at/syntaxerror/json5/JSONStringify.kt b/src/main/kotlin/at/syntaxerror/json5/JSONStringify.kt new file mode 100644 index 0000000..71a09b4 --- /dev/null +++ b/src/main/kotlin/at/syntaxerror/json5/JSONStringify.kt @@ -0,0 +1,192 @@ +/* + * MIT License + * + * Copyright (c) 2021 SyntaxError404 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package at.syntaxerror.json5 + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject + +/** + * A utility class for serializing [JSONObjects][DecodeJson5Object] and [JSONArrays][DecodeJson5Array] + * into their string representations + * + * @author SyntaxError404 + */ +class JSONStringify( + private val options: JSONOptions +) { + + private val quoteToken = if (options.quoteSingle) '\'' else '"' + private val emptyString = "$quoteToken$quoteToken" + private val indentFactor = options.indentFactor + + /** + * Converts a JSONObject into its string representation. The indentation factor enables + * pretty-printing and defines how many spaces (' ') should be placed before each key/value pair. + * A factor of `< 1` disables pretty-printing and discards any optional whitespace + * characters. + * + * `indentFactor = 2`: + * ``` + * { + * "key0": "value0", + * "key1": { + * "nested": 123 + * }, + * "key2": false + * } + * ``` + * + * `indentFactor = 0`: + * + * ``` + * {"key0":"value0","key1":{"nested":123},"key2":false} + * ``` + */ + fun encodeObject( + jsonObject: JsonObject, + indent: String = "", + ): String { + val childIndent = indent + " ".repeat(indentFactor.toInt()) + val isIndented = indentFactor > 0u + + val sb = StringBuilder() + sb.append('{') + jsonObject.forEach { (key, value) -> + if (sb.length != 1) { + sb.append(',') + } + if (isIndented) { + sb.append('\n').append(childIndent) + } + sb.append(escapeString(key)).append(':') + if (isIndented) { + sb.append(' ') + } + sb.append(encode(value, childIndent)) + } + if (isIndented) { + sb.append('\n').append(indent) + } + sb.append('}') + return sb.toString() + } + + /** + * Converts a JSONArray into its string representation. The indentation factor enables + * pretty-printing and defines how many spaces (' ') should be placed before each value. A factor + * of `< 1` disables pretty-printing and discards any optional whitespace characters. + * + * + * `indentFactor = 2`: + * ``` + * [ + * "value", + * { + * "nested": 123 + * }, + * false + * ] + * ``` + * + * `indentFactor = 0`: + * ``` + * ["value",{"nested":123},false] + * ``` + */ + fun encodeArray( + array: JsonArray, + indent: String = "", + ): String { + val childIndent = indent + " ".repeat(indentFactor.toInt()) + val isIndented = indentFactor > 0u + + val sb = StringBuilder() + sb.append('[') + for (value in array) { + if (sb.length != 1) { + sb.append(',') + } + if (isIndented) { + sb.append('\n').append(childIndent) + } + sb.append(encode(value, childIndent)) + } + if (isIndented) { + sb.append('\n').append(indent) + } + sb.append(']') + return sb.toString() + } + + private fun encode( + value: Any?, + indent: String, + ): String { + return when (value) { + null -> "null" + is JsonObject -> encodeObject(value, indent) + is JsonArray -> encodeArray(value, indent) + is String -> escapeString(value) + is Double -> { + when { + !options.allowNaN && value.isNaN() -> throw JSONException("Illegal NaN in JSON") + !options.allowInfinity && value.isInfinite() -> throw JSONException("Illegal Infinity in JSON") + else -> value.toString() + } + } + else -> value.toString() + } + } + + fun escapeString(string: String?): String { + return if (string.isNullOrEmpty()) { + emptyString + } else { + string + .asSequence() + .joinToString( + separator = "", + prefix = quoteToken.toString(), + postfix = quoteToken.toString() + ) { c: Char -> + + val formattedChar: String? = when (c) { + quoteToken -> "\\$quoteToken" + in Json5EscapeSequence.escapableChars -> Json5EscapeSequence.asEscapedString(c) + else -> when (c.category) { + CharCategory.FORMAT, + CharCategory.LINE_SEPARATOR, + CharCategory.PARAGRAPH_SEPARATOR, + CharCategory.CONTROL, + CharCategory.PRIVATE_USE, + CharCategory.SURROGATE, + CharCategory.UNASSIGNED -> String.format("\\u%04X", c) + else -> null + } + } + formattedChar ?: c.toString() + } + } + } +} diff --git a/src/main/kotlin/at/syntaxerror/json5/Json5EscapeSequence.kt b/src/main/kotlin/at/syntaxerror/json5/Json5EscapeSequence.kt new file mode 100644 index 0000000..da35df9 --- /dev/null +++ b/src/main/kotlin/at/syntaxerror/json5/Json5EscapeSequence.kt @@ -0,0 +1,31 @@ +package at.syntaxerror.json5 + +/** https://spec.json5.org/#escapes */ +enum class Json5EscapeSequence( + val char: Char, + val escaped: String, +) { + //@formatter:off + Apostrophe ( '\u0027', "\\'" ), + QuotationMark ( '\u0022', "\\\"" ), + ReverseSolidus ( '\u005C', "\\\\" ), + Backspace ( '\u0008', "\\b" ), + FormFeed ( '\u000C', "\\f" ), + LineFeed ( '\u000A', "\\n" ), + CarriageReturn ( '\u000D', "\\r" ), + HorizontalTab ( '\u0009', "\\t" ), + VerticalTab ( '\u000B', "\\v" ), + Null ( '\u0000', "\\0" ), + //@formatter:on + ; + + companion object { + private val mapCharToRepresentation = values().associate { it.char to it.escaped } + + val escapableChars = values().map { it.char } + + fun asEscapedString(char: Char): String? = mapCharToRepresentation[char] + + fun isEscapable(char: Char) = mapCharToRepresentation.containsKey(char) + } +} diff --git a/src/main/kotlin/at/syntaxerror/json5/Json5Module.kt b/src/main/kotlin/at/syntaxerror/json5/Json5Module.kt new file mode 100644 index 0000000..2367e6a --- /dev/null +++ b/src/main/kotlin/at/syntaxerror/json5/Json5Module.kt @@ -0,0 +1,46 @@ +package at.syntaxerror.json5 + + +import java.io.InputStream +import java.io.InputStreamReader +import java.io.Reader +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject + +class Json5Module( + configure: JSONOptions.() -> Unit = {} +) { + internal val options: JSONOptions = JSONOptions() + internal val stringify: JSONStringify = JSONStringify(options) + + internal val arrayDecoder = DecodeJson5Array() + internal val objectDecoder = DecodeJson5Object(this) + + init { + options.configure() + } + + fun decodeObject(string: String): JsonObject = decodeObject(string.reader()) + fun decodeObject(stream: InputStream): JsonObject = decodeObject(InputStreamReader(stream)) + + fun decodeObject(reader: Reader): JsonObject { + return reader.use { r -> + val parser = JSONParser(r, this) + objectDecoder.decode(parser) + } + } + + fun decodeArray(string: String): JsonArray = decodeArray(string.reader()) + fun decodeArray(stream: InputStream): JsonArray = decodeArray(InputStreamReader(stream)) + + fun decodeArray(reader: Reader): JsonArray { + return reader.use { r -> + val parser = JSONParser(r, this) + arrayDecoder.decode(parser) + } + } + + fun encodeToString(array: JsonArray) = stringify.encodeArray(array) + fun encodeToString(jsonObject: JsonObject) = stringify.encodeObject(jsonObject) + +} diff --git a/src/test/java/json5/UnitTests.java b/src/test/java/json5/UnitTests.java deleted file mode 100755 index 928a5b5..0000000 --- a/src/test/java/json5/UnitTests.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2021 SyntaxError404 - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package json5; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.time.Instant; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import at.syntaxerror.json5.JSONArray; -import at.syntaxerror.json5.JSONObject; -import at.syntaxerror.json5.JSONOptions; -import at.syntaxerror.json5.JSONParser; - -/** - * @author SyntaxError404 - * - */ -class UnitTests { - - @BeforeAll - static void setUpBeforeClass() throws Exception { - // compile regex patterns - JSONParser.class.toString(); - } - - @Test - void testDoubleQuoted() { - assertTrue( - parse("{ a: \"Test \\\" 123\" }") - .getString("a") - .equals("Test \" 123") - ); - } - - @Test - void testSingleQuoted() { - assertTrue( - parse("{ a: 'Test \\' 123\' }") - .getString("a") - .equals("Test ' 123") - ); - } - - @Test - void testMixedQuoted() { - assertTrue( - parse("{ a: \"Test \\' 123\" }") - .getString("a") - .equals("Test ' 123") - ); - } - - @Test - void testStringify() { - JSONOptions.setDefaultOptions( - JSONOptions.builder() - .stringifyUnixInstants(true) - .build() - ); - - JSONObject json = new JSONObject(); - - json.set("a", (Object) null); - json.set("b", false); - json.set("c", true); - json.set("d", new JSONObject()); - json.set("e", new JSONArray()); - json.set("f", Double.NaN); - json.set("g", 123e+45); - json.set("h", (float) -123e45); - json.set("i", 123L); - json.set("j", "Lorem Ipsum"); - json.set("k", Instant.now()); - - assertEquals( - json.toString(), - parse(json.toString()).toString() - ); - } - - @Test - void testEscapes() { - assertTrue( - parse("{ a: \"\\n\\r\\f\\b\\t\\v\\0\\u12Fa\\x7F\" }") - .getString("a") - .equals("\n\r\f\b\t\u000B\0\u12Fa\u007F") - ); - } - - @Test - void testMemberName() { - assertTrue( - parse("{ $Lorem\\u0041_Ipsum123指事字: 0 }") - .has("$LoremA_Ipsum123指事字") - ); - } - - @Test - void testMultiComments() { - assertTrue( - parse("/**/{/**/a/**/:/**/'b'/**/}/**/") - .has("a") - ); - } - - @Test - void testSingleComments() { - assertTrue( - parse("// test\n{ // lorem ipsum\n a: 'b'\n// test\n}// test") - .has("a") - ); - } - - /** @since 1.1.0 */ - @Test - void testInstant() { - assertTrue( - parse("{a:1338150759534}") - .isInstant("a") - ); - - assertEquals( - parse("{a:1338150759534}") - .getLong("a"), - 1338150759534L - ); - - assertEquals( - parse("{a:'2001-09-09T01:46:40Z'}") - .getString("a"), - "2001-09-09T01:46:40Z" - ); - } - - /** @since 1.1.0 */ - @Test - void testHex() { - assertEquals( - 0xCAFEBABEL, - parse("{a: 0xCAFEBABE}") - .getLong("a") - ); - } - - @Test - void testSpecial() { - assertTrue( - Double.isNaN( - parse("{a: +NaN}") - .getDouble("a") - ) - ); - - assertTrue( - Double.isInfinite( - parse("{a: -Infinity}") - .getDouble("a") - ) - ); - } - - JSONObject parse(String str) { - return new JSONObject(new JSONParser(str)); - } - -} diff --git a/src/test/kotlin/at/syntaxerror/json5/JSONArrayTests.kt b/src/test/kotlin/at/syntaxerror/json5/JSONArrayTests.kt new file mode 100644 index 0000000..7d3b16d --- /dev/null +++ b/src/test/kotlin/at/syntaxerror/json5/JSONArrayTests.kt @@ -0,0 +1,91 @@ +package at.syntaxerror.json5 + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainInOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import io.kotest.property.Arb +import io.kotest.property.arbitrary.Codepoint +import io.kotest.property.arbitrary.codepoints +import io.kotest.property.arbitrary.filter +import io.kotest.property.checkAll +import kotlinx.serialization.json.JsonPrimitive + +class JSONArrayTests : BehaviorSpec({ + + val j5 = Json5Module { } + + Given("a JSON5 array should start with '['") { + val validArrayStarter = '[' + + And("an array with a valid starting character") { + When("the valid array is parsed") { + + val valid = """ + $validArrayStarter + "I'm a string", + 10, + ] + """.trimIndent() + + val result = j5.decodeArray(valid) + + Then("expect the array can be pretty-printed") { + val pretty = j5.encodeToString(result) + pretty shouldBe + //language=JSON5 + """ + [ + "I'm a string", + 10 + ] + """.trimIndent() + } + Then("expect the array can be compact-printed") { + //language=JSON5 + result.toString() shouldBe """["I'm a string",10]""" + } + Then("expect the array matches an equivalent List") { + + assertSoftly(result) { + withClue(joinToString { it.javaClass.simpleName }) { + shouldHaveSize(2) + shouldContainInOrder(JsonPrimitive("I'm a string"), JsonPrimitive(10)) + } + } + } + } + } + + And("an array with an invalid starting character") { + val invalidArrayStarterArb = + Arb.codepoints().filter { it != Codepoint(validArrayStarter.code) } + When("the invalid array is parsed") { + + Then("expect a syntax exception") { + checkAll(invalidArrayStarterArb) { invalidArrayStarter -> + + val invalid = """ + $invalidArrayStarter + "key": "value" + ] + """.trimIndent() + + val thrown = shouldThrow { + j5.decodeArray(invalid) + } + + assertSoftly(thrown.message) { + shouldContain("must begin with") + shouldContain("$validArrayStarter") + } + } + } + } + } + } +}) diff --git a/src/test/kotlin/at/syntaxerror/json5/JSONParserTests.kt b/src/test/kotlin/at/syntaxerror/json5/JSONParserTests.kt new file mode 100644 index 0000000..5c39d44 --- /dev/null +++ b/src/test/kotlin/at/syntaxerror/json5/JSONParserTests.kt @@ -0,0 +1,102 @@ +package at.syntaxerror.json5 + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.collections.shouldContainInOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.comparables.shouldBeEqualComparingTo +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldMatch +import io.kotest.matchers.types.shouldBeInstanceOf +import io.kotest.property.Arb +import io.kotest.property.Exhaustive +import io.kotest.property.arbitrary.bind +import io.kotest.property.arbitrary.positiveLong +import io.kotest.property.checkAll +import io.kotest.property.exhaustive.collection +import java.math.BigInteger +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull +import org.intellij.lang.annotations.Language + +class JSONParserTests : BehaviorSpec({ + + val j5 = Json5Module() + + Given("a valid json5 array") { + @Language("JSON5") + val valid = """ + [ + "I'm a string", + 10, + ] + """.trimIndent() + When("JSONParser parses the array as a stream") { + + val parser = JSONParser(valid.reader(), j5) + val parsedValue = parser.nextValue() + + Then("expect the value is a JSON Array") { + assertSoftly(parsedValue) { + shouldBeInstanceOf() + shouldHaveSize(2) + shouldContainInOrder(JsonPrimitive("I'm a string"), JsonPrimitive(10)) + } + } + Then("expect there are no more values") { + val thrown = shouldThrow { + parser.nextValue() + } + assertSoftly(thrown.message) { + shouldContain("Unexpected end of data") + } + } + } + } + + Given("an initial number encoded as a hexadecimal value") { + + When("JSONParser parsers an Json5 array that contains the hexadecimal value") { + + Then("expect the parsed value equals the initial number") { + + val arbHex = Arb.bind( + Exhaustive.collection(setOf("+", "", "-")), + Exhaustive.collection(setOf("0x", "0X")), + Arb.positiveLong(), + ) { sign, prefix, long -> + val hex = long.toString(16) + val expected = BigInteger.valueOf(long).run { + if (sign == "-") negate() else abs() + } + expected.longValueExact() to (sign + prefix + hex) + } + + checkAll(arbHex) { (expectedNumber, hexString) -> + hexString shouldMatch Regex("[+-]?0[xX][0-9a-fA-F]+") + + val json5Array = """ [ $hexString ] """ + + val parser = JSONParser(json5Array.reader(), j5) + val parsedValue = parser.nextValue() + + assertSoftly(parsedValue) { + shouldBeInstanceOf() + shouldHaveSize(1) + val parsedHex = elementAt(0) + parsedHex.shouldBeInstanceOf() + assertSoftly(parsedHex.jsonPrimitive.longOrNull) { + shouldNotBeNull() + shouldBeEqualComparingTo(expectedNumber) + } + + } + } + } + } + } +}) diff --git a/src/test/kotlin/at/syntaxerror/json5/JsonToJson5Test.kt b/src/test/kotlin/at/syntaxerror/json5/JsonToJson5Test.kt new file mode 100644 index 0000000..024e6be --- /dev/null +++ b/src/test/kotlin/at/syntaxerror/json5/JsonToJson5Test.kt @@ -0,0 +1,119 @@ +package at.syntaxerror.json5 + +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray + +class JsonToJson5Test : BehaviorSpec({ + + val j5 = Json5Module() + + Given("A string-encoded JSON object") { + + //language=JSON + val json = """ + { + "widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "name": "sun1", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "name": "text1", + "hOffset": 250, + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } + } + } + """.trimIndent() + + Then("expect it can be converted to a JSON5 string") { + val jsonObject = j5.decodeObject(json) + val json5String = j5.encodeToString(jsonObject) + json5String shouldEqualJson json + } + } + + + Given("A string-encoded JSON5 object") { + + //language=JSON5 + val json5 = """ + { + // comments + unquoted: 'and you can quote me on that', + singleQuotes: 'I can use "double quotes" here', + lineBreaks: "Look, Mom! \ + No \\n's!", + hexadecimal: 0xdecaf, + leadingDecimalPoint: .8675309, + andTrailing: 8675309., + positiveSign: +1, + trailingComma: 'in objects', + andIn: [ + 'arrays', + ], + "backwardsCompatible": "with JSON", + } + """.trimIndent() + + val jsonObject: JsonObject = j5.decodeObject(json5) + + Then("expect it can be parsed to a JsonObject") { + jsonObject shouldBe buildJsonObject { + put("unquoted", "and you can quote me on that") + put("singleQuotes", """I can use "double quotes" here""") + put("lineBreaks", """Look, Mom! No \n's!""") + put("hexadecimal", 912559) + put("leadingDecimalPoint", 0.8675309) + put("andTrailing", 8675309.0) + put("positiveSign", 1) + put("trailingComma", "in objects") + putJsonArray("andIn") { add("arrays") } + put("backwardsCompatible", "with JSON") + } + } + + Then("expect it can be converted to a JSON string") { + + val jsonString = j5.encodeToString(jsonObject) + //language=JSON5 + jsonString shouldBe """ + { + "unquoted": "and you can quote me on that", + "singleQuotes": "I can use \"double quotes\" here", + "lineBreaks": "Look, Mom! No \\n's!", + "hexadecimal": 912559, + "leadingDecimalPoint": 0.8675309, + "andTrailing": 8675309.0, + "positiveSign": 1, + "trailingComma": "in objects", + "andIn": [ + "arrays" + ], + "backwardsCompatible": "with JSON" + } + """.trimIndent() + } + } +}) diff --git a/src/test/kotlin/at/syntaxerror/json5/UnitTests.kt b/src/test/kotlin/at/syntaxerror/json5/UnitTests.kt new file mode 100644 index 0000000..22b47ad --- /dev/null +++ b/src/test/kotlin/at/syntaxerror/json5/UnitTests.kt @@ -0,0 +1,235 @@ +/* + * MIT License + * + * Copyright (c) 2021 SyntaxError404 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package at.syntaxerror.json5 + +import java.time.Instant +import java.util.stream.Stream +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray +import kotlinx.serialization.json.putJsonObject +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assertions.assertAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.ValueSource + +/** + * @author SyntaxError404 + */ +internal class UnitTests { + + private val j5 = Json5Module() + + @ParameterizedTest(name = "{index}: {0}") + @CsvSource( + useHeadersInDisplayName = true, + delimiter = '|', + quoteCharacter = Char.MIN_VALUE, // prevent JUnit from interfering with our quotes + textBlock = + """ +Title | Input value | Expected value +#-------------------------------------|-----------------|----------------- +double quote in double-quoted string | "Test \" 123" | Test " 123 +double quote in single-quoted string | 'Test \" 123' | Test " 123 +single quote in double-quoted string | "Test \' 123" | Test ' 123 +single quote in single-quoted string | 'Test \' 123' | Test ' 123 +""" + ) + fun testDoubleQuoted(title: String, inputValue: String, expectedValue: String) { + assertTrue(title.isNotEmpty(), "dummy var - used for test name") + + val parsedObject = j5.decodeObject("""{ a: $inputValue }""") + + assertTrue(parsedObject.containsKey("a")) + assertEquals(expectedValue, parsedObject["a"]?.jsonPrimitive?.contentOrNull) + } + + @Test + fun testStringify() { + val jsonObject = buildJsonObject { + put("a", null as String?) + put("b", false) + put("c", true) + putJsonObject("d") {} + putJsonArray("e") {} + put("f", Double.NaN) + put("g", 123e+45) + put("h", (-123e45).toFloat()) + put("i", 123L) + put("j", "Lorem Ipsum") + put("k", Instant.ofEpochSecond(1639908193).toString()) + } + + @Language("JSON5") + val expected = + """ + { + "a": null, + "b": false, + "c": true, + "d": { + }, + "e": [ + ], + "f": NaN, + "g": 1.23E47, + "h": -Infinity, + "i": 123, + "j": "Lorem Ipsum", + "k": "2021-12-19T10:03:13Z" + } + """.trimIndent() + // TODO set up Instant encode/decode + // "k": 1639908193 + assertAll( + { assertEquals(expected, j5.encodeToString(jsonObject)) }, + { + val parsedValue = j5.decodeObject(expected) + assertEquals(expected, j5.encodeToString(parsedValue)) + }, + ) + } + + @TestFactory + fun `test escaped characters`(): Stream { + return listOf( + """ \n """ to '\n', + """ \r """ to '\r', + """ \u000c """ to '\u000c', + """ \b """ to '\b', + """ \t """ to '\t', + """ \v """ to '\u000B', + """ \0 """ to '\u0000', + """ \u12Fa """ to '\u12Fa', + """ \u007F """ to '\u007F', + ) + .map { (input, expectedChar) -> input.trim() to expectedChar } + .map { (input, expectedChar) -> + dynamicTest("expect escaped char '$input is mapped to actual char value") { + val parsedValue = j5.decodeObject("""{ a: "$input" }""") + assertTrue(parsedValue.containsKey("a")) + assertEquals(expectedChar.toString(), parsedValue["a"]?.jsonPrimitive?.contentOrNull) + } + }.stream() + } + + @Test + fun testEscapes() { + + val inputValue = """\n\r\u000c\b\t\v\0\u12Fa\x7F""" + val expectedValue = "\n\r\u000c\b\t\u000B\u0000\u12Fa\u007F" + + val parsedValue = j5.decodeObject("""{ a: "$inputValue" }""") + + assertTrue(parsedValue.containsKey("a")) + assertEquals(expectedValue, parsedValue["a"]?.jsonPrimitive?.contentOrNull) + } + + @Test + fun testMemberName() { + // note: requires UTF-8 + + val inputKey = "\$Lorem\\u0041_Ipsum123指事字" + val expectedKey = "\$LoremA_Ipsum123指事字" + + val parsedValue = j5.decodeObject("{ $inputKey: 0 }") + + assertTrue(parsedValue.containsKey(expectedKey)) + assertEquals(0, parsedValue[expectedKey]?.jsonPrimitive?.longOrNull) + } + + @ParameterizedTest + @ValueSource( + //language=JSON5 + strings = [ + """ + // test + { // lorem ipsum + a: 'b' + // test + }// test + """, + """ + /**/{ + /**/ a /**/: /**/'b' + /**/ + }/**/ + """, + ] + ) + fun testComments(inputJson: String) { + val parsedValue = j5.decodeObject(inputJson) + + assertTrue(parsedValue.containsKey("a")) + assertEquals("b", parsedValue["a"]?.jsonPrimitive?.contentOrNull) + } + + @Test + fun testHex() { + + val parsedObject = j5.decodeObject("""{ a: 0xCAFEBABE }""") + + assertTrue(parsedObject.containsKey("a")) + val actualValue = parsedObject["a"]?.jsonPrimitive?.longOrNull + assertEquals(0xCAFEBABE, actualValue) + } + + @ParameterizedTest + @ValueSource(strings = ["NaN", "-NaN", "+NaN"]) + fun `expect NaN value is parsed to NaN Double`(nanValue: String) { + + val jsonString = """ { a: $nanValue } """ + val parsedObject = j5.decodeObject(jsonString) + + assertTrue(parsedObject.containsKey("a")) + assertTrue( + parsedObject["a"]?.jsonPrimitive?.doubleOrNull?.isNaN() == true + ) + } + + @ParameterizedTest + @ValueSource(strings = ["Infinity", "-Infinity", "+Infinity"]) + fun `expect Infinity value is parsed to Infinite Double`(nanValue: String) { + + val jsonString = """ { a: $nanValue } """ + val parsedObject = j5.decodeObject(jsonString) + + assertTrue(parsedObject.containsKey("a")) + assertTrue( + parsedObject["a"]?.jsonPrimitive?.doubleOrNull?.isInfinite() == true + ) + } + +}