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.2 #2

Merged
merged 8 commits into from
Dec 11, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,4 @@ hs_err_pid*
# yarn.lock location & other
/kotlin-js-store/**
/.idea/appInsightsSettings.xml
/.idea/deploymentTargetSelector.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import pro.respawn.apiresult.ApiResult
import pro.respawn.apiresult.ApiResult.Loading.isLoading
import pro.respawn.apiresult.asResult
import pro.respawn.apiresult.chain
import pro.respawn.apiresult.fold
import pro.respawn.apiresult.map
Expand Down Expand Up @@ -41,9 +40,9 @@ data class UiState(
)

class MainViewModel(
val transactionRepository: TransactionRepository = MockTransactionRepository(),
val securityRepository: SecurityRepository = MockSecurityRepository(),
val userRepository: UserRepository = MockUserRepository(),
private val transactionRepository: TransactionRepository = MockTransactionRepository(),
private val securityRepository: SecurityRepository = MockSecurityRepository(),
private val userRepository: UserRepository = MockUserRepository(),
) : ViewModel() {

private val _state = MutableStateFlow(UiState())
Expand All @@ -52,7 +51,8 @@ class MainViewModel(
fun onClickPurchase() = viewModelScope.launch {
_state.update { it.copy(isLoading = true, result = null) }

ApiResult(state.value.userId)
state.value.userId
.asResult
.requireNotNull()
.then { userRepository.getUser(it) }
.recover { userRepository.getAnonymousUser() }
Expand Down
3 changes: 1 addition & 2 deletions 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 = 1
const val patch = 2
const val postfix = ""
const val versionName = "$majorRelease.$minorRelease.$patch$postfix"
const val url = "https://github.com/respawn-app/ApiResult"
Expand Down Expand Up @@ -45,7 +45,6 @@ feature-rich.
addAll(compilerArgs)
add("-Xjvm-default=all") // enable all jvm optimizations
add("-Xcontext-receivers")
// add("-Xuse-k2")
addAll(optIns.map { "-opt-in=$it" })
}

Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/ConfigureAndroid.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.android.build.gradle.LibraryExtension
import org.gradle.api.Project

fun Project.configureAndroid(
commonExtension: CommonExtension<*, *, *, *, *>,
commonExtension: CommonExtension<*, *, *, *, *, *>,
) = commonExtension.apply {
compileSdk = Config.compileSdk
defaultConfig {
Expand Down
8 changes: 3 additions & 5 deletions buildSrc/src/main/kotlin/ConfigureMultiplatform.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
@file:Suppress("MissingPackageDeclaration", "unused", "UNUSED_VARIABLE", "UndocumentedPublicFunction", "LongMethod")
@file:Suppress("MissingPackageDeclaration", "unused", "UndocumentedPublicFunction", "LongMethod")

import org.gradle.api.Project
import org.gradle.kotlin.dsl.getValue
import org.gradle.kotlin.dsl.getting
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

@OptIn(ExperimentalKotlinGradlePluginApi::class)
fun Project.configureMultiplatform(
ext: KotlinMultiplatformExtension,
) = ext.apply {
val libs by versionCatalog
explicitApi()

targetHierarchy.default()
applyDefaultHierarchyTemplate()

linuxX64()
linuxArm64()
Expand All @@ -26,7 +24,7 @@ fun Project.configureMultiplatform(
}

androidTarget {
publishAllLibraryVariants()
publishLibraryVariants("release")
}

jvm {
Expand Down
5 changes: 2 additions & 3 deletions buildSrc/src/main/kotlin/ConfigurePublication.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@file:Suppress("MissingPackageDeclaration", "unused")

import com.android.build.api.dsl.LibraryExtension
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import com.android.build.gradle.tasks.BundleAar
import org.gradle.api.Project
import org.gradle.api.publish.PublishingExtension
Expand All @@ -17,7 +16,7 @@ import org.gradle.plugins.signing.Sign
* Configures Maven publishing to sonatype for this project
*/
fun Project.publishMultiplatform() {
val properties = gradleLocalProperties(rootDir)
val properties by localProperties
val isReleaseBuild = properties["release"]?.toString().toBoolean()

val javadocTask = tasks.named("emptyJavadocJar") // TODO: Dokka does not support KMP javadocs for now
Expand Down Expand Up @@ -52,7 +51,7 @@ fun Project.publishAndroid(ext: LibraryExtension) = with(ext) {
}

afterEvaluate {
val properties = gradleLocalProperties(rootDir)
val properties by localProperties
val isReleaseBuild = properties["release"]?.toString().toBoolean()

requireNotNull(extensions.findByType<PublishingExtension>()).apply {
Expand Down
10 changes: 10 additions & 0 deletions buildSrc/src/main/kotlin/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType
import org.gradle.plugin.use.PluginDependency
import java.io.File
import java.io.FileInputStream
import java.util.Base64
import java.util.Properties

/**
* Load version catalog for usage in places where it is not available yet with gradle 7.x.
Expand Down Expand Up @@ -50,3 +53,10 @@ fun List<String>.toJavaArrayString() = buildString {
}

fun String.toBase64() = Base64.getEncoder().encodeToString(toByteArray())

val Project.localProperties
get() = lazy {
Properties().apply {
load(FileInputStream(File(rootProject.rootDir, "local.properties")))
}
}
61 changes: 37 additions & 24 deletions core/src/commonMain/kotlin/pro/respawn/apiresult/ApiResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"NOTHING_TO_INLINE",
"TooManyFunctions",
"ThrowingExceptionsWithoutMessageOrCause",
"INVISIBLE_REFERENCE",
"INVISIBLE_MEMBER",
)

package pro.respawn.apiresult
Expand All @@ -22,9 +24,8 @@ import kotlin.jvm.JvmName

/**
* A class that represents a result of an operation.
* Create an instance with [ApiResult.invoke] and use various operators on the resulting object.
*
* This class is **extremely efficient**: no actual objects are created,
* 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.
* ApiResult is **not** an Rx-style callback chain -
* the operators that are invoked are called **immediately** and in-place.
Expand All @@ -40,7 +41,7 @@ public sealed interface ApiResult<out T> {
* ```
* @see orNull
*/
public operator fun component1(): T? = (this as? Success<T>)?.result
public operator fun component1(): T? = orNull()

/**
* Get the [Error] component of this result or null
Expand All @@ -59,14 +60,6 @@ public sealed interface ApiResult<out T> {
*/
public operator fun not(): T = orThrow()

/**
* A loading state of an [ApiResult]
*/
public data object Loading : ApiResult<Nothing> {

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

/**
* A value of [ApiResult] for its successful state.
* @param result a successful result value
Expand Down Expand Up @@ -102,7 +95,14 @@ public sealed interface ApiResult<out T> {
*/
public val isLoading: Boolean get() = this is Loading

public companion object {
/**
* A loading state of an [ApiResult]
*/
public companion object Loading : ApiResult<Nothing> {

override fun equals(other: Any?): Boolean = other is Loading
override fun hashCode(): Int = 42
override fun toString(): String = "ApiResult.Loading"

/**
* Execute [call], catching any exceptions, and wrap it in an [ApiResult].
Expand Down Expand Up @@ -134,7 +134,7 @@ public sealed interface ApiResult<out T> {
* 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> = ApiResult(Unit)
public inline operator fun invoke(): ApiResult<Unit> = this
}
}

Expand Down Expand Up @@ -189,12 +189,12 @@ public inline infix fun <T, R : T> ApiResult<T>.or(defaultValue: R): T = orElse
/**
* @return null if [this] is an [ApiResult.Error] or [ApiResult.Loading], otherwise return self.
*/
public inline fun <T> ApiResult<T>.orNull(): T? = or(null)
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 as? Error)?.e

/**
* Throws [ApiResult.Error.e], or [NotFinishedException] if the request has not been completed yet.
Expand All @@ -206,6 +206,13 @@ public inline fun <T> ApiResult<T>.orThrow(): T = when (this) {
is Success -> result
}

/**
* Throws if [this] result is an [Error] and [Error.e] is of type [T]. Ignores all other exceptions.
*
* @return a result that can be [Error] but is guaranteed to not have an exception of type [T] wrapped.
*/
public inline fun <reified T : Exception, R> ApiResult<R>.rethrow(): ApiResult<R> = mapError<T, R> { throw it }

/**
* Fold [this] returning the result of [onSuccess] or [onError]
* By default, maps [Loading] to [Error] with [NotFinishedException]
Expand Down Expand Up @@ -327,7 +334,7 @@ public inline fun <T> ApiResult<T>.errorOnLoading(
/**
* Alias for [errorOnNull]
*/
public inline fun <T> ApiResult<T?>.requireNotNull(): ApiResult<T & Any> = errorOnNull()
public inline fun <T> ApiResult<T?>?.requireNotNull(): ApiResult<T & Any> = errorOnNull()

/**
* Throws if [this] is not [Success] and returns [Success] otherwise.
Expand Down Expand Up @@ -446,9 +453,9 @@ public inline infix fun <T, R> ApiResult<T>.tryMap(block: (T) -> R): ApiResult<R
* @see errorIf
* @see errorIfEmpty
*/
public inline fun <T> ApiResult<T?>.errorOnNull(
public inline fun <T> ApiResult<T?>?.errorOnNull(
exception: () -> Exception = { ConditionNotSatisfiedException("Value was null") },
): ApiResult<T & Any> = errorIf(exception) { it == null }.map { requireNotNull(it) }
): ApiResult<T & Any> = this?.errorIf(exception) { it == null }?.map { requireNotNull(it) } ?: Error(exception())

/**
* Maps [Error] values to nulls
Expand Down Expand Up @@ -483,7 +490,7 @@ public inline infix fun <T> ApiResult<T>.recover(another: (e: Exception) -> ApiR
*/
@JvmName("tryRecoverTyped")
public inline infix fun <reified T : Exception, R> ApiResult<R>.tryRecover(block: (T) -> R): ApiResult<R> =
recover<T, R> { ApiResult { block(it) } }
recover<T, R>(another = { ApiResult { block(it) } })

/**
* Calls [recover] catching and wrapping any exceptions thrown inside [block].
Expand Down Expand Up @@ -576,13 +583,19 @@ public inline fun <T, R> ApiResult<T>.flatMap(another: (T) -> ApiResult<R>): Api
public inline fun <T> ApiResult<T>.require(
message: () -> String? = { null },
predicate: (T) -> Boolean
): ApiResult<T> =
errorUnless(
exception = { IllegalArgumentException(message()) },
predicate = predicate
)
): ApiResult<T> = errorUnless(
exception = { IllegalArgumentException(message()) },
predicate = predicate
)

/**
* Map [this] result to [Unit], discarding the value
*/
public inline fun ApiResult<*>.unit(): ApiResult<Unit> = map {}

/**
* Create an [ApiResult] from `this` value, based on the type of it.
*
* @see ApiResult.invoke
*/
public inline val <T> T.asResult: ApiResult<T> get() = ApiResult(this)
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,21 @@ public inline fun <T> Sequence<ApiResult<T?>>.filterNulls(): Sequence<ApiResult<
/**
* Merges all [Success] results into a single [List], or if any has failed, returns [Error].
*/
public inline fun <T> Iterable<ApiResult<T>>.merge(): ApiResult<List<T>> = ApiResult { map { it.orThrow() } }
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> ApiResult.Companion.merge(vararg results: ApiResult<T>): ApiResult<List<T>> =
public inline fun <T> ApiResult.Loading.merge(vararg results: ApiResult<T>): ApiResult<List<T>> =
results.asIterable().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>
): ApiResult<List<T>> = ApiResult.merge(this, *results)

/**
* Returns a list of only [Success] values, discarding any errors
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import pro.respawn.apiresult.ApiResult.Loading
* result is [Loading]
*/
public class NotFinishedException(
message: String? = "ApiResult is still in Loading state",
message: String? = "ApiResult is still in the Loading state",
) : IllegalArgumentException(message)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public suspend inline fun <T> SuspendResult(
* Emits [ApiResult.Loading], then executes [call] and wraps it.
* @see Flow.asApiResult
*/
public inline fun <T> ApiResult.Companion.flow(
public inline fun <T> Loading.tryFlow(
crossinline call: suspend () -> T
): Flow<ApiResult<T>> = kotlinx.coroutines.flow.flow {
emit(Loading)
Expand All @@ -65,7 +65,7 @@ public inline fun <T> ApiResult.Companion.flow(
* @see Flow.asApiResult
*/
@JvmName("flowWithResult")
public inline fun <T> ApiResult.Companion.flow(
public inline fun <T> Loading.flow(
crossinline result: suspend () -> ApiResult<T>,
): Flow<ApiResult<T>> = kotlinx.coroutines.flow.flow {
emit(Loading)
Expand Down
Loading
Loading