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

endpoints4s.pekkohttp.server.Urls.scala Maven / Gradle / Ivy

There is a newer version: 2.0.1
Show newest version
package endpoints4s.pekkohttp.server

import org.apache.pekko.http.scaladsl.model.Uri

import java.net.URLEncoder
import java.nio.charset.StandardCharsets.UTF_8
import scala.collection.compat._
import org.apache.pekko.http.scaladsl.server._
import endpoints4s.algebra.Documentation
import endpoints4s.{Invalid, PartialInvariantFunctor, Tupler, Valid, Validated, algebra}

import scala.collection.mutable

/** [[algebra.Urls]] interpreter that decodes and encodes URLs.
  *
  * @group interpreters
  */
trait Urls extends algebra.Urls with StatusCodes {
  this: EndpointsWithCustomErrors =>

  trait Path[A] extends Url[A] {
    def validate(segments: List[String]): Option[(Validated[A], List[String])]
    final def validateUrl(
        path: List[String],
        query: Map[String, List[String]]
    ): Option[Validated[A]] =
      validate(path).flatMap {
        case (validA, Nil) => Some(validA)
        case (_, _)        => None
      }
    def path(a: A): Uri.Path
    final def uri(a: A): Uri = Uri.Empty.withPath(path(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] =
        new Path[B] {
          def validate(segments: List[String]): Option[(Validated[B], List[String])] =
            fa.validate(segments).map { case (validA, ss) =>
              (validA.flatMap(f), ss)
            }
          def path(b: B): Uri.Path = fa.path(g(b))
        }
      override def xmap[A, B](fa: Path[A], f: A => B, g: B => A): Path[B] =
        new Path[B] {
          def validate(segments: List[String]): Option[(Validated[B], List[String])] =
            fa.validate(segments).map { case (validA, ss) => (validA.map(f), ss) }
          def path(b: B): Uri.Path = fa.path(g(b))
        }
    }

  trait Url[A] { outer =>
    def validateUrl(
        segments: List[String],
        query: Map[String, List[String]]
    ): Option[Validated[A]]

    final def directive: Directive1[Validated[A]] = {
      (Directives.extractUri & Directives.path(
        Directives.Segments ~ Directives.Slash.?
      ) & Directives.parameterMultiMap)
        .tflatMap { case (uri, segments, query) =>
          val segmentsLeadingTrailingSlash = "" :: segments ++ {
            if (uri.path.endsWithSlash && uri.path != Uri.Path.SingleSlash)
              List("")
            else Nil
          }
          validateUrl(segmentsLeadingTrailingSlash, query) match {
            case None             => Directives.reject
            case Some(validatedA) => Directives.provide(validatedA)
          }
        }
    }
    def uri(a: A): Uri
  }

  trait QueryString[T] {
    def validate(params: Map[String, List[String]]): Validated[T]
    def encode(t: T): Uri.Query
  }

  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] = new QueryString[B] {
        def validate(params: Map[String, List[String]]): Validated[B] =
          fa.validate(params).flatMap(f)
        def encode(b: B): Uri.Query = fa.encode(g(b))
      }
      override def xmap[A, B](
          fa: QueryString[A],
          f: A => B,
          g: B => A
      ): QueryString[B] = new QueryString[B] {
        def validate(params: Map[String, List[String]]): Validated[B] = fa.validate(params).map(f)
        def encode(b: B): Uri.Query = fa.encode(g(b))
      }
    }

  /** Given a parameter name and a query string content, returns a decoded parameter
    * value of type `T`, or `Invalid` if decoding failed
    */
  trait QueryStringParam[T] {
    def decode(name: String, params: Map[String, Seq[String]]): Validated[T]
    def encode(name: String, value: T): Uri.Query
  }

  implicit lazy val queryStringParamPartialInvariantFunctor
      : PartialInvariantFunctor[QueryStringParam] =
    new PartialInvariantFunctor[QueryStringParam] {
      def xmapPartial[A, B](
          fa: QueryStringParam[A],
          f: A => Validated[B],
          g: B => A
      ): QueryStringParam[B] = new QueryStringParam[B] {
        def decode(name: String, params: Map[String, Seq[String]]): Validated[B] =
          fa.decode(name, params).flatMap(f)
        def encode(name: String, b: B): Uri.Query = fa.encode(name, g(b))
      }
      override def xmap[A, B](
          fa: QueryStringParam[A],
          f: A => B,
          g: B => A
      ): QueryStringParam[B] = new QueryStringParam[B] {
        def decode(name: String, params: Map[String, Seq[String]]): Validated[B] =
          fa.decode(name, params).map(f)
        def encode(name: String, b: B): Uri.Query = fa.encode(name, g(b))
      }
    }

  trait Segment[A] {
    def validate(s: String): Validated[A]
    def encode(a: A): Uri.Path.Segment
  }

  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] = new Segment[B] {
        def validate(s: String): Validated[B] = fa.validate(s).flatMap(f)
        def encode(b: B): Uri.Path.Segment = fa.encode(g(b))
      }
      override def xmap[A, B](
          fa: Segment[A],
          f: A => B,
          g: B => A
      ): Segment[B] = new Segment[B] {
        def validate(s: String): Validated[B] = fa.validate(s).map(f)
        def encode(b: B): Uri.Path.Segment = fa.encode(g(b))
      }
    }

  def urlWithQueryString[A, B](path: Path[A], qs: QueryString[B])(implicit
      tupler: Tupler[A, B]
  ): Url[tupler.Out] =
    new Url[tupler.Out] {
      def validateUrl(
          segments: List[String],
          query: Map[String, List[String]]
      ): Option[Validated[tupler.Out]] =
        path.validate(segments).flatMap {
          case (validA, Nil) => Some(validA.zip(qs.validate(query))(tupler))
          case (_, _)        => None
        }
      def uri(out: tupler.Out): Uri = {
        val (a, b) = tupler.unapply(out)
        path.uri(a).withQuery(qs.encode(b))
      }
    }

  //***************
  // Query strings
  //***************

  implicit lazy val stringQueryString: QueryStringParam[String] =
    new QueryStringParam[String] {
      def decode(name: String, params: Map[String, Seq[String]]): Validated[String] = {
        val maybeValue = params.get(name).flatMap(_.headOption)
        Validated.fromOption(maybeValue)("Missing value")
      }
      def encode(name: String, value: String): Uri.Query = Uri.Query(name -> value)
    }

  def qs[A](name: String, docs: Documentation)(implicit
      param: QueryStringParam[A]
  ): QueryString[A] = new QueryString[A] {
    def validate(params: Map[String, List[String]]): Validated[A] =
      param
        .decode(name, params)
        .mapErrors(
          _.map(error => s"$error for query parameter '$name'")
        )
    def encode(a: A): Uri.Query = param.encode(name, a)
  }

  type WithDefault[A] = A

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

  implicit def optionalQueryStringParam[A](implicit
      param: QueryStringParam[A]
  ): QueryStringParam[Option[A]] = new QueryStringParam[Option[A]] {
    def decode(name: String, params: Map[String, Seq[String]]): Validated[Option[A]] =
      params.get(name) match {
        case None    => Valid(None)
        case Some(_) => param.decode(name, params).map(Some(_))
      }
    def encode(name: String, value: Option[A]): Uri.Query =
      value match {
        case Some(a) => param.encode(name, a)
        case None    => Uri.Query.Empty
      }
  }

  implicit def repeatedQueryStringParam[A, CC[X] <: Iterable[X]](implicit
      param: QueryStringParam[A],
      factory: Factory[A, CC[A]]
  ): QueryStringParam[CC[A]] = new QueryStringParam[CC[A]] {
    def decode(name: String, params: Map[String, Seq[String]]): Validated[CC[A]] = {
      params.get(name) match {
        case None => Valid(factory.newBuilder.result())
        case Some(vs) =>
          vs.foldLeft[Validated[mutable.Builder[A, CC[A]]]](
            Valid(factory.newBuilder)
          ) {
            case (inv: Invalid, v) =>
              // Pretend that this was the query string and delegate to the `A` query string param
              param
                .decode(name, Map(name -> (v :: Nil)))
                .fold(_ => inv, errors => Invalid(inv.errors ++ errors))
            case (Valid(b), v) =>
              // Pretend that this was the query string and delegate to the `A` query string param
              param.decode(name, Map(name -> (v :: Nil))).map(b += _)
          }.map(_.result())
      }
    }
    def encode(name: String, as: CC[A]): Uri.Query =
      Uri.Query(as.flatMap(a => param.encode(name, a)).toSeq: _*)
  }

  def combineQueryStrings[A, B](first: QueryString[A], second: QueryString[B])(implicit
      tupler: Tupler[A, B]
  ): QueryString[tupler.Out] = new QueryString[tupler.Out] {
    def validate(params: Map[String, List[String]]): Validated[tupler.Out] =
      first.validate(params).zip(second.validate(params))
    def encode(out: tupler.Out): Uri.Query = {
      val (a, b) = tupler.unapply(out)
      Uri.Query(first.encode(a) ++ second.encode(b): _*)
    }
  }

  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] = new Url[B] {
        def validateUrl(
            segments: List[String],
            query: Map[String, List[String]]
        ): Option[Validated[B]] =
          fa.validateUrl(segments, query).map(_.flatMap(f))
        def uri(a: B): Uri = fa.uri(g(a))
      }

      override def xmap[A, B](fa: Url[A], f: A => B, g: B => A): Url[B] = new Url[B] {
        def validateUrl(
            segments: List[String],
            query: Map[String, List[String]]
        ): Option[Validated[B]] =
          fa.validateUrl(segments, query).map(_.map(f))
        def uri(a: B): Uri = fa.uri(g(a))
      }
    }

  // ********
  // Paths
  // ********

  implicit def stringSegment: Segment[String] = new Segment[String] {
    def validate(s: String): Validated[String] = Valid(s)
    def encode(s: String): Uri.Path.Segment = Uri.Path.Segment(s, Uri.Path.Empty)
  }

  def segment[A](name: String, docs: Documentation)(implicit
      s: Segment[A]
  ): Path[A] = new Path[A] {
    def validate(segments: List[String]): Option[(Validated[A], List[String])] = segments match {
      case head :: tail =>
        val validatedA =
          s.validate(head)
            .mapErrors(
              _.map(error => s"$error for segment${if (name.isEmpty) "" else s" '$name'"}")
            )
        Some((validatedA, tail))
      case Nil => None
    }
    def path(a: A): Uri.Path = s.encode(a)
  }

  def remainingSegments(name: String, docs: Documentation): Path[String] = new Path[String] {
    def validate(segments: List[String]): Option[(Validated[String], List[String])] = {
      if (segments.isEmpty) None
      else
        Some(
          (
            Valid(
              segments.map(URLEncoder.encode(_, UTF_8.name())).mkString("/")
            ),
            Nil
          )
        )
    }
    def path(segments: String): Uri.Path =
      if (segments.isEmpty) Uri.Path.Empty
      else Uri.Path(segments)
  }

  def staticPathSegment(segment: String): Path[Unit] = new Path[Unit] {
    def validate(segments: List[String]): Option[(Validated[Unit], List[String])] = {
      segments match {
        case `segment` :: tail => Some((Valid(()), tail))
        case _                 => None
      }
    }
    def path(a: Unit): Uri.Path =
      if (segment.isEmpty) Uri.Path.Empty
      else Uri.Path.Segment(segment, Uri.Path.Empty)
  }

  def chainPaths[A, B](first: Path[A], second: Path[B])(implicit
      tupler: Tupler[A, B]
  ): Path[tupler.Out] = new Path[tupler.Out] {
    def validate(p1: List[String]): Option[(Validated[tupler.Out], List[String])] =
      first.validate(p1).flatMap { case (validA, p2) =>
        second.validate(p2).map { case (validB, p3) =>
          (validA.zip(validB)(tupler), p3)
        }
      }
    def path(out: tupler.Out): Uri.Path = {
      val (a, b) = tupler.unapply(out)
      first.path(a) ++ Uri.Path.SingleSlash ++ second.path(b)
    }
  }

  /** Simpler alternative to `Directive.&()` method
    */
  protected def joinDirectives[T1, T2](
      dir1: Directive1[T1],
      dir2: Directive1[T2]
  )(implicit tupler: Tupler[T1, T2]): Directive1[tupler.Out] = {
    Directive[Tuple1[tupler.Out]] { inner =>
      dir1.tapply { case Tuple1(prefix) =>
        dir2.tapply { case Tuple1(suffix) =>
          inner(Tuple1(tupler(prefix, suffix)))
        }
      }
    }
  }

  protected def convToDirective1(directive: Directive0): Directive1[Unit] = {
    directive.tmap(_ => Tuple1(()))
  }

  /** This method is called by endpoints4s when decoding a request failed.
    *
    * The provided implementation calls `clientErrorsResponse` to complete
    * with a response containing the errors.
    *
    * This method can be overridden to customize the error reporting logic.
    */
  def handleClientErrors(invalid: Invalid): StandardRoute =
    StandardRoute(clientErrorsResponse(invalidToClientErrors(invalid)))

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy