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