PathCodec
PathCodec[A]
represents a codec for paths of type A
, comprising segments where each segment can be a literal, an integer, a long, a string, a UUID, or the trailing path.
Building PathCodecs
The PathCodec
data type offers several predefined codecs for common types:
PathCodec | Description |
---|---|
PathCodec.bool | A codec for a boolean path segment. |
PathCodec.empty | A codec for an empty path. |
PathCodec.literal | A codec for a literal path segment. |
PathCodec.long | A codec for a long path segment. |
PathCodec.string | A codec for a string path segment. |
PathCodec.uuid | A codec for a UUID path segment. |
Complex PathCodecs
can be constructed by combining them using the /
operator:
import zio.http.codec.PathCodec
import PathCodec._
val pathCodec = empty / "users" / int("user-id") / "posts" / string("post-id")
By combining PathCodec
values, the resulting PathCodec
type reflects the types of the path segments it matches. In the provided example, the type of pathCodec
is (Int, String)
because it matches a path with two segments of type Int
and String
, respectively.
Using Value Objects with PathCodecs
Other than the common PathCodec
constructors, it's also possible to transform a PathCodec
into a more specific data type using the transform
method.
This becomes particularly useful when adhering to domain-driven design principles and opting for value objects instead of primitive types:
import zio.http.codec.PathCodec
import PathCodec._
case class UserId private(value: Int)
object UserId {
def apply(value: Int): UserId =
if (value > 0)
new UserId(value)
else
throw new IllegalArgumentException("User id must be positive")
}
val userIdPathCodec: PathCodec[UserId] = int("user-id").transform(UserId.apply)(_.value)
This approach enables us to utilize the UserId
value object in our routes, and the PathCodec
will take care of the conversion between the path segment and the value object.
In the previous example, instead of throwing an exception, we can model the failure using the Either
data type and then use the transformOrFailLeft
to create a PathCodec
:
import zio.http.codec.PathCodec
import PathCodec._
case class UserId private(value: Int)
object UserId {
def apply(value: Int): Either[String, UserId] =
if (value > 0)
Right(new UserId(value))
else
Left("User id must be positive")
}
val userIdPathCodec: PathCodec[UserId] = int("user-id").transformOrFailLeft(UserId.apply)(_.value)
Here is a list of the available transformation methods:
trait PathCodec[A] {
def transform[A2](f: A => A2)(g: A2 => A): PathCodec[A2]
def transformOrFail[A2](f: A => Either[String, A2])(g: A2 => Either[String, A]): PathCodec[A2]
def transformOrFailLeft[A2](f: A => Either[String, A2])(g: A2 => A): PathCodec[A2]
def transformOrFailRight[A2](f: A => A2)(g: A2 => Either[String, A]): PathCodec[A2]
}
Here is a complete example:
import zio._
import zio.http._
import zio.Cause.{Die, Stackless}
import zio.http.codec.PathCodec
object Main extends ZIOAppDefault {
import zio.http.codec.PathCodec
import PathCodec._
case class UserId private (value: Int)
object UserId {
def apply(value: Int): Either[String, UserId] =
if (value > 0)
Right(new UserId(value))
else
Left("User id must be greater than zero")
}
val userId: PathCodec[UserId] = int("user-id").transformOrFailLeft(UserId.apply)(_.value)
val httpApp: HttpApp[Any] =
Routes(
Method.GET / "users" / userId ->
Handler.fromFunctionHandler[(UserId, Request)] { case (userId: UserId, request: Request) =>
Handler.text(userId.value.toString)
},
).handleErrorCause { case Stackless(cause, _) =>
cause match {
case Die(value, _) =>
if (value.getMessage == "User id must be greater than zero")
Response.badRequest(value.getMessage)
else
Response.internalServerError
}
}.toHttpApp
def run = Server.serve(httpApp).provide(Server.default)
}
Trailing Path Segments
Sometimes, there may be a need to match a path with a trailing segment, regardless of the number of segments it contains. This is where the trailing codec comes into play:
import zio._
import zio.http._
object TrailingExample extends ZIOAppDefault {
def staticFileHandler(path: Path): Handler[Any, Throwable, Request, Response] =
for {
file <- Handler.getResourceAsFile(path.encode)
http <-
if (file.isFile)
Handler.fromFile(file)
else
Handler.notFound
} yield http
val app =
Routes(
Method.GET / "static" / trailing ->
Handler.fromFunctionHandler[(Path, Request)] { case (path: Path, _: Request) =>
staticFileHandler(path).contramap[(Path, Request)](_._2)
},
).sandbox.toHttpApp @@ HandlerAspect.requestLogging()
val run = Server.serve(app).provide(Server.default)
}
In the provided example, if an incoming request matches the route pattern GET /static/*
, the trailing
codec will match the remaining path segments and bind them to the Path
type. Therefore, a request to /static/foo/bar/baz.txt
will match the route pattern, and the Path
will be foo/bar/baz.txt
.