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

smithy4s-fetch_sjs1_3.0.0.4.source-code.smithy4s-fetch.scala Maven / Gradle / Ivy

The newest version!
package smithy4s_fetch

import org.scalajs.dom.{Fetch, Headers, Request, RequestInfo, Response, URL}
import smithy4s.Endpoint.Middleware
import smithy4s.capability.MonadThrowLike
import smithy4s.client.*
import smithy4s.codecs.BlobEncoder
import smithy4s.http.HttpUriScheme.{Http, Https}
import smithy4s.http.{
  CaseInsensitive,
  HttpMethod,
  HttpRequest,
  HttpUnaryClientCodecs,
  Metadata
}
import smithy4s.json.Json
import smithy4s.{Blob, Endpoint}

import scala.scalajs.js.Promise
import scala.scalajs.js.typedarray.Int8Array

import scalajs.js.JSConverters.*
import smithy4s.http.HttpDiscriminator
import org.scalajs.dom.RequestInit

class SimpleRestJsonFetchClient[
    Alg[_[_, _, _, _, _]]
] private[smithy4s_fetch] (
    service: smithy4s.Service[Alg],
    uri: URL,
    middleware: Endpoint.Middleware[SimpleRestJsonFetchClient.Client],
    codecs: SimpleRestJsonCodecs
) {

  def withMaxArity(maxArity: Int): SimpleRestJsonFetchClient[Alg] =
    changeCodecs(_.copy(maxArity = maxArity))

  def withExplicitDefaultsEncoding(
      explicitDefaultsEncoding: Boolean
  ): SimpleRestJsonFetchClient[Alg] =
    changeCodecs(_.copy(explicitDefaultsEncoding = explicitDefaultsEncoding))

  def withHostPrefixInjection(
      hostPrefixInjection: Boolean
  ): SimpleRestJsonFetchClient[Alg] =
    changeCodecs(_.copy(hostPrefixInjection = hostPrefixInjection))

  def make: Alg[[I, E, O, SI, SO] =>> Promise[O]] =
    service.impl[Promise](
      UnaryClientCompiler[
        Alg,
        Promise,
        SimpleRestJsonFetchClient.Client,
        RequestInfo,
        Response
      ](
        service = service,
        toSmithy4sClient = SimpleRestJsonFetchClient.lowLevelClient(_),
        client = Fetch.fetch(_),
        middleware = middleware,
        makeClientCodecs = codecs.makeClientCodecs(uri),
        isSuccessful = resp => resp.ok
      )
    )

  private def changeCodecs(
      f: SimpleRestJsonCodecs => SimpleRestJsonCodecs
  ): SimpleRestJsonFetchClient[Alg] =
    new SimpleRestJsonFetchClient(
      service,
      uri,
      middleware,
      f(codecs)
    )

}

object SimpleRestJsonFetchClient {
  type Client = RequestInfo => Promise[Response]

  def apply[Alg[_[_, _, _, _, _]]](
      service: smithy4s.Service[Alg],
      url: String
  ) =
    new SimpleRestJsonFetchClient(
      service = service,
      uri = new URL(url),
      codecs = SimpleRestJsonCodecs,
      middleware = Endpoint.Middleware.noop
    )

  private def lowLevelClient(fetch: Client) =
    new UnaryLowLevelClient[Promise, RequestInfo, Response] {
      override def run[Output](request: RequestInfo)(
          responseCB: Response => Promise[Output]
      ): Promise[Output] =
        fetch(request).`then`(resp => responseCB(resp))
    }
}

private[smithy4s_fetch] object SimpleRestJsonCodecs
    extends SimpleRestJsonCodecs(1024, false, false)

private[smithy4s_fetch] case class SimpleRestJsonCodecs(
    maxArity: Int,
    explicitDefaultsEncoding: Boolean,
    hostPrefixInjection: Boolean
) {
  private val hintMask =
    alloy.SimpleRestJson.protocol.hintMask

  def unsafeFromSmithy4sHttpMethod(
      method: smithy4s.http.HttpMethod
  ): org.scalajs.dom.HttpMethod =
    import smithy4s.http.HttpMethod.*
    import org.scalajs.dom.HttpMethod as FetchMethod
    method match
      case GET       => FetchMethod.GET
      case PUT       => FetchMethod.PUT
      case POST      => FetchMethod.POST
      case DELETE    => FetchMethod.DELETE
      case PATCH     => FetchMethod.PATCH
      case OTHER(nm) => nm.asInstanceOf[FetchMethod]

  def toHeaders(smithyHeaders: Map[CaseInsensitive, Seq[String]]): Headers = {

    val h = new Headers()

    smithyHeaders.foreach { case (name, values) =>
      values.foreach { value =>
        h.append(name.toString, value)
      }
    }

    h
  }

  def fromSmithy4sHttpUri(uri: smithy4s.http.HttpUri): String = {
    val qp = uri.queryParams
    val protocol = {
      uri.scheme match
        case Http  => "http"
        case Https => "https"
    }
    val hostName = uri.host
    val port =
      uri.port
        .filterNot(p => uri.host.endsWith(s":$p"))
        .map(":" + _.toString)
        .getOrElse("")

    val path = "/" + uri.path.mkString("/")
    val query =
      if qp.isEmpty then ""
      else
        var b = "?"
        qp.zipWithIndex.map:
          case ((key, values), idx) =>
            if idx != 0 then b += "&"
            b += key
            for
              i <- 0 until values.length
              value = values(i)
            do
              if i == 0 then b += "=" + value
              else b += s"&$key=$value"

        b

    s"$protocol://$hostName$port$path$query"
  }

  def toSmithy4sHttpResponse(
      resp: Response
  ): Promise[smithy4s.http.HttpResponse[Blob]] = {
    resp
      .arrayBuffer()
      .`then`: body =>
        val headers = Map.newBuilder[CaseInsensitive, Seq[String]]

        resp.headers.foreach:
          case arr if arr.size >= 2 =>
            val header = arr(0)
            val values = arr.tail.toSeq
            headers += CaseInsensitive(header) -> values
          case _ =>

        smithy4s.http.HttpResponse(
          resp.status,
          headers.result(),
          Blob(new Int8Array(body).toArray)
        )

  }

  def fromSmithy4sHttpRequest(
      req: smithy4s.http.HttpRequest[Blob]
  ): Request = {
    val m = unsafeFromSmithy4sHttpMethod(req.method)
    val h = toHeaders(req.headers)
    val ri = new RequestInit {}
    if (req.body.size != 0) {
      val arr = new Int8Array(req.body.size)
      arr.set(
        req.body.toArray.toJSArray,
        0
      )
      ri.body = arr
      h.append("Content-Length", req.body.size.toString)
    }

    ri.method = m
    ri.headers = h

    new Request(fromSmithy4sHttpUri(req.uri), ri)
  }

  def toSmithy4sHttpUri(
      uri: URL,
      pathParams: Option[smithy4s.http.PathParams] = None
  ): smithy4s.http.HttpUri = {
    import smithy4s.http.*
    val uriScheme = uri.protocol match {
      case "https:" => HttpUriScheme.Https
      case "http:"  => HttpUriScheme.Http
      case _ =>
        throw UnsupportedOperationException(
          s"Protocol `${uri.protocol}` is not supported"
        )
    }

    HttpUri(
      uriScheme,
      uri.host,
      uri.port.toIntOption,
      uri.pathname
        // drop the guaranteed leading slash, so that we don't produce an empty segment for it
        .tail
        // splitting an empty path would produce a single element, so we special-case to empty
        .match {
          case ""    => IndexedSeq.empty
          case other => other.split("/")
        },
      uri.searchParams
        .entries()
        .toIterator
        .toSeq
        .groupMap(_._1)(_._2)
        .toMap,
      pathParams
    )
  }

  val jsonCodecs = Json.payloadCodecs
    .withJsoniterCodecCompiler(
      Json.jsoniter
        .withHintMask(hintMask)
        .withMaxArity(maxArity)
        .withExplicitDefaultsEncoding(explicitNulls = true)
    )

  val payloadEncoders: BlobEncoder.Compiler =
    jsonCodecs.encoders

  val payloadDecoders =
    jsonCodecs.decoders

  val errorHeaders = List(
    smithy4s.http.errorTypeHeader
  )

  def makeClientCodecs(
      uri: URL
  ): UnaryClientCodecs.Make[Promise, RequestInfo, Response] = {
    val baseRequest = HttpRequest(
      HttpMethod.POST,
      toSmithy4sHttpUri(uri, None),
      Map.empty,
      Blob.empty
    )

    HttpUnaryClientCodecs.builder
      .withBodyEncoders(payloadEncoders)
      .withSuccessBodyDecoders(payloadDecoders)
      .withErrorBodyDecoders(payloadDecoders)
      .withErrorDiscriminator(resp =>
        Promise.resolve(HttpDiscriminator.fromResponse(errorHeaders, resp))
      )
      .withMetadataDecoders(Metadata.Decoder)
      .withMetadataEncoders(
        Metadata.Encoder.withExplicitDefaultsEncoding(
          explicitDefaultsEncoding
        )
      )
      .withBaseRequest(_ => Promise.resolve(baseRequest))
      .withRequestMediaType("application/json")
      .withRequestTransformation(req =>
        Promise.resolve(fromSmithy4sHttpRequest(req))
      )
      .withResponseTransformation[Response](resp =>
        Promise.resolve(toSmithy4sHttpResponse(resp))
      )
      .withHostPrefixInjection(hostPrefixInjection)
      .build()

  }
}

given MonadThrowLike[Promise] with
  override def map[A, B](fa: Promise[A])(f: A => B): Promise[B] = fa.`then`(f)

  override def flatMap[A, B](fa: Promise[A])(f: A => Promise[B]): Promise[B] =
    fa.`then`(f)

  override def handleErrorWith[A](fa: Promise[A])(
      f: Throwable => Promise[A]
  ): Promise[A] = fa.`catch`:
    case ex: Throwable => f(ex) // TODO: does this make sense?

  override def pure[A](a: A): Promise[A] = Promise.resolve(a)

  override def raiseError[A](e: Throwable): Promise[A] = Promise.reject(e)

  override def zipMapAll[A](seq: IndexedSeq[Promise[Any]])(
      f: IndexedSeq[Any] => A
  ): Promise[A] =
    Promise.all(seq.toJSIterable).`then`(res => Promise.resolve(f(res.toArray)))

  override def zipMap[A, B, C](fa: Promise[A], fb: Promise[B])(
      f: (A, B) => C
  ): Promise[C] = Promise
    .all[Either[A, B]](
      Seq(fa.`then`(Left(_)), fb.`then`(Right(_))).toJSIterable
    )
    .`then`: arr =>
      (arr(0), arr(1)) match
        case (Left(x), Right(y)) => f(x, y)
        case (Right(y), Left(x)) => f(x, y)
        case _                   => ???




© 2015 - 2024 Weber Informatics LLC | Privacy Policy