diff --git a/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_chain.kt b/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_chain.kt index 8676208..73f6dac 100644 --- a/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_chain.kt +++ b/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_chain.kt @@ -11,34 +11,62 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -/* Regular Functions ---------------------------------------------------------------------------- */ - +/* region Regular Functions --------------------------------------------------------------------- */ /** - * Chains this [LiveData] with another [LiveData] based on a condition. + * Chains this [LiveData] with another [LiveData] based on a condition, returning a pair of values from both sources. * * @param other A function that returns another [LiveData] based on the value of this [LiveData]. - * @param condition A function that determines whether to chain with the other [LiveData]. - * @return A [LiveData] emitting pairs of values from this and the chained [LiveData]. - * - * This method observes the current [LiveData] and chains it with another [LiveData] when the provided condition is met. - * It combines the values emitted by both [LiveData] sources into a pair and emits them. - * - * Example usage: + * The [LiveData] provided by this function will be chained with the current [LiveData] if the condition is met. + * @param condition A function that determines whether to chain with the other [LiveData]. If this function + * returns `true`, the [other] function is invoked to provide the [LiveData] to be chained. + * @return A [LiveData] emitting a [Pair] of values where the first component is from the original [LiveData] and the + * second component is from the chained [LiveData]. + * + * This method allows for chaining two [LiveData] sources based on a specified condition and returns a [LiveData] + * containing a pair of values from both sources. This is particularly useful when you need to combine related data + * from two different sources and observe the combined result. + * + * ### Success Case: + * - If the `condition` function returns `true`, the [other] function is called to retrieve another [LiveData]. + * - The resulting [LiveData] emits a [Pair] containing the value from the original [LiveData] as the first component, + * and the value from the chained [LiveData] as the second component. + * - If either of the values is `null`, the corresponding component in the emitted [Pair] will be `null`. + * + * ### Failure Case: + * - If the `condition` function returns `false`, no chaining occurs, and the original [LiveData] emits its values unchanged. + * - If the [other] function throws an exception or returns `null`, the chaining process stops, and the original [LiveData] + * continues emitting its values without the paired [LiveData]. + * - If the original [LiveData] emits `null`, the [other] function might return `null`, leading to a `Pair(null, null)` being emitted. + * + * ### Example Usage: * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData> = liveData1.chainWith( - * other = { liveData2 }, - * condition = { it != null } + * // Assuming we have LiveData sources for a user's ID and user details + * val userIdLiveData: LiveData = ... + * val userDetailLiveData: LiveData> = userIdLiveData.chainWith( + * // Function to get user details based on user ID + * other = { userId -> getUserDetailLiveData(userId) }, + * // Condition to chain only for valid user IDs + * condition = { userId -> userId != null && userId > 0 } * ) + * + * // Observing the resulting LiveData + * userDetailLiveData.observe(this, Observer { result -> + * val (userId, userDetail) = result + * println("User ID: $userId, User Details: $userDetail") + * }) * ``` + * + * ### Error Handling: + * - If any exceptions are thrown during the [other] function execution, they are caught, and the resulting `LiveData` + * will not emit a value for that cycle. Instead, the original [LiveData] continues emitting its values. + * + * @param T The type of the value in the original [LiveData]. + * @param R The type of the value in the chained [LiveData]. */ fun LiveData.chainWith( other: (T?) -> LiveData, @@ -64,23 +92,54 @@ fun LiveData.chainWith( } /** - * Chains this [LiveData] with another non-nullable [LiveData] based on a condition. + * Chains this [LiveData] with another non-nullable [LiveData] based on a condition, emitting pairs of non-nullable values. * * @param other A function that returns another [LiveData] based on the non-null value of this [LiveData]. - * @param condition A function that determines whether to chain with the other [LiveData] based on a non-null value. - * @return A [LiveData] emitting pairs of non-nullable values from this and the chained [LiveData]. - * - * This method is a variant of `chainWith` that only works with non-nullable values. It throws an error if the value is null. - * - * Example usage: + * This function is invoked when the condition is met, and its result is chained with the current [LiveData]. + * @param condition A function that determines whether to chain with the other [LiveData] based on the non-null value of this [LiveData]. + * If the condition evaluates to `true`, the `other` function is called to get the chained [LiveData]. + * @return A [LiveData] emitting pairs of non-nullable values from this [LiveData] and the chained [LiveData]. + * + * This method is a variant of `chainWith` that operates exclusively with non-nullable values. It ensures that only non-null values + * are passed to the `other` function and the `condition` function. If this [LiveData] emits a null value, no further actions are taken, + * and the chaining does not occur. The resulting [LiveData] emits a [Pair] containing values from both [LiveData] sources. + * + * ### Success Case: + * - If this [LiveData] emits a non-null value, the `condition` function is evaluated. + * - If the `condition` returns `true`, the `other` function is called to get another [LiveData]. + * - The resulting [LiveData] emits a [Pair] containing the non-null value from this [LiveData] and the value from the chained [LiveData]. + * - If both values are non-null, they are combined into a [Pair] and emitted. + * + * ### Failure Case: + * - If this [LiveData] emits a null value, no chaining occurs, and the method does nothing for that emission. + * - If the `condition` function returns `false`, the chaining is not performed, and the resulting [LiveData] does not emit a value for that emission. + * - If the `other` function fails or returns null, the resulting [LiveData] does not emit a value for that emission. + * + * ### Example Usage: * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData> = liveData1.chainNotNullWith( - * other = { liveData2 }, - * condition = { it != null } + * // Assuming we have LiveData sources for an order ID and order details + * val orderIdLiveData: LiveData = MutableLiveData(123) + * val orderDetailLiveData: LiveData = orderIdLiveData.chainNotNullWith( + * // Function to get order details based on order ID + * other = { orderId -> getOrderDetailLiveData(orderId) }, + * // Condition to chain only for valid order IDs + * condition = { orderId -> orderId > 0 } * ) + * + * // Observing the resulting LiveData + * orderDetailLiveData.observe(this, Observer { result -> + * val (orderId, orderDetail) = result + * println("Order ID: $orderId, Order Details: $orderDetail") + * }) * ``` + * + * ### Error Handling: + * - If this [LiveData] emits a null value, the method ignores it and takes no further action. + * - If the `condition` function throws an exception, the exception is caught, and the condition is treated as false, so the chaining does not occur. + * - If the `other` function throws an exception, it is caught, and no value is emitted by the resulting [LiveData] for that emission. + * + * @param T The type of the value in the original [LiveData]. + * @param R The type of the value in the chained [LiveData]. */ fun LiveData.chainNotNullWith( other: (T) -> LiveData, @@ -102,97 +161,229 @@ fun LiveData.chainNotNullWith( mediator.addSource(this, ::onAReceived) return mediator } +/* endregion ------------------------------------------------------------------------------------ */ -/* Coroutine Functions -------------------------------------------------------------------------- */ - +/* region Nullable -------------------------------------------------------------------------- */ /** - * Chains this [LiveData] with another [LiveData] based on a condition, using coroutines. + * Chains this [LiveData] with another [LiveData] based on a condition, returning a pair of values from both sources. * - * @param context The [CoroutineContext] to use for the coroutine. + * @param context The [CoroutineContext] to use for the coroutine. This context controls the execution of the chaining logic, + * allowing you to specify which thread or dispatcher should be used. By default, you can use [Dispatchers.IO] for IO-bound tasks + * or [Dispatchers.Main] for UI-related tasks. * @param other A suspend function that returns another [LiveData] based on the value of this [LiveData]. - * @param condition A suspend function that determines whether to chain with the other [LiveData]. - * @return A [LiveData] emitting pairs of values from this and the chained [LiveData]. - * - * This coroutine-based method allows for asynchronous operations when chaining [LiveData] sources. - * - * Example usage: + * The [LiveData] provided by this function will be chained with the current [LiveData] if the condition is met. + * @param condition A suspend function that determines whether to chain with the other [LiveData]. If this function + * returns `true`, the [other] function is invoked to provide the [LiveData] to be chained. + * @return A [LiveData] emitting a [Pair] of values where the first component is from the original [LiveData] and the + * second component is from the chained [LiveData]. + * + * This method allows for chaining two [LiveData] sources based on a specified condition and returns a [LiveData] + * containing a pair of values from both sources. This is particularly useful when you need to combine related data + * from two different sources and observe the combined result. + * + * ### Success Case: + * - If the `condition` function returns `true`, the [other] function is called to retrieve another [LiveData]. + * - The resulting [LiveData] emits a [Pair] containing the value from the original [LiveData] as the first component, + * and the value from the chained [LiveData] as the second component. + * - If either of the values is `null`, the corresponding component in the emitted [Pair] will be `null`. + * + * ### Failure Case: + * - If the `condition` function returns `false`, no chaining occurs, and the original [LiveData] emits its values unchanged. + * - If the [other] function throws an exception or returns `null`, the chaining process stops, and the original [LiveData] + * continues emitting its values without the paired [LiveData]. + * - If the original [LiveData] emits `null`, the [other] function might return `null`, leading to a `Pair(null, null)` being emitted. + * + * ### Example Usage: * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData> = liveData1.chainWith( - * context = Dispatchers.IO, - * other = { liveData2 }, - * condition = { it != null } + * // Assuming we have LiveData sources for a user's ID and user details + * val userIdLiveData: LiveData = ... + * val userDetailLiveData: LiveData> = userIdLiveData.chainWith( + * context = Dispatchers.Main, + * // Function to get user details based on user ID + * other = { userId -> getUserDetailLiveData(userId) }, + * // Condition to chain only for valid user IDs + * condition = { userId -> userId != null && userId > 0 } * ) + * + * // Observing the resulting LiveData + * userDetailLiveData.observe(this, Observer { result -> + * val (userId, userDetail) = result + * println("User ID: $userId, User Details: $userDetail") + * }) * ``` + * + * ### Error Handling: + * - If any exceptions are thrown during the [other] function execution, they are caught, and the resulting `LiveData` + * will not emit a value for that cycle. Instead, the original [LiveData] continues emitting its values. + * + * @param T The type of the value in the original [LiveData]. + * @param R The type of the value in the chained [LiveData]. */ fun LiveData.chainWith( context: CoroutineContext, other: suspend (T?) -> LiveData, condition: suspend (T?) -> Boolean, -): LiveData> = liveData(context) { internalChainWith(other, condition).collect(::emit) } +): LiveData> = + liveData(context) { internalChainWith(other, condition).collect(::emit) } /** * Chains this [LiveData] with another [LiveData] based on a condition, using a transformation function. * - * @param context The [CoroutineContext] to use for the coroutine. + * @param context The [CoroutineContext] to use for the coroutine. This context controls the execution of the chaining logic, + * allowing you to specify which thread or dispatcher should be used. By default, you can use [Dispatchers.IO] for IO-bound tasks + * or [Dispatchers.Main] for UI-related tasks. * @param other A suspend function that returns another [LiveData] based on the value of this [LiveData]. - * @param condition A suspend function that determines whether to chain with the other [LiveData]. - * @param transform A pair consisting of a [CoroutineDispatcher] and a suspend function to transform the values. - * @return A [LiveData] emitting the transformed values. + * The [LiveData] provided by this function will be chained with the current [LiveData] if the condition is met. + * @param condition A suspend function that determines whether to chain with the other [LiveData]. If this function + * returns `true`, the [other] function is invoked to provide the [LiveData] to be chained. + * @param transform A [Transform] object consisting of a [CoroutineDispatcher], a transformation function, a failure mode, + * and an optional error-handling function. + * @return A [LiveData] emitting the transformed values as `X?`. * * This method allows for chaining two [LiveData] sources and applying a transformation function to the combined values. - * The transformation is executed in the provided [CoroutineDispatcher]. - * - * Example usage: + * The transformation function is applied in the context provided by the [CoroutineDispatcher] within [Transform], + * ensuring that the operation is performed on the appropriate thread. + * + * The `transform` parameter allows for detailed control over how the transformation is handled: + * - **dispatcher**: The [CoroutineDispatcher] that defines the thread where the transformation will be executed. + * - **func**: The suspend function that performs the transformation. It receives the values from the original and + * chained [LiveData] and produces the transformed output. + * - **failMode**: Specifies how to handle failures during the transformation: + * - [Transform.Mode.OMIT_WHEN_FAIL]: Omits the emission if the transformation fails. + * - [Transform.Mode.NULL_WHEN_FAIL]: Emits `null` if the transformation fails. + * - **onErrorReturn**: An optional suspend function that handles errors during the transformation, returning a fallback value. + * If this is provided and the transformation fails, the fallback value will be emitted instead of omitting or emitting `null`. + * + * ### Success Case: + * - If the `condition` function returns `true`, the [other] function is called to retrieve another [LiveData]. + * - The values from the original [LiveData] and the chained [LiveData] are passed to the transformation function. + * - If the transformation function completes successfully, its result is emitted by the resulting [LiveData]. + * + * ### Failure Case: + * - If the `condition` function returns `false`, no chaining occurs, and the current [LiveData] is returned unchanged. + * - If the [other] function throws an exception or returns `null`, the chaining process stops, and the current [LiveData] + * is emitted with no transformation. + * - If the transformation function throws an exception: + * - If `failMode` is [Transform.Mode.OMIT_WHEN_FAIL], the emission is omitted. + * - If `failMode` is [Transform.Mode.NULL_WHEN_FAIL], `null` is emitted. + * - If an `onErrorReturn` function is provided, its result will be emitted instead of omitting or emitting `null`. + * + * ### The `Transform.Nullable` class offers several variations: + * - **OmitFail**: + * - **Description**: Omits the result when the transformation fails, without handling the error. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired non-nullable values. + * - **Example**: + * ```kotlin + * val transform = Transform.Nullable.OmitFail( + * dispatcher = Dispatchers.Default, + * func = { intValue, stringValue -> "$intValue: $stringValue" } + * ) + * ``` + * - **NullFail**: + * - **Description**: Emits `null` when the transformation fails, without handling the error. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired nullable values. + * - **Example**: + * ```kotlin + * val transform = Transform.Nullable.NullFail( + * dispatcher = Dispatchers.IO, + * func = { intValue, stringValue -> "$intValue: $stringValue" } + * ) + * ``` + * - **Fallback**: + * - **Description**: Omits the result when the transformation fails but provides a fallback value via `onErrorReturn`. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired nullable values. + * - `onErrorReturn`: A function that handles errors during transformation, allowing you to provide an alternate result. + * - **Example**: + * ```kotlin + * val transform = Transform.Nullable.Fallback( + * dispatcher = Dispatchers.IO, + * func = { intValue, stringValue -> "$intValue: $stringValue" }, + * onErrorReturn = { error -> "Error: ${error.message}" } + * ) + * ``` + * - **Custom**: + * - **Description**: A customizable transformation that lets you define the dispatcher, fail mode, + * transformation function, and optional error handling. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. + * - `failMode`: Specifies how to handle failures during transformation. Options include: + * - `OMIT_WHEN_FAIL`: Omits the emission if the transformation fails. + * - `NULL_WHEN_FAIL`: Emits `null` if the transformation fails. + * - `func`: The transformation function to apply to the paired nullable values. + * - `onErrorReturn`: An optional function to handle errors during transformation. + * - **Example**: + * ```kotlin + * val transform = Transform.Nullable.Custom( + * dispatcher = Dispatchers.Default, + * failMode = Transform.Mode.OMIT_WHEN_FAIL, + * func = { intValue, stringValue -> "$intValue: $stringValue" }, + * onErrorReturn = { error -> "Error: ${error.message}" } + * ) + * ``` + * + * ### Example Usage: * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData = liveData1.chainWith( - * context = Dispatchers.Default, - * other = { liveData2 }, - * condition = { it != null }, - * transform = Dispatchers.IO to { a, b -> "$a$b" } + * // Assuming we have LiveData sources for a user's ID and user details + * val userIdLiveData: LiveData = ... + * val userDetailLiveData: LiveData = userIdLiveData.chainWith( + * context = Dispatchers.Main, + * // Function to get user details based on user ID + * other = { userId -> getUserDetailLiveData(userId) }, + * // Condition to chain only for valid user IDs + * condition = { userId -> userId != null && userId > 0 }, + * // Transform user details to a formatted string + * transform = Transform.Nullable.OmitFail( + * dispatcher = Dispatchers.IO, + * func = { userId, userDetail -> userDetail?.let { "${it.name}, ID: $userId" } } + * ) * ) + * + * // Observing the resulting LiveData + * userDetailLiveData.observe(this, Observer { result -> + * println("User Info: $result") + * }) * ``` + * + * ### Error Handling: + * - If any exceptions are thrown during the [other] or [transform] functions, the error is caught, and the resulting `LiveData` + * will not emit a value for that cycle, unless an `onErrorReturn` function is provided in the `Transform`. + * + * @param T The type of the value in the original [LiveData]. + * @param R The type of the value in the chained [LiveData]. + * @param X The type of the value after transformation. */ fun LiveData.chainWith( context: CoroutineContext, other: suspend (T?) -> LiveData, condition: suspend (T?) -> Boolean, - transform: Pair X?> + transform: Transform.Nullable ): LiveData = liveData(context) { - val (dispatcher, block) = transform - internalChainWith(other, condition) - .flowOn(dispatcher) - .map { (a, b) -> runCatching { block(a, b) }.getOrNull() } - .flowOn(context) - .collect(::emit) + internalChainWith(other, condition).applyTransformation(context, transform).collect(::emit) } /** - * Chains this [LiveData] with another [LiveData] based on a condition, using a simple transformation function. - * - * @param context The [CoroutineContext] to use for the coroutine. - * @param other A suspend function that returns another [LiveData] based on the value of this [LiveData]. - * @param condition A suspend function that determines whether to chain with the other [LiveData]. - * @param transform A suspend function to transform the values from this and the chained [LiveData]. - * @return A [LiveData] emitting the transformed values. - * - * This method allows for chaining two [LiveData] sources and applying a simple transformation function to the combined values. - * The transformation is executed in the provided [CoroutineContext]. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData = liveData1.chainWith( - * context = Dispatchers.IO, - * other = { liveData2 }, - * condition = { it != null }, - * transform = { a, b -> "$a$b" } - * ) - * ``` + * @see LiveData.chainWith + */ +fun LiveData.chainWith( + context: CoroutineContext, + other: suspend (T?) -> LiveData, + condition: suspend (T?) -> Boolean, + transform: Pair X?> +): LiveData = chainWith( + context = context, + other = other, + condition = condition, + transform = Transform.Nullable.OmitFail(transform.first, transform.second) +) + +/** + * @see LiveData.chainWith */ fun LiveData.chainWith( context: CoroutineContext, @@ -207,26 +398,21 @@ fun LiveData.chainWith( ) /** - * Chains this [LiveData] with another [LiveData] based on a condition, using a simple transformation function and the default [CoroutineContext]. - * - * @param other A suspend function that returns another [LiveData] based on the value of this [LiveData]. - * @param condition A suspend function that determines whether to chain with the other [LiveData]. - * @param transform A pair consisting of a [CoroutineDispatcher] and a suspend function to transform the values. - * @return A [LiveData] emitting the transformed values. - * - * This method allows for chaining two [LiveData] sources and applying a transformation function to the combined values. - * The transformation is executed in the provided [CoroutineDispatcher] and uses the default [CoroutineContext]. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData = liveData1.chainWith( - * other = { liveData2 }, - * condition = { it != null }, - * transform = Dispatchers.IO to { a, b -> "$a$b" } - * ) - * ``` + * @see LiveData.chainWith + */ +fun LiveData.chainWith( + other: suspend (T?) -> LiveData, + condition: suspend (T?) -> Boolean, + transform: Transform.Nullable +): LiveData = chainWith( + context = EmptyCoroutineContext, + other = other, + condition = condition, + transform = transform +) + +/** + * @see LiveData.chainWith */ fun LiveData.chainWith( other: suspend (T?) -> LiveData, @@ -240,27 +426,7 @@ fun LiveData.chainWith( ) /** - * Chains this [LiveData] with another [LiveData] based on a condition, - * using a simple transformation function and the default [CoroutineContext] and [CoroutineDispatcher]. - * - * @param other A suspend function that returns another [LiveData] based on the value of this [LiveData]. - * @param condition A suspend function that determines whether to chain with the other [LiveData]. - * @param transform A suspend function to transform the values from this and the chained [LiveData]. - * @return A [LiveData] emitting the transformed values. - * - * This method allows for chaining two [LiveData] sources and applying a simple transformation function to the combined values. - * The transformation is executed using the default [CoroutineContext] and [CoroutineDispatcher]. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData = liveData1.chainWith( - * other = { liveData2 }, - * condition = { it != null }, - * transform = { a, b -> "$a$b" } - * ) - * ``` + * @see LiveData.chainWith */ fun LiveData.chainWith( other: suspend (T?) -> LiveData, @@ -271,27 +437,59 @@ fun LiveData.chainWith( condition = condition, transform = Dispatchers.IO to transform ) +/* endregion ------------------------------------------------------------------------------------ */ +/* region Non Nullable -------------------------------------------------------------------------- */ /** * Chains this [LiveData] with another non-nullable [LiveData] based on a condition, using coroutines. * - * @param context The [CoroutineContext] to use for the coroutine. - * @param other A suspend function that returns another [LiveData] based on the non-null value of this [LiveData]. + * @param context The [CoroutineContext] to use for the coroutine. This context controls the execution of the chaining logic, + * allowing you to specify which thread or dispatcher should be used. By default, you can use [Dispatchers.IO] for IO-bound tasks + * or [Dispatchers.Main] for UI-related tasks. + * @param other A suspend function that returns another [LiveData] based on the non-null value of this [LiveData]. This function is + * called asynchronously, allowing you to perform non-blocking operations when determining the next [LiveData] to chain. * @param condition A suspend function that determines whether to chain with the other [LiveData] based on a non-null value. - * @return A [LiveData] emitting pairs of non-nullable values from this and the chained [LiveData]. + * The condition is evaluated asynchronously, and if it returns `true`, the chaining occurs. + * @return A [LiveData] emitting pairs of non-nullable values from this [LiveData] and the chained [LiveData]. The resulting [LiveData] + * emits a [Pair] containing the current non-null value from this [LiveData] and the value from the chained [LiveData]. * - * This coroutine-based method allows for asynchronous operations when chaining non-nullable [LiveData] sources. + * This coroutine-based method allows for asynchronous operations when chaining non-nullable [LiveData] sources. It is useful in scenarios + * where you need to perform operations like network requests, database queries, or other asynchronous tasks as part of the chaining process. * - * Example usage: + * ### Success Case: + * - When this [LiveData] emits a non-null value, the `condition` function is asynchronously evaluated. + * - If the condition returns `true`, the `other` suspend function is invoked to get the chained [LiveData]. + * - The resulting [LiveData] emits a [Pair] of values from both [LiveData] sources if both are non-null. + * + * ### Failure Case: + * - If this [LiveData] emits a null value, the chaining does not occur, and the method does nothing for that emission. + * - If the `condition` suspend function returns `false` or throws an exception, the chaining is skipped, and no value is emitted. + * - If the `other` suspend function fails or returns null, the resulting [LiveData] does not emit a value for that emission. + * + * ### Example Usage: * ``` + * // Example of chaining two LiveData sources with a condition * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData> = liveData1.chainNotNullWith( + * val liveData2: LiveData = liveData1.chainNotNullWith( * context = Dispatchers.IO, - * other = { liveData2 }, - * condition = { it != null } + * other = { value -> fetchStringLiveDataBasedOnValue(value) }, + * // Only chain if the value is greater than 0 + * condition = { value -> value > 0 } * ) + * + * // Observing the resulting LiveData + * liveData2.observe(this, Observer { result -> + * val (intValue, stringValue) = result + * println("Int Value: $intValue, String Value: $stringValue") + * }) * ``` + * + * ### Error Handling: + * - If the condition or the other function throws an exception, the exception is caught, and the method does not emit a value for that emission. + * - Null values in the original [LiveData] are ignored, ensuring that the chaining only occurs for non-null values. + * + * @param T The type of the value in the original [LiveData]. + * @param R The type of the value in the chained [LiveData]. */ fun LiveData.chainNotNullWith( context: CoroutineContext, @@ -302,66 +500,124 @@ fun LiveData.chainNotNullWith( } /** - * Chains this non-nullable [LiveData] with another non-nullable [LiveData] based on a condition, using a simple transformation function. - * - * @param context The [CoroutineContext] to use for the coroutine. - * @param other A suspend function that returns another non-nullable [LiveData] based on the value of this [LiveData]. - * @param condition A suspend function that determines whether to chain with the other [LiveData] based on a non-nullable value. - * @param transform A pair consisting of a [CoroutineDispatcher] and a suspend function to transform the non-nullable values. - * @return A [LiveData] emitting the transformed values. + * Chains this [LiveData] with another non-nullable [LiveData] based on a condition, applies a transformation, and emits the result. * - * This coroutine-based method allows for chaining two non-nullable [LiveData] sources and - * applying a simple transformation function to the combined values. - * The transformation is executed in the provided [CoroutineDispatcher]. - * - * Example usage: + * @param context The [CoroutineContext] to use for the coroutine. This context controls the execution of the chaining and transformation logic, + * allowing you to specify which thread or dispatcher should be used. Common contexts include [Dispatchers.IO] for IO-bound tasks and + * [Dispatchers.Main] for UI-related tasks. + * @param other A suspend function that returns another [LiveData] based on the non-null value of this [LiveData]. This function is + * called asynchronously, allowing non-blocking operations when determining the next [LiveData] to chain. + * @param condition A suspend function that determines whether to chain with the other [LiveData] based on a non-null value. + * The condition is evaluated asynchronously, and if it returns `true`, the chaining occurs. + * @param transform A [Transform.NotNull] object that contains the transformation logic, including the dispatcher on which the transformation + * will occur, the function to apply to the paired values, and optional error handling logic. + * @return A [LiveData] emitting transformed values from the non-nullable values of this and the chained [LiveData]. + * + * This method is useful for scenarios where you need to chain two non-nullable [LiveData] sources, apply some complex logic on their combined + * values, and emit the result. The transformation is performed asynchronously, and the method allows for error handling through the + * [Transform.NotNull] object. + * + * ### Success Case: + * - When this [LiveData] emits a non-null value, the `condition` function is asynchronously evaluated. + * - If the condition returns `true`, the `other` suspend function is invoked to get the chained [LiveData]. + * - The resulting [LiveData] emits a transformed value based on the combined non-nullable values from both [LiveData] sources. + * + * ### Failure Case: + * - If this [LiveData] emits a null value, the chaining does not occur, and the method does nothing for that emission. + * - If the `condition` suspend function returns `false` or throws an exception, the chaining is skipped, and no value is emitted. + * - If the `other` suspend function fails or returns null, the resulting [LiveData] does not emit a value for that emission. + * - If the transformation fails and error handling is not provided, the method omits the emission for that combination. + * + * ### Transform Parameter: + * The `transform` parameter is an instance of [Transform.NotNull], which allows you to define how the values from the two [LiveData] + * sources are transformed and how errors are handled during the transformation. + * + * ### The `Transform.NotNull` class offers several variations: + * - **OmitFail**: + * - **Description**: Transforms the values and omits the emission if the transformation fails. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired non-nullable values. + * - **Example**: + * ```kotlin + * val transform = Transform.NotNull.OmitFail( + * dispatcher = Dispatchers.Default, + * func = { intValue, stringValue -> "$intValue: $stringValue" } + * ) + * ``` + * - **Fallback**: + * - **Description**: Transforms the values and applies a fallback function if the transformation fails. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired non-nullable values. + * - `onErrorReturn`: A function that handles errors during transformation, allowing you to provide an alternate result. + * - **Example**: + * ```kotlin + * val transform = Transform.NotNull.Fallback( + * dispatcher = Dispatchers.IO, + * func = { intValue, stringValue -> "$intValue: $stringValue" }, + * onErrorReturn = { error -> "Error: ${error.message}" } + * ) + * ``` + * + * ### Example Usage: * ``` + * // Example of chaining two LiveData sources, applying a transformation, and handling errors * val liveData1: LiveData = MutableLiveData(1) * val liveData2: LiveData = MutableLiveData("A") * val chainedLiveData: LiveData = liveData1.chainNotNullWith( - * context = Dispatchers.Default, + * context = Dispatchers.IO, * other = { liveData2 }, - * condition = { it != null }, - * transform = Dispatchers.IO to { a, b -> "$a$b" } + * condition = { it > 0 }, + * transform = Transform.NotNull.OmitFail( + * func = { intValue, stringValue -> "$intValue: $stringValue" }, + * onErrorReturn = { error -> "Error occurred: ${error.message}" } + * ) * ) + * + * // Observing the resulting LiveData + * chainedLiveData.observe(this, Observer { result -> + * println("Transformed Value: $result") + * }) * ``` + * + * ### Error Handling: + * - If the condition or the other function throws an exception, the exception is caught, and the method does not emit a value for that emission. + * - If the transformation function throws an exception and `onErrorReturn` is provided, the fallback result is emitted. + * - Null values in the original [LiveData] are ignored, ensuring that the chaining only occurs for non-null values. + * + * @param T The type of the value in the original [LiveData]. + * @param R The type of the value in the chained [LiveData]. + * @param X The type of the value after applying the transformation. */ fun LiveData.chainNotNullWith( context: CoroutineContext, other: suspend (T) -> LiveData, condition: suspend (T) -> Boolean, - transform: Pair X> + transform: Transform.NotNull ): LiveData = liveData(context) { - val (dispatcher, block) = transform internalChainNotNullWith(other, condition) - .flowOn(dispatcher) - .mapNotNull { (a, b) -> runCatching { block.invoke(a, b) }.getOrNull() } - .flowOn(context) + .applyTransformation(context, transform) .collect(::emit) } /** - * Chains this non-nullable [LiveData] with another non-nullable [LiveData] based on a condition, - * using a simple transformation function and the default [CoroutineContext]. - * - * @param other A suspend function that returns another non-nullable [LiveData] based on the value of this [LiveData]. - * @param condition A suspend function that determines whether to chain with the other [LiveData] based on a non-nullable value. - * @param transform A pair consisting of a [CoroutineDispatcher] and a suspend function to transform the non-nullable values. - * @return A [LiveData] emitting the transformed values. - * - * This method allows for chaining two non-nullable [LiveData] sources and applying a simple transformation function to the combined values. - * The transformation is executed in the provided [CoroutineDispatcher] and uses the default [CoroutineContext]. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData = liveData1.chainNotNullWith( - * other = { liveData2 }, - * condition = { it != null }, - * transform = Dispatchers.IO to { a, b -> "$a$b" } - * ) - * ``` + * @see LiveData.chainNotNullWith + */ +fun LiveData.chainNotNullWith( + context: CoroutineContext, + other: suspend (T) -> LiveData, + condition: suspend (T) -> Boolean, + transform: Pair X> +): LiveData = chainNotNullWith( + context = context, + other = other, + condition = condition, + transform = Transform.NotNull.OmitFail(transform.first, transform.second) +) + +/** + * @see LiveData.chainNotNullWith */ fun LiveData.chainNotNullWith( context: CoroutineContext, @@ -376,27 +632,21 @@ fun LiveData.chainNotNullWith( ) /** - * Chains this non-nullable [LiveData] with another non-nullable [LiveData] based on a condition, - * using a simple transformation function and the default [CoroutineContext]. - * - * @param other A suspend function that returns another non-nullable [LiveData] based on the value of this [LiveData]. - * @param condition A suspend function that determines whether to chain with the other [LiveData] based on a non-nullable value. - * @param transform A pair consisting of a [CoroutineDispatcher] and a suspend function to transform the non-nullable values. - * @return A [LiveData] emitting the transformed values. - * - * This method allows for chaining two non-nullable [LiveData] sources and applying a transformation function to the combined values. - * The transformation is executed in the provided [CoroutineDispatcher] and uses the default [CoroutineContext]. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData = liveData1.chainNotNullWith( - * other = { liveData2 }, - * condition = { it != null }, - * transform = Dispatchers.IO to { a, b -> "$a$b" } - * ) - * ``` + * @see LiveData.chainNotNullWith + */ +fun LiveData.chainNotNullWith( + other: suspend (T) -> LiveData, + condition: suspend (T) -> Boolean, + transform: Transform.NotNull +): LiveData = chainNotNullWith( + context = EmptyCoroutineContext, + other = other, + condition = condition, + transform = transform +) + +/** + * @see LiveData.chainNotNullWith */ fun LiveData.chainNotNullWith( other: suspend (T) -> LiveData, @@ -410,27 +660,7 @@ fun LiveData.chainNotNullWith( ) /** - * Chains this non-nullable [LiveData] with another non-nullable [LiveData] based on a condition, - * using a simple transformation function and the default [CoroutineContext] and [CoroutineDispatcher]. - * - * @param other A suspend function that returns another non-nullable [LiveData] based on the value of this [LiveData]. - * @param condition A suspend function that determines whether to chain with the other [LiveData] based on a non-nullable value. - * @param transform A suspend function to transform the non-nullable values from this and the chained [LiveData]. - * @return A [LiveData] emitting the transformed values. - * - * This method allows for chaining two non-nullable [LiveData] sources and applying a transformation function to the combined values. - * The transformation is executed using the default [CoroutineContext] and [CoroutineDispatcher]. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val chainedLiveData: LiveData = liveData1.chainNotNullWith( - * other = { liveData2 }, - * condition = { it != null }, - * transform = { a, b -> "$a$b" } - * ) - * ``` + * @see LiveData.chainNotNullWith */ fun LiveData.chainNotNullWith( other: suspend (T) -> LiveData, @@ -441,9 +671,9 @@ fun LiveData.chainNotNullWith( condition = condition, transform = Dispatchers.IO to transform ) +/* endregion ------------------------------------------------------------------------------------ */ -/* Auxiliary Functions -------------------------------------------------------------------------- */ - +/* region Auxiliary Functions ------------------------------------------------------------------- */ internal suspend inline fun LiveData.internalChainNotNullWith( noinline other: suspend (T) -> LiveData, noinline condition: suspend (T) -> Boolean, @@ -476,3 +706,4 @@ internal suspend inline fun LiveData.internalChainWith( } } } +/* endregion ------------------------------------------------------------------------------------ */ diff --git a/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_combine.kt b/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_combine.kt index 3b901d9..e1e732d 100644 --- a/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_combine.kt +++ b/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_combine.kt @@ -13,8 +13,6 @@ import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -23,46 +21,41 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext /* Operator Functions --------------------------------------------------------------------------- */ - -/** - * Combines two [LiveData] objects using the `+` operator. - * - * This function merges the emissions of two [LiveData] sources into a single [LiveData] object containing pairs of values. - * - * @param other The other [LiveData] to combine with this [LiveData]. - * @return A [LiveData] that emits pairs of values from both [LiveData] sources. - * - * ### Example Usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val combinedLiveData: LiveData> = liveData1 + liveData2 - * ``` - * - * @see [combine] - * @see [plus] - */ operator fun LiveData.plus(other: LiveData) = combine(other = other) -/* Regular Functions ---------------------------------------------------------------------------- */ - +/* region Regular Functions --------------------------------------------------------------------- */ /** - * Combines two [LiveData] objects without using coroutines. + * Combines this [LiveData] with another [LiveData], allowing for nullable values, and emits pairs of their values. + * + * @param other The other [LiveData] whose values (nullable) will be combined with this [LiveData]. + * @return A [LiveData] emitting pairs of values from both [LiveData] sources, with nullability allowed. * - * This function creates a [MediatorLiveData] that merges the emissions from two [LiveData] sources into a single [LiveData]. + * This method creates a [MediatorLiveData] that listens to both [LiveData] sources. Whenever either [LiveData] emits a value, + * the combined value is emitted as a pair. The method allows for nullable values, meaning either value in the resulting pair + * can be null. It also accounts for initial values if either [LiveData] was already initialized. * - * @param other The other [LiveData] to combine with. - * @return A [LiveData] containing pairs of values from both [LiveData] sources. + * ### Behavior: + * - The method uses `AtomicBoolean` flags to ignore the first emission from each [LiveData] if they were already initialized. + * - If either [LiveData] is initialized, the initial pair of values is emitted immediately. + * - Subsequent emissions occur whenever either [LiveData] emits a new value. * * ### Example Usage: * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") + * val liveData1: LiveData = MutableLiveData(1) + * val liveData2: LiveData = MutableLiveData("A") * val combinedLiveData: LiveData> = liveData1.combine(liveData2) + * + * combinedLiveData.observe(this, Observer { pair -> + * println("Combined Value: ${pair.first}, ${pair.second}") + * }) * ``` * - * @see [combineNotNull] - * @see [plus] + * ### Error Handling: + * - If both values are null, the method emits a pair containing `null` for each value. + * - The method ensures that a pair is emitted only when there's a valid change, ignoring the first emission if it was already initialized. + * + * @param T The type of the value in this [LiveData]. + * @param R The type of the value in the other [LiveData]. */ fun LiveData.combine(other: LiveData): LiveData> { val ignoreA = AtomicBoolean(isInitialized) @@ -85,22 +78,37 @@ fun LiveData.combine(other: LiveData): LiveData> { } /** - * Combines two [LiveData] objects, ensuring both are non-null. + * Combines this [LiveData] with another [LiveData], requiring non-nullable values, and emits pairs of their values. * - * This function merges the emissions of two [LiveData] sources into a single [LiveData] only if both values are non-null. + * @param other The other [LiveData] whose non-nullable values will be combined with this [LiveData]. + * @return A [LiveData] emitting pairs of non-nullable values from both [LiveData] sources. * - * @param other The other [LiveData] to combine with. - * @return A [LiveData] containing non-null pairs of values from both [LiveData] sources. + * This method creates a [MediatorLiveData] that listens to both [LiveData] sources and emits pairs of their values + * only when both values are non-null. If either value is null, the pair is not emitted. The method also accounts for + * initial values if either [LiveData] was already initialized and non-null. + * + * ### Behavior: + * - The method uses `AtomicBoolean` flags to ignore the first emission from each [LiveData] if they were already initialized. + * - If either [LiveData] is initialized with a non-null value, the initial pair of values is emitted immediately. + * - Subsequent emissions occur whenever either [LiveData] emits a new non-null value. * * ### Example Usage: * ``` * val liveData1: LiveData = MutableLiveData(1) * val liveData2: LiveData = MutableLiveData("A") * val combinedLiveData: LiveData> = liveData1.combineNotNull(liveData2) + * + * combinedLiveData.observe(this, Observer { pair -> + * println("Combined Value: ${pair.first}, ${pair.second}") + * }) * ``` * - * @see [combine] - * @see [combineNotNull] + * ### Error Handling: + * - If either value is null, the pair is not emitted. + * - The method ensures that a pair is emitted only when both values are non-null and valid. + * + * @param T The type of the value in this [LiveData]. + * @param R The type of the value in the other [LiveData]. */ fun LiveData.combineNotNull(other: LiveData): LiveData> { val ignoreA = AtomicBoolean(isInitialized) @@ -124,27 +132,45 @@ fun LiveData.combineNotNull(other: LiveData): LiveData> } return mediator } +/* endregion ------------------------------------------------------------------------------------ */ -/* Coroutine Functions -------------------------------------------------------------------------- */ - +/* region Nullable -------------------------------------------------------------------------- */ /** - * Combines two [LiveData] objects using coroutines. + * Combines this [LiveData] with another [LiveData], allowing for nullable values, and emits pairs of their values. + * + * @param context The [CoroutineContext] to use for the coroutine. This context dictates where the combination logic will be executed, + * such as on [Dispatchers.IO] for background operations or [Dispatchers.Main] for UI-related tasks. + * @param other The other [LiveData] whose values (nullable) will be combined with this [LiveData]. + * @return A [LiveData] emitting pairs of values from both [LiveData] sources, with nullability allowed. * - * This method allows you to merge two [LiveData] sources using a coroutine context, which is useful for asynchronous operations. + * This method is useful when you have two [LiveData] sources that you want to combine into a single [LiveData] emitting pairs of their values, + * and you need to account for possible null values in either [LiveData]. * - * @param context The [CoroutineContext] in which to perform the combination. - * @param other The other [LiveData] to combine with. - * @return A [LiveData] emitting pairs of values from both LiveData sources. + * ### Success Case: + * - When either this [LiveData] or the `other` [LiveData] emits a value (nullable or non-nullable), + * a pair of these values is emitted by the resulting [LiveData]. + * - Both values in the pair can be nullable, reflecting the current states of the two [LiveData] sources. * * ### Example Usage: * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val combinedLiveData: LiveData> = liveData1.combine(Dispatchers.IO, liveData2) + * val liveData1: LiveData = MutableLiveData(1) + * val liveData2: LiveData = MutableLiveData(null) + * val combinedLiveData: LiveData> = liveData1.combine( + * context = Dispatchers.IO, + * other = liveData2 + * ) + * + * // Observing the resulting LiveData + * combinedLiveData.observe(this, Observer { pair -> + * println("Combined Value: ${pair.first}, ${pair.second}") + * }) * ``` * - * @see [combineNotNull] - * @see [plus] + * ### Error Handling: + * - If either [LiveData] emits a null value, the combination still occurs, and a pair with one or both values as null is emitted. + * + * @param T The type of the value in this [LiveData]. + * @param R The type of the value in the other [LiveData]. */ fun LiveData.combine( context: CoroutineContext, @@ -152,65 +178,141 @@ fun LiveData.combine( ): LiveData> = liveData(context) { internalCombine(other).collect(::emit) } /** - * Combines two [LiveData] objects with a transformation function using coroutines. - * - * This method merges two [LiveData] sources and applies a transformation function to the pair of values. - * - * @param context The [CoroutineContext] in which to perform the combination. - * @param other The other [LiveData] to combine with. - * @param transform A pair of [CoroutineDispatcher] and a suspend function that transforms the pair of values. - * @return A [LiveData] emitting the transformed values. + * Combines this [LiveData] with another [LiveData], applies a transformation, and emits the transformed result, allowing for nullable values. + * + * @param context The [CoroutineContext] to use for the coroutine. This context determines where the combination and transformation logic + * will be executed, allowing you to control the threading model. + * @param other The other [LiveData] whose values (nullable) will be combined with this [LiveData]. + * @param transform A [Transform.Nullable] object that contains the transformation logic, including the dispatcher on which the transformation + * will occur, the function to apply to the paired values, and optional error handling logic. + * @return A [LiveData] emitting the transformed values from the possibly nullable values of both [LiveData] sources. + * + * This method is useful when you want to combine two [LiveData] sources and apply a custom transformation to their paired values, + * while handling nullable values appropriately. The transformation is performed asynchronously, and the method provides options + * for error handling through the [Transform.Nullable] object. + * + * ### Success Case: + * - When either this [LiveData] or the `other` [LiveData] emits a value (nullable or non-nullable), + * the `transform` function is applied to these values, and the resulting transformed value is emitted by the resulting [LiveData]. + * + * ### Failure Case: + * - If both values are null, the method does not emit any value unless specifically handled by the `transform` function. + * - If the transformation function throws an exception and error handling is not provided, the method omits the emission for that combination. + * + * ### Transform Parameter: + * The `transform` parameter is an instance of [Transform.Nullable], allowing you to define how the possibly + * nullable values from the two [LiveData] sources are transformed and how errors are handled during the transformation. + * + * ### The `Transform.Nullable` class offers several variations: + * - **OmitFail**: + * - **Description**: Omits the result when the transformation fails, without handling the error. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired non-nullable values. + * - **Example**: + * ```kotlin + * val transform = Transform.Nullable.OmitFail( + * dispatcher = Dispatchers.Default, + * func = { intValue, stringValue -> "$intValue: $stringValue" } + * ) + * ``` + * - **NullFail**: + * - **Description**: Emits `null` when the transformation fails, without handling the error. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired nullable values. + * - **Example**: + * ```kotlin + * val transform = Transform.Nullable.NullFail( + * dispatcher = Dispatchers.IO, + * func = { intValue, stringValue -> "$intValue: $stringValue" } + * ) + * ``` + * - **Fallback**: + * - **Description**: Omits the result when the transformation fails but provides a fallback value via `onErrorReturn`. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired nullable values. + * - `onErrorReturn`: A function that handles errors during transformation, allowing you to provide an alternate result. + * - **Example**: + * ```kotlin + * val transform = Transform.Nullable.Fallback( + * dispatcher = Dispatchers.IO, + * func = { intValue, stringValue -> "$intValue: $stringValue" }, + * onErrorReturn = { error -> "Error: ${error.message}" } + * ) + * ``` + * - **Custom**: + * - **Description**: A customizable transformation that lets you define the dispatcher, fail mode, + * transformation function, and optional error handling. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. + * - `failMode`: Specifies how to handle failures during transformation. Options include: + * - `OMIT_WHEN_FAIL`: Omits the emission if the transformation fails. + * - `NULL_WHEN_FAIL`: Emits `null` if the transformation fails. + * - `func`: The transformation function to apply to the paired nullable values. + * - `onErrorReturn`: An optional function to handle errors during transformation. + * - **Example**: + * ```kotlin + * val transform = Transform.Nullable.Custom( + * dispatcher = Dispatchers.Default, + * failMode = Transform.Mode.OMIT_WHEN_FAIL, + * func = { intValue, stringValue -> "$intValue: $stringValue" }, + * onErrorReturn = { error -> "Error: ${error.message}" } + * ) + * ``` * * ### Example Usage: * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val transformedLiveData: LiveData = liveData1.combine(Dispatchers.IO, liveData2) { a, b -> "$a$b" } + * // Example of combining two LiveData sources, applying a transformation, and handling nullable values + * val liveData1: LiveData = MutableLiveData(1) + * val liveData2: LiveData = MutableLiveData(null) + * val transformedLiveData: LiveData = liveData1.combine( + * context = Dispatchers.IO, + * other = liveData2, + * transform = Transform.Nullable.NullFail( + * func = { intValue, stringValue -> "$intValue: $stringValue" } + * ) + * ) + * + * // Observing the resulting LiveData + * transformedLiveData.observe(this, Observer { result -> + * println("Transformed Value: $result") + * }) * ``` * - * @see [combine] - * @see [combineNotNull] + * ### Error Handling: + * - If the transformation function throws an exception and `onErrorReturn` is provided, the fallback result is emitted. + * - If no error handling is provided, the emission is omitted for that combination. + * - Null values are handled according to the specified `failMode` in the `transform` parameter. + * + * @param T The type of the value in this [LiveData]. + * @param R The type of the value in the other [LiveData]. + * @param X The type of the value after applying the transformation. */ fun LiveData.combine( context: CoroutineContext, other: LiveData, - transform: Pair X?> + transform: Transform.Nullable ): LiveData = liveData(context) { - val (dispatcher, block) = transform - internalCombine(other) - .flowOn(dispatcher) - .map { (a, b) -> runCatching { block(a, b) }.getOrNull() } - .flowOn(context) - .collect(::emit) + internalCombine(other).applyTransformation(context, transform).collect(::emit) } /** - * Combines two [LiveData] objects with a transformation function using coroutines. - * - * @param context The [CoroutineContext] in which to perform the combination. - * @param other The other [LiveData] to combine with. - * @param transform A suspend function that transforms the pair of values. - * @return A [LiveData] emitting the transformed values or `null` if the transformation fails. - * - * The transformation function is applied within the specified [CoroutineContext] and uses - * `Dispatchers.IO` by default for the transformation process. If the [transform] function throws - * an exception during its execution, the `LiveData` will emit `null` for that combination. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val transformedLiveData: LiveData = liveData1.combine(Dispatchers.Main, liveData2) { a, b -> - * if (a != null && b != null) "$a$b" else null - * } - * ``` - * - * In this example, the `combine` method merges `liveData1` and `liveData2`, and the transformation - * function concatenates the values into a string. The result is a new `LiveData` that emits - * `"1A"`. - * - * @see [LiveData.combine] - * @see [LiveData.combineNotNull] + * @see LiveData.combine + */ +fun LiveData.combine( + context: CoroutineContext, + other: LiveData, + transform: Pair X?> +): LiveData = combine( + context = context, + other = other, + transform = Transform.Nullable.OmitFail(transform.first, transform.second) +) + +/** + * @see LiveData.combine */ fun LiveData.combine( context: CoroutineContext, @@ -223,25 +325,19 @@ fun LiveData.combine( ) /** - * Combines two [LiveData] objects with a transformation function and an optional [CoroutineContext]. - * - * @param other The other [LiveData] to combine with. - * @param transform A pair consisting of a [CoroutineDispatcher] and a suspend function that transforms the pair of nullable values. - * @return A [LiveData] emitting the transformed nullable values. - * - * This method combines two `LiveData` sources, where either or both may emit nullable values, and applies the provided transformation function. - * The transformation function is executed within the specified [CoroutineDispatcher], and the combination is handled in the provided - * [CoroutineContext] or `EmptyCoroutineContext` by default. If the transformation function throws an exception, the emission for that - * combination is omitted, allowing the flow to continue without interruption. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val transformedLiveData: LiveData = liveData1.combine(liveData2, Dispatchers.Default to { a, b -> "$a$b" }) - * ``` - * - * @see combine + * @see LiveData.combine + */ +fun LiveData.combine( + other: LiveData, + transform: Transform.Nullable +): LiveData = combine( + context = EmptyCoroutineContext, + other = other, + transform = transform +) + +/** + * @see LiveData.combine */ fun LiveData.combine( other: LiveData, @@ -253,24 +349,7 @@ fun LiveData.combine( ) /** - * Combines two [LiveData] objects with a transformation function using a default [CoroutineDispatcher]. - * - * @param other The other [LiveData] to combine with. - * @param transform A suspend function that transforms the pair of nullable values. - * @return A [LiveData] emitting the transformed nullable values. - * - * This method combines two `LiveData` sources, where either or both may emit nullable values, and applies the provided transformation function. - * The transformation is executed on the default [CoroutineDispatcher] (`Dispatchers.IO`). If the transformation function throws an exception, - * the emission for that combination is omitted, allowing the flow to continue without interruption. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val transformedLiveData: LiveData = liveData1.combine(liveData2) { a, b -> "$a$b" } - * ``` - * - * @see combine + * @see LiveData.combine */ fun LiveData.combine( other: LiveData, @@ -279,25 +358,44 @@ fun LiveData.combine( other = other, transform = Dispatchers.IO to transform ) +/* endregion ------------------------------------------------------------------------------------ */ +/* region Non Nullable -------------------------------------------------------------------------- */ /** - * Combines two non-null [LiveData] objects using coroutines. + * Combines this [LiveData] with another non-nullable [LiveData] and emits a pair of their values. + * + * @param context The [CoroutineContext] to use for the coroutine. This context dictates where the combination logic will be executed, such as + * on [Dispatchers.IO] for background operations or [Dispatchers.Main] for UI-related tasks. + * @param other The other [LiveData] whose non-nullable values will be combined with this [LiveData]. + * @return A [LiveData] emitting pairs of non-nullable values from both [LiveData] sources. + * + * This method is useful when you have two [LiveData] sources that you want to combine into a single [LiveData] emitting pairs of their values. + * It ensures that both [LiveData] sources emit non-null values before combining and emitting the result. * - * This method merges two [LiveData] sources, ensuring that both are non-null, using a coroutine context. + * ### Success Case: + * - When both this [LiveData] and the `other` [LiveData] emit non-null values, a pair of these values is emitted by the resulting [LiveData]. * - * @param context The [CoroutineContext] in which to perform the combination. - * @param other The other [LiveData] to combine with. - * @return A [LiveData] emitting pairs of non-null values from both LiveData sources. + * ### Failure Case: + * - If either this [LiveData] or the `other` [LiveData] emits a null value, the method does not emit any value for that emission. + * - No combination is performed if either [LiveData] is in a null state, ensuring that only non-null pairs are emitted. * * ### Example Usage: * ``` * val liveData1: LiveData = MutableLiveData(1) * val liveData2: LiveData = MutableLiveData("A") - * val combinedLiveData: LiveData> = liveData1.combineNotNull(Dispatchers.IO, liveData2) + * val combinedLiveData: LiveData> = liveData1.combineNotNull( + * context = Dispatchers.IO, + * other = liveData2 + * ) + * + * // Observing the resulting LiveData + * combinedLiveData.observe(this, Observer { pair -> + * println("Combined Value: ${pair.first}, ${pair.second}") + * }) * ``` * - * @see [combine] - * @see [plus] + * @param T The type of the value in this [LiveData]. + * @param R The type of the value in the other [LiveData]. */ fun LiveData.combineNotNull( context: CoroutineContext, @@ -305,75 +403,109 @@ fun LiveData.combineNotNull( ): LiveData> = liveData(context) { internalCombineNotNull(other).collect(::emit) } /** - * Combines two non-null [LiveData] objects with a transformation function using coroutines. - * - * @param context The [CoroutineContext] in which to perform the combination. - * @param other The other [LiveData] to combine with. - * @param transform A pair consisting of a [CoroutineDispatcher] and a suspend function that transforms the pair of non-null values. - * @return A [LiveData] emitting the transformed non-null values. - * - * This method ensures that both [LiveData] sources emit non-null values before applying the transformation function. - * The transformation function is applied in the specified [CoroutineDispatcher] and the flow is managed within the - * provided [CoroutineContext]. If the transformation function throws an exception, the `LiveData` will simply omit - * the emission for that combination. + * Combines this [LiveData] with another non-nullable [LiveData], applies a transformation, and emits the transformed result. + * + * @param context The [CoroutineContext] to use for the coroutine. This context determines where the combination and transformation logic + * will be executed, allowing you to control the threading model. + * @param other The other [LiveData] whose non-nullable values will be combined with this [LiveData]. + * @param transform A [Transform.NotNull] object that contains the transformation logic, including the dispatcher on which the transformation + * will occur, the function to apply to the paired values, and optional error handling logic. + * @return A [LiveData] emitting the transformed values from the non-nullable values of both [LiveData] sources. + * + * This method is useful when you want to combine two non-nullable [LiveData] sources and apply a custom transformation to their paired values. + * The transformation is performed asynchronously, and the method provides options for error handling through the [Transform.NotNull] object. + * + * ### Success Case: + * - When both this [LiveData] and the `other` [LiveData] emit non-null values, the `transform` function is applied to these values, + * and the resulting transformed value is emitted by the resulting [LiveData]. + * + * ### Failure Case: + * - If either this [LiveData] or the `other` [LiveData] emits a null value, the method does not emit any value for that emission. + * - If the transformation function throws an exception and error handling is not provided, the method omits the emission for that combination. + * + * ### Transform Parameter: + * The `transform` parameter is an instance of [Transform.NotNull], allowing you to define how the values from the two [LiveData] + * sources are transformed and how errors are handled during the transformation. + * + * ### The [Transform.NotNull] class offers several variations: + * - **OmitFail**: + * - **Description**: Transforms the values and omits the emission if the transformation fails. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired non-nullable values. + * - **Example**: + * ```kotlin + * val transform = Transform.NotNull.OmitFail( + * dispatcher = Dispatchers.Default, + * func = { intValue, stringValue -> "$intValue: $stringValue" } + * ) + * ``` + * - **Fallback**: + * - **Description**: Transforms the values and applies a fallback function if the transformation fails. + * - **Constructor Parameters**: + * - `dispatcher`: The [CoroutineDispatcher] on which the transformation will occur. Default is [Dispatchers.IO]. + * - `func`: The transformation function to apply to the paired non-nullable values. + * - `onErrorReturn`: A function that handles errors during transformation, allowing you to provide an alternate result. + * - **Example**: + * ```kotlin + * val transform = Transform.NotNull.Fallback( + * dispatcher = Dispatchers.IO, + * func = { intValue, stringValue -> "$intValue: $stringValue" }, + * onErrorReturn = { error -> "Error: ${error.message}" } + * ) + * ``` * - * Example usage: + * ### Example Usage: * ``` + * // Example of combining two LiveData sources, applying a transformation, and handling errors * val liveData1: LiveData = MutableLiveData(1) * val liveData2: LiveData = MutableLiveData("A") - * val transformedLiveData: LiveData = liveData1.combineNotNull(Dispatchers.Main, liveData2, Dispatchers.IO to { a, b -> - * "$a$b" + * val transformedLiveData: LiveData = liveData1.combineNotNull( + * context = Dispatchers.IO, + * other = liveData2, + * transform = Transform.NotNull.OmitFail( + * func = { intValue, stringValue -> "$intValue: $stringValue" } + * ) + * ) + * + * // Observing the resulting LiveData + * transformedLiveData.observe(this, Observer { result -> + * println("Transformed Value: $result") * }) * ``` * - * In this example, `liveData1` and `liveData2` are combined only if both emit non-null values. - * The transformation function concatenates the two values, resulting in `"1A"` being emitted by `transformedLiveData`. + * ### Error Handling: + * - If the transformation function throws an exception and `onErrorReturn` is provided, the fallback result is emitted. + * - If no error handling is provided, the emission is omitted for that combination. + * - Null values in either [LiveData] are ignored, ensuring that the combination only occurs for non-null values. * - * @see [LiveData.combine] - * @see [LiveData.combineNotNull] - * @see [LiveData.combineNotNull] + * @param T The type of the value in this [LiveData]. + * @param R The type of the value in the other [LiveData]. + * @param X The type of the value after applying the transformation. */ fun LiveData.combineNotNull( context: CoroutineContext, other: LiveData, - transform: Pair X> + transform: Transform.NotNull ): LiveData = liveData(context) { - val (dispatcher, block) = transform - internalCombineNotNull(other) - .flowOn(dispatcher) - .mapNotNull { (a, b) -> runCatching { block(a, b) }.getOrNull() } - .flowOn(context) - .collect(::emit) + internalCombineNotNull(other).applyTransformation(context, transform).collect(::emit) } /** - * Combines two non-null [LiveData] objects with a transformation function using coroutines. - * - * @param context The [CoroutineContext] in which to perform the combination. - * @param other The other [LiveData] to combine with. - * @param transform A suspend function that transforms the pair of non-null values. - * @return A [LiveData] emitting the transformed non-null values. - * - * This method ensures that both [LiveData] sources emit non-null values before applying the transformation function. - * The transformation function is applied in the specified [CoroutineContext] using `Dispatchers.IO` by default for - * the transformation process. If the [transform] function throws an exception during its execution, the `LiveData` - * will simply omit the emission for that combination. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val transformedLiveData: LiveData = liveData1.combineNotNull(Dispatchers.Main, liveData2) { a, b -> - * "$a$b" - * } - * ``` - * - * In this example, the `combineNotNull` method merges `liveData1` and `liveData2`, applying the transformation function - * to concatenate the non-null values. The result is `"1A"` being emitted by `transformedLiveData`. - * - * @see [LiveData.combine] - * @see [LiveData.combineNotNull] - * @see [LiveData.combineNotNull] + * @see LiveData.combineNotNull + */ +fun LiveData.combineNotNull( + context: CoroutineContext, + other: LiveData, + transform: Pair X> +): LiveData = combineNotNull( + context = context, + other = other, + transform = Transform.NotNull.OmitFail(transform.first, transform.second) +) + +/** + * @see LiveData.combineNotNull */ fun LiveData.combineNotNull( context: CoroutineContext, @@ -386,25 +518,19 @@ fun LiveData.combineNotNull( ) /** - * Combines two non-null [LiveData] objects with a transformation function and an optional [CoroutineContext]. - * - * @param other The other [LiveData] to combine with. - * @param transform A pair consisting of a [CoroutineDispatcher] and a suspend function that transforms the pair of non-null values. - * @return A [LiveData] emitting the transformed non-null values. - * - * This method combines two `LiveData` sources, ensuring that both emit non-null values before applying the transformation function. - * The transformation function is executed within the specified [CoroutineDispatcher], and the combination is handled in the provided - * [CoroutineContext] or `EmptyCoroutineContext` by default. If the transformation function throws an exception, the emission for that - * combination is omitted, allowing the flow to continue without interruption. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val transformedLiveData: LiveData = liveData1.combineNotNull(liveData2, Dispatchers.Default to { a, b -> "$a$b" }) - * ``` - * - * @see combineNotNull + * @see LiveData.combineNotNull + */ +fun LiveData.combineNotNull( + other: LiveData, + transform: Transform.NotNull +): LiveData = combineNotNull( + context = EmptyCoroutineContext, + other = other, + transform = transform +) + +/** + * @see LiveData.combineNotNull */ fun LiveData.combineNotNull( other: LiveData, @@ -416,24 +542,7 @@ fun LiveData.combineNotNull( ) /** - * Combines two non-null [LiveData] objects with a transformation function using a default [CoroutineDispatcher]. - * - * @param other The other [LiveData] to combine with. - * @param transform A suspend function that transforms the pair of non-null values. - * @return A [LiveData] emitting the transformed non-null values. - * - * This method combines two `LiveData` sources, ensuring that both emit non-null values before applying the transformation function. - * The transformation is executed on the default [CoroutineDispatcher] (`Dispatchers.IO`). If the transformation function throws an exception, - * the emission for that combination is omitted, allowing the flow to continue without interruption. - * - * Example usage: - * ``` - * val liveData1: LiveData = MutableLiveData(1) - * val liveData2: LiveData = MutableLiveData("A") - * val transformedLiveData: LiveData = liveData1.combineNotNull(liveData2) { a, b -> "$a$b" } - * ``` - * - * @see combineNotNull + * @see LiveData.combineNotNull */ fun LiveData.combineNotNull( other: LiveData, @@ -442,9 +551,9 @@ fun LiveData.combineNotNull( other = other, transform = Dispatchers.IO to transform ) +/* endregion ------------------------------------------------------------------------------------ */ -/* Auxiliary Functions -------------------------------------------------------------------------- */ - +/* region Auxiliary Functions ------------------------------------------------------------------- */ internal suspend inline fun LiveData.internalCombineNotNull(other: LiveData) = internalCombine(other).mapNotNull { it.onlyWithValues() } @@ -459,3 +568,4 @@ internal suspend inline fun LiveData.internalCombine(other: LiveData LiveData.chainWith( context: CoroutineContext = EmptyCoroutineContext, other: WithResponse, condition: suspend (T?) -> Boolean, - transform: Pair>) -> DataResult> + transform: ResponseTransform ): ResponseLiveData = responseLiveData(context = context) { - val (dispatcher, block) = transform internalChainWith(other::invoke, condition) .mapNotNull { (data, result) -> data?.let(::dataResultSuccess) + result } - .flowOn(dispatcher) - .mapNotNull { result -> runCatching { block(result) }.getOrElse(::dataResultError) } - .flowOn(context) + .applyTransformation(context, transform) .collect(::emit) } +@Experimental +fun LiveData.chainWith( + context: CoroutineContext = EmptyCoroutineContext, + other: WithResponse, + condition: suspend (T?) -> Boolean, + transform: Pair>) -> DataResult> +): ResponseLiveData = chainWith( + context = context, + other = other, + condition = condition, + transform = ResponseTransform.StatusFail(transform.first, transform.second) +) + @Experimental fun LiveData.chainWith( context: CoroutineContext = EmptyCoroutineContext, @@ -83,17 +92,27 @@ fun LiveData.chainNotNullWith( context: CoroutineContext = EmptyCoroutineContext, other: NotNullWithResponse, condition: suspend (T) -> Boolean, - transform: Pair>) -> DataResult> + transform: ResponseTransform ): ResponseLiveData = responseLiveData(context = context) { - val (dispatcher, block) = transform internalChainNotNullWith(other::invoke, condition) .mapNotNull { (data, result) -> (dataResultSuccess(data) + result).onlyWithValues() } - .flowOn(dispatcher) - .mapNotNull { result -> runCatching { block(result) }.getOrElse(::dataResultError) } - .flowOn(context) + .applyTransformation(context, transform) .collect(::emit) } +@Experimental +fun LiveData.chainNotNullWith( + context: CoroutineContext = EmptyCoroutineContext, + other: NotNullWithResponse, + condition: suspend (T) -> Boolean, + transform: Pair>) -> DataResult> +): ResponseLiveData = chainNotNullWith( + context = context, + other = other, + condition = condition, + transform = ResponseTransform.StatusFail(transform.first, transform.second) +) + @Experimental fun LiveData.chainNotNullWith( context: CoroutineContext = EmptyCoroutineContext, @@ -112,33 +131,33 @@ fun LiveData.chainNotNullWith( /* Nullable ------------------------------------------------------------------------------------- */ @FunctionalInterface fun interface ResponseWith { - suspend fun invoke(result: DataResult?): LiveData + suspend fun invoke(result: DataResult): LiveData } @Experimental fun ResponseLiveData.chainWith( context: CoroutineContext = EmptyCoroutineContext, other: ResponseWith, - condition: suspend (DataResult?) -> Boolean, + condition: suspend (DataResult) -> Boolean, ): ResponseLiveData> = responseLiveData(context = context) { - internalChainWith(other::invoke, condition) - .mapNotNull { (result, data) -> result + data?.let(::dataResultSuccess) } - .collect(::emit) + internalChainWith( + condition = { result -> result?.let { condition(it) } == true }, + other = { result -> other.invoke(requireNotNull(result)) }, + ).mapNotNull { (result, data) -> result + data?.let(::dataResultSuccess) }.collect(::emit) } @Experimental fun ResponseLiveData.chainWith( context: CoroutineContext = EmptyCoroutineContext, other: ResponseWith, - condition: suspend (DataResult?) -> Boolean, - transform: Pair>) -> DataResult> + condition: suspend (DataResult) -> Boolean, + transform: ResponseTransform ): ResponseLiveData = responseLiveData(context = context) { - val (dispatcher, block) = transform - internalChainWith(other::invoke, condition) - .mapNotNull { (result, data) -> result + data?.let(::dataResultSuccess) } - .flowOn(dispatcher) - .mapNotNull { result -> runCatching { block(result) }.getOrElse(::dataResultError) } - .flowOn(context) + internalChainWith( + condition = { result -> result?.let { condition(it) } == true }, + other = { result -> other.invoke(requireNotNull(result)) }, + ).mapNotNull { (result, data) -> result + data?.let(::dataResultSuccess) } + .applyTransformation(context, transform) .collect(::emit) } @@ -146,7 +165,20 @@ fun ResponseLiveData.chainWith( fun ResponseLiveData.chainWith( context: CoroutineContext = EmptyCoroutineContext, other: ResponseWith, - condition: suspend (DataResult?) -> Boolean, + condition: suspend (DataResult) -> Boolean, + transform: Pair>) -> DataResult> +): ResponseLiveData = chainWith( + context = context, + other = other, + condition = condition, + transform = ResponseTransform.StatusFail(transform.first, transform.second) +) + +@Experimental +fun ResponseLiveData.chainWith( + context: CoroutineContext = EmptyCoroutineContext, + other: ResponseWith, + condition: suspend (DataResult) -> Boolean, transform: suspend (DataResult>) -> DataResult ): ResponseLiveData = chainWith( context = context, @@ -177,17 +209,27 @@ fun ResponseLiveData.chainNotNullWith( context: CoroutineContext = EmptyCoroutineContext, other: ResponseNotNullWith, condition: suspend (DataResult) -> Boolean, - transform: Pair>) -> DataResult> + transform: ResponseTransform ): ResponseLiveData = responseLiveData(context = context) { - val (dispatcher, block) = transform internalChainNotNullWith(other::invoke, condition) .mapNotNull { (result, data) -> (result + dataResultSuccess(data)).onlyWithValues() } - .flowOn(dispatcher) - .mapNotNull { result -> runCatching { block(result) }.getOrElse(::dataResultError) } - .flowOn(context) + .applyTransformation(context, transform) .collect(::emit) } +@Experimental +fun ResponseLiveData.chainNotNullWith( + context: CoroutineContext = EmptyCoroutineContext, + other: ResponseNotNullWith, + condition: suspend (DataResult) -> Boolean, + transform: Pair>) -> DataResult> +): ResponseLiveData = chainNotNullWith( + context = context, + other = other, + condition = condition, + transform = ResponseTransform.StatusFail(transform.first, transform.second) +) + @Experimental fun ResponseLiveData.chainNotNullWith( context: CoroutineContext = EmptyCoroutineContext, @@ -204,38 +246,56 @@ fun ResponseLiveData.chainNotNullWith( /* region Response + Response Functions ---------------------------------------------------------------- */ /* Nullable ------------------------------------------------------------------------------------- */ +@FunctionalInterface +fun interface ResponseWithResponse { + suspend fun invoke(result: DataResult): ResponseLiveData +} + @Experimental fun ResponseLiveData.chainWith( context: CoroutineContext = EmptyCoroutineContext, - other: suspend (DataResult?) -> ResponseLiveData, - condition: suspend (DataResult?) -> Boolean, + other: ResponseWithResponse, + condition: suspend (DataResult) -> Boolean, ): ResponseLiveData> = responseLiveData(context = context) { - internalChainWith(other, condition) - .mapNotNull { (resultA, resultB) -> resultA + resultB } - .collect(::emit) + internalChainWith( + condition = { result -> result?.let { condition(it) } == true }, + other = { result -> other.invoke(requireNotNull(result)) }, + ).mapNotNull { (resultA, resultB) -> resultA + resultB }.collect(::emit) } @Experimental fun ResponseLiveData.chainWith( context: CoroutineContext = EmptyCoroutineContext, - other: suspend (DataResult?) -> ResponseLiveData, - condition: suspend (DataResult?) -> Boolean, - transform: Pair>) -> DataResult> + other: ResponseWithResponse, + condition: suspend (DataResult) -> Boolean, + transform: ResponseTransform ): ResponseLiveData = responseLiveData(context = context) { - val (dispatcher, block) = transform - internalChainWith(other, condition) - .mapNotNull { (resultA, resultB) -> resultA + resultB } - .flowOn(dispatcher) - .mapNotNull { result -> runCatching { block(result) }.getOrElse(::dataResultError) } - .flowOn(context) + internalChainWith( + condition = { result -> result?.let { condition(it) } == true }, + other = { result -> other.invoke(requireNotNull(result)) }, + ).mapNotNull { (resultA, resultB) -> resultA + resultB } + .applyTransformation(context, transform) .collect(::emit) } @Experimental fun ResponseLiveData.chainWith( context: CoroutineContext = EmptyCoroutineContext, - other: suspend (DataResult?) -> ResponseLiveData, - condition: suspend (DataResult?) -> Boolean, + other: ResponseWithResponse, + condition: suspend (DataResult) -> Boolean, + transform: Pair>) -> DataResult> +): ResponseLiveData = chainWith( + context = context, + other = other, + condition = condition, + transform = ResponseTransform.StatusFail(transform.first, transform.second) +) + +@Experimental +fun ResponseLiveData.chainWith( + context: CoroutineContext = EmptyCoroutineContext, + other: ResponseWithResponse, + condition: suspend (DataResult) -> Boolean, transform: suspend (DataResult>) -> DataResult ): ResponseLiveData = chainWith( context = context, @@ -248,10 +308,10 @@ fun ResponseLiveData.chainWith( @Experimental fun ResponseLiveData.chainNotNullWith( context: CoroutineContext = EmptyCoroutineContext, - other: suspend (DataResult) -> ResponseLiveData, + other: ResponseWithResponse, condition: suspend (DataResult) -> Boolean, ): ResponseLiveData> = responseLiveData(context = context) { - internalChainNotNullWith(other, condition) + internalChainNotNullWith(other::invoke, condition) .mapNotNull { (resultA, resultB) -> (resultA + resultB).onlyWithValues() } .collect(::emit) } @@ -259,23 +319,33 @@ fun ResponseLiveData.chainNotNullWith( @Experimental fun ResponseLiveData.chainNotNullWith( context: CoroutineContext = EmptyCoroutineContext, - other: suspend (DataResult) -> ResponseLiveData, + other: ResponseWithResponse, condition: suspend (DataResult) -> Boolean, - transform: Pair>) -> DataResult> + transform: ResponseTransform ): ResponseLiveData = responseLiveData(context = context) { - val (dispatcher, block) = transform - internalChainNotNullWith(other, condition) + internalChainNotNullWith(other::invoke, condition) .mapNotNull { (resultA, resultB) -> (resultA + resultB).onlyWithValues() } - .flowOn(dispatcher) - .mapNotNull { result -> runCatching { block(result) }.getOrElse(::dataResultError) } - .flowOn(context) + .applyTransformation(context, transform) .collect(::emit) } @Experimental fun ResponseLiveData.chainNotNullWith( context: CoroutineContext = EmptyCoroutineContext, - other: suspend (DataResult) -> ResponseLiveData, + other: ResponseWithResponse, + condition: suspend (DataResult) -> Boolean, + transform: Pair>) -> DataResult> +): ResponseLiveData = chainNotNullWith( + context = context, + other = other, + condition = condition, + transform = ResponseTransform.StatusFail(transform.first, transform.second) +) + +@Experimental +fun ResponseLiveData.chainNotNullWith( + context: CoroutineContext = EmptyCoroutineContext, + other: ResponseWithResponse, condition: suspend (DataResult) -> Boolean, transform: suspend (DataResult>) -> DataResult ): ResponseLiveData = chainNotNullWith( diff --git a/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_responseTransform.kt b/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_responseTransform.kt new file mode 100644 index 0000000..4f326ea --- /dev/null +++ b/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_responseTransform.kt @@ -0,0 +1,83 @@ +@file:Suppress("Filename", "unused") + +package br.com.arch.toolkit.util + +import br.com.arch.toolkit.annotation.Experimental +import br.com.arch.toolkit.result.DataResult +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull +import kotlin.coroutines.CoroutineContext + +@Experimental +internal fun Flow>>.applyTransformation( + context: CoroutineContext, + transform: ResponseTransform +) = flowOn(transform.dispatcher).mapNotNull(transform::apply).flowOn(context) + +@Experimental +sealed class ResponseTransform { + abstract val dispatcher: CoroutineDispatcher + abstract val failMode: Mode + abstract val func: suspend (DataResult>) -> DataResult + abstract val onErrorReturn: (suspend (Throwable) -> DataResult)? + + internal suspend fun apply(data: DataResult>) = + runCatching { func(data) }.let { result -> + val finalResult = result.exceptionOrNull()?.let { error -> + onErrorReturn?.runCatching { invoke(error) } + } ?: result + + when { + finalResult.isFailure -> when (failMode) { + Mode.OMIT_WHEN_FAIL -> null + Mode.ERROR_STATUS_WHEN_FAIL -> + dataResultError(finalResult.exceptionOrNull()) + } + + else -> finalResult.getOrNull() + } + } + + @Experimental + enum class Mode { + ERROR_STATUS_WHEN_FAIL, + OMIT_WHEN_FAIL + } + + @Experimental + class StatusFail( + override val dispatcher: CoroutineDispatcher, + override val func: suspend (DataResult>) -> DataResult + ) : ResponseTransform() { + override val failMode: Mode = Mode.ERROR_STATUS_WHEN_FAIL + override val onErrorReturn: (suspend (Throwable) -> DataResult)? = null + } + + @Experimental + class OmitFail( + override val dispatcher: CoroutineDispatcher, + override val func: suspend (DataResult>) -> DataResult + ) : ResponseTransform() { + override val failMode: Mode = Mode.OMIT_WHEN_FAIL + override val onErrorReturn: (suspend (Throwable) -> DataResult)? = null + } + + @Experimental + class Fallback( + override val dispatcher: CoroutineDispatcher, + override val func: suspend (DataResult>) -> DataResult, + override val onErrorReturn: suspend (Throwable) -> DataResult + ) : ResponseTransform() { + override val failMode: Mode = Mode.ERROR_STATUS_WHEN_FAIL + } + + @Experimental + class Custom( + override val dispatcher: CoroutineDispatcher, + override val failMode: Mode, + override val func: suspend (DataResult>) -> DataResult, + override val onErrorReturn: suspend (Throwable) -> DataResult + ) : ResponseTransform() +} diff --git a/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_transform.kt b/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_transform.kt new file mode 100644 index 0000000..705d718 --- /dev/null +++ b/toolkit/event-observer/src/main/kotlin/br/com/arch/toolkit/util/_transform.kt @@ -0,0 +1,101 @@ +@file:Suppress("Filename", "unused") + +package br.com.arch.toolkit.util + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlin.coroutines.CoroutineContext + +internal fun Flow>.applyTransformation( + context: CoroutineContext, + transform: Transform.Nullable +) = flowOn(transform.dispatcher) + .map(transform::apply) + .filter { (_, isFailure) -> + when (transform.failMode) { + Transform.Mode.OMIT_WHEN_FAIL -> isFailure.not() + Transform.Mode.NULL_WHEN_FAIL -> true + } + } + .map { (result, _) -> result } + .flowOn(context) + +internal fun Flow>.applyTransformation( + context: CoroutineContext, + transform: Transform.NotNull +) = flowOn(transform.dispatcher).mapNotNull { transform.apply(it).first }.flowOn(context) + +sealed class Transform { + abstract val dispatcher: CoroutineDispatcher + abstract val failMode: Mode + abstract val func: suspend (T, R) -> X + abstract val onErrorReturn: (suspend (Throwable) -> X)? + + internal suspend fun apply(data: Pair) = runCatching { func(data.first, data.second) } + .let { result -> + val finalResult = result.exceptionOrNull() + ?.let { error -> onErrorReturn?.runCatching { invoke(error) } } + ?: result + finalResult.getOrNull() to finalResult.isFailure + } + + enum class Mode { + OMIT_WHEN_FAIL, + NULL_WHEN_FAIL + } + + sealed class Nullable : Transform() { + class OmitFail( + override val dispatcher: CoroutineDispatcher, + override val func: suspend (T?, R?) -> X? + ) : Nullable() { + override val failMode: Mode = Mode.OMIT_WHEN_FAIL + override val onErrorReturn: (suspend (Throwable) -> X?)? = null + } + + class NullFail( + override val dispatcher: CoroutineDispatcher, + override val func: suspend (T?, R?) -> X? + ) : Nullable() { + override val failMode: Mode = Mode.NULL_WHEN_FAIL + override val onErrorReturn: (suspend (Throwable) -> X?)? = null + } + + class Fallback( + override val dispatcher: CoroutineDispatcher, + override val func: suspend (T?, R?) -> X?, + override val onErrorReturn: suspend (Throwable) -> X? + ) : Nullable() { + override val failMode: Mode = Mode.OMIT_WHEN_FAIL + } + + class Custom( + override val dispatcher: CoroutineDispatcher, + override val failMode: Mode, + override val func: suspend (T?, R?) -> X?, + override val onErrorReturn: (suspend (Throwable) -> X?)? + ) : Nullable() + } + + sealed class NotNull : Transform() { + class OmitFail( + override val dispatcher: CoroutineDispatcher, + override val func: suspend (T, R) -> X + ) : NotNull() { + override val failMode: Mode = Mode.OMIT_WHEN_FAIL + override val onErrorReturn: (suspend (Throwable) -> X)? = null + } + + class Fallback( + override val dispatcher: CoroutineDispatcher, + override val func: suspend (T, R) -> X, + override val onErrorReturn: (suspend (Throwable) -> X)? + ) : NotNull() { + override val failMode: Mode = Mode.OMIT_WHEN_FAIL + } + } +} diff --git a/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/ChainNotNullTest.kt b/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/ChainNotNullTest.kt index 663cf72..5fb77e3 100644 --- a/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/ChainNotNullTest.kt +++ b/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/ChainNotNullTest.kt @@ -94,120 +94,125 @@ class ChainNotNullTest { //region Transform @Test - fun `15 - LiveData - not initialized - chainNotNullWith - transform`() = executechainNotNullWithTransform( - liveDataA = MutableLiveData(), - liveDataB = MutableLiveData(), - block = { liveDataA, liveDataB, liveData, condition, transform, observer -> - coVerify(exactly = 0) { condition(any()) } - coVerify(exactly = 0) { liveData(any()) } - coVerify(exactly = 0) { transform(any(), any()) } - coVerify(exactly = 0) { observer(any()) } - - liveDataA.value = null - advanceUntilIdle() - coVerify(exactly = 0) { condition(any()) } - coVerify(exactly = 0) { liveData(any()) } - coVerify(exactly = 0) { transform(any(), any()) } - coVerify(exactly = 0) { observer(any()) } - - liveDataB.value = null - advanceUntilIdle() - coVerify(exactly = 0) { condition(any()) } - coVerify(exactly = 0) { liveData(any()) } - coVerify(exactly = 0) { transform(any(), any()) } - coVerify(exactly = 0) { observer(any()) } - - liveDataA.value = "String" - advanceUntilIdle() - coVerify(exactly = 1) { condition("String") } - coVerify(exactly = 1) { liveData("String") } - coVerify(exactly = 0) { transform(any(), any()) } - coVerify(exactly = 0) { observer(any()) } - - liveDataB.value = 123 - advanceUntilIdle() - coVerify(exactly = 1) { condition(any()) } - coVerify(exactly = 1) { liveData(any()) } - coVerify(exactly = 1) { transform("String", 123) } - coVerify(exactly = 1) { observer("String|123") } - - liveDataA.value = null - advanceUntilIdle() - coVerify(exactly = 1) { condition(any()) } - coVerify(exactly = 1) { liveData(any()) } - coVerify(exactly = 1) { transform(any(), any()) } - coVerify(exactly = 1) { observer(any()) } - - liveDataB.value = null - advanceUntilIdle() - coVerify(exactly = 1) { condition(any()) } - coVerify(exactly = 1) { liveData(any()) } - coVerify(exactly = 1) { transform(any(), any()) } - coVerify(exactly = 1) { observer(any()) } - - liveDataB.value = 123 - advanceUntilIdle() - coVerify(exactly = 1) { condition(any()) } - coVerify(exactly = 1) { liveData(any()) } - coVerify(exactly = 1) { transform(any(), any()) } - coVerify(exactly = 1) { observer(any()) } - - liveDataA.value = "String" - advanceUntilIdle() - coVerify(exactly = 2) { condition("String") } - coVerify(exactly = 2) { liveData("String") } - coVerify(exactly = 2) { transform("String", 123) } - coVerify(exactly = 2) { observer("String|123") } - } - ) + fun `15 - LiveData - not initialized - chainNotNullWith - transform`() = + executechainNotNullWithTransform( + liveDataA = MutableLiveData(), + liveDataB = MutableLiveData(), + block = { liveDataA, liveDataB, liveData, condition, transform, observer -> + coVerify(exactly = 0) { condition(any()) } + coVerify(exactly = 0) { liveData(any()) } + coVerify(exactly = 0) { transform(any(), any()) } + coVerify(exactly = 0) { observer(any()) } + + liveDataA.value = null + advanceUntilIdle() + coVerify(exactly = 0) { condition(any()) } + coVerify(exactly = 0) { liveData(any()) } + coVerify(exactly = 0) { transform(any(), any()) } + coVerify(exactly = 0) { observer(any()) } + + liveDataB.value = null + advanceUntilIdle() + coVerify(exactly = 0) { condition(any()) } + coVerify(exactly = 0) { liveData(any()) } + coVerify(exactly = 0) { transform(any(), any()) } + coVerify(exactly = 0) { observer(any()) } + + liveDataA.value = "String" + advanceUntilIdle() + coVerify(exactly = 1) { condition("String") } + coVerify(exactly = 1) { liveData("String") } + coVerify(exactly = 0) { transform(any(), any()) } + coVerify(exactly = 0) { observer(any()) } + + liveDataB.value = 123 + advanceUntilIdle() + coVerify(exactly = 1) { condition(any()) } + coVerify(exactly = 1) { liveData(any()) } + coVerify(exactly = 1) { transform("String", 123) } + coVerify(exactly = 1) { observer("String|123") } + + liveDataA.value = null + advanceUntilIdle() + coVerify(exactly = 1) { condition(any()) } + coVerify(exactly = 1) { liveData(any()) } + coVerify(exactly = 1) { transform(any(), any()) } + coVerify(exactly = 1) { observer(any()) } + + liveDataB.value = null + advanceUntilIdle() + coVerify(exactly = 1) { condition(any()) } + coVerify(exactly = 1) { liveData(any()) } + coVerify(exactly = 1) { transform(any(), any()) } + coVerify(exactly = 1) { observer(any()) } + + liveDataB.value = 123 + advanceUntilIdle() + coVerify(exactly = 1) { condition(any()) } + coVerify(exactly = 1) { liveData(any()) } + coVerify(exactly = 1) { transform(any(), any()) } + coVerify(exactly = 1) { observer(any()) } + + liveDataA.value = "String" + advanceUntilIdle() + coVerify(exactly = 2) { condition("String") } + coVerify(exactly = 2) { liveData("String") } + coVerify(exactly = 2) { transform("String", 123) } + coVerify(exactly = 2) { observer("String|123") } + } + ) @Test - fun `16 - LiveData - initialized A - chainNotNullWith - transform`() = executechainNotNullWithTransform( - liveDataA = MutableLiveData(null), - liveDataB = MutableLiveData(), - block = { _, _, liveData, condition, transform, observer -> - coVerify(exactly = 0) { condition(any()) } - coVerify(exactly = 0) { liveData(any()) } - coVerify(exactly = 0) { transform(any(), any()) } - coVerify(exactly = 0) { observer(any()) } - } - ) + fun `16 - LiveData - initialized A - chainNotNullWith - transform`() = + executechainNotNullWithTransform( + liveDataA = MutableLiveData(null), + liveDataB = MutableLiveData(), + block = { _, _, liveData, condition, transform, observer -> + coVerify(exactly = 0) { condition(any()) } + coVerify(exactly = 0) { liveData(any()) } + coVerify(exactly = 0) { transform(any(), any()) } + coVerify(exactly = 0) { observer(any()) } + } + ) @Test - fun `17 - LiveData - initialized B - chainNotNullWith - transform`() = executechainNotNullWithTransform( - liveDataA = MutableLiveData(), - liveDataB = MutableLiveData(null), - block = { _, _, liveData, condition, transform, observer -> - coVerify(exactly = 0) { condition(any()) } - coVerify(exactly = 0) { liveData(any()) } - coVerify(exactly = 0) { transform(any(), any()) } - coVerify(exactly = 0) { observer(any()) } - } - ) + fun `17 - LiveData - initialized B - chainNotNullWith - transform`() = + executechainNotNullWithTransform( + liveDataA = MutableLiveData(), + liveDataB = MutableLiveData(null), + block = { _, _, liveData, condition, transform, observer -> + coVerify(exactly = 0) { condition(any()) } + coVerify(exactly = 0) { liveData(any()) } + coVerify(exactly = 0) { transform(any(), any()) } + coVerify(exactly = 0) { observer(any()) } + } + ) @Test - fun `18 - LiveData - initialized A B - chainNotNullWith - transform`() = executechainNotNullWithTransform( - liveDataA = MutableLiveData(null), - liveDataB = MutableLiveData(null), - block = { _, _, liveData, condition, transform, observer -> - coVerify(exactly = 0) { condition(any()) } - coVerify(exactly = 0) { liveData(any()) } - coVerify(exactly = 0) { transform(any(), any()) } - coVerify(exactly = 0) { observer(any()) } - } - ) + fun `18 - LiveData - initialized A B - chainNotNullWith - transform`() = + executechainNotNullWithTransform( + liveDataA = MutableLiveData(null), + liveDataB = MutableLiveData(null), + block = { _, _, liveData, condition, transform, observer -> + coVerify(exactly = 0) { condition(any()) } + coVerify(exactly = 0) { liveData(any()) } + coVerify(exactly = 0) { transform(any(), any()) } + coVerify(exactly = 0) { observer(any()) } + } + ) @Test - fun `19 - LiveData - initialized A B - chainNotNullWith - transform`() = executechainNotNullWithTransform( - liveDataA = MutableLiveData("String"), - liveDataB = MutableLiveData(123), - block = { _, _, liveData, condition, transform, observer -> - coVerify(exactly = 1) { condition("String") } - coVerify(exactly = 1) { liveData("String") } - coVerify(exactly = 1) { transform("String", 123) } - coVerify(exactly = 1) { observer("String|123") } - } - ) + fun `19 - LiveData - initialized A B - chainNotNullWith - transform`() = + executechainNotNullWithTransform( + liveDataA = MutableLiveData("String"), + liveDataB = MutableLiveData(123), + block = { _, _, liveData, condition, transform, observer -> + coVerify(exactly = 1) { condition("String") } + coVerify(exactly = 1) { liveData("String") } + coVerify(exactly = 1) { transform("String", 123) } + coVerify(exactly = 1) { observer("String|123") } + } + ) @Test fun `20 - LiveData - initialized A B - chainNotNullWith - transform - exception condition`() = @@ -238,7 +243,7 @@ class ChainNotNullTest { ) @Test - fun `22 - LiveData - initialized A B - chainNotNullWith - transform - exception transform`() = + fun `22 - LiveData - initialized A B - chainNotNullWith - transform - exception transform - omit`() = executechainNotNullWithTransform( liveDataA = MutableLiveData("String"), liveDataB = MutableLiveData(123), @@ -250,6 +255,21 @@ class ChainNotNullTest { coVerify(exactly = 0) { observer(any()) } } ) + + @Test + fun `23 - LiveData - initialized A B - chainNotNullWith - transform - exception transform - fallback`() = + executechainNotNullWithTransform( + liveDataA = MutableLiveData("String"), + liveDataB = MutableLiveData(123), + transformException = true, + useFallback = true, + block = { _, _, liveData, condition, transform, observer -> + coVerify(exactly = 1) { condition("String") } + coVerify(exactly = 1) { liveData("String") } + coVerify(exactly = 1) { transform("String", 123) } + coVerify(exactly = 1) { observer("fallback") } + } + ) //endregion //region Scenarios @@ -436,6 +456,7 @@ class ChainNotNullTest { conditionException: Boolean = false, liveDataException: Boolean = false, transformException: Boolean = false, + useFallback: Boolean = false, block: suspend TestScope.( a: MutableLiveData, b: MutableLiveData, @@ -469,7 +490,15 @@ class ChainNotNullTest { context = EmptyCoroutineContext, other = mockedLiveData, condition = mockedCondition, - transform = Dispatchers.Main to mockedTransform + transform = if (!useFallback) { + Transform.NotNull.OmitFail(Dispatchers.Main, mockedTransform) + } else { + Transform.NotNull.Fallback( + dispatcher = Dispatchers.Main, + func = mockedTransform, + onErrorReturn = { "fallback" } + ) + } ) liveDataC.observe(alwaysOnOwner, mockedObserver) advanceUntilIdle() diff --git a/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/ChainTest.kt b/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/ChainTest.kt index 5ece189..21a2921 100644 --- a/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/ChainTest.kt +++ b/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/ChainTest.kt @@ -238,11 +238,26 @@ class ChainTest { ) @Test - fun `22 - LiveData - initialized A B - chainWith - transform - exception transform`() = + fun `22 - LiveData - initialized A B - chainWith - transform - exception transform - omit`() = executeChainWithTransform( liveDataA = MutableLiveData("String"), liveDataB = MutableLiveData(123), transformException = true, + block = { _, _, liveData, condition, transform, observer -> + coVerify(exactly = 1) { condition("String") } + coVerify(exactly = 1) { liveData("String") } + coVerify(exactly = 1) { transform("String", 123) } + coVerify(exactly = 0) { observer(any()) } + } + ) + + @Test + fun `23 - LiveData - initialized A B - chainWith - transform - exception transform - null`() = + executeChainWithTransform( + liveDataA = MutableLiveData("String"), + liveDataB = MutableLiveData(123), + transformException = true, + failMode = Transform.Mode.NULL_WHEN_FAIL, block = { _, _, liveData, condition, transform, observer -> coVerify(exactly = 1) { condition("String") } coVerify(exactly = 1) { liveData("String") } @@ -250,6 +265,21 @@ class ChainTest { coVerify(exactly = 1) { observer(null) } } ) + + @Test + fun `24 - LiveData - initialized A B - chainWith - transform - exception transform - fallback`() = + executeChainWithTransform( + liveDataA = MutableLiveData("String"), + liveDataB = MutableLiveData(123), + transformException = true, + useFallback = true, + block = { _, _, liveData, condition, transform, observer -> + coVerify(exactly = 1) { condition("String") } + coVerify(exactly = 1) { liveData("String") } + coVerify(exactly = 1) { transform("String", 123) } + coVerify(exactly = 1) { observer("fallback") } + } + ) //endregion //region Scenarios @@ -436,6 +466,8 @@ class ChainTest { conditionException: Boolean = false, liveDataException: Boolean = false, transformException: Boolean = false, + useFallback: Boolean = false, + failMode: Transform.Mode = Transform.Mode.OMIT_WHEN_FAIL, block: suspend TestScope.( a: MutableLiveData, b: MutableLiveData, @@ -469,7 +501,12 @@ class ChainTest { context = EmptyCoroutineContext, other = mockedLiveData, condition = mockedCondition, - transform = Dispatchers.Main to mockedTransform + transform = Transform.Nullable.Custom( + dispatcher = Dispatchers.Main, + failMode = failMode, + func = mockedTransform, + onErrorReturn = if (useFallback) ({ "fallback" }) else null + ) ) liveDataC.observe(alwaysOnOwner, mockedObserver) advanceUntilIdle() diff --git a/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/CombineNotNullTest.kt b/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/CombineNotNullTest.kt index 520fcb3..6631e50 100644 --- a/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/CombineNotNullTest.kt +++ b/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/CombineNotNullTest.kt @@ -114,6 +114,19 @@ class CombineNotNullTest { coVerify(exactly = 0) { mockedObserver.invoke(any()) } } ) + + @Test + fun `14 - LiveData - initialized A B - combineNotNull - transform - exception - fallback`() = + executeCombineTransform( + liveDataA = MutableLiveData("String"), + liveDataB = MutableLiveData(123), + transformException = true, + useFallback = true, + block = { _, _, _, mockedObserver -> + coVerify(exactly = 1) { mockedObserver.invoke("fallback") } + } + ) + //endregion private fun `LiveData - not initialized - combineNotNull`(context: CoroutineContext?) = @@ -192,6 +205,7 @@ class CombineNotNullTest { liveDataA: MutableLiveData, liveDataB: MutableLiveData, transformException: Boolean = false, + useFallback: Boolean = false, block: suspend TestScope.( a: MutableLiveData, b: MutableLiveData, @@ -208,7 +222,14 @@ class CombineNotNullTest { if (transformException) error("") else "${it.invocation.args[0]}|${it.invocation.args[1]}" } - val liveDataC = liveDataA.combineNotNull(liveDataB, Dispatchers.Main.immediate to mockedTransform) + val liveDataC = liveDataA.combineNotNull( + other = liveDataB, + transform = Transform.NotNull.Fallback( + dispatcher = Dispatchers.Main, + func = mockedTransform, + onErrorReturn = if (useFallback) ({ "fallback" }) else null + ) + ) advanceUntilIdle() liveDataC.observe(alwaysOnOwner, mockedObserver) advanceUntilIdle() diff --git a/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/CombineTest.kt b/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/CombineTest.kt index f40e2cb..372195d 100644 --- a/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/CombineTest.kt +++ b/toolkit/event-observer/src/test/kotlin/br/com/arch/toolkit/util/CombineTest.kt @@ -105,14 +105,39 @@ class CombineTest { ) @Test - fun `13 - LiveData - initialized A B - combine - transform - exception`() = executeCombineTransform( - liveDataA = MutableLiveData("String"), - liveDataB = MutableLiveData(123), - transformException = true, - block = { _, _, _, mockedObserver -> - coVerify(exactly = 1) { mockedObserver.invoke(null) } - } - ) + fun `13 - LiveData - initialized A B - combine - transform - exception - omit`() = + executeCombineTransform( + liveDataA = MutableLiveData("String"), + liveDataB = MutableLiveData(123), + transformException = true, + block = { _, _, _, mockedObserver -> + coVerify(exactly = 0) { mockedObserver.invoke(any()) } + } + ) + + @Test + fun `14 - LiveData - initialized A B - combine - transform - exception - null`() = + executeCombineTransform( + liveDataA = MutableLiveData("String"), + liveDataB = MutableLiveData(123), + failMode = Transform.Mode.NULL_WHEN_FAIL, + transformException = true, + block = { _, _, _, mockedObserver -> + coVerify(exactly = 1) { mockedObserver.invoke(null) } + } + ) + + @Test + fun `15 - LiveData - initialized A B - combine - transform - exception - fallback`() = + executeCombineTransform( + liveDataA = MutableLiveData("String"), + liveDataB = MutableLiveData(123), + transformException = true, + useFallback = true, + block = { _, _, _, mockedObserver -> + coVerify(exactly = 1) { mockedObserver.invoke("fallback") } + } + ) //endregion private fun `LiveData - not initialized - combine`(context: CoroutineContext?) = executeCombine( @@ -187,6 +212,8 @@ class CombineTest { liveDataA: MutableLiveData, liveDataB: MutableLiveData, transformException: Boolean = false, + useFallback: Boolean = false, + failMode: Transform.Mode = Transform.Mode.OMIT_WHEN_FAIL, block: suspend TestScope.( a: MutableLiveData, b: MutableLiveData, @@ -203,7 +230,15 @@ class CombineTest { if (transformException) error("") else "${it.invocation.args[0]}|${it.invocation.args[1]}" } - val liveDataC = liveDataA.combine(liveDataB, Dispatchers.Main to mockedTransform) + val liveDataC = liveDataA.combine( + other = liveDataB, + transform = Transform.Nullable.Custom( + dispatcher = Dispatchers.Main, + failMode = failMode, + func = mockedTransform, + onErrorReturn = if (useFallback) ({ "fallback" }) else null + ) + ) liveDataC.observe(alwaysOnOwner, mockedObserver) advanceUntilIdle() block(liveDataA, liveDataB, mockedTransform, mockedObserver)