Skip to content

Commit 33722fb

Browse files
committed
Add module to wrap kotlin objects for validation
1 parent 2cb7fdd commit 33722fb

File tree

5 files changed

+571
-1
lines changed

5 files changed

+571
-1
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
public final class io/github/optimumcode/json/schema/objects/wrapper/ObjectWrappers {
2+
public static final fun wrapAsElement (Ljava/lang/Object;Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration;)Lio/github/optimumcode/json/schema/model/AbstractElement;
3+
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;
4+
public static final fun wrappingConfiguration ()Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration;
5+
public static final fun wrappingConfiguration (Z)Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration;
6+
public static synthetic fun wrappingConfiguration$default (ZILjava/lang/Object;)Lio/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration;
7+
}
8+
9+
public final class io/github/optimumcode/json/schema/objects/wrapper/WrappingConfiguration {
10+
public fun <init> ()V
11+
public final fun getAllowSets ()Z
12+
}
13+
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
@file:OptIn(ExperimentalWasmDsl::class)
2+
3+
import io.gitlab.arturbosch.detekt.Detekt
4+
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
5+
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
6+
import org.jetbrains.kotlin.gradle.plugin.KotlinTargetWithTests
7+
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
8+
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
9+
10+
plugins {
11+
alias(libs.plugins.kotlin.mutliplatform)
12+
alias(libs.plugins.kotlin.serialization)
13+
alias(libs.plugins.kotest.multiplatform)
14+
alias(libs.plugins.kover)
15+
alias(libs.plugins.detekt)
16+
alias(libs.plugins.ktlint)
17+
alias(libs.plugins.kotlin.dokka)
18+
convention.publication
19+
}
20+
21+
kotlin {
22+
explicitApi()
23+
24+
@OptIn(ExperimentalKotlinGradlePluginApi::class)
25+
compilerOptions {
26+
freeCompilerArgs.add("-opt-in=io.github.optimumcode.json.schema.ExperimentalApi")
27+
}
28+
jvmToolchain(11)
29+
jvm {
30+
testRuns["test"].executionTask.configure {
31+
useJUnitPlatform()
32+
}
33+
}
34+
js(IR) {
35+
browser()
36+
generateTypeScriptDefinitions()
37+
nodejs()
38+
}
39+
wasmJs {
40+
// The wasmJsBrowserTest prints all executed tests as one unformatted string
41+
// Have not found a way to suppress printing all this into console
42+
browser()
43+
nodejs()
44+
}
45+
46+
applyDefaultHierarchyTemplate()
47+
48+
val macOsTargets =
49+
listOf<KotlinTarget>(
50+
macosX64(),
51+
macosArm64(),
52+
iosX64(),
53+
iosArm64(),
54+
iosSimulatorArm64(),
55+
)
56+
57+
val linuxTargets =
58+
listOf<KotlinTarget>(
59+
linuxX64(),
60+
linuxArm64(),
61+
)
62+
63+
val windowsTargets =
64+
listOf<KotlinTarget>(
65+
mingwX64(),
66+
)
67+
68+
sourceSets {
69+
commonMain {
70+
dependencies {
71+
api(projects.jsonSchemaValidator)
72+
}
73+
}
74+
75+
commonTest {
76+
dependencies {
77+
implementation(libs.kotest.assertions.core)
78+
implementation(libs.kotest.framework.engine)
79+
implementation(kotlin("test-common"))
80+
implementation(kotlin("test-annotations-common"))
81+
}
82+
}
83+
jvmTest {
84+
dependencies {
85+
implementation(libs.kotest.runner.junit5)
86+
}
87+
}
88+
}
89+
90+
afterEvaluate {
91+
fun Task.dependsOnTargetTests(targets: List<KotlinTarget>) {
92+
targets.forEach {
93+
if (it is KotlinTargetWithTests<*, *>) {
94+
dependsOn(tasks.getByName("${it.name}Test"))
95+
}
96+
}
97+
}
98+
tasks.register("macOsAllTest") {
99+
group = "verification"
100+
description = "runs all tests for MacOS and IOS targets"
101+
dependsOnTargetTests(macOsTargets)
102+
}
103+
tasks.register("windowsAllTest") {
104+
group = "verification"
105+
description = "runs all tests for Windows targets"
106+
dependsOnTargetTests(windowsTargets)
107+
}
108+
tasks.register("linuxAllTest") {
109+
group = "verification"
110+
description = "runs all tests for Linux targets"
111+
dependsOnTargetTests(linuxTargets)
112+
dependsOn(tasks.getByName("jvmTest"))
113+
dependsOn(tasks.getByName("jsTest"))
114+
dependsOn(tasks.getByName("wasmJsTest"))
115+
}
116+
}
117+
}
118+
119+
ktlint {
120+
version.set(libs.versions.ktlint)
121+
reporters {
122+
reporter(ReporterType.HTML)
123+
}
124+
}
125+
126+
afterEvaluate {
127+
val detektAllTask by tasks.register("detektAll") {
128+
dependsOn(tasks.withType<Detekt>())
129+
}
130+
131+
tasks.named("check").configure {
132+
dependsOn(detektAllTask)
133+
}
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
@file:JvmName("ObjectWrappers")
2+
3+
package io.github.optimumcode.json.schema.objects.wrapper
4+
5+
import io.github.optimumcode.json.schema.ExperimentalApi
6+
import io.github.optimumcode.json.schema.model.AbstractElement
7+
import io.github.optimumcode.json.schema.model.ArrayElement
8+
import io.github.optimumcode.json.schema.model.ObjectElement
9+
import io.github.optimumcode.json.schema.model.PrimitiveElement
10+
import kotlin.jvm.JvmInline
11+
import kotlin.jvm.JvmName
12+
import kotlin.jvm.JvmOverloads
13+
14+
@ExperimentalApi
15+
public class WrappingConfiguration internal constructor(
16+
public val allowSets: Boolean = false,
17+
)
18+
19+
@ExperimentalApi
20+
@JvmOverloads
21+
public fun wrappingConfiguration(allowSets: Boolean = false): WrappingConfiguration = WrappingConfiguration(allowSets)
22+
23+
/**
24+
* Returns an [AbstractElement] produced by converting the [obj] value.
25+
* The [configuration] allows conversion customization.
26+
*
27+
* # The supported types
28+
*
29+
* ## Simple values:
30+
* * [String]
31+
* * [Byte]
32+
* * [Short]
33+
* * [Int]
34+
* * [Long]
35+
* * [Float]
36+
* * [Double]
37+
* * [Boolean]
38+
* * `null`
39+
*
40+
* ## Structures:
41+
* * [Map] -> keys MUST have a [String] type, values MUST be one of the supported types
42+
* * [List] -> elements MUST be one of the supported types
43+
* * [Array] -> elements MUST be one of the supported types
44+
*
45+
* If [WrappingConfiguration.allowSets] is enabled [Set] is also converted to [ArrayElement].
46+
* Please be aware that in order to have consistent verification results
47+
* the [Set] must be one of the ORDERED types, e.g. [LinkedHashSet].
48+
*/
49+
@ExperimentalApi
50+
public fun wrapAsElement(
51+
obj: Any?,
52+
configuration: WrappingConfiguration = WrappingConfiguration(),
53+
): AbstractElement {
54+
if (obj == null) {
55+
return NullWrapper
56+
}
57+
return when {
58+
obj is Map<*, *> -> checkKeysAndWrap(obj, configuration)
59+
obj is List<*> -> ListWrapper(obj.map { wrapAsElement(it, configuration) })
60+
obj is Array<*> -> ListWrapper(obj.map { wrapAsElement(it, configuration) })
61+
obj is Set<*> && configuration.allowSets ->
62+
ListWrapper(obj.map { wrapAsElement(it, configuration) })
63+
obj is String || obj is Number || obj is Boolean -> PrimitiveWrapper(numberToSupportedTypeOrOriginal(obj))
64+
else -> error("unsupported type to wrap: ${obj::class}")
65+
}
66+
}
67+
68+
private fun numberToSupportedTypeOrOriginal(obj: Any): Any =
69+
when (obj) {
70+
!is Number -> obj
71+
is Double, is Long -> obj
72+
is Byte, is Short, is Int -> obj.toLong()
73+
is Float -> obj.toDoubleSafe()
74+
else -> error("unsupported number type: ${obj::class}")
75+
}
76+
77+
private fun Float.toDoubleSafe(): Double {
78+
val double = toDouble()
79+
// in some cases the conversion from float to double
80+
// can introduce a difference between numbers. (e.g. 42.2f -> 42.2)
81+
// In this case, the only way (at the moment) is to try parsing
82+
// the double from float converted to string
83+
val floatAsString = toString()
84+
if (double.toString() == floatAsString) {
85+
return double
86+
}
87+
return floatAsString.toDouble()
88+
}
89+
90+
private fun checkKeysAndWrap(
91+
map: Map<*, *>,
92+
configuration: WrappingConfiguration,
93+
): ObjectWrapper {
94+
if (map.isEmpty()) {
95+
return ObjectWrapper(emptyMap())
96+
}
97+
98+
require(map.keys.all { it is String }) {
99+
val notStrings =
100+
map.keys.asSequence().filterNot { it is String }.mapTo(hashSetOf()) { key ->
101+
key?.let { it::class.simpleName } ?: "null"
102+
}.joinToString()
103+
"map keys must be strings, found: $notStrings"
104+
}
105+
106+
@Suppress("UNCHECKED_CAST")
107+
val elementsMap =
108+
map.mapValues { (_, value) ->
109+
wrapAsElement(value, configuration)
110+
} as Map<String, AbstractElement>
111+
return ObjectWrapper(elementsMap)
112+
}
113+
114+
@JvmInline
115+
private value class ObjectWrapper(
116+
private val map: Map<String, AbstractElement>,
117+
) : ObjectElement {
118+
override val keys: Set<String>
119+
get() = map.keys
120+
121+
override fun get(key: String): AbstractElement? = map[key]
122+
123+
override fun contains(key: String): Boolean = map.containsKey(key)
124+
125+
override val size: Int
126+
get() = map.size
127+
128+
override fun iterator(): Iterator<Pair<String, AbstractElement>> =
129+
map.asSequence().map { (key, value) -> key to value }.iterator()
130+
131+
override fun toString(): String = map.toString()
132+
}
133+
134+
@JvmInline
135+
private value class ListWrapper(
136+
private val list: List<AbstractElement>,
137+
) : ArrayElement {
138+
override fun iterator(): Iterator<AbstractElement> = list.iterator()
139+
140+
override fun get(index: Int): AbstractElement = list[index]
141+
142+
override val size: Int
143+
get() = list.size
144+
145+
override fun toString(): String = list.toString()
146+
}
147+
148+
@JvmInline
149+
private value class PrimitiveWrapper(
150+
private val value: Any,
151+
) : PrimitiveElement {
152+
override val isNull: Boolean
153+
get() = false
154+
override val isString: Boolean
155+
get() = value is String
156+
override val isBoolean: Boolean
157+
get() = value is Boolean
158+
override val isNumber: Boolean
159+
get() = value is Number
160+
override val longOrNull: Long?
161+
get() = value as? Long
162+
override val doubleOrNull: Double?
163+
get() = value as? Double
164+
override val content: String
165+
get() = value.toString()
166+
167+
override fun toString(): String = value.toString()
168+
}
169+
170+
private data object NullWrapper : PrimitiveElement {
171+
override val isNull: Boolean
172+
get() = true
173+
override val isString: Boolean
174+
get() = false
175+
override val isBoolean: Boolean
176+
get() = false
177+
override val isNumber: Boolean
178+
get() = false
179+
override val longOrNull: Long?
180+
get() = null
181+
override val doubleOrNull: Double?
182+
get() = null
183+
override val content: String
184+
get() = "null"
185+
186+
override fun toString(): String = "null"
187+
}

0 commit comments

Comments
 (0)