Created
September 20, 2023 14:45
-
-
Save 0x1b-xyz/504d16390a254a7e1cf46bfde2596ad2 to your computer and use it in GitHub Desktop.
Deep merge and deep get on kotlin `Map<String,Any>` maps
This file contains hidden or 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
import java.util.* | |
import kotlin.reflect.KClass | |
/** | |
* Produces a new map that represents a merge of [source] into [this], returning an immutable view. Map leaves will be | |
* overwritten from [source] into [this]. | |
* | |
* @param source Nested [Map<String,Any>] that will be merged over [this]. Must be [Map<String,Any>] | |
* | |
* @throws IllegalArgumentException When [source] causes a [ClassCastException] caused by a non-[String] key | |
*/ | |
fun Map<String, Any>.deepMerge(source: Map<String, Any>): Map<String, Any> = | |
deepMerge(Stack<String>(), source) | |
fun Map<String, Any>.deepMerge(path: Stack<String>, source: Map<String, Any>): Map<String, Any> { | |
return this.toMutableMap().let { destination -> | |
try { | |
for (key in source.keys) { | |
if (source[key] is Map<*, *> && destination[key] is Map<*, *>) { | |
@Suppress("UNCHECKED_CAST") | |
destination[key] = (destination[key] as Map<String, Any>).deepMerge( | |
path.also { it.push(key) }, | |
(source[key] as Map<String, Any>) | |
) | |
} else { | |
destination[key] = source[key] as Any | |
} | |
path.clear() | |
} | |
} catch (e: ClassCastException) { | |
throw IllegalArgumentException("Cannot deepMerge source['${if (path.empty()) "" else path.joinToString(".")}'] when keys are not Strings ...") | |
} | |
Collections.unmodifiableMap(destination) | |
} | |
} | |
/** | |
* Retrieves a value from a nested map using a dot-notated path. | |
* | |
* @param key Dot-notated path to the value retrieved | |
* | |
* @throws IllegalArgumentException When navigation fails because value is _null_ or we encounter a non | |
* [Map<String,Any>] entity in the path | |
*/ | |
inline fun <reified R : Any> Map<String, Any>.deepGet(key: String): R = | |
deepGet(Stack<String>().also { it.addAll(key.split(".").reversed()) }, key, R::class) | |
fun <R : Any> Map<String, Any>.deepGet(path: Stack<String>, key: String, type: KClass<R>): R { | |
val current = path.pop() | |
return if (path.empty()) { | |
try { | |
@Suppress("UNCHECKED_CAST") | |
this[current] as R | |
} catch (e: Exception) { | |
val failedAt = key.split(".").dropLast(path.size).joinToString(".") | |
throw IllegalArgumentException("Cannot deepGet key '$failedAt' as the value '${if (this[current] == null) "null" else this[current]!!::class.simpleName}' is not ${type.simpleName} ...") | |
} | |
} else { | |
val remaining = this[current] | |
if (remaining !is Map<*, *>) { | |
val failedAt = key.split(".").dropLast(path.size).joinToString(".") | |
throw IllegalArgumentException("Cannot deepGet key '$failedAt' as the value '${if (remaining == null) "null" else remaining::class.simpleName}' is not Map<String,Any> ...") | |
} | |
@Suppress("UNCHECKED_CAST") | |
(remaining as Map<String, Any>).deepGet(path, key, type) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment