Skip to content

Commit 1bd14b8

Browse files
author
Lucas Sales
committed
Adding more complete version of result class
1 parent 70b59ed commit 1bd14b8

File tree

3 files changed

+334
-13
lines changed

3 files changed

+334
-13
lines changed
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
package com.monstarlab.arch.extensions
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.map
5+
import kotlinx.coroutines.flow.onEach
6+
import java.io.Serializable
7+
import kotlin.contracts.ExperimentalContracts
8+
import kotlin.contracts.InvocationKind
9+
import kotlin.contracts.contract
10+
11+
/**
12+
* A discriminated union that encapsulates a successful outcome with a value of type [T]
13+
* or a failure with an arbitrary [Throwable] exception.
14+
*/
15+
sealed class Result<out T> : Serializable {
16+
17+
@PublishedApi
18+
internal data class Failure<out T>(val exception: Throwable) : Result<T>()
19+
20+
@PublishedApi
21+
internal data class Success<out T>(val value: T) : Result<T>()
22+
23+
/**
24+
* Returns `true` if this instance represents a failed outcome.
25+
* In this case [isSuccess] returns `false`.
26+
*/
27+
val isFailure: Boolean
28+
get() = this is Failure
29+
30+
/**
31+
* Returns `true` if this instance represents a successful outcome.
32+
* In this case [isFailure] returns `false`.
33+
*/
34+
val isSuccess: Boolean
35+
get() = this is Success
36+
37+
/**
38+
* Returns the encapsulated value if this instance represents [success][Result.isSuccess] or `null`
39+
* if it is [failure][Result.isFailure].
40+
*
41+
* This function is a shorthand for `getOrElse { null }` (see [getOrElse]) or
42+
* `fold(onSuccess = { it }, onFailure = { null })` (see [fold]).
43+
*/
44+
public fun getOrNull(): T? = when (this) {
45+
is Failure -> null
46+
is Success -> value
47+
}
48+
49+
/**
50+
* Returns the encapsulated [Throwable] exception if this instance represents [failure][isFailure] or `null`
51+
* if it is [success][isSuccess].
52+
*
53+
* This function is a shorthand for `fold(onSuccess = { null }, onFailure = { it })` (see [fold]).
54+
*/
55+
public fun exceptionOrNull(): Throwable? = when (this) {
56+
is Failure -> exception
57+
is Success -> null
58+
}
59+
60+
/**
61+
* Returns a string `Success(v)` if this instance represents [success][Result.isSuccess]
62+
* where `v` is a string representation of the value or a string `Failure(x)` if
63+
* it is [failure][isFailure] where `x` is a string representation of the exception.
64+
*/
65+
public override fun toString(): String = when (this) {
66+
is Failure -> "Failure($exception)"
67+
is Success -> "Success($value)"
68+
}
69+
70+
/**
71+
* Companion object for [Result] class that contains its constructor functions
72+
* [success] and [failure].
73+
*/
74+
public companion object {
75+
76+
/**
77+
* Returns an instance that encapsulates the given [value] as successful value.
78+
*/
79+
public fun <T> success(value: T): Result<T> = Success(value)
80+
81+
/**
82+
* Returns an instance that encapsulates the given [Throwable] [exception] as failure.
83+
*/
84+
public fun <T> failure(exception: Throwable): Result<T> = Failure(exception)
85+
}
86+
}
87+
88+
/**
89+
* Calls the specified function [block] and returns its encapsulated result if invocation was successful,
90+
* catching any [Throwable] exception that was thrown from the [block] function execution and encapsulating it as a failure.
91+
*/
92+
@Suppress("FunctionName", "TooGenericExceptionCaught")
93+
public inline fun <T> Result(block: () -> T): Result<T> {
94+
return try {
95+
Result.success(block())
96+
} catch (exception: Throwable) {
97+
Result.failure(exception)
98+
}
99+
}
100+
101+
/**
102+
* Returns the encapsulated value if this instance represents [success][Result.isSuccess] or throws the encapsulated [Throwable] exception
103+
* if it is [failure][Result.isFailure].
104+
*
105+
* This function is a shorthand for `getOrElse { throw it }` (see [getOrElse]).
106+
*/
107+
public fun <T> Result<T>.getOrThrow(): T {
108+
return when (this) {
109+
is Result.Failure -> throw exception
110+
is Result.Success -> value
111+
}
112+
}
113+
114+
/**
115+
* Returns the encapsulated value if this instance represents [success][Result.isSuccess] or the
116+
* result of [onFailure] function for the encapsulated [Throwable] exception if it is [failure][Result.isFailure].
117+
*
118+
* Note, that this function rethrows any [Throwable] exception thrown by [onFailure] function.
119+
*
120+
* This function is a shorthand for `fold(onSuccess = { it }, onFailure = onFailure)` (see [fold]).
121+
*/
122+
@OptIn(ExperimentalContracts::class)
123+
public inline fun <R, T : R> Result<T>.getOrElse(onFailure: (exception: Throwable) -> R): R {
124+
contract {
125+
callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
126+
}
127+
return when (this) {
128+
is Result.Failure -> onFailure(exception)
129+
is Result.Success -> value
130+
}
131+
}
132+
133+
/**
134+
* Returns the encapsulated value if this instance represents [success][Result.isSuccess] or the
135+
* [defaultValue] if it is [failure][Result.isFailure].
136+
*
137+
* This function is a shorthand for `getOrElse { defaultValue }` (see [getOrElse]).
138+
*/
139+
public fun <R, T : R> Result<T>.getOrDefault(defaultValue: R): R {
140+
return when (this) {
141+
is Result.Failure -> defaultValue
142+
is Result.Success -> value
143+
}
144+
}
145+
146+
/**
147+
* Returns the result of [onSuccess] for the encapsulated value if this instance represents [success][Result.isSuccess]
148+
* or the result of [onFailure] function for the encapsulated [Throwable] exception if it is [failure][Result.isFailure].
149+
*
150+
* Note, that this function rethrows any [Throwable] exception thrown by [onSuccess] or by [onFailure] function.
151+
*/
152+
@OptIn(ExperimentalContracts::class)
153+
public inline fun <R, T> Result<T>.fold(
154+
onFailure: (exception: Throwable) -> R,
155+
onSuccess: (value: T) -> R
156+
): R {
157+
contract {
158+
callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
159+
callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE)
160+
}
161+
return when (this) {
162+
is Result.Failure -> onFailure(exception)
163+
is Result.Success -> onSuccess(value)
164+
}
165+
}
166+
167+
/**
168+
* Returns the encapsulated result of the given [transform] function applied to the encapsulated value
169+
* if this instance represents [success][Result.isSuccess] or the
170+
* original encapsulated [Throwable] exception if it is [failure][Result.isFailure].
171+
*
172+
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function.
173+
* See [mapCatching] for an alternative that encapsulates exceptions.
174+
*/
175+
@OptIn(ExperimentalContracts::class)
176+
public inline fun <R, T> Result<T>.map(transform: (value: T) -> R): Result<R> {
177+
contract {
178+
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
179+
}
180+
return when (this) {
181+
is Result.Failure -> Result.failure(exception)
182+
is Result.Success -> Result.success(transform(value))
183+
}
184+
}
185+
186+
/**
187+
* Returns the encapsulated result of the given [transform] function applied to the encapsulated value
188+
* if this instance represents [success][Result.isSuccess] or the
189+
* original encapsulated [Throwable] exception if it is [failure][Result.isFailure].
190+
*
191+
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a failure.
192+
* See [map] for an alternative that rethrows exceptions from `transform` function.
193+
*/
194+
public inline fun <R, T> Result<T>.mapCatching(transform: (value: T) -> R): Result<R> {
195+
return when (this) {
196+
is Result.Failure -> Result.failure(exception)
197+
is Result.Success -> Result { transform(value) }
198+
}
199+
}
200+
201+
/**
202+
* Returns a [Result] of the given [transform] function applied to the encapsulated value
203+
* if this instance represents [success][Result.isSuccess] or the
204+
* original encapsulated [Throwable] exception if it is [failure][Result.isFailure].
205+
*
206+
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function.
207+
* See [flatMapCatching] for an alternative that encapsulates exceptions.
208+
*/
209+
@OptIn(ExperimentalContracts::class)
210+
public inline fun <R, T> Result<T>.flatMap(transform: (value: T) -> Result<R>): Result<R> {
211+
contract {
212+
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
213+
}
214+
return when (this) {
215+
is Result.Failure -> Result.failure(exception)
216+
is Result.Success -> transform(value)
217+
}
218+
}
219+
220+
/**
221+
* Returns a [Result] of the given [transform] function applied to the encapsulated value
222+
* if this instance represents [success][Result.isSuccess] or the
223+
* original encapsulated [Throwable] exception if it is [failure][Result.isFailure].
224+
*
225+
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a failure.
226+
* See [map] for an alternative that rethrows exceptions from `transform` function.
227+
*/
228+
public inline fun <R, T> Result<T>.flatMapCatching(transform: (value: T) -> Result<R>): Result<R> {
229+
return when (this) {
230+
is Result.Failure -> Result.failure(exception)
231+
is Result.Success -> Result { transform(value).getOrThrow() }
232+
}
233+
}
234+
235+
/**
236+
* Returns the encapsulated result of the given [transform] function applied to the encapsulated [Throwable] exception
237+
* if this instance represents [failure][Result.isFailure] or the
238+
* original encapsulated value if it is [success][Result.isSuccess].
239+
*
240+
* Note, that this function rethrows any [Throwable] exception thrown by [transform] function.
241+
* See [recoverCatching] for an alternative that encapsulates exceptions.
242+
*/
243+
@OptIn(ExperimentalContracts::class)
244+
public inline fun <R, T : R> Result<T>.recover(transform: (exception: Throwable) -> R): Result<R> {
245+
contract {
246+
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
247+
}
248+
return when (this) {
249+
is Result.Failure -> Result.success(transform(exception))
250+
is Result.Success -> this
251+
}
252+
}
253+
254+
/**
255+
* Returns the encapsulated result of the given [transform] function applied to the encapsulated [Throwable] exception
256+
* if this instance represents [failure][Result.isFailure] or the
257+
* original encapsulated value if it is [success][Result.isSuccess].
258+
*
259+
* This function catches any [Throwable] exception thrown by [transform] function and encapsulates it as a failure.
260+
* See [recover] for an alternative that rethrows exceptions.
261+
*/
262+
public inline fun <R, T : R> Result<T>.recoverCatching(transform: (exception: Throwable) -> R): Result<R> {
263+
return when (this) {
264+
is Result.Failure -> Result { transform(exception) }
265+
is Result.Success -> this
266+
}
267+
}
268+
269+
/**
270+
* Performs the given [action] on the encapsulated [Throwable] exception if this instance represents [failure][Result.isFailure].
271+
* Returns the original `Result` unchanged.
272+
*/
273+
@OptIn(ExperimentalContracts::class)
274+
public inline fun <T> Result<T>.onFailure(action: (exception: Throwable) -> Unit): Result<T> {
275+
contract {
276+
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
277+
}
278+
if (this is Result.Failure) action(exception)
279+
return this
280+
}
281+
282+
/**
283+
* Performs the given [action] on the encapsulated value if this instance represents [success][Result.isSuccess].
284+
* Returns the original `Result` unchanged.
285+
*/
286+
@OptIn(ExperimentalContracts::class)
287+
public inline fun <T> Result<T>.onSuccess(action: (value: T) -> Unit): Result<T> {
288+
contract {
289+
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
290+
}
291+
if (this is Result.Success) action(value)
292+
return this
293+
}
294+
295+
@OptIn(ExperimentalContracts::class)
296+
fun <T> Flow<Result<T>>.onResult(
297+
onFailure: suspend (exception: Throwable) -> Unit,
298+
onSuccess: suspend (value: T) -> Unit
299+
): Flow<Result<T>> {
300+
return onEach { result ->
301+
when (result) {
302+
is Result.Failure -> onFailure(result.exception)
303+
is Result.Success -> onSuccess(result.value)
304+
}
305+
}
306+
}
307+
308+
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
package com.monstarlab.core.usecases.resources
22

3+
import com.monstarlab.arch.extensions.Result
34
import com.monstarlab.arch.extensions.safeFlow
45
import com.monstarlab.core.data.repositories.ResourceRepository
56
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.flow.flow
68
import javax.inject.Inject
79

810
class GetResourcesUseCase @Inject constructor(
911
private val resourceRepository: ResourceRepository
1012
) {
1113

12-
fun getResources() = safeFlow {
14+
fun getResources() = flow {
1315
delay(2000)
14-
resourceRepository.getResources()
16+
emit(Result { resourceRepository.getResources() })
1517
}
1618

1719
}

app/src/main/java/com/monstarlab/features/resources/ResourceViewModel.kt

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@ package com.monstarlab.features.resources
22

33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
5-
import com.monstarlab.arch.extensions.onError
6-
import com.monstarlab.arch.extensions.onSuccess
5+
import com.monstarlab.arch.extensions.onResult
6+
import com.monstarlab.core.domain.error.toError
77
import com.monstarlab.core.domain.model.Resource
88
import com.monstarlab.core.sharedui.errorhandling.ViewError
99
import com.monstarlab.core.sharedui.errorhandling.mapToViewError
1010
import com.monstarlab.core.usecases.resources.GetResourcesUseCase
11-
import kotlinx.coroutines.flow.*
11+
import kotlinx.coroutines.flow.MutableSharedFlow
12+
import kotlinx.coroutines.flow.MutableStateFlow
13+
import kotlinx.coroutines.flow.launchIn
14+
import kotlinx.coroutines.flow.onCompletion
15+
import kotlinx.coroutines.flow.onEach
16+
import kotlinx.coroutines.flow.onStart
1217
import javax.inject.Inject
1318

1419
class ResourceViewModel @Inject constructor(
15-
private val getResourcesUseCase: GetResourcesUseCase
16-
): ViewModel() {
20+
private val getResourcesUseCase: GetResourcesUseCase
21+
) : ViewModel() {
1722

1823
val loadingFlow = MutableStateFlow<Boolean>(false)
1924
val errorFlow = MutableSharedFlow<ViewError>()
@@ -24,13 +29,19 @@ class ResourceViewModel @Inject constructor(
2429
.getResources()
2530
.onStart {
2631
loadingFlow.emit(true)
27-
}.onSuccess {
28-
resourcesFlow.value = it
29-
}.onError {
30-
errorFlow.emit(it.mapToViewError())
31-
}.onCompletion {
32+
}
33+
.onResult(
34+
onFailure = {
35+
errorFlow.emit(it.toError().mapToViewError())
36+
},
37+
onSuccess = {
38+
resourcesFlow.value = it
39+
})
40+
.onEach { result ->
41+
42+
}
43+
.onCompletion {
3244
loadingFlow.emit(false)
3345
}.launchIn(viewModelScope)
3446
}
35-
3647
}

0 commit comments

Comments
 (0)