Description
The current design tries to abstract the effect type F[_]
into Executor[F]
, but in reality, the effect type is usually determined by the implementation in HttpClient
. For example, SttpRequestHttpClient
is actually using a Future
...
May I propose simplifing this design by using a barebone tagless final design and rid of the need for Exceutor
.
Bascially
trait HttpClient[F[_]] {
def send(request: ElasticRequest): F[HttpResponse]
}
object Elastic4s {
def execute[T, U, F[_]](t: T)(implicit
client: HttpClient[F],
functor: Functor[F],
handler: Handler[T, U],
options: CommonRequestOptions): F[Response[U]] {
val request = handler.build(t)
//omitting the part that add `timeout` and `master_timeout` parameter
functor.map(client(request3)) { resp =>
handler.responseHandler.handle(resp)
.fold(
error =>
RequestFailure(resp.statusCode, resp.entity.map(_.content), resp.headers, error),
u =>
RequestSuccess(resp.statusCode, resp.entity.map(_.content), resp.headers, u)
)
}
}
With this design it's quite easy to add more HttpClient
implementations, specifically, it made it a lot easier for each HttpClient
implementation to control their effect type system. The current ElasticClient
is a simple wrapper of HttpClient
, I feel it's unnecessary for it to try to manage the effect system for HttpClient
Here is an example of a Http4s
based HttpClient
impl.
class Http4sElastic4sClient[F[_]: ContextShift](
baseUri: Uri,
blocker: Blocker,
hClient: Client[F]
)(implicit
F: Effect[F]) {
private def processResponse(resp: Response[F]): F[HttpResponse] = {
resp.as[String].map { body =>
val entity =
Some(HttpEntity.StringEntity(body, resp.contentType.map(_.renderString)))
HttpResponse(
resp.status.code,
entity,
resp.headers.toList.map(h => (h.name.value, h.value)).toMap
)
}
}
implicit val fileEncoder = EntityEncoder.fileEncoder[F](blocker)
implicit val isEncoder = EntityEncoder.inputStreamEncoder[F, InputStream](blocker)
private def send(
request: ElasticRequest
): F[HttpResponse] = {
import request._
val uri =
baseUri
.addPath(endpoint)
.withQueryParams(params)
val http4sReq = Method
.fromString(method.toUpperCase())
.map { m =>
def withBody[A](
b: A,
ct: Option[String]
)(implicit
w: EntityEncoder[F, A]
): Request[F] = {
val h = w.headers
val entity = w.toEntity(b)
val withContentLength = entity.length
.map { l =>
`Content-Length`.fromLong(l).fold(_ => h, c => h.put(c))
}
.getOrElse(h)
val newHeaders = ct
.flatMap { cts =>
`Content-Type`.parse(cts).toOption
}
.map { h =>
withContentLength.filterNot(_.is(`Content-Type`)).put(h)
}
.getOrElse(withContentLength)
Request(method = m, uri = uri, headers = newHeaders, body = entity.body)
}
entity.fold(Request[F](m, uri)) {
case StringEntity(content, _) => withBody(content)
case ByteArrayEntity(content, _) => withBody(content)
case InputStreamEntity(in, _) => withBody(F.pure(in))
case FileEntity(file: File, _) => withBody(file)
}
}
.liftTo[F]
http4sReq.flatMap(req => hClient.run(req).use(processResponse))
}
UPDATE: fixed the example HttpClient with correct content type
Activity