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

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

The newest version!
/*
 * 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 java.io.File

import zio._

import zio.http.Routes.ApplyContextAspect
import zio.http.codec.PathCodec

/**
 * An HTTP application is a collection of routes, all of whose errors have been
 * handled through conversion into HTTP responses.
 *
 * HTTP applications can be installed into a [[zio.http.Server]], which is
 * capable of using them to serve requests.
 */
final case class Routes[-Env, +Err](routes: Chunk[zio.http.Route[Env, Err]]) { self =>
  private var _tree: Routes.Tree[_] = null.asInstanceOf[Routes.Tree[_]]

  def @@[Env1 <: Env](aspect: Middleware[Env1]): Routes[Env1, Err] =
    aspect(self)

  def @@[Env0](aspect: HandlerAspect[Env0, Unit]): Routes[Env with Env0, Err] =
    aspect(self)

  def @@[Env0, Ctx <: Env](
    aspect: HandlerAspect[Env0, Ctx],
  )(implicit tag: Tag[Ctx]): Routes[Env0, Err] =
    self.transform(_ @@ aspect)

  def @@[Env0]: ApplyContextAspect[Env, Err, Env0] =
    new ApplyContextAspect[Env, Err, Env0](self)

  /**
   * Combines this HTTP application with the specified HTTP application. In case
   * of route conflicts, the routes in this HTTP application take precedence
   * over the routes in the specified HTTP application.
   */
  def ++[Env1 <: Env, Err1 >: Err](that: Routes[Env1, Err1]): Routes[Env1, Err1] =
    copy(routes = routes ++ that.routes)

  /**
   * Prepend the specified route.
   */
  def +:[Env1 <: Env, Err1 >: Err](route: zio.http.Route[Env1, Err1]): Routes[Env1, Err1] =
    copy(routes = route +: routes)

  /**
   * Appends the specified route.
   */
  def :+[Env1 <: Env, Err1 >: Err](route: zio.http.Route[Env1, Err1]): Routes[Env1, Err1] =
    copy(routes = routes :+ route)

  /**
   * Executes the HTTP application with the specified request input, returning
   * an effect that will either succeed or fail with a Response.
   */
  def apply(request: Request)(implicit ev: Err <:< Response): ZIO[Env, Response, Response] =
    runZIO(request)

  /**
   * Handles all typed errors in the routes by converting them into responses.
   * This method can be used to convert routes that do not handle their errors
   * into ones that do handle their errors.
   */
  def handleError(f: Err => Response)(implicit trace: Trace): Routes[Env, Nothing] =
    new Routes(routes.map(_.handleError(f)))

  def handleErrorZIO[Env1 <: Env](f: Err => ZIO[Env1, Nothing, Response])(implicit
    trace: Trace,
  ): Routes[Env1, Nothing] =
    new Routes(routes.map(_.handleErrorZIO(f)))

  /**
   * Handles all typed errors, as well as all non-recoverable errors, by
   * converting them into responses. This method can be used to convert routes
   * that do not handle their errors into ones that do handle their errors.
   */
  def handleErrorCause(f: Cause[Err] => Response)(implicit trace: Trace): Routes[Env, Nothing] =
    new Routes(routes.map(_.handleErrorCause(f)))

  /**
   * Handles all typed errors, as well as all non-recoverable errors, by
   * converting them into a ZIO effect that produces the response. This method
   * can be used to convert routes that do not handle their errors into ones
   * that do handle their errors.
   */
  def handleErrorCauseZIO(f: Cause[Err] => ZIO[Any, Nothing, Response])(implicit trace: Trace): Routes[Env, Nothing] =
    new Routes(routes.map(_.handleErrorCauseZIO(f)))

  /**
   * Allows the transformation of the Err type through an Effectful program
   * allowing one to build up Routes in Stages delegates to the Route.
   */
  def mapErrorZIO[Err1](fxn: Err => ZIO[Any, Err1, Response])(implicit trace: Trace): Routes[Env, Err1] =
    new Routes(routes.map(_.mapErrorZIO(fxn)))

  /**
   * Allows the transformation of the Err type through a function allowing one
   * to build up Routes in Stages delegates to the Route.
   */
  def mapError[Err1](fxn: Err => Err1): Routes[Env, Err1] =
    new Routes(routes.map(_.mapError(fxn)))

  def nest(prefix: PathCodec[Unit])(implicit trace: Trace): Routes[Env, Err] =
    new Routes(self.routes.map(_.nest(prefix)))

  /**
   * Handles all typed errors in the routes by converting them into responses,
   * taking into account the request that caused the error. This method can be
   * used to convert routes that do not handle their errors into ones that do
   * handle their errors.
   */
  def handleErrorRequest(f: (Err, Request) => Response)(implicit trace: Trace): Routes[Env, Nothing] =
    new Routes(routes.map(_.handleErrorRequest(f)))

  /**
   * Handles all typed errors in the routes by converting them into responses,
   * taking into account the request that caused the error. This method can be
   * used to convert routes that do not handle their errors into ones that do
   * handle their errors.
   */
  def handleErrorRequestCause(f: (Request, Cause[Err]) => Response)(implicit trace: Trace): Routes[Env, Nothing] =
    new Routes(routes.map(_.handleErrorRequestCause(f)))

  /**
   * Handles all typed errors, as well as all non-recoverable errors, by
   * converting them into a ZIO effect that produces the response, taking into
   * account the request that caused the error. This method can be used to
   * convert routes that do not handle their errors into ones that do handle
   * their errors.
   */
  def handleErrorRequestCauseZIO(f: (Request, Cause[Err]) => ZIO[Any, Nothing, Response])(implicit
    trace: Trace,
  ): Routes[Env, Nothing] =
    new Routes(routes.map(_.handleErrorRequestCauseZIO(f)))

  /**
   * Checks to see if the HTTP application may be defined at the specified
   * request input. Note that it is still possible for an HTTP application to
   * return a 404 Not Found response, which cannot be detected by this method.
   * This method only checks for the presence of a handler that handles the
   * method and path of the specified request.
   */
  def isDefinedAt(request: Request)(implicit ev: Err <:< Response): Boolean =
    tree(Trace.empty, ev).get(request.method, request.path).nonEmpty

  def provide[Env1 <: Env](env: Env1)(implicit tag: Tag[Env1]): Routes[Any, Err] =
    provideEnvironment(ZEnvironment(env))

  /**
   * Provides the specified environment to the HTTP application, returning a new
   * HTTP application that has no environmental requirements.
   */
  def provideEnvironment(env: ZEnvironment[Env]): Routes[Any, Err] =
    copy(routes = routes.map(_.provideEnvironment(env)))

  def run(request: Request)(implicit trace: Trace): ZIO[Env, Either[Err, Response], Response] = {

    class RouteFailure[+Err0](val err: Cause[Err0]) extends Throwable(null, null, true, false) {
      override def getMessage: String = err.unified.headOption.fold("")(_.message)

      override def getStackTrace(): Array[StackTraceElement] =
        err.unified.headOption.fold[Chunk[StackTraceElement]](Chunk.empty)(_.trace).toArray

      override def getCause(): Throwable =
        err.find { case Cause.Die(throwable, _) => throwable }
          .orElse(err.find { case Cause.Fail(value: Throwable, _) => value })
          .orNull

      override def toString =
        err.prettyPrint
    }
    var routeFailure: RouteFailure[Err] = null

    handleErrorCauseZIO { cause =>
      routeFailure = new RouteFailure(cause)
      ZIO.refailCause(Cause.die(routeFailure))
    }
      .apply(request)
      .mapErrorCause {
        case Cause.Die(value: RouteFailure[_], _) if value == routeFailure => routeFailure.err.map(Left(_))
        case cause                                                         => cause.map(Right(_))
      }
  }

  /**
   * A shortcut for `Server.install(routes) *> ZIO.never`
   */
  def serve[Env1 <: Env](implicit
    ev: Err <:< Response,
    trace: Trace,
    tag: EnvironmentTag[Env1],
  ): URIO[Env1 with Server, Nothing] = {
    Server.serve[Env1](self.handleError(_.asInstanceOf[Response]))
  }

  def run(
    method: Method = Method.GET,
    path: Path = Path.root,
    headers: Headers = Headers.empty,
    body: Body = Body.empty,
  )(implicit ev: Err <:< Response): ZIO[Env, Nothing, Response] =
    runZIO(Request(method = method, url = URL.root.path(path), headers = headers, body = body))

  /**
   * An alias for `apply`.
   */
  def runZIO(request: Request)(implicit ev: Err <:< Response): ZIO[Env, Nothing, Response] =
    toHandler(ev)(request)

  /**
   * Returns new routes that automatically translate all failures into
   * responses, using best-effort heuristics to determine the appropriate HTTP
   * status code, and attaching error details using the HTTP header `Warning`.
   */
  def sandbox(implicit trace: Trace): Routes[Env, Nothing] =
    Routes(routes.map(_.sandbox))

  /**
   * Returns a new HTTP application whose requests will be timed out after the
   * specified duration elapses.
   */
  def timeout(duration: Duration)(implicit trace: Trace): Routes[Env, Err] =
    self @@ Middleware.timeout(duration)

  /**
   * Converts the HTTP application into a request handler.
   */
  def toHandler(implicit ev: Err <:< Response): Handler[Env, Nothing, Request, Response] = {
    implicit val trace: Trace = Trace.empty
    Handler
      .fromFunctionHandler[Request] { req =>
        val chunk = tree.get(req.method, req.path)

        if (chunk.length == 0) Handler.notFound
        else if (chunk.length == 1) chunk(0)
        else {
          // TODO: Support precomputed fallback among all chunk elements:
          chunk.tail.foldLeft(chunk.head) { (acc, h) =>
            acc.catchAll { response =>
              if (response.status == Status.NotFound) h
              else Handler.fail(response)
            }
          }
        }
      }
      .merge
  }

  /**
   * Returns new Routes whose handlers are transformed by the specified
   * function.
   */
  def transform[Env1](
    f: Handler[Env, Response, Request, Response] => Handler[Env1, Response, Request, Response],
  ): Routes[Env1, Err] =
    new Routes(routes.map(_.transform(f)))

  /**
   * Accesses the underlying tree that provides fast dispatch to handlers.
   */
  def tree(implicit trace: Trace, ev: Err <:< Response): Routes.Tree[Env] = {
    if (_tree eq null) {
      _tree = Routes.Tree.fromRoutes(routes.asInstanceOf[Chunk[Route[Env, Response]]])
    }
    _tree.asInstanceOf[Routes.Tree[Env]]
  }
}

object Routes extends RoutesCompanionVersionSpecific {

  /**
   * An HTTP application that does not handle any routes.
   */
  val empty: Routes[Any, Nothing] = Routes(Chunk.empty)

  def apply[Env, Err](route: Route[Env, Err], routes: Route[Env, Err]*): Routes[Env, Err] =
    Routes(Chunk.fromIterable(route +: routes))

  def fromIterable[Env, Err](routes: Iterable[Route[Env, Err]]): Routes[Env, Err] =
    Routes(Chunk.fromIterable(routes))

  def singleton[Env, Err](h: Handler[Env, Err, (Path, Request), Response])(implicit trace: Trace): Routes[Env, Err] =
    Routes(Route.route(RoutePattern.any)(h))

  /**
   * Creates routes for serving static files from the directory `docRoot` at the
   * url path `path`.
   *
   * Example: `Routes.serveDirectory(Path.empty / "assets", new
   * File("/some/local/path"))`
   *
   * With this routes in place, a request to
   * `https://www.domain.com/assets/folder/file1.jpg` would serve the local file
   * `/some/local/path/folder/file1.jpg`.
   */
  def serveDirectory(path: Path, docRoot: File)(implicit trace: Trace): Routes[Any, Nothing] =
    empty @@ Middleware.serveDirectory(path, docRoot)

  /**
   * Creates routes for serving static files at URL path `path` from resources
   * with the given `resourcePrefix`.
   *
   * Example: `Routes.serveResources(Path.empty / "assets", "webapp")`
   *
   * With this routes in place, a request to
   * `https://www.domain.com/assets/folder/file1.jpg` would serve the file
   * `src/main/resources/webapp/folder/file1.jpg`. Note how the URL path is
   * removed and the resourcePrefix prepended.
   *
   * Most build systems support resources in the `src/main/resources` directory.
   * In the above example, the file `src/main/resources/webapp/folder/file1.jpg`
   * would be served.
   *
   * The `resourcePrefix` defaults to `"public"`. To prevent insecure sharing of
   * resource files, `resourcePrefix` must start with a `/` followed by at least
   * 1
   * [[java.lang.Character.isJavaIdentifierStart(x\$1:Char)* valid java identifier character]].
   * The `/` will be prepended if it is not present.
   */
  def serveResources(path: Path, resourcePrefix: String = "public")(implicit trace: Trace): Routes[Any, Nothing] =
    empty @@ Middleware.serveResources(path, resourcePrefix)

  private[http] final case class Tree[-Env](tree: RoutePattern.Tree[RequestHandler[Env, Response]]) { self =>
    final def ++[Env1 <: Env](that: Tree[Env1]): Tree[Env1] =
      Tree(self.tree ++ that.tree)

    final def add[Env1 <: Env](route: Route[Env1, Response])(implicit trace: Trace): Tree[Env1] =
      Tree(self.tree.addAll(route.routePattern.alternatives.map(alt => (alt, route.toHandler))))

    final def addAll[Env1 <: Env](routes: Iterable[Route[Env1, Response]])(implicit trace: Trace): Tree[Env1] =
      // only change to flatMap when Scala 2.12 is dropped
      Tree(self.tree.addAll(routes.map(r => r.routePattern.alternatives.map(alt => (alt, r.toHandler))).flatten))

    final def get(method: Method, path: Path): Chunk[RequestHandler[Env, Response]] =
      tree.get(method, path)
  }
  private[http] object Tree                                                                         {
    val empty: Tree[Any] = Tree(RoutePattern.Tree.empty)

    def fromRoutes[Env](routes: Chunk[zio.http.Route[Env, Response]])(implicit trace: Trace): Tree[Env] =
      empty.addAll(routes)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy