-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add module to wrap kotlin objects for validation
- Loading branch information
1 parent
2cb7fdd
commit 33722fb
Showing
5 changed files
with
571 additions
and
1 deletion.
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
json-schema-validator-objects/api/json-schema-validator-objects.api
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <init> ()V | ||
public final fun getAllowSets ()Z | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<KotlinTarget>( | ||
macosX64(), | ||
macosArm64(), | ||
iosX64(), | ||
iosArm64(), | ||
iosSimulatorArm64(), | ||
) | ||
|
||
val linuxTargets = | ||
listOf<KotlinTarget>( | ||
linuxX64(), | ||
linuxArm64(), | ||
) | ||
|
||
val windowsTargets = | ||
listOf<KotlinTarget>( | ||
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<KotlinTarget>) { | ||
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<Detekt>()) | ||
} | ||
|
||
tasks.named("check").configure { | ||
dependsOn(detektAllTask) | ||
} | ||
} |
187 changes: 187 additions & 0 deletions
187
...jects/src/commonMain/kotlin/io/github/optimumcode/json/schema/objects/wrapper/Wrappers.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, AbstractElement> | ||
return ObjectWrapper(elementsMap) | ||
} | ||
|
||
@JvmInline | ||
private value class ObjectWrapper( | ||
private val map: Map<String, AbstractElement>, | ||
) : ObjectElement { | ||
override val keys: Set<String> | ||
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<Pair<String, AbstractElement>> = | ||
map.asSequence().map { (key, value) -> key to value }.iterator() | ||
|
||
override fun toString(): String = map.toString() | ||
} | ||
|
||
@JvmInline | ||
private value class ListWrapper( | ||
private val list: List<AbstractElement>, | ||
) : ArrayElement { | ||
override fun iterator(): Iterator<AbstractElement> = 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" | ||
} |
Oops, something went wrong.