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

zio.http.RoutePattern.scala Maven / Gradle / Ivy

/*
 * Copyright 2023 the ZIO HTTP contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package zio.http

import scala.collection.immutable.ListMap
import scala.language.implicitConversions

import zio._

import zio.http.codec._

/**
 * A pattern for matching routes that examines both HTTP method and path. In
 * addition to specifying a method, patterns contain segment patterns, which are
 * sequences of literals, integers, longs, and other segment types.
 *
 * Typically, your entry point constructor for a route pattern would be
 * [[zio.http.Method]]:
 *
 * {{{
 * import zio.http.Method
 * import zio.http.codec.SegmentCodec._
 *
 * val pattern = Method.GET / "users" / int("user-id") / "posts" / string("post-id")
 * }}}
 *
 * However, you can use the convenience constructors in `RoutePattern`, such as
 * `RoutePattern.GET`.
 */
final case class RoutePattern[A](method: Method, pathCodec: PathCodec[A]) { self =>
  type Params = A

  /**
   * Attaches documentation to the route pattern, which may be used when
   * generating developer docs for a route.
   */
  def ??(doc: Doc): RoutePattern[A] = copy(pathCodec = pathCodec ?? doc)

  /**
   * Returns a new pattern that is extended with the specified segment pattern.
   */
  def /[B](that: PathCodec[B])(implicit combiner: Combiner[A, B]): RoutePattern[combiner.Out] =
    copy(pathCodec = pathCodec ++ that)

  /**
   * Creates a route from this pattern and the specified handler.
   */
  def ->[Env, Err, I](handler: Handler[Env, Err, I, Response])(implicit
    zippable: RequestHandlerInput[A, I],
    trace: zio.Trace,
  ): Route[Env, Err] =
    Route.route(self)(handler)(zippable.zippable, trace)

  /**
   * Creates a route from this pattern and the specified handler, which ignores
   * any parameters produced by this route pattern. This method exists for
   * performance reasons, as it avoids all overhead of propagating parameters or
   * supporting contextual middleware.
   */
  def ->[Env, Err](handler: Handler[Env, Response, Request, Response])(implicit
    trace: zio.Trace,
  ): Route[Env, Err] =
    Route.handledIgnoreParams(self)(handler)

  def alternatives: List[RoutePattern[A]] = pathCodec.alternatives.map(RoutePattern(method, _))

  /**
   * Reinteprets the type parameter, given evidence it is equal to some other
   * type.
   */
  def asType[B](implicit ev: A =:= B): RoutePattern[B] = self.asInstanceOf[RoutePattern[B]]

  /**
   * Decodes a method and path into a value of type `A`.
   */
  def decode(actual: Method, path: Path): Either[String, A] =
    if (!method.matches(actual)) {
      Left(s"Expected HTTP method ${method} but found method ${actual}")
    } else pathCodec.decode(path)

  /**
   * Returns the documentation for the route pattern, if any.
   */
  def doc: Doc = pathCodec.doc

  /**
   * Encodes a value of type `A` into the method and path that this route
   * pattern would successfully match against.
   */
  def encode(value: A): Either[String, (Method, Path)] = format(value).map((method, _))

  /**
   * Formats a value of type `A` into a path. This is useful for embedding paths
   * into HTML that is rendered by the server.
   */
  def format(value: A): Either[String, Path] = pathCodec.format(value)

  /**
   * Determines if this pattern matches the specified method and path. Rather
   * than use this method, you should just try to decode it directly, for higher
   * performance, otherwise the same information will be decoded twice.
   */
  def matches(method: Method, path: Path): Boolean = decode(method, path).isRight

  def nest(prefix: PathCodec[Unit]): RoutePattern[A] =
    copy(pathCodec = prefix ++ pathCodec)

  /**
   * Renders the route pattern as a string.
   */
  def render: String =
    s"${method.render} ${pathCodec.render}"

  /**
   * Converts the route pattern into an HttpCodec that produces the same value.
   */
  def toHttpCodec: HttpCodec[HttpCodecType.Path with HttpCodecType.Method, A] =
    MethodCodec.method(method) ++ HttpCodec.Path(pathCodec)

  override def toString: String = render

  /**
   * This exists for use with Scala custom extractor syntax, allowing route
   * patterns to match against and deconstruct tuples of methods and paths.
   */
  def unapply(tuple: (Method, Path)): Option[A] =
    decode(tuple._1, tuple._2).toOption
}
object RoutePattern                                                       {
  import PathCodec.SegmentSubtree

  val CONNECT: RoutePattern[Unit] = fromMethod(Method.CONNECT)
  val DELETE: RoutePattern[Unit]  = fromMethod(Method.DELETE)
  val GET: RoutePattern[Unit]     = fromMethod(Method.GET)
  val HEAD: RoutePattern[Unit]    = fromMethod(Method.HEAD)
  val OPTIONS: RoutePattern[Unit] = fromMethod(Method.OPTIONS)
  val PATCH: RoutePattern[Unit]   = fromMethod(Method.PATCH)
  val POST: RoutePattern[Unit]    = fromMethod(Method.POST)
  val PUT: RoutePattern[Unit]     = fromMethod(Method.PUT)
  val TRACE: RoutePattern[Unit]   = fromMethod(Method.TRACE)

  /**
   * Constructs a route pattern from a method.
   */
  def fromMethod(method: Method): RoutePattern[Unit] = RoutePattern(method, PathCodec.empty)

  /**
   * A tree of route patterns, indexed by method and path.
   */
  private[http] final case class Tree[+A](roots: ListMap[Method, SegmentSubtree[A]]) { self =>
    def ++[A1 >: A](that: Tree[A1]): Tree[A1] =
      Tree(mergeMaps(self.roots, that.roots)(_ ++ _))

    def add[A1 >: A](routePattern: RoutePattern[_], value: A1): Tree[A1] =
      self ++ Tree(routePattern, value)

    def addAll[A1 >: A](pathPatterns: Iterable[(RoutePattern[_], A1)]): Tree[A1] =
      pathPatterns.foldLeft[Tree[A1]](self) { case (tree, (p, v)) =>
        tree.add(p, v)
      }

    def get(method: Method, path: Path): Chunk[A] = {
      val wildcards = roots.get(Method.ANY) match {
        case None        => Chunk.empty
        case Some(value) => value.get(path)
      }

      (roots.get(method) match {
        case None        => Chunk.empty
        case Some(value) => value.get(path)
      }) ++ wildcards
    }

    def map[B](f: A => B): Tree[B] =
      Tree(roots.map { case (k, v) =>
        k -> v.map(f)
      })
  }
  private[http] object Tree                                                          {
    def apply[A](routePattern: RoutePattern[_], value: A): Tree[A] = {
      val segments = routePattern.pathCodec.segments

      val subtree = SegmentSubtree.single(segments, value)

      Tree(ListMap(routePattern.method -> subtree))
    }

    val empty: Tree[Nothing] = Tree(ListMap.empty)
  }

  private def mergeMaps[A, B](left: ListMap[A, B], right: ListMap[A, B])(f: (B, B) => B): ListMap[A, B] =
    right.foldLeft(left) { case (acc, (k, v)) =>
      val old = acc.get(k)

      old match {
        case None     => acc.updated(k, v)
        case Some(v0) => acc.updated(k, f(v0, v))
      }
    }

  /**
   * The any pattern matches any method and any path. It is unlikely you need to
   * use this pattern, because it would preclude the use of any other route (at
   * least, unless listed as the final route in a collection of routes).
   */
  val any: RoutePattern[Path] = RoutePattern(Method.ANY, PathCodec.trailing)

  def apply(method: Method, path: Path): RoutePattern[Unit] =
    path.segments.foldLeft[RoutePattern[Unit]](fromMethod(method)) { (pathSpec, segment) =>
      pathSpec./[Unit](PathCodec.Segment(SegmentCodec.literal(segment)))
    }

  /**
   * Constructs a route pattern from a method and a path literal. To match
   * against any method, use [[zio.http.Method.ANY]]. The specified string may
   * contain path segments, which are separated by slashes.
   */
  def apply(method: Method, pathString: String): RoutePattern[Unit] =
    apply(method, Path(pathString))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy