Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.0.3 #3

Merged
merged 5 commits into from
Dec 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
Original file line number Diff line number Diff line change
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()
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
Loading
Loading