I'm building a library that uses Sttp.client3
as the foundation that can be implemented with Synchronous and Asynchronous environments, I'm using zio-http for my service and sttp-client to interact with other services.
I have the following trait:
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]
}
And the 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)
}
}
A synchronous implementation is as follows:
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"))
)
}
}
}
So I'm looking to offer an asyncrhonous implementation with 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"))
)
}
})
}
}
Does that mean, I need to provide a Runtime at this level? It seems to me that is a bit harder to offer an API that is flexible enough to be used by ZIO, Future and others, and probably I'm missing something important here.
I probably need to change the signature of class CoingeckoApi[F[_], P]
to support an environment?
I'm trying to follow in the steps of sttp that can use multiple backends, but It seems it's a bit difficult to scale or I need to rewrite my API.
So the main issue here is that you aren't actually providing an async interface, you are still forcing ZIO to execute synchronously which will block the calling thread. You'll need your return types to be "lifted" into the effect type that your underlying client is using.
And since it seems like you will be passing the backend to the type rather than doing callsite variance that you will need to make the trait vary on the effect type:
Then your ZIO API would look like this
Then your caller would be able to call this method by doing
However, I'd still find this a little convoluted as an interface. Sttp comes with a built in
MonadError
type which you can use generically instead of creating a backend implementation for each effect type, and you can separate the request-building part from the execution part.So I would suggest instead writing it like this: