From f2cccb2414690462a3d10406c27b74b1bae9cd77 Mon Sep 17 00:00:00 2001 From: Peter Wall Date: Sat, 17 Aug 2024 13:31:42 +1000 Subject: [PATCH] Fixed bug in JSONString, minor changes --- CHANGELOG.md | 11 + README.md | 48 +- pom.xml | 2 +- src/main/kotlin/io/kjson/JSON.kt | 36 +- src/main/kotlin/io/kjson/JSONArray.kt | 11 +- src/main/kotlin/io/kjson/JSONBoolean.kt | 6 +- src/main/kotlin/io/kjson/JSONDecimal.kt | 6 +- src/main/kotlin/io/kjson/JSONInt.kt | 38 +- src/main/kotlin/io/kjson/JSONLong.kt | 32 +- src/main/kotlin/io/kjson/JSONNumber.kt | 15 + src/main/kotlin/io/kjson/JSONObject.kt | 107 +++- src/main/kotlin/io/kjson/JSONString.kt | 35 +- src/main/kotlin/io/kjson/JSONValue.kt | 57 +- .../kotlin/io/kjson/parser/ParseException.kt | 3 +- src/main/kotlin/io/kjson/parser/Parser.kt | 42 +- .../kotlin/io/kjson/util/AbstractBuilder.kt | 6 +- src/main/kotlin/io/kjson/util/LookupSet.kt | 2 + src/test/kotlin/io/kjson/JSONArrayTest.kt | 16 + src/test/kotlin/io/kjson/JSONNumberTest.kt | 55 ++ src/test/kotlin/io/kjson/JSONObjectTest.kt | 503 +++++++++++++++++- src/test/kotlin/io/kjson/JSONStringTest.kt | 3 +- src/test/kotlin/io/kjson/JSONValueTest.kt | 39 +- .../kotlin/io/kjson/testutil/ImportTest.kt | 42 ++ 23 files changed, 970 insertions(+), 145 deletions(-) create mode 100644 src/test/kotlin/io/kjson/JSONNumberTest.kt create mode 100644 src/test/kotlin/io/kjson/testutil/ImportTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e4587..bf5c931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). +## [9.1] - 2024-08-17 +### Changed +- `JSONString`: fixed bug in `toJSON()` +- `JSON`: added `toJSONArray()` and `toJSONObject()` extension functions +- `JSON`, `JSONObject`: added `of()` functions taking vararg array of `JSONObject.Property` +- `JSON`, `JSONObject`: added `DuplicateKeyOption` to functions creating `JSONObject` from `List` +- `JSONArray`, `JSONNumber`, `JSONObject`, `JSONValue`: added pseudo-constructor functions +- `JSONObject`: added `refersTo` infix function to create `JSONObject.Property` +- `JSONArray`, `JSONBoolean`, `JSONDecimal`, `JSONInt`, `JSONLong`, `JSONObject`, `JSONString`, `JSONValue`, `Parser`, + `ParseException`, `AbstractBuilder`: minor code style changes + ## [9.0] - 2024-07-24 ### Changed - `JSONArray`, `JSONDecimal`, `JSONInt`, `JSONLong`, `JSONObject`, `JSONString`: minor optimisation to `toJSON()` diff --git a/README.md b/README.md index ffe33eb..1432f39 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,20 @@ The `JSONValue` interface specifies four functions: The implementing classes are all immutable. +Following a common Kotlin pattern, there are creation functions named `JSONValue` which create the appropriate +implementing class depending on the parameter type: + +| Parameter Type | Result | +|-----------------------------------|-------------------------------| +| `String` | [`JSONString`](#jsonstring) | +| `Int` | [`JSONInt`](#jsonint) | +| `Long` | [`JSONLong`](#jsonlong) | +| `BigDecimal` | [`JSONDecimal`](#jsondecimal) | +| `Boolean` | [`JSONBoolean`](#jsonboolean) | +| `vararg JSONValue?` | [`JSONArray`](#jsonarray) | +| `vararg Pair` | [`JSONObject`](#jsonobject) | +| `vararg JSONObject.Property` | [`JSONObject`](#jsonobject) | + ### `JSONPrimitive` `JSONPrimitive` is a sealed interface (and a sub-interface of [`JSONValue`](#jsonvalue)) implemented by the classes for @@ -107,6 +121,15 @@ types will be regarded as equal. `JSONInt(27)`, `JSONLong(27)` and `JSONDecimal(27)` will all be considered equal, and will all return the same hash code. +Following a common Kotlin pattern, there are creation functions named `JSONNumber` which create the appropriate +derived class depending on the parameter type: + +| Parameter Type | Result | +|-----------------------------------|-------------------------------| +| `Int` | [`JSONInt`](#jsonint) | +| `Long` | [`JSONLong`](#jsonlong) | +| `BigDecimal` | [`JSONDecimal`](#jsondecimal) | + ### `JSONInt` The `JSONInt` class holds JSON number values that fit in a 32-bit signed integer. @@ -206,6 +229,9 @@ The class also implements the [`JSONStructure`](#jsonstructure) interface with a The constructor for `JSONArray` is not publicly accessible, but an `of()` function is available in the `companion object`, and a `build` function and the `Builder` nested class allow arrays to be constructed dynamically. +Following a common Kotlin pattern, there is also a creation function named `JSONArray` taking a `vararg` array of +`JSONValue?`. + `JSONArray` implements the `equals()` and `hashCode()` functions as specified for the Java Collections classes, so that an instance of `JSONArray` may be compared safely with an instance of any class correctly implementing `List`. @@ -244,6 +270,9 @@ and: The constructor for `JSONObject` is not publicly accessible, but an `of()` function is available in the `companion object`, and a `build` function and the `Builder` nested class allow objects to be constructed dynamically. +Following a common Kotlin pattern, there are also creation functions named `JSONObject` taking a `vararg` array of +`Pair` or `JSONObject.Property`. + `JSONObject` implements the `equals()` and `hashCode()` functions as specified for the Java Collections classes, so that an instance of `JSONObject` may be compared safely with an instance of any class correctly implementing `Map`. @@ -281,6 +310,11 @@ It has two properties: The `JSONObject.Property` object is immutable. +There is an infix function `refersTo` taking a `String` and a `JSONValue?` which creates a `JSONObject.Property`: +```kotlin + val property = "propertyName" refersTo JSONString("Property value") +``` + ### `JSONException` Error conditions will usually result in a `JSONException` being thrown. @@ -600,7 +634,7 @@ parser leniency. For example: ```kotlin val options = ParseOptions( - objectKeyDuplicate = ParseOptions.DuplicateKeyOption.ERROR, + objectKeyDuplicate = JSONObject.DuplicateKeyOption.ERROR, objectKeyUnquoted = false, objectTrailingComma = false, arrayTrailingComma = false, @@ -619,7 +653,7 @@ Under normal circumstances, the parser will throw an exception when it encounter but if such data is required to be accepted, the `objectKeyDuplicate` options setting may be used to specify the desired behaviour. -The field is an `enum` (`DuplicateKeyOption`), and the possible values are: +The field is an `enum` (`JSONObject.DuplicateKeyOption`), and the possible values are: - `ERROR`: treat the duplicate key as an error (this is the default) - `TAKE_FIRST`: take the value of the first occurrence and ignore duplicates @@ -660,25 +694,25 @@ The diagram was produced by [Dia](https://wiki.gnome.org/Apps/Dia/); the diagram ## Dependency Specification -The latest version of the library is 9.0, and it may be obtained from the Maven Central repository. +The latest version of the library is 9.1, and it may be obtained from the Maven Central repository. ### Maven ```xml io.kjson kjson-core - 9.0 + 9.1 ``` ### Gradle ```groovy - implementation "io.kjson:kjson-core:9.0" + implementation "io.kjson:kjson-core:9.1" ``` ### Gradle (kts) ```kotlin - implementation("io.kjson:kjson-core:9.0") + implementation("io.kjson:kjson-core:9.1") ``` Peter Wall -2024-07-24 +2024-08-17 diff --git a/pom.xml b/pom.xml index c02725f..87d3fc6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 kjson-core - 9.0 + 9.1 JSON Kotlin core functionality JSON Kotlin core functionality jar diff --git a/src/main/kotlin/io/kjson/JSON.kt b/src/main/kotlin/io/kjson/JSON.kt index 18c95fc..208eba5 100644 --- a/src/main/kotlin/io/kjson/JSON.kt +++ b/src/main/kotlin/io/kjson/JSON.kt @@ -28,6 +28,7 @@ package io.kjson import java.math.BigDecimal import java.util.function.IntConsumer +import io.kjson.JSONObject.DuplicateKeyOption import io.kjson.parser.Parser import io.kjson.parser.ParseOptions import io.kjson.util.getIntProperty @@ -83,10 +84,15 @@ object JSON { fun of(vararg items: JSONValue?): JSONArray = JSONArray.of(*items) /** - * Create a [JSONArray] from a `vararg` list of [Pair] of name to value pairs. + * Create a [JSONObject] from a `vararg` list of [Pair] of name to value pairs. */ fun of(vararg items: Pair): JSONObject = JSONObject.of(*items) + /** + * Create a [JSONObject] from a `vararg` list of [JSONObject.Property]s. + */ + fun of(vararg items: JSONObject.Property): JSONObject = JSONObject.of(*items) + /** * Parse a [String] to a [JSONValue] (or `null` if the string is `"null"`). */ @@ -625,3 +631,31 @@ object JSON { } } + +// NOTE: extension functions are being added here, rather than in the body of the JSON object, to simplify imports +// More functions may be moved here from the JSON object later. + +/** + * Convert a [List] of [JSONValue] items to a [JSONArray]. + */ +fun List.toJSONArray(): JSONArray = if (isEmpty()) JSONArray.EMPTY else JSONArray(toTypedArray(), size) + +/** + * Convert a [Map] to a [JSONObject]. + */ +fun Map.toJSONObject(): JSONObject = if (isEmpty()) JSONObject.EMPTY else + JSONObject.build(size = size) { + for (entry in entries) + add(entry.key, entry.value) + } + +/** + * Convert a [List] of [JSONObject.Property]s to a [JSONObject]. + */ +fun List.toJSONObject( + duplicateKeyOption: DuplicateKeyOption = DuplicateKeyOption.ERROR, +): JSONObject = if (isEmpty()) JSONObject.EMPTY else + JSONObject.build(size = size, duplicateKeyOption = duplicateKeyOption) { + for (property in this@toJSONObject) + add(property) + } diff --git a/src/main/kotlin/io/kjson/JSONArray.kt b/src/main/kotlin/io/kjson/JSONArray.kt index d9458a2..b23b591 100644 --- a/src/main/kotlin/io/kjson/JSONArray.kt +++ b/src/main/kotlin/io/kjson/JSONArray.kt @@ -283,13 +283,11 @@ class JSONArray internal constructor (private val array: Array, /** The value as a [JSONArray] (unnecessary when type is known statically). */ @Deprecated("Unnecessary (value is known to be JSONArray)", ReplaceWith("this")) - val asArray: JSONArray - get() = this + val asArray: JSONArray get() = this /** The value as a [JSONArray] or `null` (unnecessary when type is known statically). */ @Deprecated("Unnecessary (value is known to be JSONArray)", ReplaceWith("this")) - val asArrayOrNull: JSONArray - get() = this + val asArrayOrNull: JSONArray get() = this companion object { @@ -378,3 +376,8 @@ class JSONArray internal constructor (private val array: Array, } } + +/** + * Create a [JSONArray] from a `vararg` list of [JSONValue]s. + */ +fun JSONArray(vararg items: JSONValue?): JSONArray = JSONArray.of(*items) diff --git a/src/main/kotlin/io/kjson/JSONBoolean.kt b/src/main/kotlin/io/kjson/JSONBoolean.kt index d9aba66..d7ba94f 100644 --- a/src/main/kotlin/io/kjson/JSONBoolean.kt +++ b/src/main/kotlin/io/kjson/JSONBoolean.kt @@ -53,13 +53,11 @@ enum class JSONBoolean(override val value: Boolean) : JSONPrimitive { override fun toString(): String = toJSON() /** The value as a [Boolean] (optimisation of the extension value in [JSON] when the type is known statically). */ - val asBoolean: Boolean - get() = value + val asBoolean: Boolean get() = value /** The value as a [Boolean] or `null` (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asBooleanOrNull: Boolean - get() = value + val asBooleanOrNull: Boolean get() = value companion object { diff --git a/src/main/kotlin/io/kjson/JSONDecimal.kt b/src/main/kotlin/io/kjson/JSONDecimal.kt index a1feee1..a2e21e6 100644 --- a/src/main/kotlin/io/kjson/JSONDecimal.kt +++ b/src/main/kotlin/io/kjson/JSONDecimal.kt @@ -210,13 +210,11 @@ class JSONDecimal(override val value: BigDecimal) : JSONNumber(), JSONPrimitive< /** The value as a [BigDecimal] (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asDecimal: BigDecimal - get() = value + val asDecimal: BigDecimal get() = value /** The value as a [BigDecimal] or `null` (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asDecimalOrNull: BigDecimal - get() = value + val asDecimalOrNull: BigDecimal get() = value companion object { diff --git a/src/main/kotlin/io/kjson/JSONInt.kt b/src/main/kotlin/io/kjson/JSONInt.kt index 9b154f0..253eefa 100644 --- a/src/main/kotlin/io/kjson/JSONInt.kt +++ b/src/main/kotlin/io/kjson/JSONInt.kt @@ -42,7 +42,9 @@ class JSONInt(override val value: Int) : JSONNumber(), JSONPrimitive { /** * Append as a JSON string to an [Appendable]. */ - override fun appendTo(a: Appendable) = IntOutput.appendInt(a, value) + override fun appendTo(a: Appendable) { + IntOutput.appendInt(a, value) + } /** * Convert to a JSON string. @@ -57,24 +59,32 @@ class JSONInt(override val value: Int) : JSONNumber(), JSONPrimitive { /** * Output as a JSON string to an [IntConsumer]. */ - override fun outputTo(out: IntConsumer) = IntOutput.outputInt(value, out) + override fun outputTo(out: IntConsumer) { + IntOutput.outputInt(value, out) + } /** * Output as a JSON string to an [IntConsumer]. */ @Deprecated("renamed to outputTo", ReplaceWith("outputTo(out)")) - override fun output(out: IntConsumer) = IntOutput.outputInt(value, out) + override fun output(out: IntConsumer) { + IntOutput.outputInt(value, out) + } /** * Output as a JSON string to a [CoOutput]. */ - override suspend fun coOutputTo(out: CoOutput) = out.outputInt(value) + override suspend fun coOutputTo(out: CoOutput) { + out.outputInt(value) + } /** * Output as a JSON string to a [CoOutput]. */ @Deprecated("renamed to coOutputTo", ReplaceWith("coOutputTo(out)")) - override suspend fun coOutput(out: CoOutput) = out.outputInt(value) + override suspend fun coOutput(out: CoOutput) { + out.outputInt(value) + } /** * Return `true` if the value is integral (has no fractional part, or the fractional part is zero). @@ -237,32 +247,26 @@ class JSONInt(override val value: Int) : JSONNumber(), JSONPrimitive { override fun toString(): String = value.toString() /** The value as an [Int] (optimisation of the extension value in [JSON] when the type is known statically). */ - val asInt: Int - get() = value + val asInt: Int get() = value /** The value as an [Int] or `null` (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asIntOrNull: Int - get() = value + val asIntOrNull: Int get() = value /** The value as a [Long] (optimisation of the extension value in [JSON] when the type is known statically). */ - val asLong: Long - get() = value.toLong() + val asLong: Long get() = value.toLong() /** The value as a [Long] or `null` (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asLongOrNull: Long - get() = value.toLong() + val asLongOrNull: Long get() = value.toLong() /** The value as a [BigDecimal] (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asDecimal: BigDecimal - get() = value.toBigDecimal() + val asDecimal: BigDecimal get() = value.toBigDecimal() /** The value as a [BigDecimal] or `null` (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asDecimalOrNull: BigDecimal - get() = value.toBigDecimal() + val asDecimalOrNull: BigDecimal get() = value.toBigDecimal() companion object { diff --git a/src/main/kotlin/io/kjson/JSONLong.kt b/src/main/kotlin/io/kjson/JSONLong.kt index 6e2a8bb..02f3713 100644 --- a/src/main/kotlin/io/kjson/JSONLong.kt +++ b/src/main/kotlin/io/kjson/JSONLong.kt @@ -42,7 +42,9 @@ class JSONLong(override val value: Long) : JSONNumber(), JSONPrimitive { /** * Append as a JSON string to an [Appendable]. */ - override fun appendTo(a: Appendable) = IntOutput.appendLong(a, value) + override fun appendTo(a: Appendable) { + IntOutput.appendLong(a, value) + } /** * Convert to a JSON string. @@ -57,24 +59,32 @@ class JSONLong(override val value: Long) : JSONNumber(), JSONPrimitive { /** * Output as a JSON string to an [IntConsumer]. */ - override fun outputTo(out: IntConsumer) = IntOutput.outputLong(value, out) + override fun outputTo(out: IntConsumer) { + IntOutput.outputLong(value, out) + } /** * Output as a JSON string to an [IntConsumer]. */ @Deprecated("renamed to outputTo", ReplaceWith("outputTo(out)")) - override fun output(out: IntConsumer) = IntOutput.outputLong(value, out) + override fun output(out: IntConsumer) { + IntOutput.outputLong(value, out) + } /** * Output as a JSON string to a [CoOutput]. */ - override suspend fun coOutputTo(out: CoOutput) = out.outputLong(value) + override suspend fun coOutputTo(out: CoOutput) { + out.outputLong(value) + } /** * Output as a JSON string to a [CoOutput]. */ @Deprecated("renamed to coOutputTo", ReplaceWith("coOutputTo(out)")) - override suspend fun coOutput(out: CoOutput) = out.outputLong(value) + override suspend fun coOutput(out: CoOutput) { + out.outputLong(value) + } /** * Return `true` if the value is integral (has no fractional part, or the fractional part is zero). @@ -237,22 +247,18 @@ class JSONLong(override val value: Long) : JSONNumber(), JSONPrimitive { override fun toString(): String = value.toString() /** The value as a [Long] (optimisation of the extension value in [JSON] when the type is known statically). */ - val asLong: Long - get() = value + val asLong: Long get() = value /** The value as a [Long] or `null` (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asLongOrNull: Long - get() = value + val asLongOrNull: Long get() = value /** The value as a [Long] (optimisation of the extension value in [JSON] when the type is known statically). */ - val asDecimal: BigDecimal - get() = value.toBigDecimal() + val asDecimal: BigDecimal get() = value.toBigDecimal() /** The value as a [Long] or `null` (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asDecimalOrNull: BigDecimal - get() = value.toBigDecimal() + val asDecimalOrNull: BigDecimal get() = value.toBigDecimal() companion object { diff --git a/src/main/kotlin/io/kjson/JSONNumber.kt b/src/main/kotlin/io/kjson/JSONNumber.kt index 1ba9468..def8628 100644 --- a/src/main/kotlin/io/kjson/JSONNumber.kt +++ b/src/main/kotlin/io/kjson/JSONNumber.kt @@ -151,3 +151,18 @@ sealed class JSONNumber : Number(), JSONValue { abstract override fun hashCode(): Int } + +/** + * Construct a [JSONNumber] from an [Int]. + */ +fun JSONNumber(int: Int): JSONNumber = JSONInt.of(int) + +/** + * Construct a [JSONNumber] from a [Long]. + */ +fun JSONNumber(long: Long): JSONNumber = JSONLong.of(long) + +/** + * Construct a [JSONNumber] from a [BigDecimal]. + */ +fun JSONNumber(decimal: BigDecimal): JSONNumber = JSONDecimal.of(decimal) diff --git a/src/main/kotlin/io/kjson/JSONObject.kt b/src/main/kotlin/io/kjson/JSONObject.kt index 79dd81e..ac0b30c 100644 --- a/src/main/kotlin/io/kjson/JSONObject.kt +++ b/src/main/kotlin/io/kjson/JSONObject.kt @@ -228,13 +228,11 @@ class JSONObject internal constructor(private val array: Array, ov /** The value as a [JSONObject] (unnecessary when type is known statically). */ @Deprecated("Unnecessary (value is known to be JSONObject)", ReplaceWith("this")) - val asObject: JSONObject - get() = this + val asObject: JSONObject get() = this /** The value as a [JSONObject] or `null` (unnecessary when type is known statically). */ @Deprecated("Unnecessary (value is known to be JSONObject)", ReplaceWith("this")) - val asObjectOrNull: JSONObject - get() = this + val asObjectOrNull: JSONObject get() = this /** * Get a `JSONObject` containing the set of [Property]s bounded by `fromIndex` (inclusive) and `toIndex` @@ -246,18 +244,6 @@ class JSONObject internal constructor(private val array: Array, ov else -> JSONObject(array.copyOfRange(fromIndex, toIndex), toIndex - fromIndex) } - /** - * A class to represent a property (name-value pair). - */ - class Property(key: String, value: JSONValue?) : ImmutableMapEntry(key, value) { - - constructor(pair: Pair) : this(pair.first, pair.second) - - val name: String - get() = key - - } - companion object { /** An empty [JSONObject]. */ @@ -266,26 +252,59 @@ class JSONObject internal constructor(private val array: Array, ov /** * Create a [JSONObject] from a `vararg` list of [Pair]s of name and value. */ - fun of(vararg items: Pair): JSONObject = - if (items.isEmpty()) EMPTY else JSONObject(Array(items.size) { i -> Property(items[i]) }, items.size) + fun of( + vararg items: Pair, + duplicateKeyOption: DuplicateKeyOption = DuplicateKeyOption.ERROR, + ): JSONObject = if (items.isEmpty()) EMPTY else + build(size = items.size, duplicateKeyOption = duplicateKeyOption) { + for (item in items) + add(item.first, item.second) + } + + /** + * Create a [JSONObject] from a `vararg` list of [Property]s. + */ + fun of( + vararg properties: Property, + duplicateKeyOption: DuplicateKeyOption = DuplicateKeyOption.ERROR, + ): JSONObject = if (properties.isEmpty()) EMPTY else + build(size = properties.size, duplicateKeyOption = duplicateKeyOption) { + for (property in properties) + add(property) + } /** * Create a [JSONObject] from a [Map]. */ fun from(map: Map): JSONObject = if (map.isEmpty()) EMPTY else - JSONObject(map.entries.map { Property(it.key, it.value) }.toTypedArray(), map.size) + build(size = map.size) { + for (entry in map.entries) + add(entry.key, entry.value) + } /** * Create a [JSONObject] from a [List] of [Pair]s of name and value. */ - fun from(list: List>): JSONObject = - if (list.isEmpty()) EMPTY else JSONObject(Array(list.size) { i -> Property(list[i]) }, list.size) + fun from( + list: List>, + duplicateKeyOption: DuplicateKeyOption = DuplicateKeyOption.ERROR, + ): JSONObject = if (list.isEmpty()) EMPTY else + build(size = list.size, duplicateKeyOption = duplicateKeyOption) { + for (item in list) + add(item.first, item.second) + } /** * Create a [JSONObject] from a [List] of [Property]. */ - fun fromProperties(list: List): JSONObject = - if (list.isEmpty()) EMPTY else JSONObject(list.toTypedArray(), list.size) + fun fromProperties( + list: List, + duplicateKeyOption: DuplicateKeyOption = DuplicateKeyOption.ERROR, + ): JSONObject = if (list.isEmpty()) EMPTY else + build(size = list.size, duplicateKeyOption = duplicateKeyOption) { + for (property in list) + add(property) + } /** * Create a [JSONObject] by applying the supplied block to a [Builder], and then taking the result. @@ -299,6 +318,17 @@ class JSONObject internal constructor(private val array: Array, ov } + /** + * A class to represent a property (name-value pair). + */ + class Property(key: String, value: JSONValue?) : ImmutableMapEntry(key, value) { + + constructor(pair: Pair) : this(pair.first, pair.second) + + val name: String get() = key + + } + /** * Option for handling duplicate keys. */ @@ -424,3 +454,34 @@ class JSONObject internal constructor(private val array: Array, ov } } + +/** + * Create a [JSONObject] from a `vararg` list of [JSONObject.Property]s. + */ +fun JSONObject( + vararg items: Pair, + duplicateKeyOption: JSONObject.DuplicateKeyOption = JSONObject.DuplicateKeyOption.ERROR, +): JSONObject = + if (items.isEmpty()) JSONObject.EMPTY else + JSONObject.build(size = items.size, duplicateKeyOption = duplicateKeyOption) { + for (item in items) + add(JSONObject.Property(item)) + } + +/** + * Create a [JSONObject] from a `vararg` list of [JSONObject.Property]s. + */ +fun JSONObject( + vararg properties: JSONObject.Property, + duplicateKeyOption: JSONObject.DuplicateKeyOption = JSONObject.DuplicateKeyOption.ERROR, +): JSONObject = + if (properties.isEmpty()) JSONObject.EMPTY else + JSONObject.build(size = properties.size, duplicateKeyOption = duplicateKeyOption) { + for (property in properties) + add(property) + } + +/** + * Create a [JSONObject.Property] from a [String] and a [JSONValue]?. + */ +infix fun String.refersTo(json: JSONValue?): JSONObject.Property = JSONObject.Property(this, json) diff --git a/src/main/kotlin/io/kjson/JSONString.kt b/src/main/kotlin/io/kjson/JSONString.kt index ee2d53a..8d431ad 100644 --- a/src/main/kotlin/io/kjson/JSONString.kt +++ b/src/main/kotlin/io/kjson/JSONString.kt @@ -39,7 +39,7 @@ import net.pwall.util.CoOutput class JSONString(override val value: String) : JSONPrimitive, CharSequence { /** The length of the string */ - override val length = value.length + override val length: Int = value.length /** * Get a single [Char] from the string. @@ -55,7 +55,7 @@ class JSONString(override val value: String) : JSONPrimitive, CharSequen * Convert to a JSON string. */ override fun toJSON(): String = if (value.isEmpty()) - "" + "\"\"" else buildString(((value.length * 9) shr 3) + 2) { // 12.5% extra for escape sequences, plus 2 for quotes appendTo(this) @@ -64,29 +64,39 @@ class JSONString(override val value: String) : JSONPrimitive, CharSequen /** * Append as a JSON string to an [Appendable]. */ - override fun appendTo(a: Appendable) = JSONFunctions.appendString(a, value, false) + override fun appendTo(a: Appendable) { + JSONFunctions.appendString(a, value, false) + } /** * Output as a JSON string to an [IntConsumer]. */ - override fun outputTo(out: IntConsumer) = JSONFunctions.outputString(value, false, out) + override fun outputTo(out: IntConsumer) { + JSONFunctions.outputString(value, false, out) + } /** * Output as a JSON string to an [IntConsumer]. */ @Deprecated("renamed to outputTo", ReplaceWith("outputTo(out)")) - override fun output(out: IntConsumer) = JSONFunctions.outputString(value, false, out) + override fun output(out: IntConsumer) { + JSONFunctions.outputString(value, false, out) + } /** * Output as a JSON string to a [CoOutput]. */ - override suspend fun coOutputTo(out: CoOutput) = out.outputString(value, false) + override suspend fun coOutputTo(out: CoOutput) { + out.outputString(value, false) + } /** * Output as a JSON string to a [CoOutput]. */ @Deprecated("renamed to coOutputTo", ReplaceWith("coOutputTo(out)")) - override suspend fun coOutput(out: CoOutput) = out.outputString(value, false) + override suspend fun coOutput(out: CoOutput) { + out.outputString(value, false) + } /** * Compare the value to another value. @@ -104,18 +114,19 @@ class JSONString(override val value: String) : JSONPrimitive, CharSequen override fun toString(): String = value /** The value as a [String] (optimisation of the extension value in [JSON] when the type is known statically). */ - val asString: String - get() = value + val asString: String get() = value /** The value as a [String] or `null` (optimisation of the extension value in [JSON] when the type is known * statically). */ - val asStringOrNull: String - get() = value + val asStringOrNull: String get() = value companion object { + /** An empty [String]. */ + const val EMPTY_STRING = "" + /** An empty [JSONString]. */ - val EMPTY = JSONString("") + val EMPTY = JSONString(EMPTY_STRING) /** * Create a [JSONString] from the given [CharSequence]. diff --git a/src/main/kotlin/io/kjson/JSONValue.kt b/src/main/kotlin/io/kjson/JSONValue.kt index 1e65c55..7cb968d 100644 --- a/src/main/kotlin/io/kjson/JSONValue.kt +++ b/src/main/kotlin/io/kjson/JSONValue.kt @@ -25,6 +25,7 @@ package io.kjson +import java.math.BigDecimal import java.util.function.IntConsumer import io.kjson.JSON.accept @@ -51,23 +52,71 @@ sealed interface JSONValue { /** * Output as a JSON string to an [IntConsumer]. */ - fun outputTo(out: IntConsumer) = out.accept(toJSON()) + fun outputTo(out: IntConsumer) { + out.accept(toJSON()) + } /** * Output as a JSON string to an [IntConsumer]. */ @Deprecated("renamed to outputTo", ReplaceWith("outputTo(out)")) - fun output(out: IntConsumer) = out.accept(toJSON()) + fun output(out: IntConsumer) { + out.accept(toJSON()) + } /** * Output as a JSON string to a [CoOutput]. */ - suspend fun coOutputTo(out: CoOutput) = out.output(toJSON()) + suspend fun coOutputTo(out: CoOutput) { + out.output(toJSON()) + } /** * Output as a JSON string to a [CoOutput]. */ @Deprecated("renamed to coOutputTo", ReplaceWith("coOutputTo(out)")) - suspend fun coOutput(out: CoOutput) = out.output(toJSON()) + suspend fun coOutput(out: CoOutput) { + out.output(toJSON()) + } } + +/** + * Construct a [JSONValue] from an [Int]. + */ +fun JSONValue(int: Int): JSONValue = JSONInt.of(int) + +/** + * Construct a [JSONValue] from a [Long]. + */ +fun JSONValue(long: Long): JSONValue = JSONLong.of(long) + +/** + * Construct a [JSONValue] from a [BigDecimal]. + */ +fun JSONValue(decimal: BigDecimal): JSONValue = JSONDecimal.of(decimal) + +/** + * Construct a [JSONValue] from a [String]. + */ +fun JSONValue(string: String): JSONValue = JSONString.of(string) + +/** + * Construct a [JSONValue] from a [Boolean]. + */ +fun JSONValue(boolean: Boolean): JSONValue = JSONBoolean.of(boolean) + +/** + * Construct a [JSONValue] from an array of [JSONValue]?. + */ +fun JSONValue(vararg items: JSONValue?): JSONValue = JSONArray.of(*items) + +/** + * Construct a [JSONValue] from an array of [Pair]s (of [String] and [JSONValue]?). + */ +fun JSONValue(vararg items: Pair): JSONValue = JSONObject.of(*items) + +/** + * Construct a [JSONValue] from an array of [JSONObject.Property]s. + */ +fun JSONValue(vararg properties: JSONObject.Property): JSONValue = JSONObject.of(*properties) diff --git a/src/main/kotlin/io/kjson/parser/ParseException.kt b/src/main/kotlin/io/kjson/parser/ParseException.kt index da104ba..abce818 100644 --- a/src/main/kotlin/io/kjson/parser/ParseException.kt +++ b/src/main/kotlin/io/kjson/parser/ParseException.kt @@ -38,7 +38,6 @@ class ParseException( override val key: String = rootPointer, ) : JSONException(text, key) { - val pointer: String - get() = key + val pointer: String get() = key } diff --git a/src/main/kotlin/io/kjson/parser/Parser.kt b/src/main/kotlin/io/kjson/parser/Parser.kt index 8ae1aca..b394226 100644 --- a/src/main/kotlin/io/kjson/parser/Parser.kt +++ b/src/main/kotlin/io/kjson/parser/Parser.kt @@ -157,8 +157,7 @@ object Parser { private fun parseString(tm: TextMatcher, pointer: String): String = try { JSONFunctions.parseString(tm) - } - catch (iae: IllegalArgumentException) { + } catch (iae: IllegalArgumentException) { throw ParseException(iae.message ?: "Error parsing JSON string", pointer) } @@ -167,26 +166,27 @@ object Parser { val negative = tm.match('-') if (tm.matchDec()) { val integerLength = tm.resultLength - if (integerLength > 1 && tm.resultChar == '0') - throw ParseException(ILLEGAL_NUMBER, pointer) - if (tm.match('.')) { - if (!tm.matchDec()) - throw ParseException(ILLEGAL_NUMBER, pointer) - skipExponent(tm, pointer) - } - else if (!skipExponent(tm, pointer)) { - // no decimal point or "e"/"E" - try JSONInt or JSONLong - if (integerLength < MAX_INTEGER_DIGITS_LENGTH) - return JSONInt.of(tm.getResultInt(negative)) - try { - val result = tm.getResultLong(negative) - return if (result >= Int.MIN_VALUE && result <= Int.MAX_VALUE) - JSONInt.of(result.toInt()) - else - JSONLong.of(result) + when { + integerLength > 1 && tm.resultChar == '0' -> throw ParseException(ILLEGAL_NUMBER, pointer) + tm.match('.') -> { + if (!tm.matchDec()) + throw ParseException(ILLEGAL_NUMBER, pointer) + skipExponent(tm, pointer) } - catch (_: NumberFormatException) { - // too big for long - drop through to BigDecimal + !skipExponent(tm, pointer) -> { + // no decimal point or "e"/"E" - try JSONInt or JSONLong + if (integerLength < MAX_INTEGER_DIGITS_LENGTH) + return JSONInt.of(tm.getResultInt(negative)) + try { + val result = tm.getResultLong(negative) + return if (result >= Int.MIN_VALUE && result <= Int.MAX_VALUE) + JSONInt.of(result.toInt()) + else + JSONLong.of(result) + } + catch (_: NumberFormatException) { + // too big for long - drop through to BigDecimal + } } } return JSONDecimal.of(tm.getString(numberStart, tm.index).toBigDecimal()) diff --git a/src/main/kotlin/io/kjson/util/AbstractBuilder.kt b/src/main/kotlin/io/kjson/util/AbstractBuilder.kt index 213c974..c87b599 100644 --- a/src/main/kotlin/io/kjson/util/AbstractBuilder.kt +++ b/src/main/kotlin/io/kjson/util/AbstractBuilder.kt @@ -32,16 +32,14 @@ import io.kjson.JSONStructure * An abstract base class for the `Builder` classes of `JSONArray` and `JSONObject`. * * @author Peter Wall - * @param T the type of the array entry */ abstract class AbstractBuilder(private var array: Array?) { private var count: Int = 0 - val size: Int - get() = count + val size: Int get() = count - fun checkArray() = array ?: throw JSONException("Builder is closed") + protected fun checkArray(): Array = array ?: throw JSONException("Builder is closed") protected fun internalAdd(value: T?) { var validArray = checkArray() diff --git a/src/main/kotlin/io/kjson/util/LookupSet.kt b/src/main/kotlin/io/kjson/util/LookupSet.kt index b9ae157..916f8fb 100644 --- a/src/main/kotlin/io/kjson/util/LookupSet.kt +++ b/src/main/kotlin/io/kjson/util/LookupSet.kt @@ -37,6 +37,8 @@ package io.kjson.util */ class LookupSet(override val size: Int = 1, val check: (T) -> Boolean): Set { + // TODO - rename to PseudoSet? (name better represents its purpose) + override fun isEmpty(): Boolean = false override fun iterator(): Iterator = throw UnsupportedOperationException() diff --git a/src/test/kotlin/io/kjson/JSONArrayTest.kt b/src/test/kotlin/io/kjson/JSONArrayTest.kt index 6d8504f..692d4d2 100644 --- a/src/test/kotlin/io/kjson/JSONArrayTest.kt +++ b/src/test/kotlin/io/kjson/JSONArrayTest.kt @@ -66,6 +66,14 @@ class JSONArrayTest { expect("[9999,8888]") { testArray.toJSON() } } + @Test fun `should create JSONArray using JSONArray function`() { + val testArray = JSONArray(JSONInt(9999), JSONInt(8888)) + expect(2) { testArray.size } + expect(JSONInt(9999)) { testArray[0] } + expect(JSONInt(8888)) { testArray[1] } + expect("[9999,8888]") { testArray.toJSON() } + } + @Test fun `should create JSONArray using List`() { val testArray = JSONArray.from(listOf(JSONString("Hello"), JSONString("World"))) expect(2) { testArray.size } @@ -74,6 +82,14 @@ class JSONArrayTest { expect("[\"Hello\",\"World\"]") { testArray.toJSON() } } + @Test fun `should create JSONArray using List extension function`() { + val testArray = listOf(JSONString("Hello"), JSONString("World")).toJSONArray() + expect(2) { testArray.size } + expect(JSONString("Hello")) { testArray[0] } + expect(JSONString("World")) { testArray[1] } + expect("[\"Hello\",\"World\"]") { testArray.toJSON() } + } + @Test fun `should compare to other List`() { val list = listOf(JSONInt(123), JSONInt(456)) val testArray = JSONArray.from(list) diff --git a/src/test/kotlin/io/kjson/JSONNumberTest.kt b/src/test/kotlin/io/kjson/JSONNumberTest.kt new file mode 100644 index 0000000..8621e1c --- /dev/null +++ b/src/test/kotlin/io/kjson/JSONNumberTest.kt @@ -0,0 +1,55 @@ +/* + * @(#) JSONNumberTest.kt + * + * kjson-core JSON Kotlin core functionality + * Copyright (c) 2024 Peter Wall + * + * 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 io.kjson + +import kotlin.test.Test +import kotlin.test.assertIs +import kotlin.test.assertSame +import kotlin.test.expect + +import java.math.BigDecimal + +class JSONNumberTest { + + @Test fun `should create JSONNumber of correct type using JSONNumber function`() { + JSONNumber(123).let { + assertIs(it) + expect(123) { it.value } + } + JSONNumber(1234567890123456789).let { + assertIs(it) + expect(1234567890123456789) { it.value } + } + JSONNumber(BigDecimal.ONE).let { + assertIs(it) + expect(BigDecimal.ONE) { it.value } + } + assertSame(JSONInt.ZERO, JSONNumber(0)) + assertSame(JSONLong.ZERO, JSONNumber(0L)) + assertSame(JSONDecimal.ZERO, JSONNumber(BigDecimal.ZERO)) + } + +} diff --git a/src/test/kotlin/io/kjson/JSONObjectTest.kt b/src/test/kotlin/io/kjson/JSONObjectTest.kt index 03d5f11..a931445 100644 --- a/src/test/kotlin/io/kjson/JSONObjectTest.kt +++ b/src/test/kotlin/io/kjson/JSONObjectTest.kt @@ -50,13 +50,197 @@ class JSONObjectTest { assertTrue(jsonObject.isNotEmpty()) } - @Test fun `should create empty JSONObject using of`() { - val jsonObject = JSONObject.of() - expect(0) { jsonObject.size } - expect("{}") { jsonObject.toJSON() } - expect("{}") { jsonObject.toString() } - assertTrue(jsonObject.isEmpty()) - assertFalse(jsonObject.isNotEmpty()) + @Test fun `should create JSONObject using of with duplicateKeyOption`() { + for (duplicateKeyOption in JSONObject.DuplicateKeyOption.entries) { + val jsonObject = JSONObject.of( + "abc" to JSONInt(12345), + "def" to JSONString("X"), + duplicateKeyOption = duplicateKeyOption, + ) + expect(2) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect(JSONString("X")) { jsonObject["def"] } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + } + + @Test fun `should report duplicate key error creating JSONObject using of`() { + assertFailsWith { + JSONObject.of("abc" to JSONInt(12345), "abc" to JSONString("X")) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should report duplicate key error creating JSONObject using of with duplicateKeyOption ERROR`() { + assertFailsWith { + JSONObject.of( + "abc" to JSONInt(12345), + "abc" to JSONString("X"), + duplicateKeyOption = JSONObject.DuplicateKeyOption.ERROR, + ) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should accept first key creating JSONObject using of with duplicateKeyOption TAKE_FIRST`() { + val jsonObject = JSONObject.of( + "abc" to JSONInt(12345), + "abc" to JSONString("X"), + duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_FIRST, + ) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept last key creating JSONObject using of with duplicateKeyOption TAKE_LAST`() { + val jsonObject = JSONObject.of( + "abc" to JSONInt(12345), + "abc" to JSONString("X"), + duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_LAST, + ) + expect(1) { jsonObject.size } + expect(JSONString("X")) { jsonObject["abc"] } + expect("""{"abc":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept matching duplicate creating JSONObject using of with CHECK_IDENTICAL`() { + val jsonObject = JSONObject.of( + "abc" to JSONInt(12345), + "abc" to JSONInt(12345), + duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL, + ) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should report duplicate key error creating JSONObject using of with CHECK_IDENTICAL`() { + assertFailsWith { + JSONObject.of( + "abc" to JSONInt(12345), + "abc" to JSONString("X"), + duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL, + ) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should create JSONObject using JSONObject function`() { + val jsonObject = JSONObject("abc" refersTo JSONInt(12345), "def" refersTo JSONString("X")) + expect(2) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect(JSONString("X")) { jsonObject["def"] } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should create JSONObject using JSONObject function with duplicateKeyOption`() { + for (duplicateKeyOption in JSONObject.DuplicateKeyOption.entries) { + val jsonObject = JSONObject( + "abc" refersTo JSONInt(12345), + "def" refersTo JSONString("X"), + duplicateKeyOption = duplicateKeyOption + ) + expect(2) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect(JSONString("X")) { jsonObject["def"] } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + } + + @Test fun `should report duplicate key error creating JSONObject using JSONObject function`() { + assertFailsWith { + JSONObject("abc" to JSONInt(12345), "abc" to JSONString("X")) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should report duplicate key error creating JSONObject using JSONObject function with option ERROR`() { + assertFailsWith { + JSONObject( + "abc" to JSONInt(12345), + "abc" to JSONString("X"), + duplicateKeyOption = JSONObject.DuplicateKeyOption.ERROR, + ) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should accept first key creating JSONObject using JSONObject function with option TAKE_FIRST`() { + val jsonObject = JSONObject( + "abc" to JSONInt(12345), + "abc" to JSONString("X"), + duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_FIRST, + ) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept last key creating JSONObject using JSONObject function with option TAKE_LAST`() { + val jsonObject = JSONObject( + "abc" to JSONInt(12345), + "abc" to JSONString("X"), + duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_LAST, + ) + expect(1) { jsonObject.size } + expect(JSONString("X")) { jsonObject["abc"] } + expect("""{"abc":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept matching duplicate creating JSONObject using JSONObject function with CHECK_IDENTICAL`() { + val jsonObject = JSONObject( + "abc" to JSONInt(12345), + "abc" to JSONInt(12345), + duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL, + ) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should report duplicate key error creating JSONObject using JSONObject function with CHECK_IDENTICAL`() { + assertFailsWith { + JSONObject( + "abc" to JSONInt(12345), + "abc" to JSONString("X"), + duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL, + ) + }.let { + expect("Duplicate key - abc") { it.message } + } } @Test fun `should create JSONObject using from Map`() { @@ -71,6 +255,18 @@ class JSONObjectTest { assertTrue(jsonObject.isNotEmpty()) } + @Test fun `should create JSONObject using from Map extension function`() { + val map = mapOf("abc" to JSONInt(12345), "def" to JSONString("X")) + val jsonObject = map.toJSONObject() + expect(2) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect(JSONString("X")) { jsonObject["def"] } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + @Test fun `should create JSONObject using from List`() { val list = listOf>("abc" to JSONInt(12345), "def" to JSONString("X")) val jsonObject = JSONObject.from(list) @@ -83,12 +279,208 @@ class JSONObjectTest { assertTrue(jsonObject.isNotEmpty()) } + @Test fun `should create JSONObject using from List with duplicateKeyOption`() { + for (duplicateKeyOption in JSONObject.DuplicateKeyOption.entries) { + val list = listOf>("abc" to JSONInt(12345), "def" to JSONString("X")) + val jsonObject = JSONObject.from(list, duplicateKeyOption = duplicateKeyOption) + expect(2) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect(JSONString("X")) { jsonObject["def"] } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + } + + @Test fun `should report duplicate key error creating JSONObject using from List`() { + assertFailsWith { + val list = listOf>("abc" to JSONInt(12345), "abc" to JSONString("X")) + JSONObject.from(list) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should report duplicate key error creating JSONObject using from List with duplicateKeyOption ERROR`() { + assertFailsWith { + val list = listOf>("abc" to JSONInt(12345), "abc" to JSONString("X")) + JSONObject.from(list, duplicateKeyOption = JSONObject.DuplicateKeyOption.ERROR) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should accept first key creating JSONObject using from List with duplicateKeyOption TAKE_FIRST`() { + val list = listOf>("abc" to JSONInt(12345), "abc" to JSONString("X")) + val jsonObject = JSONObject.from(list, duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_FIRST) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept last key creating JSONObject using from List with duplicateKeyOption TAKE_LAST`() { + val list = listOf>("abc" to JSONInt(12345), "abc" to JSONString("X")) + val jsonObject = JSONObject.from(list, duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_LAST) + expect(1) { jsonObject.size } + expect(JSONString("X")) { jsonObject["abc"] } + expect("""{"abc":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept matching duplicate creating JSONObject using from List with CHECK_IDENTICAL`() { + val list = listOf>("abc" to JSONInt(12345), "abc" to JSONInt(12345)) + val jsonObject = JSONObject.from(list, duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should report duplicate key error creating JSONObject using from List with CHECK_IDENTICAL`() { + val list = listOf>("abc" to JSONInt(12345), "abc" to JSONString("X")) + assertFailsWith { + JSONObject.from(list, duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + @Test fun `should create JSONObject using fromProperties`() { + val properties = listOf( + "abc" refersTo JSONInt(12345), + "def" refersTo JSONString("X"), + ) + val jsonObject = JSONObject.fromProperties(properties) + expect(2) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect(JSONString("X")) { jsonObject["def"] } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should create JSONObject using fromProperties with duplicateKeyOption`() { + for (duplicateKeyOption in JSONObject.DuplicateKeyOption.entries) { + val properties = listOf( + "abc" refersTo JSONInt(12345), + "def" refersTo JSONString("X"), + ) + val jsonObject = JSONObject.fromProperties(properties, duplicateKeyOption = duplicateKeyOption) + expect(2) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect(JSONString("X")) { jsonObject["def"] } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + } + + @Test fun `should report duplicate key error creating JSONObject using fromProperties`() { + val properties = listOf( + "abc" refersTo JSONInt(12345), + "abc" refersTo JSONString("X"), + ) + assertFailsWith { + JSONObject.fromProperties(properties) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should report duplicate key error creating JSONObject using fromProperties with option ERROR`() { + val properties = listOf( + "abc" refersTo JSONInt(12345), + "abc" refersTo JSONString("X"), + ) + assertFailsWith { + JSONObject.fromProperties(properties, duplicateKeyOption = JSONObject.DuplicateKeyOption.ERROR) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should accept first key creating JSONObject using fromProperties with duplicateKeyOption TAKE_FIRST`() { + val properties = listOf( + "abc" refersTo JSONInt(12345), + "abc" refersTo JSONString("X"), + ) + val jsonObject = JSONObject.fromProperties( + properties, + duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_FIRST, + ) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept last key creating JSONObject using fromProperties with duplicateKeyOption TAKE_LAST`() { + val properties = listOf( + "abc" refersTo JSONInt(12345), + "abc" refersTo JSONString("X"), + ) + val jsonObject = JSONObject.fromProperties( + properties, + duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_LAST, + ) + expect(1) { jsonObject.size } + expect(JSONString("X")) { jsonObject["abc"] } + expect("""{"abc":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept matching duplicate creating JSONObject using fromProperties with CHECK_IDENTICAL`() { + val properties = listOf( + "abc" refersTo JSONInt(12345), + "abc" refersTo JSONInt(12345), + ) + val jsonObject = JSONObject.fromProperties( + properties, + duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL, + ) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should report duplicate key error creating JSONObject using fromProperties with CHECK_IDENTICAL`() { + val properties = listOf( + "abc" refersTo JSONInt(12345), + "abc" refersTo JSONString("X"), + ) + assertFailsWith { + JSONObject.fromProperties( + properties, + duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL, + ) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should create JSONObject using extension function`() { val properties = listOf( JSONObject.Property("abc", JSONInt(12345)), JSONObject.Property("def", JSONString("X")), ) - val jsonObject = JSONObject.fromProperties(properties) + val jsonObject = properties.toJSONObject() expect(2) { jsonObject.size } expect(JSONInt(12345)) { jsonObject["abc"] } expect(JSONString("X")) { jsonObject["def"] } @@ -98,6 +490,101 @@ class JSONObjectTest { assertTrue(jsonObject.isNotEmpty()) } + @Test fun `should create JSONObject using extension function with duplicateKeyOption`() { + for (duplicateKeyOption in JSONObject.DuplicateKeyOption.entries) { + val properties = listOf( + JSONObject.Property("abc", JSONInt(12345)), + JSONObject.Property("def", JSONString("X")), + ) + val jsonObject = properties.toJSONObject(duplicateKeyOption = duplicateKeyOption) + expect(2) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect(JSONString("X")) { jsonObject["def"] } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":12345,"def":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + } + + @Test fun `should report duplicate key error creating JSONObject using extension function`() { + val properties = listOf( + JSONObject.Property("abc", JSONInt(12345)), + JSONObject.Property("abc", JSONString("X")), + ) + assertFailsWith { + properties.toJSONObject() + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should report duplicate key error creating JSONObject using extension function with option ERROR`() { + val properties = listOf( + JSONObject.Property("abc", JSONInt(12345)), + JSONObject.Property("abc", JSONString("X")), + ) + assertFailsWith { + properties.toJSONObject(duplicateKeyOption = JSONObject.DuplicateKeyOption.ERROR) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + + @Test fun `should accept first key creating JSONObject using extension function with TAKE_FIRST`() { + val properties = listOf( + JSONObject.Property("abc", JSONInt(12345)), + JSONObject.Property("abc", JSONString("X")), + ) + val jsonObject = properties.toJSONObject(duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_FIRST) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept last key creating JSONObject using extension function with TAKE_LAST`() { + val properties = listOf( + JSONObject.Property("abc", JSONInt(12345)), + JSONObject.Property("abc", JSONString("X")), + ) + val jsonObject = properties.toJSONObject(duplicateKeyOption = JSONObject.DuplicateKeyOption.TAKE_LAST) + expect(1) { jsonObject.size } + expect(JSONString("X")) { jsonObject["abc"] } + expect("""{"abc":"X"}""") { jsonObject.toJSON() } + expect("""{"abc":"X"}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should accept matching duplicate creating JSONObject using extension function with CHECK_IDENTICAL`() { + val properties = listOf( + JSONObject.Property("abc", JSONInt(12345)), + JSONObject.Property("abc", JSONInt(12345)), + ) + val jsonObject = properties.toJSONObject(duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL) + expect(1) { jsonObject.size } + expect(JSONInt(12345)) { jsonObject["abc"] } + expect("""{"abc":12345}""") { jsonObject.toJSON() } + expect("""{"abc":12345}""") { jsonObject.toString() } + assertFalse(jsonObject.isEmpty()) + assertTrue(jsonObject.isNotEmpty()) + } + + @Test fun `should report duplicate key error creating JSONObject using extension function with CHECK_IDENTICAL`() { + val properties = listOf( + JSONObject.Property("abc", JSONInt(12345)), + JSONObject.Property("abc", JSONString("X")), + ) + assertFailsWith { + properties.toJSONObject(duplicateKeyOption = JSONObject.DuplicateKeyOption.CHECK_IDENTICAL) + }.let { + expect("Duplicate key - abc") { it.message } + } + } + @Test fun `should build JSONObject using Builder`() { val json = JSONObject.Builder { add("first", JSONInt(123)) diff --git a/src/test/kotlin/io/kjson/JSONStringTest.kt b/src/test/kotlin/io/kjson/JSONStringTest.kt index 7a354a4..32725fb 100644 --- a/src/test/kotlin/io/kjson/JSONStringTest.kt +++ b/src/test/kotlin/io/kjson/JSONStringTest.kt @@ -53,7 +53,8 @@ class JSONStringTest { @Test fun `should use EMPTY`() { val testString = JSONString.of("") assertSame(JSONString.EMPTY, testString) - expect("") { testString.toString() } + expect(JSONString.EMPTY_STRING) { testString.toString() } + expect("\"\"") { testString.toJSON() } } @Test fun `should get value using stringValue`() { diff --git a/src/test/kotlin/io/kjson/JSONValueTest.kt b/src/test/kotlin/io/kjson/JSONValueTest.kt index c235857..4fc55eb 100644 --- a/src/test/kotlin/io/kjson/JSONValueTest.kt +++ b/src/test/kotlin/io/kjson/JSONValueTest.kt @@ -26,6 +26,7 @@ package io.kjson import kotlin.test.Test +import kotlin.test.assertIs import kotlin.test.expect import java.math.BigDecimal @@ -55,14 +56,25 @@ class JSONValueTest { expect("[123,321]") { test.toString() } } - @Test fun `should create JSONValue of correct type`() { - expect("int") { getType(JSON.of(123)) } - expect("long") { getType(JSON.of(0L)) } - expect("decimal") { getType(JSON.of(BigDecimal.ONE)) } - expect("string") { getType(JSON.of("hello")) } - expect("boolean") { getType(JSON.of(true)) } - expect("array") { getType(JSON.of(JSON.of(0), JSON.of(1))) } - expect("object") { getType(JSON.of("alpha" to JSON.of(0), "beta" to JSON.of(1))) } + @Test fun `should create JSONValue of correct type using of function`() { + assertIs(JSON.of(123)) + assertIs(JSON.of(0L)) + assertIs(JSON.of(BigDecimal.ONE)) + assertIs(JSON.of("hello")) + assertIs(JSON.of(true)) + assertIs(JSON.of(JSONValue(0), JSONValue(1))) + assertIs(JSON.of("alpha" to JSONValue(0), "beta" to JSONValue(1))) + } + + @Test fun `should create JSONValue of correct type using JSONValue function`() { + assertIs(JSONValue(123)) + assertIs(JSONValue(0L)) + assertIs(JSONValue(BigDecimal.ONE)) + assertIs(JSONValue("hello")) + assertIs(JSONValue(true)) + assertIs(JSONValue(JSONValue(0), JSONValue(1))) + assertIs(JSONValue("alpha" to JSONValue(0), "beta" to JSONValue(1))) + assertIs(JSONValue("alpha" refersTo JSONValue(0), "beta" refersTo JSONValue(1))) } companion object { @@ -72,17 +84,6 @@ class JSONValueTest { private fun createJSONValueNull(): JSONValue? = null - private fun getType(jsonValue: JSONValue?): String = when (jsonValue) { - null -> "null" - is JSONInt -> "int" - is JSONLong -> "long" - is JSONDecimal -> "decimal" - is JSONString -> "string" - is JSONBoolean -> "boolean" - is JSONArray -> "array" - is JSONObject -> "object" - } - } } diff --git a/src/test/kotlin/io/kjson/testutil/ImportTest.kt b/src/test/kotlin/io/kjson/testutil/ImportTest.kt new file mode 100644 index 0000000..992d873 --- /dev/null +++ b/src/test/kotlin/io/kjson/testutil/ImportTest.kt @@ -0,0 +1,42 @@ +/* + * @(#) ImportTest.kt + * + * kjson-core JSON Kotlin core functionality + * Copyright (c) 2024 Peter Wall + * + * 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 io.kjson.testutil + +import kotlin.test.Test +import kotlin.test.expect + +import io.kjson.JSONInt +import io.kjson.JSONValue + +class ImportTest { + + @Test fun `should import JSONValue once for both class and function`() { + val intValue1: JSONValue = JSONInt(123) + val intValue2 = JSONValue(123) + expect(intValue1) { intValue2 } + } + +}