Skip to content

Commit

Permalink
2.0.0-beta02 (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nek-12 authored Jul 5, 2024
2 parents f0f07cf + 73c8585 commit db33ae4
Show file tree
Hide file tree
Showing 12 changed files with 831 additions and 63 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ error handling **on steroids**.

## Features

* ApiResult is **lightweight**. The library tries to inline operators and reduce allocations where possible.
* ApiResult is **lightweight**. The library creates no objects, makes no allocations or virtual function resolutions.
Most of the code is inlined.
* ApiResult offers 90+ operators covering most of possible use cases to turn your
code from imperative and procedural to declarative and functional, which is more readable and extensible.
* ApiResult defines a contract that you can use in your code. No one will be able to obtain the result of a computation
without being forced to handle errors at compilation time.
* The library has 129 tests for 92% operator coverage.

## Preview

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ object Config {
const val majorRelease = 2
const val minorRelease = 0
const val patch = 0
const val postfix = "-alpha01"
const val postfix = "-beta01"
const val versionName = "$majorRelease.$minorRelease.$patch$postfix"
const val url = "https://github.com/respawn-app/ApiResult"
const val licenseName = "The Apache Software License, Version 2.0"
Expand Down
1 change: 1 addition & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ android {

dependencies {
commonMainApi(libs.kotlin.coroutines.core)
jvmTestImplementation(libs.bundles.unittest)
}
103 changes: 52 additions & 51 deletions core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import kotlin.jvm.JvmName
* the operators that are invoked are called **immediately** and in-place.
*/
@JvmInline
public value class ApiResult<out T> @PublishedApi internal constructor(@PublishedApi internal val value: Any?) {
public value class ApiResult<out T> private constructor(@PublishedApi internal val value: Any?) {

/**
* Get the [Success] component of this result or null
Expand All @@ -45,7 +45,7 @@ public value class ApiResult<out T> @PublishedApi internal constructor(@Publishe
* ```
* @see orNull
*/
public operator fun component1(): T? = orNull()
public inline operator fun component1(): T? = orNull()

/**
* Get the [Error] component of this result or null
Expand All @@ -56,27 +56,35 @@ public value class ApiResult<out T> @PublishedApi internal constructor(@Publishe
* ```
* @see exceptionOrNull
*/
public operator fun component2(): Exception? = exceptionOrNull()
public inline operator fun component2(): Exception? = exceptionOrNull()

/**
* Bang operator returns the result or throws if it is an [Error] or [Loading]
* This is equivalent to calling [orThrow]
*/
public operator fun not(): T = orThrow()
public inline operator fun not(): T = orThrow()

/**
* The state of [ApiResult] that represents an error.
* @param e wrapped [Exception]
*/
@JvmInline
@PublishedApi
internal value class Error(@JvmField val e: Exception) {
internal value class Error private constructor(@JvmField val e: Exception) {

override fun toString(): String = "ApiResult.Error: message=${e.message} and cause: $e"

companion object {

fun create(e: Exception) = Error(e)
}
}

@PublishedApi
internal data object Loading
internal data object Loading {

override fun toString(): String = "ApiResult.Loading"
}

/**
* Whether this is [Success]
Expand All @@ -91,10 +99,10 @@ public value class ApiResult<out T> @PublishedApi internal constructor(@Publishe
/**
* Whether this is [Loading]
*/
public inline val isLoading: Boolean get() = value is Loading
public inline val isLoading: Boolean get() = value === Loading

override fun toString(): String = when {
value is Error || value is Loading -> value.toString()
value is Error || value === Loading -> value.toString()
else -> "ApiResult.Success: $value"
}

Expand All @@ -103,17 +111,17 @@ public value class ApiResult<out T> @PublishedApi internal constructor(@Publishe
/**
* Create a successful [ApiResult] value
*/
public inline fun <T> Success(value: T): ApiResult<T> = ApiResult(value)
public fun <T> Success(value: T): ApiResult<T> = ApiResult(value = value)

/**
* Create an error [ApiResult] value
*/
public inline fun <T> Error(value: Exception): ApiResult<T> = ApiResult(Error(e = value))
public fun <T> Error(e: Exception): ApiResult<T> = ApiResult(value = Error.create(e = e))

/**
* Create a loading [ApiResult] value
*/
public inline fun <T> Loading(): ApiResult<T> = ApiResult(value = Loading)
public fun <T> Loading(): ApiResult<T> = ApiResult(value = Loading)

/**
* Create an [ApiResult] instance using the given value.
Expand All @@ -123,7 +131,7 @@ public value class ApiResult<out T> @PublishedApi internal constructor(@Publishe
* If you want to directly create a success value of an [Exception], use [Success]
*/
public inline operator fun <T> invoke(value: T): ApiResult<T> = when (value) {
is Exception -> Error(value = value)
is Exception -> Error(e = value)
else -> Success(value)
}

Expand All @@ -135,11 +143,11 @@ public value class ApiResult<out T> @PublishedApi internal constructor(@Publishe
* [CancellationException]s are rethrown.
*/
public inline operator fun <T> invoke(call: () -> T): ApiResult<T> = try {
Success(call())
Success(value = call())
} catch (e: CancellationException) {
throw e
} catch (expected: Exception) {
Error(expected)
Error(e = expected)
}

/**
Expand Down Expand Up @@ -195,10 +203,13 @@ public inline infix fun <T, R : T> ApiResult<T>.orElse(block: (e: Exception) ->
}

/**
* If [this] is [Error], returns [defaultValue].
* If [this] is [Error] or [Loading], returns [defaultValue].
* @see orElse
*/
public inline infix fun <T, R : T> ApiResult<T>.or(defaultValue: R): T = orElse { defaultValue }
public inline infix fun <T, R : T> ApiResult<T>.or(defaultValue: R): T = when (value) {
is Error, is Loading -> defaultValue
else -> value as T
}

/**
* @return null if [this] is an [ApiResult.Error] or [ApiResult.Loading], otherwise return self.
Expand Down Expand Up @@ -312,7 +323,9 @@ public inline fun <T> ApiResult<T>.errorIf(
callsInPlace(predicate, InvocationKind.AT_MOST_ONCE)
callsInPlace(exception, InvocationKind.AT_MOST_ONCE)
}
return if (isSuccess && predicate(value as T)) Error(value = exception()) else this
if (!isSuccess) return this
if (!predicate(value as T)) return this
return Error(e = exception())
}

/**
Expand All @@ -326,7 +339,7 @@ public inline fun <T> ApiResult<T>.errorOnLoading(
}

return when (value) {
is Loading -> Error(value = exception())
is Loading -> Error(e = exception())
else -> this
}
}
Expand All @@ -337,10 +350,10 @@ public inline fun <T> ApiResult<T>.errorOnLoading(
public inline fun <T> ApiResult<T?>?.requireNotNull(): ApiResult<T & Any> = errorOnNull()

/**
* Throws if [this] is not [Success] and returns [Success] otherwise.
* Alias for [orThrow]
* @see orThrow
*/
public inline fun <T> ApiResult<T>.require(): ApiResult<T> = Success(!this)
public inline fun <T> ApiResult<T>.require(): T = orThrow()

/**
* Change the type of the [Success] to [R] without affecting [Error]/[Loading] results
Expand All @@ -352,25 +365,19 @@ public inline infix fun <T, R> ApiResult<T>.map(block: (T) -> R): ApiResult<R> {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return fold(
onSuccess = { Success(value = block(it)) },
onError = { Error(value = it) },
onLoading = { ApiResult.Loading() }
)
if (isSuccess) return Success(value = block(value as T))
return this as ApiResult<R>
}

/**
* Map the [Success] result using [transform], and if the result is not a success, return [default]
*/
public inline fun <T, R> ApiResult<T>.mapOrDefault(default: () -> R, transform: (T) -> R): R {
public inline fun <T, R> ApiResult<T>.mapOrDefault(default: (e: Exception) -> R, transform: (T) -> R): R {
contract {
callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
callsInPlace(default, InvocationKind.AT_MOST_ONCE)
}
return fold(
onSuccess = { transform(it) },
onError = { default() }
)
return map(transform).orElse(default)
}

/**
Expand All @@ -379,10 +386,7 @@ public inline fun <T, R> ApiResult<T>.mapOrDefault(default: () -> R, transform:
public inline fun <T, R> ApiResult<T>.mapEither(
success: (T) -> R,
error: (Exception) -> Exception,
): ApiResult<R> = fold(
onSuccess = { Success(success(it)) },
onError = { Error(value = error(it)) }
)
): ApiResult<R> = map(success).mapError(error)

/**
* Maps [Loading] to a [Success], not affecting other states.
Expand Down Expand Up @@ -414,7 +418,7 @@ public inline infix fun <reified R : Exception, T> ApiResult<T>.mapError(block:
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return when {
value is Error && value.e is R -> Error(value = block(value.e))
value is Error && value.e is R -> Error(e = block(value.e))
else -> this
}
}
Expand All @@ -427,11 +431,10 @@ public inline fun <T> ApiResult<T>.mapErrorToCause(): ApiResult<T> = mapError {
/**
* Unwrap an ApiResult<ApiResult<T>> to be ApiResult<T>
*/
public inline fun <T> ApiResult<ApiResult<T>>.unwrap(): ApiResult<T> = fold(
onSuccess = { it },
onError = { Error(value = it) },
onLoading = { ApiResult.Loading() },
)
public inline fun <T> ApiResult<ApiResult<T>>.unwrap(): ApiResult<T> = when (value) {
is Error, is Loading -> this
else -> value
} as ApiResult<T>

/**
* Change the type of successful result to [R], also wrapping [block]
Expand All @@ -450,17 +453,17 @@ public inline infix fun <T, R> ApiResult<T>.tryMap(
* @see errorIf
* @see errorIfEmpty
*/
public inline fun <T> ApiResult<T?>?.errorOnNull(
public inline fun <T : Any> ApiResult<T?>?.errorOnNull(
exception: () -> Exception = { ConditionNotSatisfiedException("Value was null") },
): ApiResult<T & Any> {
): ApiResult<T> {
contract {
returns() implies (this@errorOnNull != null)
returnsNotNull()
}
return when (this?.value) {
is Error -> Error(value = value.e)
return when (val r = this?.value) {
is Error -> Error(e = r.e)
is Loading -> ApiResult.Loading()
null -> Error(value = exception())
else -> ApiResult(value = value)
null -> Error(e = exception())
else -> Success(value = r as T)
}
}

Expand Down Expand Up @@ -531,10 +534,8 @@ public inline fun <T> ApiResult<T>.recoverIf(
callsInPlace(condition, InvocationKind.AT_MOST_ONCE)
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
return when {
value is Error && condition(value.e) -> block(value.e)
else -> ApiResult(value = value)
}
if (value !is Error || !condition(value.e)) return this
return block(value.e)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public inline fun <T> ApiResult.Companion.flow(
public inline fun <T> Flow<T>.asApiResult(): Flow<ApiResult<T>> = this
.map { it.asResult }
.onStart { emit(ApiResult.Loading()) }
.catchExceptions { emit(ApiResult.Error(value = it)) }
.catchExceptions { emit(ApiResult.Error(e = it)) }

/**
* Maps each success value of [this] flow using [transform]
Expand Down
Loading

0 comments on commit db33ae4

Please sign in to comment.