diff --git a/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ConditionPair.kt b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ConditionPair.kt deleted file mode 100644 index 1bdfea8..0000000 --- a/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ConditionPair.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.errors.mappers - -data class ConditionPair( - val condition: (Throwable) -> Boolean, - val mapper: ThrowableMapper -) diff --git a/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ExceptionMappersStorage.kt b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ExceptionMappersStorage.kt index 4c7fd8c..08a4cd2 100644 --- a/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ExceptionMappersStorage.kt +++ b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ExceptionMappersStorage.kt @@ -10,20 +10,35 @@ import dev.icerock.moko.resources.desc.desc import kotlin.native.concurrent.ThreadLocal import kotlin.reflect.KClass -internal typealias ThrowableMapper = (Throwable) -> Any - @Suppress("TooManyFunctions") @ThreadLocal object ExceptionMappersStorage { - private val fallbackValuesMap: MutableMap, Any> = mutableMapOf( - StringDesc::class to MR.strings.moko_errors_unknownError.desc() + private val containers: MutableMap, MappersContainer<*>> = mutableMapOf( + StringDesc::class to MappersContainer( + mappers = emptyList(), + fallback = { MR.strings.moko_errors_unknownError.desc() } + ) ) + private val notifiers: MutableList<(Throwable, KClass<*>, Any) -> Unit> = mutableListOf() + + private fun getOrCreateContainer(resultClass: KClass): MappersContainer { + val existContainer: MappersContainer<*>? = containers[resultClass] + if (existContainer != null) return existContainer as MappersContainer - private val mappersMap: MutableMap, MutableMap, ThrowableMapper>> = - mutableMapOf() - private val conditionMappers: MutableMap, MutableList> = - mutableMapOf() + return MappersContainer( + mappers = emptyList(), + fallback = { throw FallbackValueNotFoundException(resultClass) } + ).also { containers[resultClass] = it } + } + + private fun updateContainer( + resultClass: KClass, + block: (MappersContainer) -> MappersContainer + ) { + val container: MappersContainer = getOrCreateContainer(resultClass) + containers[resultClass] = block(container) + } /** * Register simple mapper (E) -> T. @@ -33,11 +48,16 @@ object ExceptionMappersStorage { exceptionClass: KClass, mapper: (E) -> T ): ExceptionMappersStorage { - if (!mappersMap.containsKey(resultClass)) { - mappersMap[resultClass] = mutableMapOf() + updateContainer( + resultClass + ) { container -> + container.copy( + mappers = container.mappers + ThrowableMapperItem( + mapper = { mapper(it as E) }, + isApplied = { it::class == exceptionClass } + ) + ) } - @Suppress("UNCHECKED_CAST") - mappersMap[resultClass]?.put(exceptionClass, mapper as ThrowableMapper) return this } @@ -46,12 +66,19 @@ object ExceptionMappersStorage { */ fun register( resultClass: KClass, - conditionPair: ConditionPair + isApplied: (Throwable) -> Boolean, + mapper: (Throwable) -> T ): ExceptionMappersStorage { - if (!conditionMappers.containsKey(resultClass)) { - conditionMappers[resultClass] = mutableListOf() + updateContainer( + resultClass + ) { container -> + container.copy( + mappers = container.mappers + ThrowableMapperItem( + mapper = mapper, + isApplied = isApplied + ) + ) } - conditionMappers[resultClass]?.add(conditionPair) return this } @@ -76,10 +103,8 @@ object ExceptionMappersStorage { noinline mapper: (Throwable) -> T ): ExceptionMappersStorage = register( resultClass = T::class, - conditionPair = ConditionPair( - condition, - mapper as ThrowableMapper - ) + isApplied = condition, + mapper = mapper ) /** @@ -91,19 +116,27 @@ object ExceptionMappersStorage { */ fun find( resultClass: KClass, - throwable: E, - exceptionClass: KClass + throwable: E ): ((E) -> T)? { - @Suppress("UNCHECKED_CAST") - val mapper = conditionMappers[resultClass] - ?.find { it.condition(throwable) } - ?.mapper as? ((E) -> T) - ?: mappersMap[resultClass]?.get(exceptionClass) as? ((E) -> T) + val container: MappersContainer? = containers[resultClass] as MappersContainer? - return if (mapper == null && throwable !is Exception) { + if (container == null && throwable !is Exception) { throw throwable - } else { - mapper + } else if (container == null) { + return null + } + + val mapper: (Throwable) -> T = container.mappers + .firstOrNull { it.isApplied(throwable) } + ?.mapper + ?: container.fallback + + return { exception -> + val result: T = mapper(exception) + notifiers.forEach { notifier -> + notifier(exception, resultClass, result) + } + result } } @@ -116,16 +149,21 @@ object ExceptionMappersStorage { */ inline fun find(throwable: E): ((E) -> T)? = find( resultClass = T::class, - throwable = throwable, - exceptionClass = throwable::class + throwable = throwable ) /** * Sets fallback (default) value for [T] errors type. */ fun setFallbackValue(clazz: KClass, value: T): ExceptionMappersStorage { - fallbackValuesMap[clazz] = value - return ExceptionMappersStorage + updateContainer( + clazz + ) { container -> + container.copy( + fallback = { value } + ) + } + return this } /** @@ -135,36 +173,46 @@ object ExceptionMappersStorage { setFallbackValue(T::class, value) /** - * Returns fallback (default) value for [T] errors type. - * If there is no default value for the class [T], then [FallbackValueNotFoundException] - * exception will be thrown. + * Sets fallback (default) factory for [T] errors type. */ - fun getFallbackValue(clazz: KClass): T { - @Suppress("UNCHECKED_CAST") - return fallbackValuesMap[clazz] as? T - ?: throw FallbackValueNotFoundException(clazz) + fun setFallbackFactory( + clazz: KClass, + factory: (Throwable) -> T + ): ExceptionMappersStorage { + updateContainer( + clazz + ) { container -> + container.copy( + fallback = factory + ) + } + return this } - /** - * Returns fallback (default) value for [T] errors type. - * If there is no default value for the class [T], then [FallbackValueNotFoundException] - * exception will be thrown. - */ - inline fun getFallbackValue(): T = getFallbackValue(T::class) + inline fun setFallbackFactory( + noinline factory: (Throwable) -> T + ): ExceptionMappersStorage = setFallbackFactory(T::class, factory) /** * Factory method that creates mappers (Throwable) -> T with a registered fallback value for * class [T]. */ fun throwableMapper(clazz: KClass): (e: E) -> T { - val fallback = getFallbackValue(clazz) return { e -> - find(clazz, e, e::class)?.invoke(e) ?: fallback + find(clazz, e)?.invoke(e) ?: throw FallbackValueNotFoundException(clazz) } } - inline fun throwableMapper(): (e: E) -> T { - return dev.icerock.moko.errors.mappers.throwableMapper() + /** + * Listen all mappers calls. Useful for logging + * + * @param block - lambda that will be called when exception map to some class + */ + fun onEach( + block: (Throwable, KClass<*>, Any) -> Unit + ): ExceptionMappersStorage { + notifiers.add(block) + return this } } diff --git a/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/MappersContainer.kt b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/MappersContainer.kt new file mode 100644 index 0000000..475f712 --- /dev/null +++ b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/MappersContainer.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.errors.mappers + +internal data class MappersContainer( + val mappers: List>, + val fallback: ThrowableMapper +) diff --git a/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ThrowableMapperItem.kt b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ThrowableMapperItem.kt new file mode 100644 index 0000000..f93b456 --- /dev/null +++ b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ThrowableMapperItem.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.errors.mappers + +internal typealias ThrowableMapper = (Throwable) -> T + +internal data class ThrowableMapperItem( + val mapper: ThrowableMapper, + val isApplied: (Throwable) -> Boolean +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a748a23..edbad45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] kotlinVersion = "1.8.10" -androidAppCompatVersion = "1.2.0" -materialDesignVersion = "1.4.0" -androidLifecycleVersion = "2.1.0" -androidCoreTestingVersion = "2.1.0" -coroutinesVersion = "1.6.0-native-mt" +androidAppCompatVersion = "1.6.1" +materialDesignVersion = "1.8.0" +androidLifecycleVersion = "2.2.0" +androidCoreTestingVersion = "2.2.0" +coroutinesVersion = "1.6.4" mokoMvvmVersion = "0.16.0" mokoResourcesVersion = "0.21.2" mokoErrorsVersion = "0.7.0" diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ExceptionStorage.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ExceptionStorage.kt index 429e59c..39dd5ff 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ExceptionStorage.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ExceptionStorage.kt @@ -14,4 +14,7 @@ fun initExceptionStorage() { .register { MR.strings.illegalArgumentText.desc() } + .onEach { throwable, kClass, result -> + println("$throwable mapped to $kClass with result $result") + } }