Skip to content

Commit de5fc98

Browse files
authored
Merge pull request #11 from lucassales2/feature/enhancedResult
Adding complete version of result class
2 parents 3242c70 + 00ba09c commit de5fc98

File tree

6 files changed

+365
-42
lines changed

6 files changed

+365
-42
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 SealedResult<out T> : Serializable {
16+
17+
@PublishedApi
18+
internal data class Failure<out T>(val exception: Throwable) : SealedResult<T>()
19+
20+
@PublishedApi
21+
internal data class Success<out T>(val value: T) : SealedResult<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][SealedResult.isSuccess] or `null`
39+
* if it is [failure][SealedResult.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][SealedResult.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 [SealedResult] 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): SealedResult<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): SealedResult<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> SealedResult(block: () -> T): SealedResult<T> {
94+
return try {
95+
SealedResult.success(block())
96+
} catch (exception: Throwable) {
97+
SealedResult.failure(exception)
98+
}
99+
}
100+
101+
/**
102+
* Returns the encapsulated value if this instance represents [success][SealedResult.isSuccess] or throws the encapsulated [Throwable] exception
103+
* if it is [failure][SealedResult.isFailure].
104+
*
105+
* This function is a shorthand for `getOrElse { throw it }` (see [getOrElse]).
106+
*/
107+
public fun <T> SealedResult<T>.getOrThrow(): T {
108+
return when (this) {
109+
is SealedResult.Failure -> throw exception
110+
is SealedResult.Success -> value
111+
}
112+
}
113+
114+
/**
115+
* Returns the encapsulated value if this instance represents [success][SealedResult.isSuccess] or the
116+
* result of [onFailure] function for the encapsulated [Throwable] exception if it is [failure][SealedResult.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> SealedResult<T>.getOrElse(onFailure: (exception: Throwable) -> R): R {
124+
contract {
125+
callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
126+
}
127+
return when (this) {
128+
is SealedResult.Failure -> onFailure(exception)
129+
is SealedResult.Success -> value
130+
}
131+
}
132+
133+
/**
134+
* Returns the encapsulated value if this instance represents [success][SealedResult.isSuccess] or the
135+
* [defaultValue] if it is [failure][SealedResult.isFailure].
136+
*
137+
* This function is a shorthand for `getOrElse { defaultValue }` (see [getOrElse]).
138+
*/
139+
public fun <R, T : R> SealedResult<T>.getOrDefault(defaultValue: R): R {
140+
return when (this) {
141+
is SealedResult.Failure -> defaultValue
142+
is SealedResult.Success -> value
143+
}
144+
}
145+
146+
/**
147+
* Returns the result of [onSuccess] for the encapsulated value if this instance represents [success][SealedResult.isSuccess]
148+
* or the result of [onFailure] function for the encapsulated [Throwable] exception if it is [failure][SealedResult.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> SealedResult<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 SealedResult.Failure -> onFailure(exception)
163+
is SealedResult.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][SealedResult.isSuccess] or the
170+
* original encapsulated [Throwable] exception if it is [failure][SealedResult.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> SealedResult<T>.map(transform: (value: T) -> R): SealedResult<R> {
177+
contract {
178+
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
179+
}
180+
return when (this) {
181+
is SealedResult.Failure -> SealedResult.failure(exception)
182+
is SealedResult.Success -> SealedResult.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][SealedResult.isSuccess] or the
189+
* original encapsulated [Throwable] exception if it is [failure][SealedResult.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> SealedResult<T>.mapCatching(transform: (value: T) -> R): SealedResult<R> {
195+
return when (this) {
196+
is SealedResult.Failure -> SealedResult.failure(exception)
197+
is SealedResult.Success -> SealedResult { transform(value) }
198+
}
199+
}
200+
201+
/**
202+
* Returns a [SealedResult] of the given [transform] function applied to the encapsulated value
203+
* if this instance represents [success][SealedResult.isSuccess] or the
204+
* original encapsulated [Throwable] exception if it is [failure][SealedResult.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> SealedResult<T>.flatMap(transform: (value: T) -> SealedResult<R>): SealedResult<R> {
211+
contract {
212+
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
213+
}
214+
return when (this) {
215+
is SealedResult.Failure -> SealedResult.failure(exception)
216+
is SealedResult.Success -> transform(value)
217+
}
218+
}
219+
220+
/**
221+
* Returns a [SealedResult] of the given [transform] function applied to the encapsulated value
222+
* if this instance represents [success][SealedResult.isSuccess] or the
223+
* original encapsulated [Throwable] exception if it is [failure][SealedResult.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> SealedResult<T>.flatMapCatching(transform: (value: T) -> SealedResult<R>): SealedResult<R> {
229+
return when (this) {
230+
is SealedResult.Failure -> SealedResult.failure(exception)
231+
is SealedResult.Success -> SealedResult { 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][SealedResult.isFailure] or the
238+
* original encapsulated value if it is [success][SealedResult.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> SealedResult<T>.recover(transform: (exception: Throwable) -> R): SealedResult<R> {
245+
contract {
246+
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
247+
}
248+
return when (this) {
249+
is SealedResult.Failure -> SealedResult.success(transform(exception))
250+
is SealedResult.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][SealedResult.isFailure] or the
257+
* original encapsulated value if it is [success][SealedResult.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> SealedResult<T>.recoverCatching(transform: (exception: Throwable) -> R): SealedResult<R> {
263+
return when (this) {
264+
is SealedResult.Failure -> SealedResult { transform(exception) }
265+
is SealedResult.Success -> this
266+
}
267+
}
268+
269+
/**
270+
* Performs the given [action] on the encapsulated [Throwable] exception if this instance represents [failure][SealedResult.isFailure].
271+
* Returns the original `Result` unchanged.
272+
*/
273+
@OptIn(ExperimentalContracts::class)
274+
public inline fun <T> SealedResult<T>.onFailure(action: (exception: Throwable) -> Unit): SealedResult<T> {
275+
contract {
276+
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
277+
}
278+
if (this is SealedResult.Failure) action(exception)
279+
return this
280+
}
281+
282+
/**
283+
* Performs the given [action] on the encapsulated value if this instance represents [success][SealedResult.isSuccess].
284+
* Returns the original `Result` unchanged.
285+
*/
286+
@OptIn(ExperimentalContracts::class)
287+
public inline fun <T> SealedResult<T>.onSuccess(action: (value: T) -> Unit): SealedResult<T> {
288+
contract {
289+
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
290+
}
291+
if (this is SealedResult.Success) action(value)
292+
return this
293+
}
294+
295+
@OptIn(ExperimentalContracts::class)
296+
fun <T> Flow<SealedResult<T>>.onResult(
297+
onFailure: suspend (exception: Throwable) -> Unit,
298+
onSuccess: suspend (value: T) -> Unit
299+
): Flow<SealedResult<T>> {
300+
return onEach { result ->
301+
when (result) {
302+
is SealedResult.Failure -> onFailure(result.exception)
303+
is SealedResult.Success -> onSuccess(result.value)
304+
}
305+
}
306+
}
307+
308+
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package com.monstarlab.core.usecases.blog
22

3-
import com.monstarlab.arch.extensions.UseCaseResult
4-
import com.monstarlab.arch.extensions.safeFlow
3+
import com.monstarlab.arch.extensions.SealedResult
54
import com.monstarlab.core.data.repositories.PostRepository
65
import com.monstarlab.core.domain.model.Post
76
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.flow
88
import javax.inject.Inject
99

1010
class GetPostsUseCase @Inject constructor(
1111
private val postRepository: PostRepository
12-
){
13-
fun getPosts(): Flow<UseCaseResult<List<Post>>> = safeFlow {
14-
postRepository.getPosts()
12+
) {
13+
fun getPosts(): Flow<SealedResult<List<Post>>> = flow {
14+
emit(SealedResult { postRepository.getPosts() })
1515
}
1616
}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package com.monstarlab.core.usecases.resources
22

3-
import com.monstarlab.arch.extensions.safeFlow
3+
import com.monstarlab.arch.extensions.SealedResult
44
import com.monstarlab.core.data.repositories.ResourceRepository
55
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.flow.flow
67
import javax.inject.Inject
78

89
class GetResourcesUseCase @Inject constructor(
910
private val resourceRepository: ResourceRepository
1011
) {
1112

12-
fun getResources() = safeFlow {
13+
fun getResources() = flow {
1314
delay(2000)
14-
resourceRepository.getResources()
15+
emit(SealedResult { resourceRepository.getResources() })
1516
}
1617

1718
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package com.monstarlab.core.usecases.user
22

3-
import com.monstarlab.arch.extensions.*
3+
import com.monstarlab.arch.extensions.SealedResult
44
import com.monstarlab.core.data.repositories.UserRepository
55
import com.monstarlab.core.domain.model.User
66
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.flow
78
import javax.inject.Inject
89

910
class LoginUseCase @Inject constructor(
10-
private val userRepository: UserRepository
11+
private val userRepository: UserRepository
1112
) {
1213

13-
fun login(email: String, password: String): Flow<UseCaseResult<User>> = safeFlow {
14+
fun login(email: String, password: String): Flow<SealedResult<User>> = flow {
1415
userRepository.login(email, password)
15-
userRepository.getUser()
16+
emit(SealedResult { userRepository.getUser() })
1617
}
17-
1818
}

0 commit comments

Comments
 (0)