From da821429411e1dc0ed9b4295a42b34797a281817 Mon Sep 17 00:00:00 2001
From: Nek-12 <vaizin.nikita@gmail.com>
Date: Thu, 30 May 2024 17:34:38 +0300
Subject: [PATCH] feat!: Refactor ApiResult to be an inline class

---
 buildSrc/src/main/kotlin/Config.kt            |   6 +-
 .../kotlin/pro/respawn/apiresult/ApiResult.kt | 242 +++++++++---------
 .../pro/respawn/apiresult/CollectionResult.kt |  59 +++--
 .../pro/respawn/apiresult/SuspendResult.kt    |  17 +-
 4 files changed, 159 insertions(+), 165 deletions(-)

diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt
index 56c64bd..f9d8095 100644
--- a/buildSrc/src/main/kotlin/Config.kt
+++ b/buildSrc/src/main/kotlin/Config.kt
@@ -15,10 +15,10 @@ object Config {
 
     const val artifactId = "$group.$artifact"
 
-    const val majorRelease = 1
-    const val minorRelease = 1
+    const val majorRelease = 2
+    const val minorRelease = 0
     const val patch = 0
-    const val postfix = ""
+    const val postfix = "-alpha01"
     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"
diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt
index d730160..bee2a40 100644
--- a/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt
+++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt
@@ -5,30 +5,36 @@
     "unused",
     "NOTHING_TO_INLINE",
     "TooManyFunctions",
-    "ThrowingExceptionsWithoutMessageOrCause",
+    "ThrowingExceptionsWithoutMessageOrCause", "UNCHECKED_CAST",
 )
 
 package pro.respawn.apiresult
 
+import pro.respawn.apiresult.ApiResult.Companion.Error
+import pro.respawn.apiresult.ApiResult.Companion.Success
 import pro.respawn.apiresult.ApiResult.Error
 import pro.respawn.apiresult.ApiResult.Loading
-import pro.respawn.apiresult.ApiResult.Success
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
 import kotlin.coroutines.cancellation.CancellationException
+import kotlin.jvm.JvmField
 import kotlin.jvm.JvmInline
 import kotlin.jvm.JvmName
 
 /**
  * A class that represents a result of an operation.
  *
- * This class is **efficient**: no actual objects are created unless dynamic type resolution is required,
- * all operations are inlined and no function resolution is performed.
+ * This class is **efficient**:
+ * * no actual objects are created,
+ * * all operations are inlined
+ * * no function resolution is performed.
+ *
  * ApiResult is **not** an Rx-style callback chain -
  * the operators that are invoked are called **immediately** and in-place.
  */
-public sealed interface ApiResult<out T> {
+@JvmInline
+public value class ApiResult<out T> @PublishedApi internal constructor(@PublishedApi internal val value: Any?) {
 
     /**
      * Get the [Success] component of this result or null
@@ -58,47 +64,69 @@ public sealed interface ApiResult<out T> {
      */
     public operator fun not(): T = orThrow()
 
-    /**
-     * A value of [ApiResult] for its successful state.
-     * @param result a successful result value
-     */
-    @JvmInline
-    public value class Success<out T>(public val result: T) : ApiResult<T> {
-
-        override fun toString(): String = "ApiResult.Success: $result"
-    }
-
     /**
      * The state of [ApiResult] that represents an error.
      * @param e wrapped [Exception]
      */
     @JvmInline
-    public value class Error(public val e: Exception) : ApiResult<Nothing> {
+    @PublishedApi
+    internal value class Error(@JvmField val e: Exception) {
 
-        override fun toString(): String = "ApiResult.Error: message=$message and cause: $e"
+        override fun toString(): String = "ApiResult.Error: message=${e.message} and cause: $e"
     }
 
+    @PublishedApi
+    internal data object Loading
+
     /**
      * Whether this is [Success]
      */
-    public val isSuccess: Boolean get() = this is Success
+    public inline val isSuccess: Boolean get() = !isError && !isLoading
 
     /**
      *  Whether this is [Error]
      */
-    public val isError: Boolean get() = this is Error
+    public inline val isError: Boolean get() = value is Error
 
     /**
      * Whether this is [Loading]
      */
-    public val isLoading: Boolean get() = this is Loading
+    public inline val isLoading: Boolean get() = value is Loading
 
-    /**
-     * A loading state of an [ApiResult]
-     */
-    public data object Loading : ApiResult<Nothing>
+    override fun toString(): String = when {
+        value is Error || value is Loading -> value.toString()
+        else -> "ApiResult.Success: $value"
+    }
 
     public companion object {
+
+        /**
+         * Create a successful [ApiResult] value
+         */
+        public inline fun <T> Success(value: T): ApiResult<T> = ApiResult(value)
+
+        /**
+         * Create an error [ApiResult] value
+         */
+        public inline fun <T> Error(value: Exception): ApiResult<T> = ApiResult(Error(e = value))
+
+        /**
+         * Create a loading [ApiResult] value
+         */
+        public inline fun <T> Loading(): ApiResult<T> = ApiResult(value = Loading)
+
+        /**
+         * Create an [ApiResult] instance using the given value.
+         * When [value] is an Exception, an error result will be created.
+         * Otherwise, a success result will be created.
+         *
+         * 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)
+            else -> Success(value)
+        }
+
         /**
          * Execute [call], catching any exceptions, and wrap it in an [ApiResult].
          *
@@ -115,19 +143,7 @@ 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].
-         *  @see asResult
-         */
-        public inline operator fun <T> invoke(value: T): ApiResult<T> = when (value) {
-            is Loading -> value
-            is Exception -> Error(value)
-            else -> Success(value)
-        }
-
-        /**
-         * Returns an [Success] (Unit) value.
+         * Returns an [ApiResult](Unit) value.
          * Use this for applying operators such as `require` and `mapWrapping` to build chains of operators that should
          * start with an empty value.
          */
@@ -138,42 +154,43 @@ public sealed interface ApiResult<out T> {
 /**
  * [ApiResult.Error.e]'s stack trace as string
  */
-public inline val Error.stackTrace: String get() = e.stackTraceToString()
+public inline val ApiResult<*>.stackTrace: String? get() = exceptionOrNull()?.stackTraceToString()
 
 /**
  * [ApiResult.Error.e]'s cause
  */
-public inline val Error.cause: Throwable? get() = e.cause
+public inline val ApiResult<*>.cause: Throwable? get() = exceptionOrNull()?.cause
 
 /**
  * [ApiResult.Error.e]'s message.
  */
-public inline val Error.message: String? get() = e.message
+public inline val ApiResult<*>.message: String? get() = exceptionOrNull()?.message
 
 /**
  * Execute [block] wrapping it in an [ApiResult]
  * @see ApiResult.invoke
  */
-public inline fun <T, R> T.runResulting(block: T.() -> R): ApiResult<R> = ApiResult { block() }
+public inline fun <T, R> T.runResulting(block: T.() -> R): ApiResult<R> = ApiResult(call = { block() })
 
 /**
  * Executes [block], wrapping it in an [ApiResult]
  * @see ApiResult.invoke
  */
-public inline fun <T> runResulting(block: () -> T): ApiResult<T> = ApiResult { block() }
+public inline fun <T> runResulting(block: () -> T): ApiResult<T> = ApiResult(call = { block() })
 
 /**
- * Executes [block] if [this] is an [ApiResult.Error], otherwise returns [ApiResult.Success.result]
+ * Executes [block] if [this] is an [ApiResult.Error], otherwise returns [ApiResult.value]
  * [Loading] will result in [NotFinishedException]
  */
+@Suppress("UNCHECKED_CAST")
 public inline infix fun <T, R : T> ApiResult<T>.orElse(block: (e: Exception) -> R): T {
     contract {
         callsInPlace(block, InvocationKind.AT_MOST_ONCE)
     }
-    return when (this) {
-        is Success -> result
-        is Error -> block(e)
+    return when (value) {
+        is Error -> block(value.e)
         is Loading -> block(NotFinishedException())
+        else -> value as T
     }
 }
 
@@ -191,17 +208,13 @@ public inline fun <T> ApiResult<T>?.orNull(): T? = this?.or(null)
 /**
  * @return exception if [this] is [Error] or null
  */
-public inline fun <T> ApiResult<T>?.exceptionOrNull(): Exception? = (this as? Error)?.e
+public inline fun <T> ApiResult<T>?.exceptionOrNull(): Exception? = (this?.value as? Error)?.e
 
 /**
  * Throws [ApiResult.Error.e], or [NotFinishedException] if the request has not been completed yet.
  * @see ApiResult.not
  */
-public inline fun <T> ApiResult<T>.orThrow(): T = when (this) {
-    is Loading -> throw NotFinishedException()
-    is Error -> throw e
-    is Success -> result
-}
+public inline fun <T> ApiResult<T>.orThrow(): T = orElse { throw it }
 
 /**
  * Throws if [this] result is an [Error] and [Error.e] is of type [T]. Ignores all other exceptions.
@@ -214,6 +227,7 @@ public inline fun <reified T : Exception, R> ApiResult<R>.rethrow(): ApiResult<R
  * Fold [this] returning the result of [onSuccess] or [onError]
  * By default, maps [Loading] to [Error] with [NotFinishedException]
  */
+@Suppress("UNCHECKED_CAST")
 public inline fun <T, R> ApiResult<T>.fold(
     onSuccess: (result: T) -> R,
     onError: (e: Exception) -> R,
@@ -223,10 +237,10 @@ public inline fun <T, R> ApiResult<T>.fold(
         callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE)
         callsInPlace(onError, InvocationKind.AT_MOST_ONCE)
     }
-    return when (this) {
-        is Success -> onSuccess(result)
-        is Error -> onError(e)
+    return when (value) {
+        is Error -> onError(value.e)
         is Loading -> onLoading?.invoke() ?: onError(NotFinishedException())
+        else -> onSuccess(value as T)
     }
 }
 
@@ -239,7 +253,7 @@ public inline infix fun <T> ApiResult<T>.onError(block: (Exception) -> Unit): Ap
     contract {
         callsInPlace(block, InvocationKind.AT_MOST_ONCE)
     }
-    if (this is Error) block(e)
+    if (value is Error) block(value.e)
     return this
 }
 
@@ -247,12 +261,8 @@ public inline infix fun <T> ApiResult<T>.onError(block: (Exception) -> Unit): Ap
  * Invoke a given block if [this] is [Error] and it's [Error.e] is of type [E].
  */
 @JvmName("onErrorTyped")
-public inline infix fun <reified E : Exception, T> ApiResult<T>.onError(block: (E) -> Unit): ApiResult<T> {
-    contract {
-        callsInPlace(block, InvocationKind.AT_MOST_ONCE)
-    }
-    if (this is Error && e is E) block(e)
-    return this
+public inline infix fun <reified E : Exception, T> ApiResult<T>.onError(block: (E) -> Unit): ApiResult<T> = onError {
+    if (it is E) block(it)
 }
 
 /**
@@ -264,7 +274,7 @@ public inline infix fun <T> ApiResult<T>.onSuccess(block: (T) -> Unit): ApiResul
     contract {
         callsInPlace(block, InvocationKind.AT_MOST_ONCE)
     }
-    if (this is Success) block(result)
+    if (isSuccess) block(value as T)
     return this
 }
 
@@ -277,7 +287,7 @@ public inline infix fun <T> ApiResult<T>.onLoading(block: () -> Unit): ApiResult
     contract {
         callsInPlace(block, InvocationKind.AT_MOST_ONCE)
     }
-    if (this is Loading) block()
+    if (isLoading) block()
     return this
 }
 
@@ -302,7 +312,7 @@ public inline fun <T> ApiResult<T>.errorIf(
         callsInPlace(predicate, InvocationKind.AT_MOST_ONCE)
         callsInPlace(exception, InvocationKind.AT_MOST_ONCE)
     }
-    return if (this is Success && predicate(result)) Error(exception()) else this
+    return if (isSuccess && predicate(value as T)) Error(value = exception()) else this
 }
 
 /**
@@ -313,11 +323,10 @@ public inline fun <T> ApiResult<T>.errorOnLoading(
 ): ApiResult<T> {
     contract {
         callsInPlace(exception, InvocationKind.AT_MOST_ONCE)
-        returns() implies (this@errorOnLoading !is Loading)
     }
 
-    return when (this) {
-        is Loading -> Error(exception())
+    return when (value) {
+        is Loading -> Error(value = exception())
         else -> this
     }
 }
@@ -331,7 +340,7 @@ public inline fun <T> ApiResult<T?>?.requireNotNull(): ApiResult<T & Any> = erro
  * Throws if [this] is not [Success] and returns [Success] otherwise.
  * @see orThrow
  */
-public inline fun <T> ApiResult<T>.require(): Success<T> = Success(!this)
+public inline fun <T> ApiResult<T>.require(): ApiResult<T> = Success(!this)
 
 /**
  * Change the type of the [Success] to [R] without affecting [Error]/[Loading] results
@@ -343,11 +352,11 @@ public inline infix fun <T, R> ApiResult<T>.map(block: (T) -> R): ApiResult<R> {
     contract {
         callsInPlace(block, InvocationKind.AT_MOST_ONCE)
     }
-    return when (this) {
-        is Success -> Success(block(result))
-        is Error -> Error(e)
-        is Loading -> this
-    }
+    return fold(
+        onSuccess = { Success(value = block(it)) },
+        onError = { Error(value = it) },
+        onLoading = { ApiResult.Loading() }
+    )
 }
 
 /**
@@ -358,10 +367,10 @@ public inline fun <T, R> ApiResult<T>.mapOrDefault(default: () -> R, transform:
         callsInPlace(transform, InvocationKind.AT_MOST_ONCE)
         callsInPlace(default, InvocationKind.AT_MOST_ONCE)
     }
-    return when (this) {
-        is Success -> transform(result)
-        else -> default()
-    }
+    return fold(
+        onSuccess = { transform(it) },
+        onError = { default() }
+    )
 }
 
 /**
@@ -370,11 +379,10 @@ 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> = when (this) {
-    is Error -> Error(error(e))
-    is Loading -> this
-    is Success -> Success(success(result))
-}
+): ApiResult<R> = fold(
+    onSuccess = { Success(success(it)) },
+    onError = { Error(value = error(it)) }
+)
 
 /**
  * Maps [Loading] to a [Success], not affecting other states.
@@ -386,10 +394,7 @@ public inline infix fun <T, R : T> ApiResult<T>.mapLoading(block: () -> R): ApiR
     contract {
         callsInPlace(block, InvocationKind.AT_MOST_ONCE)
     }
-    return when (this) {
-        is Success, is Error -> this
-        is Loading -> Success(block())
-    }
+    return if (isLoading) Success(block()) else this
 }
 
 /**
@@ -409,7 +414,7 @@ public inline infix fun <reified R : Exception, T> ApiResult<T>.mapError(block:
         callsInPlace(block, InvocationKind.AT_MOST_ONCE)
     }
     return when {
-        this is Error && e is R -> Error(block(e))
+        value is Error && value.e is R -> Error(value = block(value.e))
         else -> this
     }
 }
@@ -424,8 +429,8 @@ public inline fun <T> ApiResult<T>.mapErrorToCause(): ApiResult<T> = mapError {
  */
 public inline fun <T> ApiResult<ApiResult<T>>.unwrap(): ApiResult<T> = fold(
     onSuccess = { it },
-    onError = { Error(it) },
-    onLoading = { Loading }
+    onError = { Error(value = it) },
+    onLoading = { ApiResult.Loading() },
 )
 
 /**
@@ -435,8 +440,9 @@ public inline fun <T> ApiResult<ApiResult<T>>.unwrap(): ApiResult<T> = fold(
  * @see mapError
  * @see mapLoading
  */
-public inline infix fun <T, R> ApiResult<T>.tryMap(block: (T) -> R): ApiResult<R> =
-    map { ApiResult { block(it) } }.unwrap()
+public inline infix fun <T, R> ApiResult<T>.tryMap(
+    block: (T) -> R
+): ApiResult<R> = map { ApiResult(call = { block(it) }) }.unwrap()
 
 /**
  * Make this result an [Error] if [Success] value was null.
@@ -450,19 +456,19 @@ public inline fun <T> ApiResult<T?>?.errorOnNull(
     contract {
         returns() implies (this@errorOnNull != null)
     }
-    return this?.errorIf(exception) { it == null }?.map { it!! } ?: Error(exception())
+    return when (this?.value) {
+        is Error -> Error(value = value.e)
+        is Loading -> ApiResult.Loading()
+        null -> Error(value = exception())
+        else -> ApiResult(value = value)
+    }
 }
 
 /**
  * Maps [Error] values to nulls
  * @see orNull
  */
-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
-}
+public inline fun <T> ApiResult<T>.nullOnError(): ApiResult<T?> = if (isError) Success(null) else this
 
 /**
  * Recover from an exception of type [R], else no-op.
@@ -475,7 +481,7 @@ public inline fun <T> ApiResult<T>.nullOnError(): ApiResult<T?> {
 public inline infix fun <reified T : Exception, R> ApiResult<R>.recover(
     another: (e: T) -> ApiResult<R>
 ): ApiResult<R> = when {
-    this is Error && e is T -> another(e)
+    value is Error && value.e is T -> another(value.e)
     else -> this
 }
 
@@ -483,19 +489,16 @@ 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> {
-    contract {
-        returns() implies (this@recover !is Error)
-    }
-    return recover<Exception, T>(another)
-}
+public inline infix fun <T> ApiResult<T>.recover(
+    another: (e: Exception) -> ApiResult<T>
+): ApiResult<T> = recover<Exception, T>(another)
 
 /**
  * calls [recover] catching and wrapping any exceptions thrown inside [block].
  */
 @JvmName("tryRecoverTyped")
 public inline infix fun <reified T : Exception, R> ApiResult<R>.tryRecover(block: (T) -> R): ApiResult<R> =
-    recover<T, R>(another = { ApiResult { block(it) } })
+    recover<T, R>(another = { ApiResult(call = { block(it) }) })
 
 /**
  * Calls [recover] catching and wrapping any exceptions thrown inside [block].
@@ -503,12 +506,7 @@ 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> {
-    contract {
-        returns() implies (this@tryRecover !is Error)
-    }
-    return tryRecover<Exception, T>(block)
-}
+): ApiResult<T> = tryRecover<Exception, T>(block)
 
 /**
  * Recover from an [Error] only if the [condition] is true, else no-op.
@@ -518,7 +516,7 @@ public inline infix fun <T> ApiResult<T>.tryRecover(
 public inline fun <T> ApiResult<T>.tryRecoverIf(
     condition: (Exception) -> Boolean,
     block: (Exception) -> T,
-): ApiResult<T> = recoverIf(condition) { ApiResult { block(it) } }
+): ApiResult<T> = recoverIf(condition) { ApiResult(call = { block(it) }) }
 
 /**
  * Recover from an [Error] only if the [condition] is true, else no-op.
@@ -534,8 +532,8 @@ public inline fun <T> ApiResult<T>.recoverIf(
         callsInPlace(block, InvocationKind.AT_MOST_ONCE)
     }
     return when {
-        this is Error && condition(e) -> block(e)
-        else -> this
+        value is Error && condition(value.e) -> block(value.e)
+        else -> ApiResult(value = value)
     }
 }
 
@@ -550,10 +548,7 @@ public inline infix fun <T> ApiResult<T>.chain(another: (T) -> ApiResult<*>): Ap
     contract {
         callsInPlace(another, InvocationKind.AT_MOST_ONCE)
     }
-    return when (this) {
-        is Loading, is Error -> this
-        is Success -> another(result).map { result }
-    }
+    return map { outer -> another(outer).map { outer } }.unwrap()
 }
 
 /**
@@ -566,8 +561,9 @@ public inline infix fun <T> ApiResult<T>.chain(another: (T) -> ApiResult<*>): Ap
  * @see [ApiResult.chain]
  * @see [ApiResult.then]
  */
-public inline infix fun <T> ApiResult<T>.tryChain(block: (T) -> Unit): ApiResult<T> =
-    chain(another = { ApiResult { block(it) } })
+public inline infix fun <T> ApiResult<T>.tryChain(
+    block: (T) -> Unit
+): ApiResult<T> = chain(another = { ApiResult(call = { block(it) }) })
 
 /**
  * Call [another] and if it succeeds, continue with [another]'s result.
@@ -628,7 +624,7 @@ public inline infix fun <T, R> ApiResult<T>.apply(block: T.() -> R): ApiResult<R
  */
 public inline fun <reified R, T> ApiResult<T>.requireIs(
     exception: (T) -> Exception = { value ->
-        "Result value is of type ${value?.let { it::class.simpleName }} but expected ${R::class}"
+        "Result value is of type ${value?.let { it::class.simpleName }} but expected ${R::class.simpleName}"
             .let(::ConditionNotSatisfiedException)
     },
 ): ApiResult<R> = tryMap { value ->
diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt
index 3e15ef2..d19df1b 100644
--- a/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt
+++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/CollectionResult.kt
@@ -9,8 +9,6 @@
 
 package pro.respawn.apiresult
 
-import pro.respawn.apiresult.ApiResult.Error
-import pro.respawn.apiresult.ApiResult.Success
 import kotlin.jvm.JvmName
 
 /**
@@ -126,84 +124,84 @@ public inline infix fun <T : Sequence<R>, R> ApiResult<T>.filter(
 ): ApiResult<Sequence<R>> = map { it.filter(block) }
 
 /**
- * Filters only [Error] values
+ * Returns only error values
  */
-public inline fun <T> Iterable<ApiResult<T>>.filterErrors(): List<Error> = filterIsInstance<Error>()
+public inline fun <T> Iterable<ApiResult<T>>.filterErrors(): List<Exception> = mapNotNull { it.exceptionOrNull() }
 
 /**
- * Filters only [Error] values
+ * Returns only error values
  */
-public inline fun <T> Sequence<ApiResult<T>>.filterErrors(): Sequence<Error> = filterIsInstance<Error>()
+public inline fun <T> Sequence<ApiResult<T>>.filterErrors(): Sequence<Exception> = mapNotNull { it.exceptionOrNull() }
 
 /**
- * Filters only [Success] values
+ * Returns only successful values
  */
-public inline fun <T> Iterable<ApiResult<T>>.filterSuccesses(): List<Success<T>> = filterIsInstance<Success<T>>()
+public inline fun <T> Iterable<ApiResult<T>>.filterSuccesses(): List<T> = mapNotNull { it.orNull() }
 
 /**
- * Filters only [Success] values
+ * Returns only successful values
  */
-public inline fun <T> Sequence<ApiResult<T>>.filterSuccesses(): Sequence<Success<T>> = filterIsInstance<Success<T>>()
+public inline fun <T> Sequence<ApiResult<T>>.filterSuccesses(): Sequence<T> = mapNotNull { it.orNull() }
 
 /**
- * Filters all null values of [Success]es
+ * Filters all null values of results
  */
 public inline fun <T> Iterable<ApiResult<T?>>.filterNulls(): List<ApiResult<T & Any>> =
-    filter { it !is Success || it.result != null }.mapResults { it!! }
+    filter { !it.isSuccess || it.value != null }.mapResults { it!! }
 
 /**
- * Filters all null values of [Success]es
+ * Filters all null values of results
  */
 public inline fun <T> Sequence<ApiResult<T?>>.filterNulls(): Sequence<ApiResult<T & Any>> =
-    filter { it !is Success || it.result != null }.mapResults { it!! }
+    filter { !it.isSuccess || it.value != null }.mapResults { it!! }
 
 /**
- * Merges all [Success] results into a single [List], or if any has failed, returns [Error].
+ * Merges all results into a single [List], or if any has failed, returns [Error].
  */
+@JvmName("merge2")
 public inline fun <T> Iterable<ApiResult<T>>.merge(): ApiResult<List<T>> = ApiResult { map { !it } }
 
 /**
  * Merges all [results] into a single [List], or if any has failed, returns [Error].
  */
-public inline fun <T> merge(vararg results: ApiResult<T>): ApiResult<List<T>> = results.asIterable().merge()
+public inline fun <T> merge(results: Iterable<ApiResult<T>>): ApiResult<List<T>> = results.merge()
 
 /**
  * Merges [this] results and all other [results] into a single result of type [T].
  */
 public inline fun <T> ApiResult<T>.merge(
-    vararg results: ApiResult<T>
+    results: Iterable<ApiResult<T>>
 ): ApiResult<List<T>> = sequence {
     yield(this@merge)
-    yieldAll(results.iterator())
+    yieldAll(results)
 }.asIterable().merge()
 
 /**
- * Returns a list of only [Success] values, discarding any errors
+ * Returns a list of only successful values, discarding any errors
  */
-public inline fun <T> Iterable<ApiResult<T>>.values(): List<T> = asSequence()
-    .filterSuccesses()
-    .map { it.result }
-    .toList()
+public inline fun <T> Iterable<ApiResult<T>>.values(): List<T> = filterSuccesses()
 
 /**
- * Return the first [Success] value, or an [Error] if no success was found
+ * Return the first successful value, or an [Error] if no success was found
+ *
+ * Exception will always be [NoSuchElementException]
  *
- * [Error.e] will always be [NoSuchElementException]
  * @see firstSuccessOrNull
  * @see firstSuccessOrThrow
  */
-public inline fun <T> Iterable<ApiResult<T>>.firstSuccess(): ApiResult<T> =
-    ApiResult { asSequence().filterIsInstance<Success<T>>().first().result }
+public inline fun <T> Iterable<ApiResult<T>>.firstSuccess(): ApiResult<T> = ApiResult {
+    asSequence().filterSuccesses().first()
+}
 
 /**
- * Return the first [Success] value, or throw if no success was found
+ * Return the first successful value, or throw if no success was found
  * @see firstSuccess
  * @see firstSuccessOrNull
  */
 public inline fun <T> Iterable<ApiResult<T>>.firstSuccessOrThrow(): T = firstSuccess().orThrow()
 
 /**
- * Return the first [Success] value, or null if no success was found
+ * Return the first successful value, or null if no success was found
  * @see firstSuccess
  * @see firstSuccessOrThrow
  */
@@ -228,10 +226,11 @@ public inline fun <T, R> Iterable<T>.mapResulting(
  * - First is the [ApiResult.Success] results
  * - Seconds is [ApiResult.Error] or errors produced by [ApiResult.Loading] (see [ApiResult.errorOnLoading]
  */
+@Suppress("UNCHECKED_CAST")
 public fun <T> Sequence<ApiResult<T>>.accumulate(): Pair<List<T>, List<Exception>> {
     val (success, other) = partition { it.isSuccess }
     return Pair(
-        success.map { (it as Success).result },
+        success.map { it.value as T },
         other.mapNotNull { it.errorOnLoading().exceptionOrNull() }
     )
 }
diff --git a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt
index b9ad6f5..9aae9a2 100644
--- a/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt
+++ b/core/src/commonMain/kotlin/pro/respawn/apiresult/SuspendResult.kt
@@ -21,7 +21,6 @@ import kotlinx.coroutines.supervisorScope
 import kotlinx.coroutines.withContext
 import pro.respawn.apiresult.ApiResult.Error
 import pro.respawn.apiresult.ApiResult.Loading
-import pro.respawn.apiresult.ApiResult.Success
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.jvm.JvmName
@@ -47,7 +46,7 @@ public inline fun <T> Flow<T>.catchExceptions(
 public suspend inline fun <T> SuspendResult(
     context: CoroutineContext = EmptyCoroutineContext,
     noinline block: suspend CoroutineScope.() -> T,
-): ApiResult<T> = withContext(context) { ApiResult { supervisorScope(block) } }
+): ApiResult<T> = withContext(context) { ApiResult(call = { supervisorScope(block) }) }
 
 /**
  * Emits [ApiResult.Loading], then executes [call] and wraps it.
@@ -56,8 +55,8 @@ public suspend inline fun <T> SuspendResult(
 public inline fun <T> ApiResult.Companion.tryFlow(
     crossinline call: suspend () -> T
 ): Flow<ApiResult<T>> = kotlinx.coroutines.flow.flow {
-    emit(Loading)
-    emit(ApiResult { call() })
+    emit(Loading())
+    emit(ApiResult(call = { call() }))
 }
 
 /**
@@ -68,23 +67,23 @@ public inline fun <T> ApiResult.Companion.tryFlow(
 public inline fun <T> ApiResult.Companion.flow(
     crossinline result: suspend () -> ApiResult<T>,
 ): Flow<ApiResult<T>> = kotlinx.coroutines.flow.flow {
-    emit(Loading)
+    emit(Loading())
     emit(result())
 }
 
 /**
  * Emits [Loading] before this flow starts to be collected.
- * Then maps all values to [Success] and catches [Exception]s and maps them to [Error]s
+ * Then maps all results to their values, then catches [Exception]s and maps them to [Error]s
  * @see ApiResult.Companion.flow
  * @see SuspendResult
  */
 public inline fun <T> Flow<T>.asApiResult(): Flow<ApiResult<T>> = this
     .map { it.asResult }
-    .onStart { emit(Loading) }
-    .catchExceptions { emit(Error(it)) }
+    .onStart { emit(ApiResult.Loading()) }
+    .catchExceptions { emit(ApiResult.Error(value = it)) }
 
 /**
- * Maps each [Success] value of [this] flow using [transform]
+ * Maps each success value of [this] flow using [transform]
  */
 public inline fun <T, R> Flow<ApiResult<T>>.mapResults(
     crossinline transform: suspend (T) -> R