Updated on 2026-08-14
This commit is contained in:
parent
bc00198797
commit
ddb8de4964
20 changed files with 447 additions and 84 deletions
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.domain.core.lce
|
||||
|
||||
import arrow.core.identity
|
||||
import com.tangem.domain.core.utils.flatMap
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
|
||||
/**
|
||||
* A sealed class representing the three states of a data load operation: Loading, Content, and Error.
|
||||
*
|
||||
* @param E The type of the error object.
|
||||
* @param C The type of the content object.
|
||||
*/
|
||||
sealed class Lce<out E : Any, out C : Any> {
|
||||
|
||||
/**
|
||||
* Represents the loading state, which may contain partial content.
|
||||
*
|
||||
* @param partialContent The partial content that has been loaded so far, if any.
|
||||
*/
|
||||
data class Loading<C : Any>(val partialContent: C?) : Lce<Nothing, C>()
|
||||
|
||||
/**
|
||||
* Represents the content state, which contains the loaded content.
|
||||
*
|
||||
* @param content The loaded content.
|
||||
*/
|
||||
data class Content<C : Any>(val content: C) : Lce<Nothing, C>()
|
||||
|
||||
/**
|
||||
* Represents the error state, which contains an error object.
|
||||
*
|
||||
* @param error The error that occurred during loading.
|
||||
*/
|
||||
data class Error<E : Any>(val error: E) : Lce<E, Nothing>()
|
||||
|
||||
/**
|
||||
* Applies the given functions to the content, error, or partial content of this Lce, depending on its state.
|
||||
*
|
||||
* @param ifLoading The function to apply if this is a [Loading] state.
|
||||
* @param ifContent The function to apply if this is a [Content] state.
|
||||
* @param ifError The function to apply if this is an [Error] state.
|
||||
* @return The result of applying the corresponding function.
|
||||
*/
|
||||
inline fun <T> fold(
|
||||
ifLoading: (partialContent: C?) -> T,
|
||||
ifContent: (content: C) -> T,
|
||||
ifError: (error: E) -> T,
|
||||
): T = when (this) {
|
||||
is Loading -> ifLoading(partialContent)
|
||||
is Error -> ifError(error)
|
||||
is Content -> ifContent(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the content of this [Lce] by applying the given function.
|
||||
* If this is a [Content] state, the function is applied to the [Content.content].
|
||||
* If this is a [Loading] state and partialContent is present,
|
||||
* the function is applied to the [Loading.partialContent].
|
||||
*
|
||||
* @param ifContent The function to apply to the content or partial content.
|
||||
* @return A new [Lce] instance containing the result of applying the function.
|
||||
*/
|
||||
inline fun <T : Any> map(ifContent: (C) -> T): Lce<E, T> = flatMap { content, isLoading ->
|
||||
if (isLoading) {
|
||||
lceLoading(ifContent(content))
|
||||
} else {
|
||||
ifContent(content).lceContent()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the error of this [Lce] by applying the given function, if this is an [Error] state.
|
||||
*
|
||||
* @param ifError The function to apply to the error.
|
||||
* @return A new [Lce] with the transformed error, or this [Lce] unchanged if it is not an [Error] state.
|
||||
*/
|
||||
inline fun <T : Any> mapError(ifError: (E) -> T): Lce<T, C> = when (this) {
|
||||
is Loading -> this
|
||||
is Content -> this
|
||||
is Error -> ifError(error).lceError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of this [Lce] if it's a [Lce.Content] or partial content if it's a [Lce.Loading],
|
||||
* `null` if it's a [Lce.Error].
|
||||
*
|
||||
* @return The content of this [Lce] or `null`.
|
||||
*/
|
||||
fun getOrNull(): C? = fold(
|
||||
ifLoading = ::identity,
|
||||
ifContent = ::identity,
|
||||
ifError = { null },
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.tangem.domain.core.lce
|
||||
|
||||
import arrow.core.raise.Raise
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.channels.ProducerScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.experimental.ExperimentalTypeInference
|
||||
|
||||
/**
|
||||
* A [Flow] of [Lce]
|
||||
*
|
||||
* @param E The type of the error object.
|
||||
* @param C The type of the content object.
|
||||
* */
|
||||
typealias LceFlow<E, C> = Flow<Lce<E, C>>
|
||||
|
||||
/**
|
||||
* A class that wraps a [LceRaise] instance for [Lce] type within a [ProducerScope].
|
||||
* It provides methods to handle [Lce] instances and raise errors within a [Flow].
|
||||
*
|
||||
* @property raise The [LceRaise] instance that this class wraps.
|
||||
* @property scope The [ProducerScope] that this class operates within.
|
||||
* @property ifLoading The function to call if a loading state is raised.
|
||||
*/
|
||||
class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
||||
private val raise: LceRaise<E>,
|
||||
private val scope: ProducerScope<Lce<E, C>>,
|
||||
private val ifLoading: LceRaise<E>.(C) -> Lce<E, C>,
|
||||
) : Raise<Lce<E, Nothing>> by raise,
|
||||
CoroutineScope by scope {
|
||||
|
||||
/**
|
||||
* Raises an [Lce] instance within the [ProducerScope].
|
||||
* It closes the [ProducerScope] after raise.
|
||||
*
|
||||
* @param r The [Lce] instance to raise.
|
||||
*/
|
||||
override fun raise(r: Lce<E, Nothing>): Nothing {
|
||||
scope.launch(NonCancellable) {
|
||||
scope.send(r)
|
||||
scope.close()
|
||||
}
|
||||
|
||||
raise.raise(r)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a content value within the [ProducerScope].
|
||||
* If the content is still loading, it calls [ifLoading] lambda to retrieve a state.
|
||||
* Otherwise, it wraps the content in a [Lce.Content] state.
|
||||
*
|
||||
* @param content The content value to send.
|
||||
* @param isStillLoading A flag indicating whether the content is still loading.
|
||||
*/
|
||||
suspend fun send(content: C, isStillLoading: Boolean = false) {
|
||||
val value = if (isStillLoading) {
|
||||
ifLoading(raise, content)
|
||||
} else {
|
||||
content.lceContent()
|
||||
}
|
||||
|
||||
scope.send(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [LceFlow] by executing the given [block] within a [LceFlowScope] context.
|
||||
*
|
||||
* Flow starts with a [Lce.Loading] state.
|
||||
*
|
||||
* @param ifLoading The function to call if the [block] raises a [Lce.Loading] state.
|
||||
* By default, it creates a new [Lce.Loading] state with the value returned by the [block].
|
||||
* @param block The block to execute within a [LceFlowScope] context.
|
||||
* @return A [LceFlow] representing the result of the [block].
|
||||
*/
|
||||
@OptIn(ExperimentalTypeInference::class)
|
||||
fun <E : Any, C : Any> lceFlow(
|
||||
ifLoading: LceRaise<E>.(C) -> Lce<E, C> = { lceLoading(partialContent = it) },
|
||||
@BuilderInference block: suspend LceFlowScope<E, C>.() -> Unit,
|
||||
): LceFlow<E, C> {
|
||||
return channelFlow {
|
||||
trySend(lceLoading())
|
||||
|
||||
lce {
|
||||
val scope = LceFlowScope(
|
||||
raise = this@lce,
|
||||
scope = this@channelFlow,
|
||||
ifLoading = ifLoading,
|
||||
)
|
||||
|
||||
block(scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.domain.core.lce
|
||||
|
||||
import arrow.atomic.Atomic
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.recover
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import kotlin.experimental.ExperimentalTypeInference
|
||||
|
||||
/**
|
||||
* A class that wraps a [Raise] instance for [Lce] type.
|
||||
* It provides methods to handle [Lce] instances and raise errors.
|
||||
*
|
||||
* @property raise The [Raise] instance that this class wraps.
|
||||
* @property isLoading An [Atomic] boolean flag indicating whether a loading operation is in progress.
|
||||
*/
|
||||
class LceRaise<E : Any> @PublishedApi internal constructor(
|
||||
private val raise: Raise<Lce<E, Nothing>>,
|
||||
) : Raise<Lce<E, Nothing>> by raise {
|
||||
|
||||
val isLoading: Atomic<Boolean> = Atomic(false)
|
||||
|
||||
/**
|
||||
* Binds the content of this [Lce] instance and handles its state.
|
||||
* If this is a [Lce.Loading] state, sets the [isLoading] flag to true and calls the [ifLoading] function.
|
||||
* If this is a [Lce.Content] state, returns the content.
|
||||
* If this is a [Lce.Error] state, raises the error.
|
||||
*
|
||||
* @param ifLoading The function to call if this is a [Lce.Loading] state.
|
||||
* By default, it raises a new [Lce.Loading] state.
|
||||
* @return The content of this [Lce] instance.
|
||||
*/
|
||||
fun <C : Any> Lce<E, C>.bind(ifLoading: (partialContent: C?) -> C = { raise(lceLoading()) }): C = when (this) {
|
||||
is Lce.Loading -> {
|
||||
isLoading.set(true)
|
||||
|
||||
ifLoading(partialContent)
|
||||
}
|
||||
is Lce.Content -> content
|
||||
is Lce.Error -> raise(r = this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the content of this [Lce] instance and handles its state.
|
||||
* If this is a [Lce.Loading] state, sets the [isLoading] flag to true and returns the partial content.
|
||||
* If this is a [Lce.Content] state, returns the content.
|
||||
* If this is a [Lce.Error] state, raises the error.
|
||||
*
|
||||
* @return The content of this [Lce] instance.
|
||||
*/
|
||||
fun <C : Any> Lce<E, C>.bindOrNull(): C? = when (this) {
|
||||
is Lce.Loading -> {
|
||||
isLoading.set(true)
|
||||
|
||||
partialContent
|
||||
}
|
||||
is Lce.Content -> content
|
||||
is Lce.Error -> raise(r = this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [Lce] instance by executing the given [block] within a [LceRaise] context.
|
||||
*
|
||||
* @param ifLoading The function to call if the [block] raises a [Lce.Loading] state.
|
||||
* By default, it creates a new [Lce.Loading] state with the value returned by the [block].
|
||||
* @param block The block to execute within a [LceRaise] context.
|
||||
* @return A [Lce] instance representing the result of the [block].
|
||||
*/
|
||||
@OptIn(ExperimentalTypeInference::class)
|
||||
inline fun <E : Any, C : Any> lce(
|
||||
ifLoading: LceRaise<E>.(C) -> Lce<E, C> = { lceLoading(partialContent = it) },
|
||||
@BuilderInference block: LceRaise<E>.() -> C,
|
||||
): Lce<E, C> = recover(
|
||||
block = {
|
||||
val raise = LceRaise(raise = this)
|
||||
val value = block(raise)
|
||||
|
||||
if (raise.isLoading.get()) {
|
||||
ifLoading(raise, value)
|
||||
} else {
|
||||
value.lceContent()
|
||||
}
|
||||
},
|
||||
recover = { e: Lce<E, Nothing> -> e },
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.domain.core.utils
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* [Flow] of [Either]
|
||||
*
|
||||
* @param E type of left value
|
||||
* @param A type of right value
|
||||
* */
|
||||
typealias EitherFlow<E, A> = Flow<Either<E, A>>
|
||||
|
||||
/**
|
||||
* Converts an [Either] instance to a [Lce] instance.
|
||||
* If this is a [Either.Left], it is converted to a [Lce.Error] with the same error.
|
||||
* If this is a [Either.Right], it is converted to a [Lce.Content] or [Lce.Loading] with the same content,
|
||||
* depending on the [isStillLoading] parameter.
|
||||
*
|
||||
* @param isStillLoading A flag indicating whether the content is still loading.
|
||||
* If true, the [Either.Right] is converted to a [Lce.Loading].
|
||||
* @return A [Lce] instance containing the same content or error as this [Either],
|
||||
* and possibly indicating a loading state.
|
||||
*/
|
||||
inline fun <reified E : Any, reified T : Any> Either<E, T>.toLce(isStillLoading: Boolean = false): Lce<E, T> {
|
||||
return when (this) {
|
||||
is Either.Left -> Lce.Error(value)
|
||||
is Either.Right -> {
|
||||
if (isStillLoading) {
|
||||
Lce.Loading(value)
|
||||
} else {
|
||||
Lce.Content(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.domain.core.utils
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.identity
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
|
||||
/**
|
||||
* Creates a [Lce.Loading] instance with optional partial content.
|
||||
*
|
||||
* @param partialContent The partial content that has been loaded so far, if any.
|
||||
* @return A [Lce.Loading] instance.
|
||||
*/
|
||||
fun <C : Any> lceLoading(partialContent: C? = null): Lce<Nothing, C> = Lce.Loading(partialContent)
|
||||
|
||||
/**
|
||||
* Wraps the receiver object in a [Lce.Content] instance.
|
||||
*
|
||||
* @return A [Lce.Content] instance containing the receiver object.
|
||||
*/
|
||||
fun <C : Any> C.lceContent(): Lce<Nothing, C> = Lce.Content(content = this)
|
||||
|
||||
/**
|
||||
* Wraps the receiver object in a [Lce.Error] instance.
|
||||
*
|
||||
* @return A [Lce.Error] instance containing the receiver object.
|
||||
*/
|
||||
fun <E : Any> E.lceError(): Lce<E, Nothing> = Lce.Error(error = this)
|
||||
|
||||
/**
|
||||
* Transforms this [Lce] instance by applying the given function into a new [Lce] instance.
|
||||
*
|
||||
* @param block The function to apply to the content of this [Lce].
|
||||
* @return A new [Lce] instance containing the result of applying the function.
|
||||
* */
|
||||
inline fun <E : Any, C : Any, T : Any> Lce<E, C>.flatMap(
|
||||
block: (content: C, isLoading: Boolean) -> Lce<E, T>,
|
||||
): Lce<E, T> = when (this) {
|
||||
is Lce.Loading -> partialContent?.let { block(it, true) } ?: lceLoading()
|
||||
is Lce.Content -> block(content, false)
|
||||
is Lce.Error -> this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of this [Lce] if it's a [Lce.Content] or applies the given functions if it's a [Lce.Loading] or [Lce.Error].
|
||||
*
|
||||
* @param ifLoading The function to apply if this is a [Lce.Loading] state.
|
||||
* @param ifError The function to apply if this is a [Lce.Error] state.
|
||||
* @return The content of this [Lce] or the result of applying the corresponding function.
|
||||
*/
|
||||
inline fun <E : Any, C : Any> Lce<E, C>.getOrElse(ifLoading: (maybeContent: C?) -> C, ifError: (error: E) -> C): C {
|
||||
return fold(
|
||||
ifLoading = ifLoading,
|
||||
ifContent = ::identity,
|
||||
ifError = ifError,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this [Lce] into an [Either] instance.
|
||||
* If this is a [Lce.Content], the content is wrapped in a [Either.Right].
|
||||
* If this is a [Lce.Error], the error is wrapped in a [Either.Left].
|
||||
* If this is a [Lce.Loading], the [ifLoading] function is applied to the partial content and the result is wrapped in a
|
||||
* [Either.Right].
|
||||
*
|
||||
* @param ifLoading The function to apply if this is a [Lce.Loading] state.
|
||||
* @return An [Either] instance containing the content or error of this [Lce],
|
||||
* or the result of applying the [ifLoading] function to the partial content.
|
||||
*/
|
||||
inline fun <E : Any, C : Any> Lce<E, C>.toEither(ifLoading: (maybeContent: C?) -> C): Either<E, C> = fold(
|
||||
ifLoading = { ifLoading(it).right() },
|
||||
ifContent = { it.right() },
|
||||
ifError = { it.left() },
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue