diff --git a/zio-http/src/main/scala/zio/http/Route.scala b/zio-http/src/main/scala/zio/http/Route.scala index 4592f923c..6e3fae680 100644 --- a/zio-http/src/main/scala/zio/http/Route.scala +++ b/zio-http/src/main/scala/zio/http/Route.scala @@ -17,6 +17,8 @@ package zio.http import zio._ +import zio.http.codec.PathCodec + /* * Represents a single route, which has either handled its errors by converting * them into responses, or which has polymorphic errors, which must later be @@ -207,6 +209,16 @@ sealed trait Route[-Env, +Err] { self => */ def location: Trace + def nest(prefix: PathCodec[Unit])(implicit ev: Err <:< Response): Route[Env, Err] = + self match { + case Provided(route, env) => Provided(route.nest(prefix), env) + case Augmented(route, aspect) => Augmented(route.nest(prefix), aspect) + case Handled(routePattern, handler, location) => Handled(routePattern.nest(prefix), handler, location) + + case Unhandled(rpm, handler, zippable, location) => + Unhandled(rpm.prefix(prefix), handler, zippable, location) + } + final def provideEnvironment(env: ZEnvironment[Env]): Route[Any, Err] = Route.Provided(self, env) @@ -316,6 +328,16 @@ object Route { Route.route[A, Env1](self)(handler) } + def prefix(path: PathCodec[Unit]): Builder[Env, A] = + new Builder[Env, A] { + type PathInput = self.PathInput + type Context = self.Context + + def routePattern: RoutePattern[PathInput] = self.routePattern.nest(path) + def aspect: HandlerAspect[Env, Context] = self.aspect + def zippable: Zippable.Out[PathInput, Context, A] = self.zippable + } + def provideEnvironment(env: ZEnvironment[Env]): Route.Builder[Any, A] = { implicit val z = zippable diff --git a/zio-http/src/main/scala/zio/http/RoutePattern.scala b/zio-http/src/main/scala/zio/http/RoutePattern.scala index 932605e0b..f54cc50c9 100644 --- a/zio-http/src/main/scala/zio/http/RoutePattern.scala +++ b/zio-http/src/main/scala/zio/http/RoutePattern.scala @@ -122,6 +122,9 @@ final case class RoutePattern[A](method: Method, pathCodec: PathCodec[A]) { self */ 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. */ diff --git a/zio-http/src/main/scala/zio/http/Routes.scala b/zio-http/src/main/scala/zio/http/Routes.scala index a13d4f811..b8b953c87 100644 --- a/zio-http/src/main/scala/zio/http/Routes.scala +++ b/zio-http/src/main/scala/zio/http/Routes.scala @@ -17,6 +17,8 @@ package zio.http import zio._ +import zio.http.codec.PathCodec + /** * Represents a collection of routes, each of which is defined by a pattern and * a handler. This data type can be thought of as modeling a routing table, @@ -90,6 +92,9 @@ final class Routes[-Env, +Err] private (val routes: Chunk[zio.http.Route[Env, Er def handleErrorCauseZIO(f: Cause[Err] => ZIO[Any, Nothing, Response])(implicit trace: Trace): Routes[Env, Nothing] = new Routes(routes.map(_.handleErrorCauseZIO(f))) + def nest(prefix: PathCodec[Unit])(implicit trace: Trace, ev: Err <:< Response): 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 diff --git a/zio-http/src/main/scala/zio/http/codec/PathCodec.scala b/zio-http/src/main/scala/zio/http/codec/PathCodec.scala index 4cea864b5..dbb3fb470 100644 --- a/zio-http/src/main/scala/zio/http/codec/PathCodec.scala +++ b/zio-http/src/main/scala/zio/http/codec/PathCodec.scala @@ -21,7 +21,7 @@ import scala.language.implicitConversions import zio._ -import zio.http.Path +import zio.http._ /** * A codec for paths, which consists of segments, where each segment may be a @@ -48,6 +48,11 @@ sealed trait PathCodec[A] { self => final def /[B](that: PathCodec[B])(implicit combiner: Combiner[A, B]): PathCodec[combiner.Out] = self ++ that + final def /[Env](routes: Routes[Env, Response])(implicit + ev: PathCodec[A] <:< PathCodec[Unit], + ): Routes[Env, Response] = + routes.nest(ev(self)) + final def asType[B](implicit ev: A =:= B): PathCodec[B] = self.asInstanceOf[PathCodec[B]] /** diff --git a/zio-http/src/test/scala/zio/http/RouteSpec.scala b/zio-http/src/test/scala/zio/http/RouteSpec.scala index 8a1aa26a0..ab24e57f1 100644 --- a/zio-http/src/test/scala/zio/http/RouteSpec.scala +++ b/zio-http/src/test/scala/zio/http/RouteSpec.scala @@ -23,6 +23,16 @@ object RouteSpec extends ZIOHttpSpec { def extractStatus(response: Response): Status = response.status def spec = suite("RouteSpec")( + suite("Route#prefix")( + test("prefix should add a prefix to the route") { + val route = + Method.GET / "foo" -> handler(Response.ok) + + val prefixed = route.nest("bar") + + assertTrue(prefixed.isDefinedAt(Request.get(url"/bar/foo"))) + }, + ), suite("Route#sandbox")( test("infallible route does not change under sandbox") { val route =