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)
}
}