From 3f11a93aad525ca4ed23387a881a6bed088ac5f3 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Fri, 4 Aug 2023 21:55:08 -0400 Subject: [PATCH] Add successOrNull and fold extensions I've written or seen variations of these a few times in the wild, so promoting them to a top-level set of APIs now to ease use. This introduces three new functional extensions to `ApiResult`: `successOrNull()`, `successOrElse()`, and `fold()`. These allow easy happy path-ing in user code to coerce results into a concrete value. --- api/eithernet.api | 7 + .../java/com/slack/eithernet/Extensions.kt | 64 +++++++++ .../com/slack/eithernet/ExtensionsTest.kt | 132 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 src/main/java/com/slack/eithernet/Extensions.kt create mode 100644 src/test/kotlin/com/slack/eithernet/ExtensionsTest.kt diff --git a/api/eithernet.api b/api/eithernet.api index ff5f7ed..b89e4d6 100644 --- a/api/eithernet.api +++ b/api/eithernet.api @@ -70,6 +70,13 @@ public abstract interface annotation class com/slack/eithernet/DecodeErrorBody : public abstract interface annotation class com/slack/eithernet/ExperimentalEitherNetApi : java/lang/annotation/Annotation { } +public final class com/slack/eithernet/ExtensionsKt { + public static final fun fold (Lcom/slack/eithernet/ApiResult;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun fold (Lcom/slack/eithernet/ApiResult;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun successOrElse (Lcom/slack/eithernet/ApiResult;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; + public static final fun successOrNull (Lcom/slack/eithernet/ApiResult;)Ljava/lang/Object; +} + public abstract interface annotation class com/slack/eithernet/InternalEitherNetApi : java/lang/annotation/Annotation { } diff --git a/src/main/java/com/slack/eithernet/Extensions.kt b/src/main/java/com/slack/eithernet/Extensions.kt new file mode 100644 index 0000000..ac99890 --- /dev/null +++ b/src/main/java/com/slack/eithernet/Extensions.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.slack.eithernet + +/** If [ApiResult.Success], returns the underlying [T] value. Otherwise, returns null. */ +public fun ApiResult.successOrNull(): T? = + when (this) { + is ApiResult.Success -> value + else -> null + } + +/** If [ApiResult.Success], returns the underlying [T] value. Otherwise, returns null. */ +public inline fun ApiResult.successOrElse( + defaultValue: (ApiResult.Failure) -> T +): T = + when (this) { + is ApiResult.Success -> value + is ApiResult.Failure -> defaultValue(this) + } + +/** Transforms an [ApiResult] into a [C] value. */ +public fun ApiResult.fold( + onSuccess: (ApiResult.Success) -> C, + onFailure: (ApiResult.Failure) -> C, +): C { + @Suppress("UNCHECKED_CAST") + return fold( + onSuccess, + onFailure as (ApiResult.Failure.NetworkFailure) -> C, + onFailure as (ApiResult.Failure.UnknownFailure) -> C, + onFailure, + onFailure, + ) +} + +/** Transforms an [ApiResult] into a [C] value. */ +public fun ApiResult.fold( + onSuccess: (ApiResult.Success) -> C, + onNetworkFailure: (ApiResult.Failure.NetworkFailure) -> C, + onUnknownFailure: (ApiResult.Failure.UnknownFailure) -> C, + onHttpFailure: (ApiResult.Failure.HttpFailure) -> C, + onApiFailure: (ApiResult.Failure.ApiFailure) -> C, +): C { + return when (this) { + is ApiResult.Success -> onSuccess(this) + is ApiResult.Failure.ApiFailure -> onApiFailure(this) + is ApiResult.Failure.HttpFailure -> onHttpFailure(this) + is ApiResult.Failure.NetworkFailure -> onNetworkFailure(this) + is ApiResult.Failure.UnknownFailure -> onUnknownFailure(this) + } +} diff --git a/src/test/kotlin/com/slack/eithernet/ExtensionsTest.kt b/src/test/kotlin/com/slack/eithernet/ExtensionsTest.kt new file mode 100644 index 0000000..c431c90 --- /dev/null +++ b/src/test/kotlin/com/slack/eithernet/ExtensionsTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.slack.eithernet + +import com.google.common.truth.Truth.assertThat +import okio.IOException +import org.junit.Test + +@Suppress("ThrowsCount") +class ExtensionsTest { + + @Test + fun successOrNullWithSuccess() { + val result = ApiResult.success("Hello") + assertThat(result.successOrNull()).isEqualTo("Hello") + } + + @Test + fun successOrNullWithFailure() { + val result: ApiResult<*, *> = ApiResult.unknownFailure(Throwable()) + assertThat(result.successOrNull()).isNull() + } + + @Test + fun successOrElseWithSuccess() { + val result = ApiResult.success("Hello") + assertThat(result.successOrElse { error("") }).isEqualTo("Hello") + } + + @Test + fun successOrElseWithFailure() { + val result = ApiResult.unknownFailure(Throwable()) + assertThat(result.successOrElse { "Hello" }).isEqualTo("Hello") + } + + @Test + fun successOrElseWithCustomFailure() { + val result = ApiResult.unknownFailure(Throwable()) + assertThat( + result.successOrElse { failure -> + when (failure) { + is ApiResult.Failure.UnknownFailure -> "Hello" + else -> throw AssertionError() + } + } + ) + .isEqualTo("Hello") + } + + @Test + fun foldSuccess() { + val result = ApiResult.success("Hello") + val folded = result.fold({ it.value }, { "Failure" }) + assertThat(folded).isEqualTo("Hello") + } + + @Test + fun foldFailure() { + val result = ApiResult.apiFailure("Hello") + val folded = result.fold({ throw AssertionError() }, { "Failure" }) + assertThat(folded).isEqualTo("Failure") + } + + @Test + fun foldApiFailure() { + val result = ApiResult.apiFailure("Hello") + val folded = + result.fold( + onApiFailure = { "Failure" }, + onSuccess = { throw AssertionError() }, + onHttpFailure = { throw AssertionError() }, + onNetworkFailure = { throw AssertionError() }, + onUnknownFailure = { throw AssertionError() }, + ) + assertThat(folded).isEqualTo("Failure") + } + + @Test + fun foldHttpFailure() { + val result = ApiResult.httpFailure(404, "Hello") + val folded = + result.fold( + onSuccess = { throw AssertionError() }, + onHttpFailure = { "Failure" }, + onApiFailure = { throw AssertionError() }, + onNetworkFailure = { throw AssertionError() }, + onUnknownFailure = { throw AssertionError() }, + ) + assertThat(folded).isEqualTo("Failure") + } + + @Test + fun foldUnknownFailure() { + val result = ApiResult.unknownFailure(Throwable()) + val folded = + result.fold( + onSuccess = { throw AssertionError() }, + onUnknownFailure = { "Failure" }, + onApiFailure = { throw AssertionError() }, + onNetworkFailure = { throw AssertionError() }, + onHttpFailure = { throw AssertionError() }, + ) + assertThat(folded).isEqualTo("Failure") + } + + @Test + fun foldNetworkFailure() { + val result = ApiResult.networkFailure(IOException()) + val folded = + result.fold( + onNetworkFailure = { "Failure" }, + onSuccess = { throw AssertionError() }, + onApiFailure = { throw AssertionError() }, + onUnknownFailure = { throw AssertionError() }, + onHttpFailure = { throw AssertionError() }, + ) + assertThat(folded).isEqualTo("Failure") + } +}