0

我正在构建一个库,Sttp.client3用作可以通过同步和异步环境实现的基础,我使用 zio-http 作为我的服务和 sttp-client 与其他服务交互。

我有以下特点:

trait CoingeckoApiClient extends CoingeckoClient {

  .....
   override def ping: Either[CoingeckoApiError, PingResponse] =
    get[PingResponse](endpoint = "ping", QueryParams())

  def get[T](endpoint: String, queryParams: QueryParams)(
      using Format[T]
  ): Either[CoingeckoApiError, T]
}

和 API

class CoingeckoApi[F[_], P](using val backend: SttpBackend[F, P]) {
  def get(endpoint: String, params: QueryParams): F[Response[Either[String, String]]] = {
      val apiUrl = s"${CoingeckoApi.baseUrl}/$endpoint"
      basicRequest
        .get(
          uri"$apiUrl"
            .withParams(params)
        )
        .send(backend)
  }
}

一个同步的实现如下:

class CoingeckoApiBasic(api: CoingeckoApi[Identity, Any]) extends CoingeckoApiClient {
  def get[T](endpoint: String, queryParams: QueryParams)(using Format[T]): Either[CoingeckoApiError, T] =
    api.get(endpoint, queryParams).body match {
      case Left(json) =>
        Json.parse(json).validate[CoingeckoApiError] match {
          case JsSuccess(value, _) => Left(value)
          case JsError(errors) =>
            Left(CoingeckoApiError.internalApiError(Some("Unknown Api Error")))
        }

      case Right(json) =>
        Json.parse(json).validate[T] match {
          case JsSuccess(value, _) =>
            Right(value)
          case JsError(errors) =>
            Left(
              CoingeckoApiError
                .internalApiError(Some(s"Invalid Response for $endpoint"))
            )
        }
    }
}

所以我希望用 ZIO 提供一个异步实现

class CoingeckoApiZIO(api: CoingeckoApi[UIO, Any]) extends CoingeckoApiClient {
  def get[T](endpoint: String, queryParams: QueryParams)(using Format[T]): Either[CoingeckoApiError, T] =
  Runtime.unsafeRun {
    api.get(endpoint, queryParams).map(r => r.body match {
      case Left(json) =>
        Json.parse(json).validate[CoingeckoApiError] match {
          case JsSuccess(value, _) => Left(value)
          case JsError(errors) =>
            Left(CoingeckoApiError.internalApiError(Some("Unknown Api Error")))
        }

      case Right(json) =>
        Json.parse(json).validate[T] match {
          case JsSuccess(value, _) =>
            Right(value)
          case JsError(errors) =>
            Left(
              CoingeckoApiError
                .internalApiError(Some(s"Invalid Response for $endpoint"))
            )
        }
    })
  }
}

这是否意味着,我需要在这个级别提供一个运行时?在我看来,提供一个足够灵活以供 ZIO、Future 和其他人使用的 API 似乎有点困难,而且我可能在这里遗漏了一些重要的东西。

我可能需要更改签名class CoingeckoApi[F[_], P]以支持环境?

我正在尝试遵循可以使用多个后端的 sttp 的步骤,但它似乎有点难以扩展,或者我需要重写我的 API。

4

1 回答 1

0

所以这里的主要问题是你实际上并没有提供异步接口,你仍然强制 ZIO 同步执行,这将阻塞调用线程。您需要将您的返回类型“提升”为您的基础客户端正在使用的效果类型。

而且由于您似乎会将后端传递给类型,而不是进行调用站点方差,因此您需要使特征随效果类型而变化:

trait CoingeckoApiClient[F[_]] extends CoingeckoClient[F] {

  .....
 override def ping: F[Either[CoingeckoApiError, PingResponse]] =
  get[PingResponse](endpoint = "ping", QueryParams())

 def get[T](endpoint: String, queryParams: QueryParams)(
  using Format[T]
 ): Either[CoingeckoApiError, T]
}

然后你的 ZIO API 看起来像这样

class CoingeckoApiZIO(api: CoingeckoApi[Task, Any]) extends CoingeckoApiClient[Task] {
  def get[T](endpoint: String, queryParams: QueryParams)(using Format[T]): Task[Either[CoingeckoApiError, T]] =
    api.get(endpoint, queryParams).map(r => r.body match {
      case Left(json) =>
        Json.parse(json).validate[CoingeckoApiError] match {
          case JsSuccess(value, _) => Left(value)
          case JsError(errors) =>
            Left(CoingeckoApiError.internalApiError(Some("Unknown Api Error")))
        }

      case Right(json) =>
        Json.parse(json).validate[T] match {
          case JsSuccess(value, _) =>
            Right(value)
          case JsError(errors) =>
            Left(
              CoingeckoApiError
                .internalApiError(Some(s"Invalid Response for $endpoint"))
            )
        }
    })
}

然后你的调用者就可以调用这个方法

AsyncHttpZioBackend().flatMap { backend =>
  val client = new CoingeckoApiZIO(new CoingeckoApi(backend))
  client.get("", QueryParams(...))
}

但是,我仍然觉得这作为一个界面有点令人费解。Sttp 带有一个内置MonadError类型,您可以通用地使用它,而不是为每种效果类型创建后端实现,并且您可以将请求构建部分与执行部分分开。

所以我建议改为这样写:

object CoingeckoRequests {
  final val baseUrl = "https://api.coingecko.com/api/v3"

  def get(endpoint: String, params: QueryParams): Request[Either[String, String]], Any] = {
      val apiUrl = s"${CoingeckoApi.baseUrl}/$endpoint"
      basicRequest
        .get(uri"$apiUrl?params")
  }
}

class CoingeckoApiClientImpl[F[_]](backend: SttpBackend[F, Any]) extends CoingeckoApiClient[F] {
    def get[T](endpoint: String, queryParams: QueryParams)(using Format[T]): F[Either[CoingeckoApiError, T]] =
      backend.send(
        CoingeckoRequests.get(endpoint, queryParams)
          .response(asJson[T])
         // Handle your error response here
      )
}
于 2021-10-02T04:58:58.143 回答