All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.whispersystems.signalservice.api.NetworkResult.kt Maven / Gradle / Ivy

There is a newer version: 2.15.3_unofficial_107
Show newest version
/*
 * Copyright 2023 Signal Messenger, LLC
 * SPDX-License-Identifier: AGPL-3.0-only
 */

package org.whispersystems.signalservice.api

import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import java.io.IOException

typealias StatusCodeErrorAction = (NetworkResult.StatusCodeError<*>) -> Unit

/**
 * A helper class that wraps the result of a network request, turning common exceptions
 * into sealed classes, with optional request chaining.
 *
 * This was designed to be a middle ground between the heavy reliance on specific exceptions
 * in old network code (which doesn't translate well to kotlin not having checked exceptions)
 * and plain rx, which still doesn't free you from having to catch exceptions and translate
 * things to sealed classes yourself.
 *
 * If you have a very complicated network request with lots of different possible response types
 * based on specific errors, this isn't for you. You're likely better off writing your own
 * sealed class. However, for the majority of requests which just require getting a model from
 * the success case and the status code of the error, this can be quite convenient.
 */
sealed class NetworkResult(
  private val statusCodeErrorActions: MutableSet = mutableSetOf()
) {
  companion object {
    /**
     * A convenience method to capture the common case of making a request.
     * Perform the network action in the [fetcher], returning your result.
     * Common exceptions will be caught and translated to errors.
     */
    @JvmStatic
    fun  fromFetch(fetcher: Fetcher): NetworkResult = try {
      Success(fetcher.fetch())
    } catch (e: NonSuccessfulResponseCodeException) {
      StatusCodeError(e)
    } catch (e: IOException) {
      NetworkError(e)
    } catch (e: Throwable) {
      ApplicationError(e)
    }
  }

  /** Indicates the request was successful */
  data class Success(val result: T) : NetworkResult()

  /** Indicates a generic network error occurred before we were able to process a response. */
  data class NetworkError(val exception: IOException) : NetworkResult()

  /** Indicates we got a response, but it was a non-2xx response. */
  data class StatusCodeError(val code: Int, val body: String?, val exception: NonSuccessfulResponseCodeException) : NetworkResult() {
    constructor(e: NonSuccessfulResponseCodeException) : this(e.code, e.body, e)
  }

  /** Indicates that the application somehow failed in a way unrelated to network activity. Usually a runtime crash. */
  data class ApplicationError(val throwable: Throwable) : NetworkResult()

  /**
   * Returns the result if successful, otherwise turns the result back into an exception and throws it.
   *
   * Useful for bridging to Java, where you may want to use try-catch.
   */
  fun successOrThrow(): T {
    when (this) {
      is Success -> return result
      is NetworkError -> throw exception
      is StatusCodeError -> throw exception
      is ApplicationError -> throw throwable
    }
  }

  /**
   * Returns the [Throwable] associated with the result, or null if the result is successful.
   */
  fun getCause(): Throwable? {
    return when (this) {
      is Success -> null
      is NetworkError -> exception
      is StatusCodeError -> exception
      is ApplicationError -> throwable
    }
  }

  /**
   * Takes the output of one [NetworkResult] and transforms it into another if the operation is successful.
   * If it's non-successful, [transform] lambda is not run, and instead the original failure will be propagated.
   * Useful for changing the type of a result.
   *
   * ```kotlin
   * val user: NetworkResult = NetworkResult
   *   .fromFetch { fetchRemoteUserModel() }
   *   .map { it.toLocalUserModel() }
   * ```
   */
  fun  map(transform: (T) -> R): NetworkResult {
    return when (this) {
      is Success -> Success(transform(this.result)).runOnStatusCodeError(statusCodeErrorActions)
      is NetworkError -> NetworkError(exception).runOnStatusCodeError(statusCodeErrorActions)
      is ApplicationError -> ApplicationError(throwable).runOnStatusCodeError(statusCodeErrorActions)
      is StatusCodeError -> StatusCodeError(code, body, exception).runOnStatusCodeError(statusCodeErrorActions)
    }
  }

  /**
   * Takes the output of one [NetworkResult] and passes it as the input to another if the operation is successful.
   * If it's non-successful, the [result] lambda is not run, and instead the original failure will be propagated.
   * Useful for chaining operations together.
   *
   * ```kotlin
   * val networkResult: NetworkResult = NetworkResult
   *   .fromFetch { fetchAuthCredential() }
   *   .then {
   *     NetworkResult.fromFetch { credential -> fetchData(credential) }
   *   }
   * ```
   */
  fun  then(result: (T) -> NetworkResult): NetworkResult {
    return when (this) {
      is Success -> result(this.result).runOnStatusCodeError(statusCodeErrorActions)
      is NetworkError -> NetworkError(exception).runOnStatusCodeError(statusCodeErrorActions)
      is ApplicationError -> ApplicationError(throwable).runOnStatusCodeError(statusCodeErrorActions)
      is StatusCodeError -> StatusCodeError(code, body, exception).runOnStatusCodeError(statusCodeErrorActions)
    }
  }

  /**
   * Will perform an operation if the result at this point in the chain is successful. Note that it runs if the chain is _currently_ successful. It does not
   * depend on anything futher down the chain.
   *
   * ```kotlin
   * val networkResult: NetworkResult = NetworkResult
   *   .fromFetch { fetchAuthCredential() }
   *   .runIfSuccessful { storeMyCredential(it) }
   * ```
   */
  fun runIfSuccessful(result: (T) -> Unit): NetworkResult {
    if (this is Success) {
      result(this.result)
    }
    return this
  }

  /**
   * Specify an action to be run when a status code error occurs. When a result is a [StatusCodeError] or is transformed into one further down the chain via
   * a future [map] or [then], this code will be run. There can only ever be a single status code error in a chain, and therefore this lambda will only ever
   * be run a single time.
   *
   * This is a low-visibility way of doing things, so use sparingly.
   *
   * ```kotlin
   * val result = NetworkResult
   *   .fromFetch { getAuth() }
   *   .runOnStatusCodeError { error -> logError(error) }
   *   .then { credential ->
   *     NetworkResult.fromFetch { fetchUserDetails(credential) }
   *   }
   * ```
   */
  fun runOnStatusCodeError(action: StatusCodeErrorAction): NetworkResult {
    return runOnStatusCodeError(setOf(action))
  }

  internal fun runOnStatusCodeError(actions: Collection): NetworkResult {
    if (actions.isEmpty()) {
      return this
    }

    statusCodeErrorActions += actions

    if (this is StatusCodeError) {
      statusCodeErrorActions.forEach { it.invoke(this) }
      statusCodeErrorActions.clear()
    }

    return this
  }

  fun interface Fetcher {
    @Throws(Exception::class)
    fun fetch(): T
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy