From e9e436632624931bc1b494edd0f9c2a794f4f5ce Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Tue, 3 Sep 2024 19:09:45 +0400 Subject: [PATCH 01/19] Add module to wrap kotlin objects for validation --- .../api/json-schema-validator-objects.api | 13 + .../build.gradle.kts | 134 ++++++++++ .../json/schema/objects/wrapper/Wrappers.kt | 187 ++++++++++++++ .../schema/objects/wrapper/WrappersTest.kt | 235 ++++++++++++++++++ settings.gradle.kts | 3 +- 5 files changed, 571 insertions(+), 1 deletion(-) create mode 100644 json-schema-validator-objects/api/json-schema-validator-objects.api create mode 100644 json-schema-validator-objects/build.gradle.kts create mode 100644 json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt create mode 100644 json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt diff --git a/json-schema-validator-objects/api/json-schema-validator-objects.api b/json-schema-validator-objects/api/json-schema-validator-objects.api new file mode 100644 index 00000000..6f863a7e --- /dev/null +++ b/json-schema-validator-objects/api/json-schema-validator-objects.api @@ -0,0 +1,13 @@ +public final class io/github/optimumcode/json/schema/objects/wrapper/ObjectWrappers { + public static final fun wrapAsElement (Ljava/lang/Object;Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration;)Lio/github/optimumcode/json/schema/model/AbstractElement; + public static synthetic fun wrapAsElement$default (Ljava/lang/Object;Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration;ILjava/lang/Object;)Lio/github/optimumcode/json/schema/model/AbstractElement; + public static final fun wrappingConfiguration ()Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration; + public static final fun wrappingConfiguration (Z)Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration; + public static synthetic fun wrappingConfiguration$default (ZILjava/lang/Object;)Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration; +} + +public final class io/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration { + public fun ()V + public final fun getAllowSets ()Z +} + diff --git a/json-schema-validator-objects/build.gradle.kts b/json-schema-validator-objects/build.gradle.kts new file mode 100644 index 00000000..cd67b135 --- /dev/null +++ b/json-schema-validator-objects/build.gradle.kts @@ -0,0 +1,134 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import io.gitlab.arturbosch.detekt.Detekt +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.KotlinTargetWithTests +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl +import org.jlleitschuh.gradle.ktlint.reporter.ReporterType + +plugins { + alias(libs.plugins.kotlin.mutliplatform) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotest.multiplatform) + alias(libs.plugins.kover) + alias(libs.plugins.detekt) + alias(libs.plugins.ktlint) + alias(libs.plugins.kotlin.dokka) + convention.publication +} + +kotlin { + explicitApi() + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + freeCompilerArgs.add("-opt-in=io.github.optimumcode.json.schema.ExperimentalApi") + } + jvmToolchain(11) + jvm { + testRuns["test"].executionTask.configure { + useJUnitPlatform() + } + } + js(IR) { + browser() + generateTypeScriptDefinitions() + nodejs() + } + wasmJs { + // The wasmJsBrowserTest prints all executed tests as one unformatted string + // Have not found a way to suppress printing all this into console + browser() + nodejs() + } + + applyDefaultHierarchyTemplate() + + val macOsTargets = + listOf( + macosX64(), + macosArm64(), + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ) + + val linuxTargets = + listOf( + linuxX64(), + linuxArm64(), + ) + + val windowsTargets = + listOf( + mingwX64(), + ) + + sourceSets { + commonMain { + dependencies { + api(projects.jsonSchemaValidator) + } + } + + commonTest { + dependencies { + implementation(libs.kotest.assertions.core) + implementation(libs.kotest.framework.engine) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + jvmTest { + dependencies { + implementation(libs.kotest.runner.junit5) + } + } + } + + afterEvaluate { + fun Task.dependsOnTargetTests(targets: List) { + targets.forEach { + if (it is KotlinTargetWithTests<*, *>) { + dependsOn(tasks.getByName("${it.name}Test")) + } + } + } + tasks.register("macOsAllTest") { + group = "verification" + description = "runs all tests for MacOS and IOS targets" + dependsOnTargetTests(macOsTargets) + } + tasks.register("windowsAllTest") { + group = "verification" + description = "runs all tests for Windows targets" + dependsOnTargetTests(windowsTargets) + } + tasks.register("linuxAllTest") { + group = "verification" + description = "runs all tests for Linux targets" + dependsOnTargetTests(linuxTargets) + dependsOn(tasks.getByName("jvmTest")) + dependsOn(tasks.getByName("jsTest")) + dependsOn(tasks.getByName("wasmJsTest")) + } + } +} + +ktlint { + version.set(libs.versions.ktlint) + reporters { + reporter(ReporterType.HTML) + } +} + +afterEvaluate { + val detektAllTask by tasks.register("detektAll") { + dependsOn(tasks.withType()) + } + + tasks.named("check").configure { + dependsOn(detektAllTask) + } +} \ No newline at end of file diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt new file mode 100644 index 00000000..01de26bf --- /dev/null +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt @@ -0,0 +1,187 @@ +@file:JvmName("ObjectWrappers") + +package io.github.optimumcode.json.schema.objects.wrapper + +import io.github.optimumcode.json.schema.ExperimentalApi +import io.github.optimumcode.json.schema.model.AbstractElement +import io.github.optimumcode.json.schema.model.ArrayElement +import io.github.optimumcode.json.schema.model.ObjectElement +import io.github.optimumcode.json.schema.model.PrimitiveElement +import kotlin.jvm.JvmInline +import kotlin.jvm.JvmName +import kotlin.jvm.JvmOverloads + +@ExperimentalApi +public class WrappingConfiguration internal constructor( + public val allowSets: Boolean = false, +) + +@ExperimentalApi +@JvmOverloads +public fun wrappingConfiguration(allowSets: Boolean = false): WrappingConfiguration = WrappingConfiguration(allowSets) + +/** + * Returns an [AbstractElement] produced by converting the [obj] value. + * The [configuration] allows conversion customization. + * + * # The supported types + * + * ## Simple values: + * * [String] + * * [Byte] + * * [Short] + * * [Int] + * * [Long] + * * [Float] + * * [Double] + * * [Boolean] + * * `null` + * + * ## Structures: + * * [Map] -> keys MUST have a [String] type, values MUST be one of the supported types + * * [List] -> elements MUST be one of the supported types + * * [Array] -> elements MUST be one of the supported types + * + * If [WrappingConfiguration.allowSets] is enabled [Set] is also converted to [ArrayElement]. + * Please be aware that in order to have consistent verification results + * the [Set] must be one of the ORDERED types, e.g. [LinkedHashSet]. + */ +@ExperimentalApi +public fun wrapAsElement( + obj: Any?, + configuration: WrappingConfiguration = WrappingConfiguration(), +): AbstractElement { + if (obj == null) { + return NullWrapper + } + return when { + obj is Map<*, *> -> checkKeysAndWrap(obj, configuration) + obj is List<*> -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is Array<*> -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is Set<*> && configuration.allowSets -> + ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is String || obj is Number || obj is Boolean -> PrimitiveWrapper(numberToSupportedTypeOrOriginal(obj)) + else -> error("unsupported type to wrap: ${obj::class}") + } +} + +private fun numberToSupportedTypeOrOriginal(obj: Any): Any = + when (obj) { + !is Number -> obj + is Double, is Long -> obj + is Byte, is Short, is Int -> obj.toLong() + is Float -> obj.toDoubleSafe() + else -> error("unsupported number type: ${obj::class}") + } + +private fun Float.toDoubleSafe(): Double { + val double = toDouble() + // in some cases the conversion from float to double + // can introduce a difference between numbers. (e.g. 42.2f -> 42.2) + // In this case, the only way (at the moment) is to try parsing + // the double from float converted to string + val floatAsString = toString() + if (double.toString() == floatAsString) { + return double + } + return floatAsString.toDouble() +} + +private fun checkKeysAndWrap( + map: Map<*, *>, + configuration: WrappingConfiguration, +): ObjectWrapper { + if (map.isEmpty()) { + return ObjectWrapper(emptyMap()) + } + + require(map.keys.all { it is String }) { + val notStrings = + map.keys.asSequence().filterNot { it is String }.mapTo(hashSetOf()) { key -> + key?.let { it::class.simpleName } ?: "null" + }.joinToString() + "map keys must be strings, found: $notStrings" + } + + @Suppress("UNCHECKED_CAST") + val elementsMap = + map.mapValues { (_, value) -> + wrapAsElement(value, configuration) + } as Map + return ObjectWrapper(elementsMap) +} + +@JvmInline +private value class ObjectWrapper( + private val map: Map, +) : ObjectElement { + override val keys: Set + get() = map.keys + + override fun get(key: String): AbstractElement? = map[key] + + override fun contains(key: String): Boolean = map.containsKey(key) + + override val size: Int + get() = map.size + + override fun iterator(): Iterator> = + map.asSequence().map { (key, value) -> key to value }.iterator() + + override fun toString(): String = map.toString() +} + +@JvmInline +private value class ListWrapper( + private val list: List, +) : ArrayElement { + override fun iterator(): Iterator = list.iterator() + + override fun get(index: Int): AbstractElement = list[index] + + override val size: Int + get() = list.size + + override fun toString(): String = list.toString() +} + +@JvmInline +private value class PrimitiveWrapper( + private val value: Any, +) : PrimitiveElement { + override val isNull: Boolean + get() = false + override val isString: Boolean + get() = value is String + override val isBoolean: Boolean + get() = value is Boolean + override val isNumber: Boolean + get() = value is Number + override val longOrNull: Long? + get() = value as? Long + override val doubleOrNull: Double? + get() = value as? Double + override val content: String + get() = value.toString() + + override fun toString(): String = value.toString() +} + +private data object NullWrapper : PrimitiveElement { + override val isNull: Boolean + get() = true + override val isString: Boolean + get() = false + override val isBoolean: Boolean + get() = false + override val isNumber: Boolean + get() = false + override val longOrNull: Long? + get() = null + override val doubleOrNull: Double? + get() = null + override val content: String + get() = "null" + + override fun toString(): String = "null" +} \ No newline at end of file diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt new file mode 100644 index 00000000..e13d9d59 --- /dev/null +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt @@ -0,0 +1,235 @@ +package io.github.optimumcode.json.schema.objects.wrapper + +import io.github.optimumcode.json.schema.model.ArrayElement +import io.github.optimumcode.json.schema.model.ObjectElement +import io.github.optimumcode.json.schema.model.PrimitiveElement +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldStartWith +import io.kotest.matchers.types.shouldBeInstanceOf + +class WrappersTest : FunSpec() { + init { + fun Any?.str(): String = + when (this) { + is Array<*> -> this.contentToString() + else -> toString() + } + + fun Any?.type(): String = this?.let { "(${it::class.simpleName}) " } ?: "" + + mapOf( + emptyMap() to ObjectElement::class, + listOf() to ArrayElement::class, + emptyArray() to ArrayElement::class, + "test" to PrimitiveElement::class, + 42 to PrimitiveElement::class, + 42L to PrimitiveElement::class, + 42.2 to PrimitiveElement::class, + 42.2f to PrimitiveElement::class, + true to PrimitiveElement::class, + null to PrimitiveElement::class, + ).forEach { (obj, wrapperClass) -> + test("element ${obj.str()} ${obj.type()}is wrapped into ${wrapperClass.simpleName}") { + wrapperClass.isInstance(wrapAsElement(obj)).shouldBeTrue() + } + } + + test("primitive wrapper for null") { + wrapAsElement(null).shouldBeInstanceOf { + assertSoftly { + it.isString.shouldBeFalse() + it.isNumber.shouldBeFalse() + it.isBoolean.shouldBeFalse() + it.isNull.shouldBeTrue() + it.content shouldBe "null" + it.longOrNull.shouldBeNull() + it.doubleOrNull.shouldBeNull() + } + } + } + + test("primitive wrapper for boolean") { + wrapAsElement(true).shouldBeInstanceOf { + assertSoftly { + it.isString.shouldBeFalse() + it.isNumber.shouldBeFalse() + it.isBoolean.shouldBeTrue() + it.isNull.shouldBeFalse() + it.content shouldBe "true" + it.longOrNull.shouldBeNull() + it.doubleOrNull.shouldBeNull() + } + } + } + + test("primitive wrapper for number") { + wrapAsElement(42).shouldBeInstanceOf { + assertSoftly { + it.isString.shouldBeFalse() + it.isNumber.shouldBeTrue() + it.isBoolean.shouldBeFalse() + it.isNull.shouldBeFalse() + it.content shouldBe "42" + it.longOrNull shouldBe 42L + it.doubleOrNull.shouldBeNull() + } + } + } + + test("primitive wrapper for string") { + wrapAsElement("42").shouldBeInstanceOf { + assertSoftly { + it.isString.shouldBeTrue() + it.isNumber.shouldBeFalse() + it.isBoolean.shouldBeFalse() + it.isNull.shouldBeFalse() + it.content shouldBe "42" + it.longOrNull.shouldBeNull() + it.doubleOrNull.shouldBeNull() + } + } + } + + test("object wrapper") { + wrapAsElement( + buildMap { + put("a", "hello") + put("b", listOf()) + put("c", mapOf()) + put("d", null) + }, + ).shouldBeInstanceOf { + assertSoftly { + it.size shouldBe 4 + it.keys shouldContainExactly setOf("a", "b", "c", "d") + it["a"].shouldBeInstanceOf() + it["b"].shouldBeInstanceOf() + it["c"].shouldBeInstanceOf() + it["d"].shouldBeInstanceOf { + it.isNull.shouldBeTrue() + } + it["e"].shouldBeNull() + ("a" in it).shouldBeTrue() + ("e" in it).shouldBeFalse() + } + } + } + + test("array wrapper") { + wrapAsElement( + buildList { + add("hello") + add(mapOf()) + add(listOf()) + add(null) + }, + ).shouldBeInstanceOf { + assertSoftly { + it.size shouldBe 4 + it[0].shouldBeInstanceOf() + it[1].shouldBeInstanceOf() + it[2].shouldBeInstanceOf() + it[3].shouldBeInstanceOf { + it.isNull.shouldBeTrue() + } + } + } + } + + test("set is not allowed by default") { + shouldThrow { + wrapAsElement(setOf("a")) + }.message.shouldStartWith("unsupported type to wrap:") + } + + test("set is allowed if configuration is provided") { + val element = + shouldNotThrowAny { + wrapAsElement( + setOf("a"), + wrappingConfiguration( + allowSets = true, + ), + ) + } + element.shouldBeInstanceOf { + it.size shouldBe 1 + it[0].shouldBeInstanceOf() + } + } + + mapOf(42 to "Int", null to "null").forEach { (key, type) -> + test("map with key '${key.str()}' ${key.type()} is not allowed") { + shouldThrow { + wrapAsElement( + mapOf(key to "test"), + ) + }.message.shouldBe("map keys must be strings, found: $type") + } + } + + mapOf( + 42.toByte() to 42L, + 42.toShort() to 42L, + 42 to 42L, + 42L to 42L, + ).forEach { (originalNumber, convertedNumber) -> + val name = + "integer number $originalNumber ${originalNumber.type()}" + + "converted to $convertedNumber ${convertedNumber.type()}" + test(name) { + wrapAsElement(originalNumber).shouldBeInstanceOf { + it.longOrNull.shouldNotBeNull() + .shouldBe(convertedNumber) + it.doubleOrNull.shouldBeNull() + } + } + } + + mapOf( + 42.2f to 42.2, + 42.5f to 42.5, + 42.5 to 42.5, + ).forEach { (originalNumber, convertedNumber) -> + val name = + "floating number $originalNumber ${originalNumber.type()}" + + "converted to $convertedNumber ${convertedNumber.type()}" + test(name) { + wrapAsElement(originalNumber).shouldBeInstanceOf { + it.doubleOrNull.shouldNotBeNull() + .shouldBe(convertedNumber) + it.longOrNull.shouldBeNull() + } + } + } + + test("other number implementations are not allowed") { + shouldThrow { + wrapAsElement( + object : Number() { + override fun toByte(): Byte = TODO("Not yet implemented") + + override fun toDouble(): Double = TODO("Not yet implemented") + + override fun toFloat(): Float = TODO("Not yet implemented") + + override fun toInt(): Int = TODO("Not yet implemented") + + override fun toLong(): Long = TODO("Not yet implemented") + + override fun toShort(): Short = TODO("Not yet implemented") + }, + ) + }.message.shouldStartWith("unsupported number type:") + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 6925b470..65290fcb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,4 +4,5 @@ rootProject.name = "json-schema-validator-root" include(":test-suites") include(":benchmark") -include(":json-schema-validator") \ No newline at end of file +include(":json-schema-validator") +include(":json-schema-validator-objects") \ No newline at end of file From af80007d1bdc8c643d8630ccfd97fe007227b39f Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sun, 29 Dec 2024 20:34:28 +0400 Subject: [PATCH 02/19] Correct gradle script for new project --- json-schema-validator-objects/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json-schema-validator-objects/build.gradle.kts b/json-schema-validator-objects/build.gradle.kts index cd67b135..a2a916a5 100644 --- a/json-schema-validator-objects/build.gradle.kts +++ b/json-schema-validator-objects/build.gradle.kts @@ -2,9 +2,9 @@ import io.gitlab.arturbosch.detekt.Detekt import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import org.jetbrains.kotlin.gradle.plugin.KotlinTargetWithTests -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl import org.jlleitschuh.gradle.ktlint.reporter.ReporterType plugins { From 1ad34e310a3597b5addabf65137dd0603ac8fb8a Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sun, 29 Dec 2024 21:30:20 +0400 Subject: [PATCH 03/19] Correct number type check for JS. Add clues for tests --- .../build.gradle.kts | 16 +++ .../json/schema/objects/wrapper/Wrappers.kt | 21 +++- .../schema/objects/wrapper/WrappersTest.kt | 106 ++++++++++-------- .../schema/objects/wrapper/Wrappers.js.kt | 3 + .../schema/objects/wrapper/Wrappers.noJs.kt | 3 + 5 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.js.kt create mode 100644 json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.noJs.kt diff --git a/json-schema-validator-objects/build.gradle.kts b/json-schema-validator-objects/build.gradle.kts index a2a916a5..d88ab19c 100644 --- a/json-schema-validator-objects/build.gradle.kts +++ b/json-schema-validator-objects/build.gradle.kts @@ -72,6 +72,22 @@ kotlin { } } + val noJsMain by creating { + dependsOn(commonMain.get()) + } + + jvmMain { + dependsOn(noJsMain) + } + + wasmJsMain { + dependsOn(noJsMain) + } + + nativeMain { + dependsOn(noJsMain) + } + commonTest { dependencies { implementation(libs.kotest.assertions.core) diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt index 01de26bf..d402f2f0 100644 --- a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt @@ -65,12 +65,23 @@ public fun wrapAsElement( } } +/** + * Returns `true` if the [value] is an integer ([Byte], [Short], [Int], [Long]). + * Otherwise, returns `false`. + * + * Required because JS platform matches all types except Long with `number` type. + * Refer to the [KT-18177](https://youtrack.jetbrains.com/issue/KT-18177/) for additional details + */ +internal expect fun isInteger(value: Number): Boolean + private fun numberToSupportedTypeOrOriginal(obj: Any): Any = - when (obj) { - !is Number -> obj - is Double, is Long -> obj - is Byte, is Short, is Int -> obj.toLong() - is Float -> obj.toDoubleSafe() + when { + obj !is Number -> obj + obj is Long -> obj + isInteger(obj) -> obj.toLong() + obj is Double -> obj + // due to KT-18177 this won't be invoked for Float on JS platform + obj is Float -> obj.toDoubleSafe() else -> error("unsupported number type: ${obj::class}") } diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt index e13d9d59..712dac47 100644 --- a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt @@ -3,10 +3,14 @@ package io.github.optimumcode.json.schema.objects.wrapper import io.github.optimumcode.json.schema.model.ArrayElement import io.github.optimumcode.json.schema.model.ObjectElement import io.github.optimumcode.json.schema.model.PrimitiveElement +import io.kotest.assertions.asClue import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.Platform +import io.kotest.core.platform import io.kotest.core.spec.style.FunSpec +import io.kotest.core.test.Enabled import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.collections.shouldContainExactly @@ -44,57 +48,57 @@ class WrappersTest : FunSpec() { } test("primitive wrapper for null") { - wrapAsElement(null).shouldBeInstanceOf { + wrapAsElement(null).shouldBeInstanceOf { el -> assertSoftly { - it.isString.shouldBeFalse() - it.isNumber.shouldBeFalse() - it.isBoolean.shouldBeFalse() - it.isNull.shouldBeTrue() - it.content shouldBe "null" - it.longOrNull.shouldBeNull() - it.doubleOrNull.shouldBeNull() + "isString".asClue { el.isString.shouldBeFalse() } + "isNumber".asClue { el.isNumber.shouldBeFalse() } + "isBoolean".asClue { el.isBoolean.shouldBeFalse() } + "isNull".asClue { el.isNull.shouldBeTrue() } + "content".asClue { el.content shouldBe "null" } + "longOrNull".asClue { el.longOrNull.shouldBeNull() } + "doubleOrNull".asClue { el.doubleOrNull.shouldBeNull() } } } } test("primitive wrapper for boolean") { - wrapAsElement(true).shouldBeInstanceOf { + wrapAsElement(true).shouldBeInstanceOf { el -> assertSoftly { - it.isString.shouldBeFalse() - it.isNumber.shouldBeFalse() - it.isBoolean.shouldBeTrue() - it.isNull.shouldBeFalse() - it.content shouldBe "true" - it.longOrNull.shouldBeNull() - it.doubleOrNull.shouldBeNull() + "isString".asClue { el.isString.shouldBeFalse() } + "isNumber".asClue { el.isNumber.shouldBeFalse() } + "isBoolean".asClue { el.isBoolean.shouldBeTrue() } + "isNull".asClue { el.isNull.shouldBeFalse() } + "content".asClue { el.content shouldBe "true" } + "longOrNull".asClue { el.longOrNull.shouldBeNull() } + "doubleOrNull".asClue { el.doubleOrNull.shouldBeNull() } } } } test("primitive wrapper for number") { - wrapAsElement(42).shouldBeInstanceOf { + wrapAsElement(42).shouldBeInstanceOf { el -> assertSoftly { - it.isString.shouldBeFalse() - it.isNumber.shouldBeTrue() - it.isBoolean.shouldBeFalse() - it.isNull.shouldBeFalse() - it.content shouldBe "42" - it.longOrNull shouldBe 42L - it.doubleOrNull.shouldBeNull() + "isString".asClue { el.isString.shouldBeFalse() } + "isNumber".asClue { el.isNumber.shouldBeTrue() } + "isBoolean".asClue { el.isBoolean.shouldBeFalse() } + "isNull".asClue { el.isNull.shouldBeFalse() } + "content".asClue { el.content shouldBe "42" } + "longOrNull".asClue { el.longOrNull shouldBe 42L } + "doubleOrNull".asClue { el.doubleOrNull.shouldBeNull() } } } } test("primitive wrapper for string") { - wrapAsElement("42").shouldBeInstanceOf { + wrapAsElement("42").shouldBeInstanceOf { el -> assertSoftly { - it.isString.shouldBeTrue() - it.isNumber.shouldBeFalse() - it.isBoolean.shouldBeFalse() - it.isNull.shouldBeFalse() - it.content shouldBe "42" - it.longOrNull.shouldBeNull() - it.doubleOrNull.shouldBeNull() + "isString".asClue { el.isString.shouldBeTrue() } + "isNumber".asClue { el.isNumber.shouldBeFalse() } + "isBoolean".asClue { el.isBoolean.shouldBeFalse() } + "isNull".asClue { el.isNull.shouldBeFalse() } + "content".asClue { el.content shouldBe "42" } + "longOrNull".asClue { el.longOrNull.shouldBeNull() } + "doubleOrNull".asClue { el.doubleOrNull.shouldBeNull() } } } } @@ -212,24 +216,32 @@ class WrappersTest : FunSpec() { } } - test("other number implementations are not allowed") { - shouldThrow { - wrapAsElement( - object : Number() { - override fun toByte(): Byte = TODO("Not yet implemented") + test("other number implementations are not allowed") + .config( + enabledOrReasonIf = { + when (platform) { + Platform.JS -> Enabled.disabled("you cannot create a class that is a Number on JS") + else -> Enabled.enabled + } + }, + ) { + shouldThrow { + wrapAsElement(MyNumber()) + }.message.shouldStartWith("unsupported number type:") + } + } +} - override fun toDouble(): Double = TODO("Not yet implemented") +private class MyNumber : Number() { + override fun toByte(): Byte = TODO("Not yet implemented") - override fun toFloat(): Float = TODO("Not yet implemented") + override fun toDouble(): Double = TODO("Not yet implemented") - override fun toInt(): Int = TODO("Not yet implemented") + override fun toFloat(): Float = TODO("Not yet implemented") - override fun toLong(): Long = TODO("Not yet implemented") + override fun toInt(): Int = TODO("Not yet implemented") - override fun toShort(): Short = TODO("Not yet implemented") - }, - ) - }.message.shouldStartWith("unsupported number type:") - } - } + override fun toLong(): Long = TODO("Not yet implemented") + + override fun toShort(): Short = TODO("Not yet implemented") } \ No newline at end of file diff --git a/json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.js.kt b/json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.js.kt new file mode 100644 index 00000000..dfb2bf29 --- /dev/null +++ b/json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.js.kt @@ -0,0 +1,3 @@ +package io.github.optimumcode.json.schema.objects.wrapper + +internal actual fun isInteger(value: Number): Boolean = js("return Number.isInteger(value)") \ No newline at end of file diff --git a/json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.noJs.kt b/json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.noJs.kt new file mode 100644 index 00000000..0cd769f4 --- /dev/null +++ b/json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.noJs.kt @@ -0,0 +1,3 @@ +package io.github.optimumcode.json.schema.objects.wrapper + +internal actual fun isInteger(value: Number): Boolean = value is Byte || value is Short || value is Int || value is Long \ No newline at end of file From 08e2809ab63f976a0e1302e2bfd2f3f5aeffb97f Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sun, 29 Dec 2024 21:42:43 +0400 Subject: [PATCH 04/19] Disable tests related to wasm precition problems --- .../schema/objects/wrapper/WrappersTest.kt | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt index 712dac47..51bd37a0 100644 --- a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt @@ -11,6 +11,7 @@ import io.kotest.core.Platform import io.kotest.core.platform import io.kotest.core.spec.style.FunSpec import io.kotest.core.test.Enabled +import io.kotest.core.test.EnabledOrReasonIf import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.collections.shouldContainExactly @@ -199,15 +200,31 @@ class WrappersTest : FunSpec() { } } - mapOf( - 42.2f to 42.2, - 42.5f to 42.5, - 42.5 to 42.5, - ).forEach { (originalNumber, convertedNumber) -> + class DoubleConversionTestCase( + val initial: Any, + val expected: Double, + ) + + mapOf( + DoubleConversionTestCase( + 42.2f, + 42.2, + ) to { + if (platform == Platform.WasmJs) { + Enabled.disabled("problems with precision on wasm platform") + } else { + Enabled.enabled + } + }, + DoubleConversionTestCase(42.5f, 42.5) to { Enabled.enabled }, + DoubleConversionTestCase(42.5, 42.5) to { Enabled.enabled }, + ).forEach { (tc, condition) -> + val originalNumber = tc.initial + val convertedNumber = tc.expected val name = "floating number $originalNumber ${originalNumber.type()}" + "converted to $convertedNumber ${convertedNumber.type()}" - test(name) { + test(name).config(enabledOrReasonIf = condition) { wrapAsElement(originalNumber).shouldBeInstanceOf { it.doubleOrNull.shouldNotBeNull() .shouldBe(convertedNumber) From 3dd1a149e2470200d3bca9836b33d458b3fa7f23 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sun, 29 Dec 2024 21:52:02 +0400 Subject: [PATCH 05/19] Suppress detekt warning --- .../github/optimumcode/json/schema/objects/wrapper/Wrappers.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt index d402f2f0..635621f2 100644 --- a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt @@ -1,4 +1,5 @@ @file:JvmName("ObjectWrappers") +@file:Suppress("detekt:MatchingDeclarationName") package io.github.optimumcode.json.schema.objects.wrapper @@ -60,6 +61,7 @@ public fun wrapAsElement( obj is Array<*> -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) obj is Set<*> && configuration.allowSets -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is String || obj is Number || obj is Boolean -> PrimitiveWrapper(numberToSupportedTypeOrOriginal(obj)) else -> error("unsupported type to wrap: ${obj::class}") } From 5bdd38cdb821ea850ff923f610db603c3422c8c5 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sun, 29 Dec 2024 21:53:34 +0400 Subject: [PATCH 06/19] Increase Xmx for gradle daemon --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c0b46b0d..c359f5e1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ kotlin.code.style=official kotlin.js.compiler=ir -org.gradle.jvmargs=-Xmx1G +org.gradle.jvmargs=-Xmx1536M org.gradle.java.installations.auto-download=false org.gradle.daemon=false From 69f32d143b9ac26e67806ec8fc0513ff659890e5 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sun, 29 Dec 2024 22:11:09 +0400 Subject: [PATCH 07/19] Add validation tests for map --- .../schema/objects/wrapper/ValidationTest.kt | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/ValidationTest.kt diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/ValidationTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/ValidationTest.kt new file mode 100644 index 00000000..31bc6719 --- /dev/null +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/ValidationTest.kt @@ -0,0 +1,116 @@ +package io.github.optimumcode.json.schema.objects.wrapper + +import io.github.optimumcode.json.pointer.JsonPointer +import io.github.optimumcode.json.schema.JsonSchema +import io.github.optimumcode.json.schema.OutputCollector +import io.github.optimumcode.json.schema.SchemaType +import io.github.optimumcode.json.schema.ValidationOutput +import io.kotest.assertions.asClue +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe + +class ValidationTest : FunSpec() { + init { + val schema = + JsonSchema.fromDefinition( + """ + { + "properties": { + "simple": { + "type": "integer" + }, + "collection": { + "type": "array", + "items": { + "type": "string" + } + }, + "sub-map": { + "properties": { + "inner": { + "type": "string" + } + } + } + }, + "additionalProperties": false, + "required": ["simple"] + } + """.trimIndent(), + defaultType = SchemaType.DRAFT_2020_12, + ) + + test("valid object") { + val result = + schema.validate( + wrapAsElement( + mapOf( + "simple" to 1, + "collection" to + listOf( + "test1", + "test2", + ), + "sub-map" to + mapOf( + "inner" to "inner1", + ), + ), + ), + OutputCollector.basic(), + ) + + result.asClue { + it.valid shouldBe true + } + } + + test("invalid object") { + val result = + schema.validate( + wrapAsElement( + mapOf( + "simple" to 1.5, + "collection" to + listOf( + "test1", + 1, + ), + "sub-map" to + mapOf( + "inner" to 42, + ), + ), + ), + OutputCollector.basic(), + ) + + result.asClue { + it.valid shouldBe false + it.errors shouldHaveSize 3 + it.errors.shouldContainExactlyInAnyOrder( + ValidationOutput.OutputUnit( + valid = false, + keywordLocation = JsonPointer("/properties/simple/type"), + instanceLocation = JsonPointer("/simple"), + error = "element is not a integer", + ), + ValidationOutput.OutputUnit( + valid = false, + keywordLocation = JsonPointer("/properties/collection/items/type"), + instanceLocation = JsonPointer("/collection/1"), + error = "element is not a string", + ), + ValidationOutput.OutputUnit( + valid = false, + keywordLocation = JsonPointer("/properties/sub-map/properties/inner/type"), + instanceLocation = JsonPointer("/sub-map/inner"), + error = "element is not a string", + ), + ) + } + } + } +} \ No newline at end of file From 2a2a8d0f4040c81f00770c09572457c7f85c51cc Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Thu, 2 Jan 2025 20:30:22 +0400 Subject: [PATCH 08/19] Add support for Char type --- .../json/schema/objects/wrapper/Wrappers.kt | 36 +++++++++++++-- .../schema/objects/wrapper/WrappersTest.kt | 46 ++++++++++++++++++- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt index 635621f2..f862fd0d 100644 --- a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt @@ -14,12 +14,28 @@ import kotlin.jvm.JvmOverloads @ExperimentalApi public class WrappingConfiguration internal constructor( + /** + * If set to `false` an exception is thrown when wrapping a [Set]. + * If set to `true`, [Set] is wrapped the same way as [List] + */ public val allowSets: Boolean = false, + /** + * If set to `false` the [Char] is converted to [String]. + * If set to `true` the [Char] is converted to a codepoint (and then to [Long]) + */ + public val charAsCodepoint: Boolean = false, ) @ExperimentalApi @JvmOverloads -public fun wrappingConfiguration(allowSets: Boolean = false): WrappingConfiguration = WrappingConfiguration(allowSets) +public fun wrappingConfiguration( + allowSets: Boolean = false, + charAsCodepoint: Boolean = false, +): WrappingConfiguration = + WrappingConfiguration( + allowSets = allowSets, + charAsCodepoint = charAsCodepoint, + ) /** * Returns an [AbstractElement] produced by converting the [obj] value. @@ -36,6 +52,7 @@ public fun wrappingConfiguration(allowSets: Boolean = false): WrappingConfigurat * * [Float] * * [Double] * * [Boolean] + * * [Char] * * `null` * * ## Structures: @@ -47,6 +64,7 @@ public fun wrappingConfiguration(allowSets: Boolean = false): WrappingConfigurat * Please be aware that in order to have consistent verification results * the [Set] must be one of the ORDERED types, e.g. [LinkedHashSet]. */ +@JvmOverloads @ExperimentalApi public fun wrapAsElement( obj: Any?, @@ -62,11 +80,13 @@ public fun wrapAsElement( obj is Set<*> && configuration.allowSets -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) - obj is String || obj is Number || obj is Boolean -> PrimitiveWrapper(numberToSupportedTypeOrOriginal(obj)) + isPrimitive(obj) -> PrimitiveWrapper(convertToSupportedType(obj, configuration)) else -> error("unsupported type to wrap: ${obj::class}") } } +private fun isPrimitive(obj: Any): Boolean = obj is String || obj is Number || obj is Boolean || obj is Char + /** * Returns `true` if the [value] is an integer ([Byte], [Short], [Int], [Long]). * Otherwise, returns `false`. @@ -76,9 +96,17 @@ public fun wrapAsElement( */ internal expect fun isInteger(value: Number): Boolean -private fun numberToSupportedTypeOrOriginal(obj: Any): Any = +private fun convertToSupportedType( + obj: Any, + configuration: WrappingConfiguration, +): Any = when { - obj !is Number -> obj + obj !is Number -> + if (obj is Char) { + if (configuration.charAsCodepoint) obj.code.toLong() else obj.toString() + } else { + obj + } obj is Long -> obj isInteger(obj) -> obj.toLong() obj is Double -> obj diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt index 51bd37a0..7de79297 100644 --- a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt @@ -36,6 +36,7 @@ class WrappersTest : FunSpec() { listOf() to ArrayElement::class, emptyArray() to ArrayElement::class, "test" to PrimitiveElement::class, + 't' to PrimitiveElement::class, 42 to PrimitiveElement::class, 42L to PrimitiveElement::class, 42.2 to PrimitiveElement::class, @@ -76,7 +77,7 @@ class WrappersTest : FunSpec() { } } - test("primitive wrapper for number") { + test("primitive wrapper for integer number") { wrapAsElement(42).shouldBeInstanceOf { el -> assertSoftly { "isString".asClue { el.isString.shouldBeFalse() } @@ -90,6 +91,20 @@ class WrappersTest : FunSpec() { } } + test("primitive wrapper for floating number") { + wrapAsElement(42.5).shouldBeInstanceOf { el -> + assertSoftly { + "isString".asClue { el.isString.shouldBeFalse() } + "isNumber".asClue { el.isNumber.shouldBeTrue() } + "isBoolean".asClue { el.isBoolean.shouldBeFalse() } + "isNull".asClue { el.isNull.shouldBeFalse() } + "content".asClue { el.content shouldBe "42.5" } + "longOrNull".asClue { el.longOrNull.shouldBeNull() } + "doubleOrNull".asClue { el.doubleOrNull shouldBe 42.5 } + } + } + } + test("primitive wrapper for string") { wrapAsElement("42").shouldBeInstanceOf { el -> assertSoftly { @@ -104,6 +119,35 @@ class WrappersTest : FunSpec() { } } + test("primitive wrapper for char") { + wrapAsElement('4').shouldBeInstanceOf { el -> + assertSoftly { + "isString".asClue { el.isString.shouldBeTrue() } + "isNumber".asClue { el.isNumber.shouldBeFalse() } + "isBoolean".asClue { el.isBoolean.shouldBeFalse() } + "isNull".asClue { el.isNull.shouldBeFalse() } + "content".asClue { el.content shouldBe "4" } + "longOrNull".asClue { el.longOrNull.shouldBeNull() } + "doubleOrNull".asClue { el.doubleOrNull.shouldBeNull() } + } + } + } + + test("primitive wrapper for char as codepoint") { + wrapAsElement('4', wrappingConfiguration(charAsCodepoint = true)) + .shouldBeInstanceOf { el -> + assertSoftly { + "isString".asClue { el.isString.shouldBeFalse() } + "isNumber".asClue { el.isNumber.shouldBeTrue() } + "isBoolean".asClue { el.isBoolean.shouldBeFalse() } + "isNull".asClue { el.isNull.shouldBeFalse() } + "content".asClue { el.content shouldBe "52" } + "longOrNull".asClue { el.longOrNull shouldBe 52L } + "doubleOrNull".asClue { el.doubleOrNull.shouldBeNull() } + } + } + } + test("object wrapper") { wrapAsElement( buildMap { From bd3398ab76916893e98e08b260c525a2bd8f87a3 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Thu, 2 Jan 2025 20:38:03 +0400 Subject: [PATCH 09/19] Change package name --- .../schema/{objects/wrapper => wrappers/objects}/Wrappers.kt | 2 +- .../{objects/wrapper => wrappers/objects}/ValidationTest.kt | 2 +- .../{objects/wrapper => wrappers/objects}/WrappersTest.kt | 2 +- .../schema/{objects/wrapper => wrappers/objects}/Wrappers.js.kt | 2 +- .../{objects/wrapper => wrappers/objects}/Wrappers.noJs.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/{objects/wrapper => wrappers/objects}/Wrappers.kt (99%) rename json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/{objects/wrapper => wrappers/objects}/ValidationTest.kt (98%) rename json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/{objects/wrapper => wrappers/objects}/WrappersTest.kt (99%) rename json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/{objects/wrapper => wrappers/objects}/Wrappers.js.kt (61%) rename json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/{objects/wrapper => wrappers/objects}/Wrappers.noJs.kt (67%) diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt similarity index 99% rename from json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt rename to json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt index f862fd0d..4e310228 100644 --- a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt @@ -1,7 +1,7 @@ @file:JvmName("ObjectWrappers") @file:Suppress("detekt:MatchingDeclarationName") -package io.github.optimumcode.json.schema.objects.wrapper +package io.github.optimumcode.json.schema.wrappers.objects import io.github.optimumcode.json.schema.ExperimentalApi import io.github.optimumcode.json.schema.model.AbstractElement diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/ValidationTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/ValidationTest.kt similarity index 98% rename from json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/ValidationTest.kt rename to json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/ValidationTest.kt index 31bc6719..efc680b0 100644 --- a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/ValidationTest.kt +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/ValidationTest.kt @@ -1,4 +1,4 @@ -package io.github.optimumcode.json.schema.objects.wrapper +package io.github.optimumcode.json.schema.wrappers.objects import io.github.optimumcode.json.pointer.JsonPointer import io.github.optimumcode.json.schema.JsonSchema diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt similarity index 99% rename from json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt rename to json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt index 7de79297..b525b219 100644 --- a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/objects/wrapper/WrappersTest.kt +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt @@ -1,4 +1,4 @@ -package io.github.optimumcode.json.schema.objects.wrapper +package io.github.optimumcode.json.schema.wrappers.objects import io.github.optimumcode.json.schema.model.ArrayElement import io.github.optimumcode.json.schema.model.ObjectElement diff --git a/json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.js.kt b/json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.js.kt similarity index 61% rename from json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.js.kt rename to json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.js.kt index dfb2bf29..badaf6ad 100644 --- a/json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.js.kt +++ b/json-schema-validator-objects/src/jsMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.js.kt @@ -1,3 +1,3 @@ -package io.github.optimumcode.json.schema.objects.wrapper +package io.github.optimumcode.json.schema.wrappers.objects internal actual fun isInteger(value: Number): Boolean = js("return Number.isInteger(value)") \ No newline at end of file diff --git a/json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.noJs.kt b/json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.noJs.kt similarity index 67% rename from json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.noJs.kt rename to json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.noJs.kt index 0cd769f4..f3704d17 100644 --- a/json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.noJs.kt +++ b/json-schema-validator-objects/src/noJsMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.noJs.kt @@ -1,3 +1,3 @@ -package io.github.optimumcode.json.schema.objects.wrapper +package io.github.optimumcode.json.schema.wrappers.objects internal actual fun isInteger(value: Number): Boolean = value is Byte || value is Short || value is Int || value is Long \ No newline at end of file From fa3ccca63097d183d7651a5ac44a2ac6a17c2510 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Fri, 3 Jan 2025 20:00:36 +0400 Subject: [PATCH 10/19] Update API dump --- .../api/json-schema-validator-objects.api | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/json-schema-validator-objects/api/json-schema-validator-objects.api b/json-schema-validator-objects/api/json-schema-validator-objects.api index 6f863a7e..71d1134c 100644 --- a/json-schema-validator-objects/api/json-schema-validator-objects.api +++ b/json-schema-validator-objects/api/json-schema-validator-objects.api @@ -1,13 +1,16 @@ -public final class io/github/optimumcode/json/schema/objects/wrapper/ObjectWrappers { - public static final fun wrapAsElement (Ljava/lang/Object;Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration;)Lio/github/optimumcode/json/schema/model/AbstractElement; - public static synthetic fun wrapAsElement$default (Ljava/lang/Object;Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration;ILjava/lang/Object;)Lio/github/optimumcode/json/schema/model/AbstractElement; - public static final fun wrappingConfiguration ()Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration; - public static final fun wrappingConfiguration (Z)Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration; - public static synthetic fun wrappingConfiguration$default (ZILjava/lang/Object;)Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration; +public final class io/github/optimumcode/json/schema/wrappers/objects/ObjectWrappers { + public static final fun wrapAsElement (Ljava/lang/Object;)Lio/github/optimumcode/json/schema/model/AbstractElement; + public static final fun wrapAsElement (Ljava/lang/Object;Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration;)Lio/github/optimumcode/json/schema/model/AbstractElement; + public static synthetic fun wrapAsElement$default (Ljava/lang/Object;Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration;ILjava/lang/Object;)Lio/github/optimumcode/json/schema/model/AbstractElement; + public static final fun wrappingConfiguration ()Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; + public static final fun wrappingConfiguration (Z)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; + public static final fun wrappingConfiguration (ZZ)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; + public static synthetic fun wrappingConfiguration$default (ZZILjava/lang/Object;)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; } -public final class io/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration { +public final class io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration { public fun ()V public final fun getAllowSets ()Z + public final fun getCharAsCodepoint ()Z } From fcada9f6c2bbbfa58828fc5daadbc50d116161c0 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 1 Mar 2025 14:49:03 +0100 Subject: [PATCH 11/19] Remove dokka plugin --- json-schema-validator-objects/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/json-schema-validator-objects/build.gradle.kts b/json-schema-validator-objects/build.gradle.kts index d88ab19c..d352d7fe 100644 --- a/json-schema-validator-objects/build.gradle.kts +++ b/json-schema-validator-objects/build.gradle.kts @@ -14,7 +14,6 @@ plugins { alias(libs.plugins.kover) alias(libs.plugins.detekt) alias(libs.plugins.ktlint) - alias(libs.plugins.kotlin.dokka) convention.publication } From fa31c41502abfa66009a65ceedbb92d36d604449 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 1 Mar 2025 14:50:12 +0100 Subject: [PATCH 12/19] Add README for new module --- json-schema-validator-objects/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 json-schema-validator-objects/README.md diff --git a/json-schema-validator-objects/README.md b/json-schema-validator-objects/README.md new file mode 100644 index 00000000..d201fa2e --- /dev/null +++ b/json-schema-validator-objects/README.md @@ -0,0 +1,19 @@ +# JSON schema object wrapper + +This module allows wrapping Kotlin objects (e.g. Map, List, primitives) to `AbstractElement` that can be validated by JSON schema. + +## Usage + +```kotlin +val schema = JsonSchema.fromDefinition(/*schema*/) + +val obj = mapOf( + "a" to 42, + "b" to listOf("test"), + "c" to mapOf( + "inner" to 42, + ), +) + +val result = schema.validate(wrapAsElement(obj), OutputCollector.flag()) +``` \ No newline at end of file From 75adf689cd69239079b467b36f87896e8e97b290 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 1 Mar 2025 15:10:44 +0100 Subject: [PATCH 13/19] Add support for primitive arrays --- .../optimumcode/json/schema/wrappers/objects/Wrappers.kt | 7 +++++++ .../json/schema/wrappers/objects/WrappersTest.kt | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt index 4e310228..cb8fe60c 100644 --- a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt @@ -77,6 +77,13 @@ public fun wrapAsElement( obj is Map<*, *> -> checkKeysAndWrap(obj, configuration) obj is List<*> -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) obj is Array<*> -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is LongArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is IntArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is ShortArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is DoubleArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is FloatArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is CharArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is ByteArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) obj is Set<*> && configuration.allowSets -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt index b525b219..13bb3fcc 100644 --- a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt @@ -35,6 +35,13 @@ class WrappersTest : FunSpec() { emptyMap() to ObjectElement::class, listOf() to ArrayElement::class, emptyArray() to ArrayElement::class, + byteArrayOf() to ArrayElement::class, + shortArrayOf() to ArrayElement::class, + intArrayOf() to ArrayElement::class, + longArrayOf() to ArrayElement::class, + floatArrayOf() to ArrayElement::class, + doubleArrayOf() to ArrayElement::class, + charArrayOf() to ArrayElement::class, "test" to PrimitiveElement::class, 't' to PrimitiveElement::class, 42 to PrimitiveElement::class, From 22d7a41fcf6f443287bcc95fbbdf0cfdca2e4f47 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 1 Mar 2025 15:32:29 +0100 Subject: [PATCH 14/19] Add support for encoding byte array as base64 string --- .../json/schema/wrappers/objects/Wrappers.kt | 19 +++++++++++++++- .../wrappers/objects/internal/Base64.kt | 7 ++++++ .../schema/wrappers/objects/WrappersTest.kt | 22 ++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/internal/Base64.kt diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt index cb8fe60c..1d08e18e 100644 --- a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt @@ -8,6 +8,7 @@ import io.github.optimumcode.json.schema.model.AbstractElement import io.github.optimumcode.json.schema.model.ArrayElement import io.github.optimumcode.json.schema.model.ObjectElement import io.github.optimumcode.json.schema.model.PrimitiveElement +import io.github.optimumcode.json.schema.wrappers.objects.internal.encodeBase64 import kotlin.jvm.JvmInline import kotlin.jvm.JvmName import kotlin.jvm.JvmOverloads @@ -24,6 +25,11 @@ public class WrappingConfiguration internal constructor( * If set to `true` the [Char] is converted to a codepoint (and then to [Long]) */ public val charAsCodepoint: Boolean = false, + /** + * If set to `true` the [ByteArray] is encoded using Base64 encoding and wrapped as a [PrimitiveElement]. + * Otherwise, the [ByteArray] is wrapped as an [ArrayElement]. + */ + public val byteArrayAsBase64String: Boolean = true, ) @ExperimentalApi @@ -31,10 +37,12 @@ public class WrappingConfiguration internal constructor( public fun wrappingConfiguration( allowSets: Boolean = false, charAsCodepoint: Boolean = false, + byteArrayAsBase64String: Boolean = true, ): WrappingConfiguration = WrappingConfiguration( allowSets = allowSets, charAsCodepoint = charAsCodepoint, + byteArrayAsBase64String = byteArrayAsBase64String, ) /** @@ -59,10 +67,14 @@ public fun wrappingConfiguration( * * [Map] -> keys MUST have a [String] type, values MUST be one of the supported types * * [List] -> elements MUST be one of the supported types * * [Array] -> elements MUST be one of the supported types + * * [CharArray], [ByteArray], [ShortArray], [IntArray], [LongArray], [FloatArray], [DoubleArray] * * If [WrappingConfiguration.allowSets] is enabled [Set] is also converted to [ArrayElement]. * Please be aware that in order to have consistent verification results * the [Set] must be one of the ORDERED types, e.g. [LinkedHashSet]. + * + * If [WrappingConfiguration.byteArrayAsBase64String] is enabled (enabled by default) + * a [ByteArray] will be encoded using Base64 and wrapped as a [PrimitiveElement]. */ @JvmOverloads @ExperimentalApi @@ -83,7 +95,12 @@ public fun wrapAsElement( obj is DoubleArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) obj is FloatArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) obj is CharArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) - obj is ByteArray -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) + obj is ByteArray -> + if (configuration.byteArrayAsBase64String) { + PrimitiveWrapper(obj.encodeBase64()) + } else { + ListWrapper(obj.map { wrapAsElement(it, configuration) }) + } obj is Set<*> && configuration.allowSets -> ListWrapper(obj.map { wrapAsElement(it, configuration) }) diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/internal/Base64.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/internal/Base64.kt new file mode 100644 index 00000000..b85b97d9 --- /dev/null +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/internal/Base64.kt @@ -0,0 +1,7 @@ +package io.github.optimumcode.json.schema.wrappers.objects.internal + +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +@OptIn(ExperimentalEncodingApi::class) +internal fun ByteArray.encodeBase64(): String = Base64.Default.encode(this) \ No newline at end of file diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt index 13bb3fcc..fd881da4 100644 --- a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt @@ -26,6 +26,13 @@ class WrappersTest : FunSpec() { fun Any?.str(): String = when (this) { is Array<*> -> this.contentToString() + is ByteArray -> this.contentToString() + is ShortArray -> this.contentToString() + is IntArray -> this.contentToString() + is LongArray -> this.contentToString() + is FloatArray -> this.contentToString() + is DoubleArray -> this.contentToString() + is CharArray -> this.contentToString() else -> toString() } @@ -35,7 +42,8 @@ class WrappersTest : FunSpec() { emptyMap() to ObjectElement::class, listOf() to ArrayElement::class, emptyArray() to ArrayElement::class, - byteArrayOf() to ArrayElement::class, + // by default ByteArray is encoded as base64 string + byteArrayOf() to PrimitiveElement::class, shortArrayOf() to ArrayElement::class, intArrayOf() to ArrayElement::class, longArrayOf() to ArrayElement::class, @@ -297,6 +305,18 @@ class WrappersTest : FunSpec() { wrapAsElement(MyNumber()) }.message.shouldStartWith("unsupported number type:") } + + test("byte array can be wrapped as an array element") { + wrapAsElement( + byteArrayOf(42), + wrappingConfiguration( + byteArrayAsBase64String = false, + ), + ).shouldBeInstanceOf { + it.size shouldBe 1 + it.single().shouldBeInstanceOf() + } + } } } From 80155f82e68c7086a7703a4db2d7b439efac5acf Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 1 Mar 2025 15:34:05 +0100 Subject: [PATCH 15/19] Update API dump --- .../api/json-schema-validator-objects.api | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/json-schema-validator-objects/api/json-schema-validator-objects.api b/json-schema-validator-objects/api/json-schema-validator-objects.api index 71d1134c..aec8c9db 100644 --- a/json-schema-validator-objects/api/json-schema-validator-objects.api +++ b/json-schema-validator-objects/api/json-schema-validator-objects.api @@ -5,12 +5,14 @@ public final class io/github/optimumcode/json/schema/wrappers/objects/ObjectWrap public static final fun wrappingConfiguration ()Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; public static final fun wrappingConfiguration (Z)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; public static final fun wrappingConfiguration (ZZ)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; - public static synthetic fun wrappingConfiguration$default (ZZILjava/lang/Object;)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; + public static final fun wrappingConfiguration (ZZZ)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; + public static synthetic fun wrappingConfiguration$default (ZZZILjava/lang/Object;)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; } public final class io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration { public fun ()V public final fun getAllowSets ()Z + public final fun getByteArrayAsBase64String ()Z public final fun getCharAsCodepoint ()Z } From 31e26bb8dc40de3bc89af27eba63129edeed63f4 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 1 Mar 2025 16:04:43 +0100 Subject: [PATCH 16/19] Use builder pattern to create configuration --- .../json/schema/wrappers/objects/Wrappers.kt | 34 +------------- .../wrappers/objects/WrappingConfiguration.kt | 47 +++++++++++++++++++ .../schema/wrappers/objects/WrappersTest.kt | 14 +++--- 3 files changed, 55 insertions(+), 40 deletions(-) create mode 100644 json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration.kt diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt index 1d08e18e..9e49fc0b 100644 --- a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/Wrappers.kt @@ -13,38 +13,6 @@ import kotlin.jvm.JvmInline import kotlin.jvm.JvmName import kotlin.jvm.JvmOverloads -@ExperimentalApi -public class WrappingConfiguration internal constructor( - /** - * If set to `false` an exception is thrown when wrapping a [Set]. - * If set to `true`, [Set] is wrapped the same way as [List] - */ - public val allowSets: Boolean = false, - /** - * If set to `false` the [Char] is converted to [String]. - * If set to `true` the [Char] is converted to a codepoint (and then to [Long]) - */ - public val charAsCodepoint: Boolean = false, - /** - * If set to `true` the [ByteArray] is encoded using Base64 encoding and wrapped as a [PrimitiveElement]. - * Otherwise, the [ByteArray] is wrapped as an [ArrayElement]. - */ - public val byteArrayAsBase64String: Boolean = true, -) - -@ExperimentalApi -@JvmOverloads -public fun wrappingConfiguration( - allowSets: Boolean = false, - charAsCodepoint: Boolean = false, - byteArrayAsBase64String: Boolean = true, -): WrappingConfiguration = - WrappingConfiguration( - allowSets = allowSets, - charAsCodepoint = charAsCodepoint, - byteArrayAsBase64String = byteArrayAsBase64String, - ) - /** * Returns an [AbstractElement] produced by converting the [obj] value. * The [configuration] allows conversion customization. @@ -80,7 +48,7 @@ public fun wrappingConfiguration( @ExperimentalApi public fun wrapAsElement( obj: Any?, - configuration: WrappingConfiguration = WrappingConfiguration(), + configuration: WrappingConfiguration = WrappingConfiguration.create(), ): AbstractElement { if (obj == null) { return NullWrapper diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration.kt new file mode 100644 index 00000000..5835144a --- /dev/null +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration.kt @@ -0,0 +1,47 @@ +package io.github.optimumcode.json.schema.wrappers.objects + +import io.github.optimumcode.json.schema.ExperimentalApi +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +@ExperimentalApi +public class WrappingConfiguration internal constructor( + /** + * If set to `false` an exception is thrown when wrapping a [Set]. + * If set to `true`, [Set] is wrapped the same way as [List] + */ + public val allowSets: Boolean, + /** + * If set to `false` the [Char] is converted to [String]. + * If set to `true` the [Char] is converted to a codepoint (and then to [Long]) + */ + public val charAsCodepoint: Boolean, + /** + * If set to `true` the [ByteArray] is encoded using Base64 encoding and wrapped as a [io.github.optimumcode.json.schema.model.PrimitiveElement]. + * Otherwise, the [ByteArray] is wrapped as an [io.github.optimumcode.json.schema.model.ArrayElement]. + */ + public val byteArrayAsBase64String: Boolean, +) { + public companion object { + @ExperimentalApi + @JvmStatic + @JvmOverloads + public fun create(configuration: WrappingConfigurationBuilder.() -> Unit = {}): WrappingConfiguration = + WrappingConfigurationBuilder().apply(configuration).build() + } +} + +@ExperimentalApi +public class WrappingConfigurationBuilder internal constructor() { + public var allowSets: Boolean = false + public var charAsCodepoint: Boolean = false + public var byteArrayAsBase64String: Boolean = true + + internal fun build(): WrappingConfiguration { + return WrappingConfiguration( + allowSets = allowSets, + charAsCodepoint = charAsCodepoint, + byteArrayAsBase64String = byteArrayAsBase64String, + ) + } +} \ No newline at end of file diff --git a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt index fd881da4..3ecf3f12 100644 --- a/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt +++ b/json-schema-validator-objects/src/commonTest/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappersTest.kt @@ -149,7 +149,7 @@ class WrappersTest : FunSpec() { } test("primitive wrapper for char as codepoint") { - wrapAsElement('4', wrappingConfiguration(charAsCodepoint = true)) + wrapAsElement('4', WrappingConfiguration.create { charAsCodepoint = true }) .shouldBeInstanceOf { el -> assertSoftly { "isString".asClue { el.isString.shouldBeFalse() } @@ -220,9 +220,9 @@ class WrappersTest : FunSpec() { shouldNotThrowAny { wrapAsElement( setOf("a"), - wrappingConfiguration( - allowSets = true, - ), + WrappingConfiguration.create { + allowSets = true + }, ) } element.shouldBeInstanceOf { @@ -309,9 +309,9 @@ class WrappersTest : FunSpec() { test("byte array can be wrapped as an array element") { wrapAsElement( byteArrayOf(42), - wrappingConfiguration( - byteArrayAsBase64String = false, - ), + WrappingConfiguration.create { + byteArrayAsBase64String = false + }, ).shouldBeInstanceOf { it.size shouldBe 1 it.single().shouldBeInstanceOf() From cc5326a0bc1120cf00be088706a48d569f772c8e Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 1 Mar 2025 16:09:17 +0100 Subject: [PATCH 17/19] Update api dump --- .../api/json-schema-validator-objects.api | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/json-schema-validator-objects/api/json-schema-validator-objects.api b/json-schema-validator-objects/api/json-schema-validator-objects.api index aec8c9db..a10efe24 100644 --- a/json-schema-validator-objects/api/json-schema-validator-objects.api +++ b/json-schema-validator-objects/api/json-schema-validator-objects.api @@ -2,17 +2,29 @@ public final class io/github/optimumcode/json/schema/wrappers/objects/ObjectWrap public static final fun wrapAsElement (Ljava/lang/Object;)Lio/github/optimumcode/json/schema/model/AbstractElement; public static final fun wrapAsElement (Ljava/lang/Object;Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration;)Lio/github/optimumcode/json/schema/model/AbstractElement; public static synthetic fun wrapAsElement$default (Ljava/lang/Object;Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration;ILjava/lang/Object;)Lio/github/optimumcode/json/schema/model/AbstractElement; - public static final fun wrappingConfiguration ()Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; - public static final fun wrappingConfiguration (Z)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; - public static final fun wrappingConfiguration (ZZ)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; - public static final fun wrappingConfiguration (ZZZ)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; - public static synthetic fun wrappingConfiguration$default (ZZZILjava/lang/Object;)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; } public final class io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration { - public fun ()V + public static final field Companion Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration$Companion; + public static final fun create ()Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; + public static final fun create (Lkotlin/jvm/functions/Function1;)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; public final fun getAllowSets ()Z public final fun getByteArrayAsBase64String ()Z public final fun getCharAsCodepoint ()Z } +public final class io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration$Companion { + public final fun create ()Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; + public final fun create (Lkotlin/jvm/functions/Function1;)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; + public static synthetic fun create$default (Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration$Companion;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration; +} + +public final class io/github/optimumcode/json/schema/wrappers/objects/WrappingConfigurationBuilder { + public final fun getAllowSets ()Z + public final fun getByteArrayAsBase64String ()Z + public final fun getCharAsCodepoint ()Z + public final fun setAllowSets (Z)V + public final fun setByteArrayAsBase64String (Z)V + public final fun setCharAsCodepoint (Z)V +} + From f6ed3d8ba01f5e0be6658ca2e876fbbeec73b329 Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 1 Mar 2025 16:11:15 +0100 Subject: [PATCH 18/19] Update doc for properties --- .../wrappers/objects/WrappingConfiguration.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration.kt b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration.kt index 5835144a..54c0ca4f 100644 --- a/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration.kt +++ b/json-schema-validator-objects/src/commonMain/kotlin/io/github/optimumcode/json/schema/wrappers/objects/WrappingConfiguration.kt @@ -17,8 +17,9 @@ public class WrappingConfiguration internal constructor( */ public val charAsCodepoint: Boolean, /** - * If set to `true` the [ByteArray] is encoded using Base64 encoding and wrapped as a [io.github.optimumcode.json.schema.model.PrimitiveElement]. - * Otherwise, the [ByteArray] is wrapped as an [io.github.optimumcode.json.schema.model.ArrayElement]. + * If set to `true` the [ByteArray] is encoded using Base64 encoding + * and wrapped as a [PrimitiveElement][io.github.optimumcode.json.schema.model.PrimitiveElement]. + * Otherwise, the [ByteArray] is wrapped as an [ArrayElement][io.github.optimumcode.json.schema.model.ArrayElement]. */ public val byteArrayAsBase64String: Boolean, ) { @@ -33,8 +34,19 @@ public class WrappingConfiguration internal constructor( @ExperimentalApi public class WrappingConfigurationBuilder internal constructor() { + /** + * @see WrappingConfiguration.allowSets + */ public var allowSets: Boolean = false + + /** + * @see WrappingConfiguration.charAsCodepoint + */ public var charAsCodepoint: Boolean = false + + /** + * @see WrappingConfiguration.byteArrayAsBase64String + */ public var byteArrayAsBase64String: Boolean = true internal fun build(): WrappingConfiguration { From dfabda33f7cdf161d586eb66c8c57a31a73988bc Mon Sep 17 00:00:00 2001 From: Oleg Smirnov Date: Sat, 1 Mar 2025 16:37:39 +0100 Subject: [PATCH 19/19] Include new module into build workflows --- .github/workflows/release.yml | 4 +++- .github/workflows/snapshot_release.yml | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9eaf9779..fad572fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -58,7 +58,9 @@ jobs: gradle-version: wrapper - name: Build and publish release run: > - ./gradlew --no-daemon --info :json-schema-validator:assemble + ./gradlew --no-daemon --info + :json-schema-validator:assemble + :json-schema-validator-objects:assemble publish closeAndReleaseStagingRepositories -Pversion=${{ needs.version.outputs.RELEASE_VERSION }} -x :benchmark:benchmark diff --git a/.github/workflows/snapshot_release.yml b/.github/workflows/snapshot_release.yml index e2abac22..00b3bf99 100644 --- a/.github/workflows/snapshot_release.yml +++ b/.github/workflows/snapshot_release.yml @@ -8,6 +8,7 @@ on: - 'build.gradle.kts' - 'gradle.properties' - 'json-schema-validator/**' + - 'json-schema-validator-objects/**' - 'gradle/**' - 'generator/**' - '.github/workflows/snapshot_release.yml' @@ -54,6 +55,7 @@ jobs: --no-daemon --info :json-schema-validator:assemble + :json-schema-validator-objects:assemble publish -x :benchmark:benchmark env: