Skip to content

Commit

Permalink
Add successOrNull and fold extensions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ZacSweers committed Aug 5, 2023
1 parent 925da70 commit 3f11a93
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 0 deletions.
7 changes: 7 additions & 0 deletions api/eithernet.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
}

Expand Down
64 changes: 64 additions & 0 deletions src/main/java/com/slack/eithernet/Extensions.kt
Original file line number Diff line number Diff line change
@@ -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 <T : Any, E : Any> ApiResult<T, E>.successOrNull(): T? =
when (this) {
is ApiResult.Success -> value
else -> null
}

/** If [ApiResult.Success], returns the underlying [T] value. Otherwise, returns null. */
public inline fun <T : Any, E : Any> ApiResult<T, E>.successOrElse(
defaultValue: (ApiResult.Failure<E>) -> T
): T =
when (this) {
is ApiResult.Success -> value
is ApiResult.Failure -> defaultValue(this)
}

/** Transforms an [ApiResult] into a [C] value. */
public fun <T : Any, E : Any, C> ApiResult<T, E>.fold(
onSuccess: (ApiResult.Success<T>) -> C,
onFailure: (ApiResult.Failure<E>) -> 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 <T : Any, E : Any, C> ApiResult<T, E>.fold(
onSuccess: (ApiResult.Success<T>) -> C,
onNetworkFailure: (ApiResult.Failure.NetworkFailure) -> C,
onUnknownFailure: (ApiResult.Failure.UnknownFailure) -> C,
onHttpFailure: (ApiResult.Failure.HttpFailure<E>) -> C,
onApiFailure: (ApiResult.Failure.ApiFailure<E>) -> 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)
}
}
132 changes: 132 additions & 0 deletions src/test/kotlin/com/slack/eithernet/ExtensionsTest.kt
Original file line number Diff line number Diff line change
@@ -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")
}
}

0 comments on commit 3f11a93

Please sign in to comment.