Skip to content

Commit

Permalink
Added harness-http-client-2 for JS
Browse files Browse the repository at this point in the history
  • Loading branch information
Kalin-Rudnicki committed Apr 10, 2024
1 parent e9e5e3f commit 7cc1792
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,94 @@
package harness.http.client

import harness.endpoint.error.DecodingFailure
import harness.endpoint.spec.*
import harness.endpoint.transfer.*
import harness.endpoint.types.*
import harness.endpoint.types.Types.*
import harness.web.*
import harness.zio.*
import org.scalajs.dom.XMLHttpRequest
import scala.scalajs.js.URIUtils.encodeURIComponent
import zio.*

trait HttpClientPlatformSpecificImpl { self: HttpClientPlatformSpecific =>

override val defaultClient: HttpClient =
new HttpClient {

private inline def encodeQueryParam(key: String, value: String): String =
s"${encodeURIComponent(key)}=${encodeURIComponent(value)}"

private inline def encodeQueryParams(queryParams: List[(String, String)]): String =
if (queryParams.isEmpty) ""
else queryParams.map(encodeQueryParam(_, _)).mkString("?", "&", "")

private inline def makeUrl(url: String, paths: List[String], queryParams: List[(String, String)]): String =
s"$url${paths.map(p => s"/${encodeURIComponent(p)}").mkString}${encodeQueryParams(queryParams)}"

private inline def makeXHR: Task[XMLHttpRequest] =
ZIO.attempt { new XMLHttpRequest }

private inline def openXHR(xhr: XMLHttpRequest, method: HttpMethod, url: String, paths: List[String], queryParams: List[(String, String)]): Task[Unit] =
ZIO.attempt {
xhr.open(
method = method.method,
url = makeUrl(url, paths, queryParams),
async = true,
)
}

private inline def setHeaders(xhr: XMLHttpRequest, headers: Map[String, List[String]]): Task[Unit] =
ZIO.foreachDiscard(headers.toList) { (k, vs) => ZIO.foreachDiscard(vs) { v => ZIO.attempt { xhr.setRequestHeader(k, v) } } }

private inline def readResponseBody[ET <: EndpointType.Any](
outputBodySchema: BodySchema[OutputBody[ET]],
errorSchema: ErrorSchema[Error[ET]],
)(
requestParams: HttpRequestParams,
xhr: XMLHttpRequest,
): IO[Error[ET], Receive[OutputBody[ET]]] =
for {
responseCode: HttpCode <- ZIO.attempt { xhr.status }.mapBoth(new RuntimeException("Error getting response code", _), HttpCode(_)).orDie
response <-
if (responseCode.is2xx)
outputBodySchema match {
case BodySchema.Encoded(schema) =>
ZIO.attempt { xhr.responseText }.orDie.flatMap {
schema.decode(_) match {
case Right(value) => ZIO.succeed(value)
case Left(error) => ZIO.die(DecodingFailure(DecodingFailure.Source.Body, error))
}
}
case BodySchema.None => ZIO.unit
case BodySchema.Stream => ZIO.dieMessage("Can not handle Stream response")
}
else
ZIO.attempt { xhr.responseText }.orDie.flatMap { stringBody =>
errorSchema.decode(responseCode, stringBody) match {
case Some(Right(error)) => ZIO.fail(error)
case Some(Left(error)) => ZIO.die(DecodingFailure(DecodingFailure.Source.Body, error))
case None =>
responseCode match {
case HttpCode.`404` =>
ZIO.dieMessage(s"Target HTTP server does not handle: ${requestParams.paths.mkString("/", "/", "")}\n$stringBody")
case HttpCode.`405` =>
ZIO.dieMessage(s"Target HTTP server does not handle ${requestParams.method.method}: ${requestParams.paths.mkString("/", "/", "")}\n$stringBody")
case _ =>
ZIO.die(DecodingFailure(DecodingFailure.Source.Body, s"Unexpected response code: $responseCode"))
}
}
}
} yield response.asInstanceOf[Receive[OutputBody[ET]]]

private inline def send(xhr: XMLHttpRequest, body: OutputStream): RIO[Logger, Unit] =
body match {
case OutputStream.Empty => ZIO.attempt { xhr.send() }
case OutputStream.Str(string) => ZIO.attempt { xhr.send(string) }
case OutputStream.File(WrappedJsPath(file)) => ZIO.attempt { xhr.send(file) }
case _ => ZIO.dieMessage("TODO : send of invalid body not supported")
}

override def send_internal[ET <: EndpointType.Any](
inputBodySchema: BodySchema[InputBody[ET]],
outputBodySchema: BodySchema[OutputBody[ET]],
Expand All @@ -18,7 +97,20 @@ trait HttpClientPlatformSpecificImpl { self: HttpClientPlatformSpecific =>
request: HttpRequestParams,
body: Send[InputBody[ET]],
): ZIO[Logger & Telemetry, Error[ET], Receive[OutputBody[ET]]] =
???
ZIO
.asyncZIO[Logger, Throwable, XMLHttpRequest] { register =>
for {
xhr <- makeXHR
_ <- openXHR(xhr, request.method, request.url, request.paths, request.queryParams)
_ <- setHeaders(xhr, request.headers)
_ <- ZIO.attempt { xhr.onload = { _ => register(ZIO.succeed(xhr)) } }
_ <- send(xhr, inputBodySchema.out(body))
} yield ()
}
.orDie
.flatMap { xhr =>
readResponseBody[ET](outputBodySchema, errorSchema)(request, xhr)
}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ trait HttpClientPlatformSpecificImpl { self: HttpClientPlatformSpecific =>
else queryParams.map(encodeQueryParam(_, _)).mkString("?", "&", "")

private inline def makeUrl(url: String, paths: List[String], queryParams: List[(String, String)]): RIO[Logger, URL] =
ZIO.attempt { new URL(s"$url${paths.map(URLEncoder.encode(_, "UTF-8")).mkString("/", "/", "")}${encodeQueryParams(queryParams)}") }
ZIO.attempt { new URL(s"$url${paths.map(p => s"/${URLEncoder.encode(p, "UTF-8")}").mkString}${encodeQueryParams(queryParams)}") }

private inline def getConnection(url: URL): RIO[Scope, HttpURLConnection] =
ZIO.attempt { url.openConnection() }.mapError(new RuntimeException(s"Error opening URL connection for: $url", _)).flatMap {
Expand Down Expand Up @@ -75,7 +75,7 @@ trait HttpClientPlatformSpecificImpl { self: HttpClientPlatformSpecific =>
InputStream(body),
)

private def getBody[ET <: EndpointType.Any](
private def readResponseBody[ET <: EndpointType.Any](
outputBodySchema: BodySchema[OutputBody[ET]],
errorSchema: ErrorSchema[Error[ET]],
)(
Expand All @@ -93,9 +93,9 @@ trait HttpClientPlatformSpecificImpl { self: HttpClientPlatformSpecific =>
case None =>
responseParams.responseCode match {
case HttpCode.`404` =>
ZIO.dieMessage(s"Target HTTP server does not handle: ${requestParams.paths.mkString("/", "/", "")}\n${stringBody}")
ZIO.dieMessage(s"Target HTTP server does not handle: ${requestParams.paths.mkString("/", "/", "")}\n$stringBody")
case HttpCode.`405` =>
ZIO.dieMessage(s"Target HTTP server does not handle ${requestParams.method.method}: ${requestParams.paths.mkString("/", "/", "")}\n${stringBody}")
ZIO.dieMessage(s"Target HTTP server does not handle ${requestParams.method.method}: ${requestParams.paths.mkString("/", "/", "")}\n$stringBody")
case _ =>
ZIO.die(DecodingFailure(DecodingFailure.Source.Body, s"Unexpected response code: ${responseParams.responseCode}"))
}
Expand All @@ -120,7 +120,7 @@ trait HttpClientPlatformSpecificImpl { self: HttpClientPlatformSpecific =>
_ <- setHeaders(con, requestParams.headers).orDie
_ <- setBody(con, inputBodySchema.out(body)).orDie
(responseParams, stream) <- getResponseParamsAndStream(con).orDie
result <- getBody[ET](outputBodySchema, errorSchema)(requestParams, responseParams, stream)
result <- readResponseBody[ET](outputBodySchema, errorSchema)(requestParams, responseParams, stream)
} yield result
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ final class JsClient extends HttpClient[JsClient.RequestT, JsClient.ResponseT] {
JsClient.bodyOps,
)

private inline def setReturn(xhr: XMLHttpRequest, register: RIO[Logger, HttpResponse.Result[JsClient.ResponseT]] => Unit): Task[Unit] =
private inline def setReturn[T](xhr: XMLHttpRequest, register: RIO[Logger, HttpResponse.Result[JsClient.ResponseT]] => Unit): Task[Unit] =
ZIO.attempt {
xhr.onload = { _ => register(getResponse(xhr)) }
}
Expand Down

0 comments on commit 7cc1792

Please sign in to comment.