diff --git a/core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt b/core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt index 7281b95f..cc4bfb4a 100644 --- a/core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt +++ b/core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt @@ -17,7 +17,7 @@ import java.util.concurrent.Executors * @property ioDispatcher Dispatcher running IO tasks, defaults to `Dispatchers.IO` * @property storageProvider Provider for storage class, defaults to `ConcreteStorageProvider` * @property collectDeviceId collect deviceId, defaults to `false` - * @property trackApplicationLifecycleEvents automatically track Lifecycle events, defaults to `false` + * @property trackApplicationLifecycleEvents automatically send track for Lifecycle events (eg: Application Opened, Application Backgrounded, etc.), defaults to `false` * @property useLifecycleObserver enables the use of LifecycleObserver to track Application lifecycle events. Defaults to `false`. * @property trackDeepLinks automatically track [Deep link][https://developer.android.com/training/app-links/deep-linking] opened based on intents, defaults to `false` * @property flushAt count of events at which we flush events, defaults to `20` diff --git a/core/src/main/java/com/segment/analytics/kotlin/core/utilities/Base64Utils.kt b/core/src/main/java/com/segment/analytics/kotlin/core/utilities/Base64Utils.kt index fbb4a3f7..542bb09e 100644 --- a/core/src/main/java/com/segment/analytics/kotlin/core/utilities/Base64Utils.kt +++ b/core/src/main/java/com/segment/analytics/kotlin/core/utilities/Base64Utils.kt @@ -3,7 +3,7 @@ package com.segment.analytics.kotlin.core.utilities // Encode string to base64 fun encodeToBase64(str: String) = encodeToBase64(str.toByteArray()) -// Encode byte-array to base64 +// Encode byte-array to base64, this implementation is not url-safe fun encodeToBase64(bytes: ByteArray) = buildString { val wData = ByteArray(3) // working data var i = 0 diff --git a/core/src/main/java/com/segment/analytics/kotlin/core/utilities/JSON.kt b/core/src/main/java/com/segment/analytics/kotlin/core/utilities/JSON.kt index 429ff6bf..86cc1c29 100644 --- a/core/src/main/java/com/segment/analytics/kotlin/core/utilities/JSON.kt +++ b/core/src/main/java/com/segment/analytics/kotlin/core/utilities/JSON.kt @@ -1,10 +1,13 @@ package com.segment.analytics.kotlin.core.utilities +import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObjectBuilder import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.doubleOrNull import kotlinx.serialization.json.floatOrNull @@ -83,6 +86,57 @@ fun JsonObject.getMapSet(key: String): Set>? { null } +// Utility function to apply key-mappings (deep traversal) and an optional value transform +fun JsonObject.mapTransform( + keyMapper: Map, + valueTransform: ((key: String, value: JsonElement) -> JsonElement)? = null +): JsonObject = buildJsonObject { + val original = this@mapTransform + original.forEach { (key, value) -> + var newKey: String = key + var newVal: JsonElement = value + // does this key1 have a mapping? + keyMapper[key]?.let { mappedKey -> + newKey = mappedKey + } + + // is this value a dictionary? + if (value is JsonObject) { + // if so, lets recurse... + newVal = value.mapTransform(keyMapper, valueTransform) + } else if (value is JsonArray) { + newVal = value.mapTransform(keyMapper, valueTransform) + } + if (newVal !is JsonObject && valueTransform != null) { + // it's not a dictionary apply our transform. + // note: if it's an array, we've processed any dictionaries inside + // already, but this gives the opportunity to apply a transform to the other + // items in the array that weren't dictionaries. + + newVal = valueTransform(newKey, newVal) + } + put(newKey, newVal) + } +} + +// Utility function to apply key-mappings (deep traversal) and an optional value transform +fun JsonArray.mapTransform( + keyMapper: Map, + valueTransform: ((key: String, value: JsonElement) -> JsonElement)? = null +): JsonArray = buildJsonArray { + val original = this@mapTransform + original.forEach { item: JsonElement -> + var newValue = item + if (item is JsonObject) { + newValue = item.mapTransform(keyMapper, valueTransform) + } else if (item is JsonArray) { + newValue = item.mapTransform(keyMapper, valueTransform) + } + add(newValue) + } +} + + // Utility function to transform keys in JsonObject. Only acts on root level keys fun JsonObject.transformKeys(transform: (String) -> String): JsonObject { return JsonObject(this.mapKeys { transform(it.key) }) diff --git a/core/src/test/kotlin/com/segment/analytics/kotlin/core/JSONTests.kt b/core/src/test/kotlin/com/segment/analytics/kotlin/core/JSONTests.kt index a0d3d9e1..a21ae64d 100644 --- a/core/src/test/kotlin/com/segment/analytics/kotlin/core/JSONTests.kt +++ b/core/src/test/kotlin/com/segment/analytics/kotlin/core/JSONTests.kt @@ -6,6 +6,8 @@ import com.segment.analytics.kotlin.core.utilities.getInt import com.segment.analytics.kotlin.core.utilities.getMapSet import com.segment.analytics.kotlin.core.utilities.getString import com.segment.analytics.kotlin.core.utilities.getStringSet +import com.segment.analytics.kotlin.core.utilities.mapTransform +import com.segment.analytics.kotlin.core.utilities.toContent import com.segment.analytics.kotlin.core.utilities.transformKeys import com.segment.analytics.kotlin.core.utilities.transformValues import kotlinx.serialization.json.* @@ -337,4 +339,167 @@ class JSONTests { assertEquals("M", getString("Mr. Freeze")) } } + + @Test + fun `can map keys + nested keys using mapTransform`() { + val keyMapper = mapOf( + "item" to "\$item", + "phone" to "\$phone", + "name" to "\$name", + ) + val map = buildJsonObject { + put("company", buildJsonObject { + put("phone", "123-456-7890") + put("name", "Wayne Industries") + }) + put("family", buildJsonArray { + add(buildJsonObject { put("name", "Mary") }) + add(buildJsonObject { put("name", "Thomas") }) + }) + put("name", "Bruce") + put("last_name", "wayne") + put("item", "Grapple") + } + val newMap = map.mapTransform(keyMapper) + with(newMap) { + assertTrue(containsKey("\$name")) + assertTrue(containsKey("\$item")) + assertTrue(containsKey("last_name")) + with(get("company")!!.jsonObject) { + assertTrue(containsKey("\$phone")) + assertTrue(containsKey("\$name")) + } + with(get("family")!!.jsonArray) { + assertTrue(get(0).jsonObject.containsKey("\$name")) + assertTrue(get(1).jsonObject.containsKey("\$name")) + } + } + } + + @Test + fun `can transform values using mapTransform`() { + val map = buildJsonObject { + put("company", buildJsonObject { + put("phone", "123-456-7890") + put("name", "Wayne Industries") + }) + put("family", buildJsonArray { + add(buildJsonObject { put("name", "Mary") }) + add(buildJsonObject { put("name", "Thomas") }) + }) + put("name", "Bruce") + put("last_name", "wayne") + put("item", "Grapple") + } + val newMap = map.mapTransform(emptyMap()) { newKey, value -> + var newVal = value + if (newKey == "phone") { + val foo = value.jsonPrimitive.toContent() + if (foo is String) { + newVal = JsonPrimitive(foo.replace("-", "")) + } + } + newVal + } + with(newMap) { + with(get("company")!!.jsonObject) { + assertEquals("1234567890", getString("phone")) + } + } + } + + @Test + fun `can map keys + transform values using mapTransform`() { + val keyMapper = mapOf( + "item" to "\$item", + "phone" to "\$phone", + "name" to "\$name", + ) + val map = buildJsonObject { + put("company", buildJsonObject { + put("phone", "123-456-7890") + put("name", "Wayne Industries") + }) + put("family", buildJsonArray { + add(buildJsonObject { put("name", "Mary") }) + add(buildJsonObject { put("name", "Thomas") }) + }) + put("name", "Bruce") + put("last_name", "wayne") + put("item", "Grapple") + } + val newMap = map.mapTransform(keyMapper) { newKey, value -> + var newVal = value + if (newKey == "\$phone") { + val foo = value.jsonPrimitive.toContent() + if (foo is String) { + newVal = JsonPrimitive(foo.replace("-", "")) + } + } + newVal + } + with(newMap) { + assertTrue(containsKey("\$name")) + assertTrue(containsKey("\$item")) + assertTrue(containsKey("last_name")) + with(get("company")!!.jsonObject) { + assertTrue(containsKey("\$phone")) + assertTrue(containsKey("\$name")) + assertEquals("1234567890", getString("\$phone")) + } + with(get("family")!!.jsonArray) { + assertTrue(get(0).jsonObject.containsKey("\$name")) + assertTrue(get(1).jsonObject.containsKey("\$name")) + } + } + } + + @Test + fun `can map keys + transform values using mapTransform on JsonArray`() { + val keyMapper = mapOf( + "item" to "\$item", + "phone" to "\$phone", + "name" to "\$name", + ) + val list = buildJsonArray { + add(buildJsonObject { + put("phone", "123-456-7890") + put("name", "Wayne Industries") + }) + add(buildJsonArray { + add(buildJsonObject { put("name", "Mary") }) + add(buildJsonObject { put("name", "Thomas") }) + }) + add(buildJsonObject { + put("name", "Bruce") + put("last_name", "wayne") + put("item", "Grapple") + }) + } + val newList = list.mapTransform(keyMapper) { newKey, value -> + var newVal = value + if (newKey == "\$phone") { + val foo = value.jsonPrimitive.toContent() + if (foo is String) { + newVal = JsonPrimitive(foo.replace("-", "")) + } + } + newVal + } + with(newList) { + get(0).jsonObject.let { + assertTrue(it.containsKey("\$phone")) + assertTrue(it.containsKey("\$name")) + } + get(1).jsonArray.let { + assertEquals(buildJsonObject { put("\$name", "Mary") }, it[0]) + assertEquals(buildJsonObject { put("\$name", "Thomas") }, it[1]) + } + get(2).jsonObject.let { + assertTrue(it.containsKey("\$name")) + assertTrue(it.containsKey("\$item")) + assertTrue(it.containsKey("last_name")) + } + } + } } \ No newline at end of file