Skip to content

Commit ee33cfd

Browse files
authored
add new expermental apis for loose types conversion (#104)
1 parent 77ae411 commit ee33cfd

File tree

2 files changed

+307
-1
lines changed
  • core/src
    • main/java/com/segment/analytics/kotlin/core/utilities
    • test/kotlin/com/segment/analytics/kotlin/core

2 files changed

+307
-1
lines changed

core/src/main/java/com/segment/analytics/kotlin/core/utilities/JSON.kt

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,21 @@
22

33
package com.segment.analytics.kotlin.core.utilities
44

5+
import kotlinx.serialization.ExperimentalSerializationApi
6+
import kotlinx.serialization.KSerializer
7+
import kotlinx.serialization.builtins.BooleanArraySerializer
8+
import kotlinx.serialization.builtins.ByteArraySerializer
9+
import kotlinx.serialization.builtins.CharArraySerializer
10+
import kotlinx.serialization.builtins.DoubleArraySerializer
11+
import kotlinx.serialization.builtins.FloatArraySerializer
12+
import kotlinx.serialization.builtins.IntArraySerializer
13+
import kotlinx.serialization.builtins.LongArraySerializer
14+
import kotlinx.serialization.builtins.ShortArraySerializer
15+
import kotlinx.serialization.builtins.serializer
516
import kotlinx.serialization.json.Json
617
import kotlinx.serialization.json.JsonArray
718
import kotlinx.serialization.json.JsonElement
19+
import kotlinx.serialization.json.JsonNull
820
import kotlinx.serialization.json.JsonObject
921
import kotlinx.serialization.json.JsonObjectBuilder
1022
import kotlinx.serialization.json.JsonPrimitive
@@ -17,6 +29,7 @@ import kotlinx.serialization.json.intOrNull
1729
import kotlinx.serialization.json.jsonObject
1830
import kotlinx.serialization.json.longOrNull
1931
import kotlinx.serialization.json.put
32+
import kotlin.reflect.KClass
2033

2134
val EncodeDefaultsJson = Json {
2235
encodeDefaults = true
@@ -204,4 +217,207 @@ operator fun MutableMap<String, JsonElement>.set(key:String, value: Number) {
204217

205218
operator fun MutableMap<String, JsonElement>.set(key:String, value: Boolean) {
206219
this[key] = JsonPrimitive(value)
220+
}
221+
222+
223+
@OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class)
224+
val primitiveSerializers = mapOf(
225+
String::class to String.serializer(),
226+
Char::class to Char.serializer(),
227+
CharArray::class to CharArraySerializer(),
228+
Double::class to Double.serializer(),
229+
DoubleArray::class to DoubleArraySerializer(),
230+
Float::class to Float.serializer(),
231+
FloatArray::class to FloatArraySerializer(),
232+
Long::class to Long.serializer(),
233+
LongArray::class to LongArraySerializer(),
234+
Int::class to Int.serializer(),
235+
IntArray::class to IntArraySerializer(),
236+
Short::class to Short.serializer(),
237+
ShortArray::class to ShortArraySerializer(),
238+
Byte::class to Byte.serializer(),
239+
ByteArray::class to ByteArraySerializer(),
240+
Boolean::class to Boolean.serializer(),
241+
BooleanArray::class to BooleanArraySerializer(),
242+
Unit::class to Unit.serializer(),
243+
UInt::class to UInt.serializer(),
244+
ULong::class to ULong.serializer(),
245+
UByte::class to UByte.serializer(),
246+
UShort::class to UShort.serializer()
247+
)
248+
249+
/**
250+
* Experimental API that can be used to convert primitive
251+
* values to their equivalent JsonElement representation.
252+
*/
253+
inline fun <reified T : Any> serializerFor(value: KClass<out T>): KSerializer<T>? {
254+
val serializer = primitiveSerializers[value] ?: return null
255+
return serializer as KSerializer<T>
256+
}
257+
258+
/**
259+
* Experimental API that can be used to convert Map
260+
* values to their equivalent JsonElement representation.
261+
*/
262+
fun Map<String, Any>.toJsonElement(): JsonElement {
263+
return buildJsonObject {
264+
for ((key, value) in this@toJsonElement) {
265+
if (value is JsonElement) {
266+
put(key, value)
267+
} else {
268+
put(key, value.toJsonElement())
269+
}
270+
}
271+
}
272+
}
273+
274+
/**
275+
* Experimental API that can be used to convert Array
276+
* values to their equivalent JsonElement representation.
277+
*/
278+
fun Array<Any>.toJsonElement(): JsonArray {
279+
return buildJsonArray {
280+
for (item in this@toJsonElement) {
281+
if (item is JsonElement) {
282+
add(item)
283+
} else {
284+
add(item.toJsonElement())
285+
}
286+
}
287+
}
288+
}
289+
290+
/**
291+
* Experimental API that can be used to convert Collection
292+
* values to their equivalent JsonElement representation.
293+
*/
294+
fun Collection<Any>.toJsonElement(): JsonArray {
295+
// Specifically chose Collection over Iterable, bcos
296+
// Iterable is more widely overriden, whereas Collection
297+
// is more in line with our target types eg: Lists, Sets etc
298+
return buildJsonArray {
299+
for (item in this@toJsonElement) {
300+
if (item is JsonElement) {
301+
add(item)
302+
} else {
303+
add(item.toJsonElement())
304+
}
305+
}
306+
}
307+
}
308+
309+
/**
310+
* Experimental API that can be used to convert Pair
311+
* values to their equivalent JsonElement representation.
312+
*/
313+
fun Pair<Any, Any>.toJsonElement(): JsonElement {
314+
val v1 = first.toJsonElement()
315+
val v2 = second.toJsonElement()
316+
return buildJsonObject {
317+
put("first", v1)
318+
put("second", v2)
319+
}
320+
}
321+
322+
/**
323+
* Experimental API that can be used to convert Triple
324+
* values to their equivalent JsonElement representation.
325+
*/
326+
fun Triple<Any, Any, Any>.toJsonElement(): JsonElement {
327+
val v1 = first.toJsonElement()
328+
val v2 = second.toJsonElement()
329+
val v3 = third.toJsonElement()
330+
return buildJsonObject {
331+
put("first", v1)
332+
put("second", v2)
333+
put("third", v3)
334+
}
335+
}
336+
337+
/**
338+
* Experimental API that can be used to convert Map.Entry
339+
* values to their equivalent JsonElement representation.
340+
*/
341+
fun Map.Entry<Any, Any>.toJsonElement(): JsonElement {
342+
val key = key.toJsonElement()
343+
val value = value.toJsonElement()
344+
return buildJsonObject {
345+
put("key", key)
346+
put("value", value)
347+
}
348+
}
349+
350+
/**
351+
* Experimental API that can be used to convert most kotlin
352+
* primitive values to their equivalent JsonElement representation.
353+
* Primitive here should mean any types declared in Kotlin SDK or JVM,
354+
* and not brought in by an external library.
355+
*
356+
* Any unknown custom type will be representated as JsonNull
357+
*
358+
* Currently supported types
359+
* - String
360+
* - Char
361+
* - CharArray
362+
* - Double
363+
* - DoubleArray
364+
* - Float
365+
* - FloatArray
366+
* - Long
367+
* - LongArray
368+
* - Int
369+
* - IntArray
370+
* - Short
371+
* - ShortArray
372+
* - Byte
373+
* - ByteArray
374+
* - Boolean
375+
* - BooleanArray
376+
* - Unit
377+
* - UInt
378+
* - ULong
379+
* - UByte
380+
* - UShort
381+
* - Collection<Any>
382+
* - Map<String, Any>
383+
* - Array<Any>
384+
* - Pair<Any, Any>
385+
* - Triple<Any, Any>
386+
* - Map.Entry<Any, Any>
387+
*
388+
* Happy to accept more supported types in the future
389+
*/
390+
fun Any.toJsonElement(): JsonElement {
391+
when (this) {
392+
is Map<*, *> -> {
393+
val value = this as Map<String, Any>
394+
return value.toJsonElement()
395+
}
396+
is Array<*> -> {
397+
val value = this as Array<Any>
398+
return value.toJsonElement()
399+
}
400+
is Collection<*> -> {
401+
val value = this as Collection<Any>
402+
return value.toJsonElement()
403+
}
404+
is Pair<*, *> -> {
405+
val value = this as Pair<Any, Any>
406+
return value.toJsonElement()
407+
}
408+
is Triple<*, *, *> -> {
409+
val value = this as Triple<Any, Any, Any>
410+
return value.toJsonElement()
411+
}
412+
is Map.Entry<*, *> -> {
413+
val value = this as Map.Entry<Any, Any>
414+
return value.toJsonElement()
415+
}
416+
else -> {
417+
serializerFor(this::class)?.let {
418+
return Json.encodeToJsonElement(it, this)
419+
}
420+
}
421+
}
422+
return JsonNull
207423
}

core/src/test/kotlin/com/segment/analytics/kotlin/core/JSONTests.kt

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
package com.segment.analytics.kotlin.core
22

3-
import com.segment.analytics.kotlin.core.utilities.*
3+
import com.segment.analytics.kotlin.core.utilities.getBoolean
4+
import com.segment.analytics.kotlin.core.utilities.getDouble
5+
import com.segment.analytics.kotlin.core.utilities.getInt
6+
import com.segment.analytics.kotlin.core.utilities.getLong
7+
import com.segment.analytics.kotlin.core.utilities.getMapList
8+
import com.segment.analytics.kotlin.core.utilities.getString
9+
import com.segment.analytics.kotlin.core.utilities.getStringSet
10+
import com.segment.analytics.kotlin.core.utilities.mapTransform
11+
import com.segment.analytics.kotlin.core.utilities.putAll
12+
import com.segment.analytics.kotlin.core.utilities.putUndefinedIfNull
13+
import com.segment.analytics.kotlin.core.utilities.set
14+
import com.segment.analytics.kotlin.core.utilities.toContent
15+
import com.segment.analytics.kotlin.core.utilities.toJsonElement
16+
import com.segment.analytics.kotlin.core.utilities.transformKeys
17+
import com.segment.analytics.kotlin.core.utilities.transformValues
18+
import com.segment.analytics.kotlin.core.utilities.updateJsonObject
19+
import kotlinx.serialization.json.JsonNull
420
import kotlinx.serialization.json.JsonPrimitive
521
import kotlinx.serialization.json.add
622
import kotlinx.serialization.json.boolean
@@ -764,4 +780,78 @@ class JSONTests {
764780
assertEquals(true, obj.getBoolean("boolean"))
765781
}
766782
}
783+
784+
@Nested
785+
inner class ToJsonElementTests {
786+
@Test
787+
fun `serialize loose hashmap object correctly`() {
788+
val obj = HashMap<String, Any>().apply {
789+
put("key1", "Some String")
790+
put("key2", true)
791+
put("key3", false)
792+
put("key4", 10)
793+
put("key5", 10.2)
794+
put("key6", 10.3f)
795+
put("key7", listOf(1, 2, 3))
796+
put("key8", arrayOf(1, 2, 3))
797+
put("key9", setOf(1, 2, 3))
798+
put("key10", IntArray(3) { x -> x })
799+
put("key11", (1 to 2))
800+
put("key12", mapOf("1" to 1, "2" to 2))
801+
put("key13", Triple(1, 2, 3))
802+
put("key14", mapOf("1" to 1, "2" to 2, "3" to mapOf("new" to mapOf("4" to 2))))
803+
put("key15", (1 to 2 to 3))
804+
put("key16", listOf("1", listOf("1", "2", listOf("1", "2"))))
805+
}.toJsonElement()
806+
val expected = buildJsonObject {
807+
put("key1", "Some String")
808+
put("key2", true)
809+
put("key3", false)
810+
put("key4", 10)
811+
put("key5", 10.2)
812+
put("key6", 10.3f)
813+
put("key7", buildJsonArray { add(1); add(2); add(3) })
814+
put("key8", buildJsonArray { add(1); add(2); add(3) })
815+
put("key9", buildJsonArray { add(1); add(2); add(3) })
816+
put("key10", buildJsonArray { add(1); add(2); add(3) })
817+
put("key10", buildJsonArray { add(0); add(1); add(2) })
818+
put("key11", buildJsonObject { put("first", 1); put("second", 2) })
819+
put("key12", buildJsonObject { put("1", 1); put("2", 2) })
820+
put("key13", buildJsonObject { put("first", 1); put("second", 2); put("third", 3) })
821+
put("key14",
822+
buildJsonObject {
823+
put("1", 1)
824+
put("2", 2)
825+
put("3",
826+
buildJsonObject { put("new", buildJsonObject { put("4", 2) }) }
827+
)
828+
})
829+
put("key15",
830+
buildJsonObject {
831+
put("first", buildJsonObject { put("first", 1); put("second", 2) })
832+
put("second", 3)
833+
})
834+
put("key16",
835+
buildJsonArray {
836+
add("1")
837+
add(buildJsonArray {
838+
add("1"); add("2"); add(buildJsonArray { add("1"); add("2") })
839+
})
840+
})
841+
}
842+
assertEquals(expected, obj)
843+
}
844+
845+
@Test
846+
fun `serialize unknown non-primitive object as JsonNull`() {
847+
class Color(val hex: String)
848+
val obj = HashMap<String, Any>().apply {
849+
put("key16", Color("#FFF"))
850+
}.toJsonElement()
851+
val expected = buildJsonObject {
852+
put("key16", JsonNull)
853+
}
854+
assertEquals(expected, obj)
855+
}
856+
}
767857
}

0 commit comments

Comments
 (0)