Skip to main content
Version: 2.0.x

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:

PathCodecDescription
PathCodec.boolA codec for a boolean path segment.
PathCodec.emptyA codec for an empty path.
PathCodec.literalA codec for a literal path segment.
PathCodec.longA codec for a long path segment.
PathCodec.stringA codec for a string path segment.
PathCodec.uuidA 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.