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

endpoints4s.sttp.client.Urls.scala Maven / Gradle / Ivy

The newest version!
package endpoints4s.sttp.client

import scala.collection.compat.Factory
import java.net.URLEncoder
import java.nio.charset.StandardCharsets.UTF_8

import endpoints4s.algebra.Documentation
import endpoints4s.{PartialInvariantFunctor, Tupler, Validated, algebra}

/** @group interpreters
  */
trait Urls extends algebra.Urls {
  val utf8Name = UTF_8.name()

  trait QueryString[A] {
    def encodeQueryString(a: A): Option[String]
  }

  implicit lazy val queryStringPartialInvariantFunctor: PartialInvariantFunctor[QueryString] =
    new PartialInvariantFunctor[QueryString] {
      def xmapPartial[A, B](
          fa: QueryString[A],
          f: A => Validated[B],
          g: B => A
      ): QueryString[B] =
        (b: B) => fa.encodeQueryString(g(b))
    }

  def combineQueryStrings[A, B](first: QueryString[A], second: QueryString[B])(implicit
      tupler: Tupler[A, B]
  ): QueryString[tupler.Out] =
    (ab: tupler.Out) => {
      val (a, b) = tupler.unapply(ab)

      (first.encodeQueryString(a), second.encodeQueryString(b)) match {
        case (Some(left), Some(right)) => Some(s"$left&$right")
        case (Some(left), None)        => Some(left)
        case (None, Some(right))       => Some(right)
        case (None, None)              => None
      }
    }

  def qs[A](name: String, docs: Documentation)(implicit
      param: QueryStringParam[A]
  ): QueryString[A] =
    a => {
      val params = param(a)
      if (params.isEmpty) None
      else Some(param(a).map(v => s"$name=$v").mkString("&"))
    }

  type WithDefault[A] = Option[A]

  override def optQsWithDefault[A](name: String, default: A, docs: Documentation = None)(implicit
      value: QueryStringParam[A]
  ): QueryString[WithDefault[A]] =
    qs(name, docs)(optionalQueryStringParam(value))

  /** a query string parameter can have zero or several values */
  type QueryStringParam[A] = A => List[String]

  implicit lazy val queryStringParamPartialInvariantFunctor
      : PartialInvariantFunctor[QueryStringParam] =
    new PartialInvariantFunctor[QueryStringParam] {
      def xmapPartial[A, B](
          fa: A => List[String],
          f: A => Validated[B],
          g: B => A
      ): B => List[String] =
        (b: B) => fa(g(b))
    }

  implicit def optionalQueryStringParam[A](implicit
      param: QueryStringParam[A]
  ): QueryStringParam[Option[A]] = {
    case Some(a) => param(a)
    case None    => Nil
  }

  implicit def repeatedQueryStringParam[A, CC[X] <: Iterable[X]](implicit
      param: QueryStringParam[A],
      factory: Factory[A, CC[A]]
  ): QueryStringParam[CC[A]] =
    as => as.iterator.flatMap(param).toList

  implicit lazy val stringQueryString: QueryStringParam[String] = s =>
    URLEncoder.encode(s, utf8Name) :: Nil

  trait Segment[A] {
    def encode(a: A): String
  }

  implicit lazy val segmentPartialInvariantFunctor: PartialInvariantFunctor[Segment] =
    new PartialInvariantFunctor[Segment] {
      def xmapPartial[A, B](
          fa: Segment[A],
          f: A => Validated[B],
          g: B => A
      ): Segment[B] = (b: B) => fa.encode(g(b))
    }

  implicit lazy val stringSegment: Segment[String] =
    Urls.encodeSegment(_)

  trait Path[A] extends Url[A]

  implicit lazy val pathPartialInvariantFunctor: PartialInvariantFunctor[Path] =
    new PartialInvariantFunctor[Path] {
      def xmapPartial[A, B](
          fa: Path[A],
          f: A => Validated[B],
          g: B => A
      ): Path[B] = (b: B) => fa.encode(g(b))
    }

  def staticPathSegment(segment: String) = (_: Unit) => segment

  def segment[A](name: String, docs: Documentation)(implicit
      s: Segment[A]
  ): Path[A] = a => s.encode(a)

  def remainingSegments(name: String, docs: Documentation): Path[String] =
    s => s

  def chainPaths[A, B](first: Path[A], second: Path[B])(implicit
      tupler: Tupler[A, B]
  ): Path[tupler.Out] =
    (ab: tupler.Out) => {
      val (a, b) = tupler.unapply(ab)
      s"${first.encode(a)}/${second.encode(b)}"
    }

  trait Url[A] {
    def encode(a: A): String
  }

  def urlWithQueryString[A, B](path: Path[A], qs: QueryString[B])(implicit
      tupler: Tupler[A, B]
  ): Url[tupler.Out] =
    (ab: tupler.Out) => {
      val (a, b) = tupler.unapply(ab)

      qs.encodeQueryString(b) match {
        case Some(q) => s"${path.encode(a)}?$q"
        case None    => path.encode(a)
      }
    }

  implicit lazy val urlPartialInvariantFunctor: PartialInvariantFunctor[Url] =
    new PartialInvariantFunctor[Url] {
      def xmapPartial[A, B](
          fa: Url[A],
          f: A => Validated[B],
          g: B => A
      ): Url[B] = (b: B) => fa.encode(g(b))
    }

}

private object Urls {
  val noEncodeChars = "-_.~:@!$&'()*+,;=".toCharArray().sorted
  val hexChars = "0123456789ABCDEF".toCharArray()

  def shouldEncode(c: Char): Boolean =
    if (
      (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      java.util.Arrays.binarySearch(noEncodeChars, c) >= 0
    ) false
    else true

  def encodeSegment(s: String): String = {
    val in = UTF_8.encode(s)
    val out = new StringBuilder(in.remaining() * 3)
    while (in.hasRemaining) {
      val c = in.get.toChar
      if (shouldEncode(c)) {
        out
          .append('%')
          .append(hexChars((c >> 4) & 0xf))
          .append(hexChars(c & 0xf))
      } else {
        out.append(c)
      }
    }
    out.result()
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy