Skip to content

Commit

Permalink
1.0.3 (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nek-12 committed Dec 25, 2023
2 parents 9f16fb8 + 2046434 commit ad8b174
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 75 deletions.
7 changes: 7 additions & 0 deletions .idea/detekt.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Config.kt
Expand Up @@ -17,7 +17,7 @@ object Config {

const val majorRelease = 1
const val minorRelease = 0
const val patch = 2
const val patch = 3
const val postfix = ""
const val versionName = "$majorRelease.$minorRelease.$patch$postfix"
const val url = "https://github.com/respawn-app/ApiResult"
Expand Down
122 changes: 75 additions & 47 deletions core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt
Expand Up @@ -106,6 +106,7 @@ public sealed interface ApiResult<out T> {

/**
* Execute [call], catching any exceptions, and wrap it in an [ApiResult].
*
* Caught exceptions are mapped to [ApiResult.Error]s.
* [Throwable]s are not caught on purpose.
* [CancellationException]s are rethrown.
Expand All @@ -119,9 +120,10 @@ public sealed interface ApiResult<out T> {
}

/**
* * If T is an exception, will produce [ApiResult.Error]
* * If T is Loading, will produce [ApiResult.Loading]
* * Otherwise [ApiResult.Success]<T>
* * If [T] is an exception, will produce [ApiResult.Error]
* * If [T] is Loading, will produce [ApiResult.Loading]
* * Otherwise [ApiResult.Success].
* @see asResult
*/
public inline operator fun <T> invoke(value: T): ApiResult<T> = when (value) {
is Loading -> value
Expand All @@ -130,28 +132,28 @@ public sealed interface ApiResult<out T> {
}

/**
* Returns an ApiResult(Unit) value.
* Returns an [Success] (Unit) value.
* Use this for applying operators such as `require` and `mapWrapping` to build chains of operators that should
* start with an empty value.
*/
public inline operator fun invoke(): ApiResult<Unit> = this
public inline operator fun invoke(): ApiResult<Unit> = Success(Unit)
}
}

/**
* [ApiResult.Error.e]'s stack trace as string
*/
public val Error.stackTrace: String get() = e.stackTraceToString()
public inline val Error.stackTrace: String get() = e.stackTraceToString()

/**
* [ApiResult.Error.e]'s cause
*/
public val Error.cause: Throwable? get() = e.cause
public inline val Error.cause: Throwable? get() = e.cause

/**
* [ApiResult.Error.e]'s message.
*/
public val Error.message: String? get() = e.message
public inline val Error.message: String? get() = e.message

/**
* Execute [block] wrapping it in an [ApiResult]
Expand Down Expand Up @@ -242,9 +244,8 @@ public inline infix fun <T> ApiResult<T>.onError(block: (Exception) -> Unit): Ap
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return apply {
if (this is Error) block(e)
}
if (this is Error) block(e)
return this
}

/**
Expand All @@ -255,9 +256,8 @@ public inline infix fun <reified E : Exception, T> ApiResult<T>.onError(block: (
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return apply {
if (this is Error && e is E) block(e)
}
if (this is Error && e is E) block(e)
return this
}

/**
Expand All @@ -269,7 +269,8 @@ public inline infix fun <T> ApiResult<T>.onSuccess(block: (T) -> Unit): ApiResul
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return apply { if (this is Success) block(result) }
if (this is Success) block(result)
return this
}

/**
Expand All @@ -281,7 +282,8 @@ public inline infix fun <T> ApiResult<T>.onLoading(block: () -> Unit): ApiResult
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return apply { if (this is Loading) block() }
if (this is Loading) block()
return this
}

/**
Expand All @@ -293,19 +295,6 @@ public inline fun <T> ApiResult<T>.errorUnless(
predicate: (T) -> Boolean,
): ApiResult<T> = errorIf(exception) { !predicate(it) }

/**
* Makes [this] an [Error] if [predicate] returns false
* @see errorIf
*/
@Deprecated(
"renamed to errorUnless",
ReplaceWith("this.errorUnless(exception, predicate)", "pro.respawn.apiresult.errorUnless")
)
public inline fun <T> ApiResult<T>.errorIfNot(
exception: () -> Exception = { ConditionNotSatisfiedException() },
predicate: (T) -> Boolean,
): ApiResult<T> = errorUnless(exception, predicate)

/**
* Makes [this] an [Error] if [predicate] returns true
* @see errorUnless
Expand All @@ -326,9 +315,16 @@ public inline fun <T> ApiResult<T>.errorIf(
*/
public inline fun <T> ApiResult<T>.errorOnLoading(
exception: () -> Exception = { NotFinishedException() }
): ApiResult<T> = when (this) {
is Loading -> Error(exception())
else -> this
): ApiResult<T> {
contract {
callsInPlace(exception, InvocationKind.AT_MOST_ONCE)
returns() implies (this@errorOnLoading !is Loading)
}

return when (this) {
is Loading -> Error(exception())
else -> this
}
}

/**
Expand Down Expand Up @@ -455,13 +451,23 @@ public inline infix fun <T, R> ApiResult<T>.tryMap(block: (T) -> R): ApiResult<R
*/
public inline fun <T> ApiResult<T?>?.errorOnNull(
exception: () -> Exception = { ConditionNotSatisfiedException("Value was null") },
): ApiResult<T & Any> = this?.errorIf(exception) { it == null }?.map { requireNotNull(it) } ?: Error(exception())
): ApiResult<T & Any> {
contract {
returns() implies (this@errorOnNull != null)
}
return this?.errorIf(exception) { it == null }?.map { it!! } ?: Error(exception())
}

/**
* Maps [Error] values to nulls
* @see orNull
*/
public inline fun <T> ApiResult<T>.nullOnError(): ApiResult<T?> = if (this is Error) Success(null) else this
public inline fun <T> ApiResult<T>.nullOnError(): ApiResult<T?> {
contract {
returns() implies (this@nullOnError !is Error)
}
return if (this is Error) Success(null) else this
}

/**
* Recover from an exception of type [R], else no-op.
Expand All @@ -482,8 +488,12 @@ public inline infix fun <reified T : Exception, R> ApiResult<R>.recover(
* Recover from an exception. Does not affect [Loading]
* See also the typed version of this function to recover from a specific exception type
*/
public inline infix fun <T> ApiResult<T>.recover(another: (e: Exception) -> ApiResult<T>): ApiResult<T> =
recover<Exception, T>(another)
public inline infix fun <T> ApiResult<T>.recover(another: (e: Exception) -> ApiResult<T>): ApiResult<T> {
contract {
returns() implies (this@recover !is Error)
}
return recover<Exception, T>(another)
}

/**
* calls [recover] catching and wrapping any exceptions thrown inside [block].
Expand All @@ -498,23 +508,38 @@ public inline infix fun <reified T : Exception, R> ApiResult<R>.tryRecover(block
*/
public inline infix fun <T> ApiResult<T>.tryRecover(
block: (e: Exception) -> T
): ApiResult<T> = tryRecover<Exception, T>(block)
): ApiResult<T> {
contract {
returns() implies (this@tryRecover !is Error)
}
return tryRecover<Exception, T>(block)
}

/**
* Recover from an [Error] only if the [condition] is true, else no-op.
* Does not affect [Loading]
* @see recover
* @see recoverIf
*/
public inline fun <T> ApiResult<T>.tryRecoverIf(
condition: (Exception) -> Boolean,
block: (Exception) -> T,
): ApiResult<T> = recoverIf(condition) { ApiResult { block(it) } }

/**
* Recover from an [Error] only if the [condition] is true, else no-op.
* Does not affect [Loading]
* @see tryRecoverIf
*/
public inline fun <T> ApiResult<T>.recoverIf(
condition: (Exception) -> Boolean,
block: (Exception) -> T
block: (Exception) -> ApiResult<T>,
): ApiResult<T> {
contract {
callsInPlace(condition, InvocationKind.AT_MOST_ONCE)
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return when {
this is Error && condition(e) -> Success(block(e))
this is Error && condition(e) -> block(e)
else -> this
}
}
Expand All @@ -532,10 +557,7 @@ public inline infix fun <T> ApiResult<T>.chain(another: (T) -> ApiResult<*>): Ap
}
return when (this) {
is Loading, is Error -> this
is Success -> another(result).fold(
onSuccess = { this },
onError = { Error(it) },
)
is Success -> another(result).map { result }
}
}

Expand Down Expand Up @@ -575,16 +597,17 @@ public inline infix fun <T, R> ApiResult<T>.then(another: (T) -> ApiResult<R>):
* @see ApiResult.then
* @see ApiResult.chain
*/
public inline fun <T, R> ApiResult<T>.flatMap(another: (T) -> ApiResult<R>): ApiResult<R> = then(another)
public inline infix fun <T, R> ApiResult<T>.flatMap(another: (T) -> ApiResult<R>): ApiResult<R> = then(another)

/**
* Makes [this] an error with [IllegalArgumentException] using specified [message] if the [predicate] returns false
* Makes [this] an error with [ConditionNotSatisfiedException]
* using specified [message] if the [predicate] returns false.
*/
public inline fun <T> ApiResult<T>.require(
message: () -> String? = { null },
predicate: (T) -> Boolean
): ApiResult<T> = errorUnless(
exception = { IllegalArgumentException(message()) },
exception = { ConditionNotSatisfiedException(message()) },
predicate = predicate
)

Expand All @@ -599,3 +622,8 @@ public inline fun ApiResult<*>.unit(): ApiResult<Unit> = map {}
* @see ApiResult.invoke
*/
public inline val <T> T.asResult: ApiResult<T> get() = ApiResult(this)

/**
* Alias for [map] that takes [this] as a parameter
*/
public inline infix fun <T, R> ApiResult<T>.apply(block: T.() -> R): ApiResult<R> = map(block)
Expand Up @@ -19,7 +19,7 @@ import kotlin.jvm.JvmName
public inline fun <T> ApiResult<Collection<T>>.orEmpty(): Collection<T> = or(emptyList())

/**
* Returns [emptyList] if [this]'s collection is empty
* Returns [emptyList] if [this]'s list is empty
*/
public inline fun <T> ApiResult<List<T>>.orEmpty(): List<T> = or(emptyList())

Expand Down Expand Up @@ -70,15 +70,15 @@ public inline fun <T, R : Sequence<T>> ApiResult<R>.errorIfEmpty(
/**
* Executes [ApiResult.map] on each value of the collection
*/
public inline fun <T, R> ApiResult<Iterable<T>>.mapValues(
public inline infix fun <T, R> ApiResult<Iterable<T>>.mapValues(
transform: (T) -> R
): ApiResult<List<R>> = map { it.map(transform) }

/**
* Executes [ApiResult.map] on each value of the sequence
*/
@JvmName("sequenceMapValues")
public inline fun <T, R> ApiResult<Sequence<T>>.mapValues(
public inline infix fun <T, R> ApiResult<Sequence<T>>.mapValues(
noinline transform: (T) -> R
): ApiResult<Sequence<R>> = map { it.map(transform) }

Expand Down Expand Up @@ -113,8 +113,9 @@ public inline infix fun <T> Sequence<ApiResult<T>>.mapErrors(
/**
* Filter the underlying collection.
*/
public inline infix fun <T : Iterable<R>, R> ApiResult<T>.filter(block: (R) -> Boolean): ApiResult<List<R>> =
map { it.filter(block) }
public inline infix fun <T : Iterable<R>, R> ApiResult<T>.filter(
block: (R) -> Boolean
): ApiResult<List<R>> = map { it.filter(block) }

/**
* Filter the underlying sequence.
Expand Down Expand Up @@ -190,20 +191,18 @@ public inline fun <T> Iterable<ApiResult<T>>.values(): List<T> = asSequence()
* @see firstSuccessOrThrow
*/
public inline fun <T> Iterable<ApiResult<T>>.firstSuccess(): ApiResult<T> =
ApiResult { (first { it is Success } as Success<T>).result }
ApiResult { asSequence().filterIsInstance<Success<T>>().first().result }

/**
* Return the first [Success] value, or throw if no success was found
* @see firstSuccess
* @see firstSuccessOrNull
*/
public inline fun <T> Iterable<ApiResult<T>>.firstSuccessOrThrow(): T =
(first { it is Success } as Success<T>).orThrow()
public inline fun <T> Iterable<ApiResult<T>>.firstSuccessOrThrow(): T = firstSuccess().orThrow()

/**
* Return the first [Success] value, or null if no success was found
* @see firstSuccess
* @see firstSuccessOrThrow
*/
public inline fun <T> Iterable<ApiResult<T>>.firstSuccessOrNull(): T? =
(first { it is Success } as? Success<T>)?.orNull()
public inline fun <T> Iterable<ApiResult<T>>.firstSuccessOrNull(): T? = firstSuccess().orNull()
Expand Up @@ -8,7 +8,7 @@ import pro.respawn.apiresult.ApiResult.Loading
*/
public class NotFinishedException(
message: String? = "ApiResult is still in the Loading state",
) : IllegalArgumentException(message)
) : IllegalStateException(message)

/**
* Exception representing unsatisfied condition when using [errorIf]
Expand Down
17 changes: 12 additions & 5 deletions core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt
Expand Up @@ -79,7 +79,7 @@ public inline fun <T> Loading.flow(
* @see SuspendResult
*/
public inline fun <T> Flow<T>.asApiResult(): Flow<ApiResult<T>> = this
.map { ApiResult(it) }
.map { it.asResult }
.onStart { emit(Loading) }
.catchExceptions { emit(Error(it)) }

Expand All @@ -97,11 +97,18 @@ public inline fun <T, R> Flow<ApiResult<T>>.mapResults(
*
* [ApiResult.Companion.invoke] already throws [CancellationException]s.
*/
public inline fun <T> ApiResult<T>.rethrowCancellation(): ApiResult<T> =
recover<CancellationException, T> { throw it }
public inline fun <T> ApiResult<T>.rethrowCancellation(): ApiResult<T> = rethrow<CancellationException, _>()

/**
* Invokes [block] each time [this] flow emits an [ApiResult.Success] value
*/
public inline fun <T> Flow<ApiResult<T>>.onEachResult(crossinline block: suspend (T) -> Unit): Flow<ApiResult<T>> =
onEach { result -> result.onSuccess { block(it) } }
public inline fun <T> Flow<ApiResult<T>>.onEachResult(
crossinline block: suspend (T) -> Unit
): Flow<ApiResult<T>> = onEach { result -> result.onSuccess { block(it) } }

/**
* Invokes [block] each time [this] flow emits an [ApiResult.Success] value
*/
public inline fun <T> Flow<ApiResult<T>>.onEachSuccess(
crossinline block: suspend (T) -> Unit
): Flow<ApiResult<T>> = onEachResult(block)

0 comments on commit ad8b174

Please sign in to comment.