Paths
A Path represents a pattern to match against the path component of the request's URI. Paths
are created by calling the /
method on a Path
to add segments to the pattern. For example
Path / "user" / "create"
matches the path /user/create
.
To create a path without any segments you can use Path.root
.
Capturing Path Segments
Use a Param to capture part of the path for later use by the handler. For example
Path / "user" / Param.int / "view"
matches /user/<id>/view
, where <id>
is an Int
, and makes the Int
value available to the request handler.
Matching All Segments
A Path
will fail to match if the URI's path has more segments than the
Path
matches. So Path / "user" / "create"
will not match
/user/create/1234
. Use Segment.all
to match and ignore all the segments
to the end of the URI's path. For example
Path / "assets" / Segment.all
will match /assets/
, /assets/example.css
, and /assets/css/example.css
.
To capture all segments to the end of the URI's path, use an instance of
Param.All
such as Param.seq
. So
Path / "assets" / Param.seq
will capture the remainder of the URI's path as a Seq[String]
.
A Path
that matches all segments is called a closed path. Attempting to add an
element to a closed path will result in an exception.
Path / Segment.all / "crash"
// java.lang.IllegalStateException: Cannot add a segment or parameter to a closed path.
//
// A path is closed when it has a segment or parameter that matches all remaining elements.
// A closed path cannot have additional segments of parameters added to it.
// at krop.route.Path.assertOpen(Path.scala:121)
// at krop.route.Path.$div(Path.scala:90)
// at repl.MdocSession$MdocApp.$init$$$anonfun$1(paths.md:41)
Capturing Query Parameters
A Path
can also match and capture query parameters. For instance, the following path captures the query parameter id
as an Int
.
Path / "user" :? Query("id", Param.int)
Multiple parameters can be captured. This example captures an Int
and String
.
Path / "user" :? Query("id", Param.int).and("name", Param.string)
There can be multiple parameters with the same name. How this is handled depends on the underlying Param
. A Param
that captures only a single element, such as Param.int
or Param.string
, will only capture the first of multiple parameters. A Param
that captures multiple elements, such as Param.seq
will capture all the parameters with the given name. For example, this will capture all parameters called name
, producing a Seq[String]
.
Path / "user" :? Query("name", Param.seq)
A parameter can be optional. To indicate this we need to work directly with QueryParam, which has so far been hidden by convenience methods in the examples above.
Constructing a QueryParam
requires a name and a Param
, which is the same as we've seen above.
val param = QueryParam("id", Param.int)
We can also call the optional
constructor on the QueryParam
companion object to create an optional query parameter. Optional parameters don't cause a route to fail to match if the parameter is missing. Instead None
is returned.
val optional = QueryParam.optional("id", Param.int)
To collect all the query parameters as a Map[String, List[String]]
use QueryParam.all
.
val all = QueryParam.all
Query Parameter Semantics
Query parameter semantics can be quite complex. There are four cases to consider:
- A parameter exists under the given name and the associated value can be parsed.
- A parameter exists under the given name and the associated value cannot be parsed.
- A parameter exists under the given name but there is no associated value.
- No parameter exists under the given name.
The first case is the straightforward one where query parameter parsing always succeeds.
val required = QueryParam("id", Param.int)
val optional = QueryParam.optional("id", Param.int)
required.parse(Map("id" -> List("1")))
// res8: Either[QueryParseFailure, Int] = Right(value = 1)
optional.parse(Map("id" -> List("1")))
// res9: Either[QueryParseFailure, Option[Int]] = Right(
// value = Some(value = 1)
// )
In the second case both required and optional query parameters fail.
required.parse(Map("id" -> List("abc")))
// res10: Either[QueryParseFailure, Int] = Left(
// value = ValueParsingFailed(
// name = "id",
// value = "abc",
// param = One(
// name = "<Int>",
// parse = krop.route.Param$$$Lambda$14325/426657188@bb6f357,
// unparse = krop.route.Param$$$Lambda$14326/225835231@6bfbd694
// )
// )
// )
optional.parse(Map("id" -> List("abc")))
// res11: Either[QueryParseFailure, Option[Int]] = Left(
// value = ValueParsingFailed(
// name = "id",
// value = "abc",
// param = One(
// name = "<Int>",
// parse = krop.route.Param$$$Lambda$14325/426657188@bb6f357,
// unparse = krop.route.Param$$$Lambda$14326/225835231@6bfbd694
// )
// )
// )
A required parameter will fail in the third case, but an optional parameter will succeed with None
.
required.parse(Map("id" -> List()))
// res12: Either[QueryParseFailure, Int] = Left(
// value = NoValuesForName(name = "id")
// )
optional.parse(Map("id" -> List()))
// res13: Either[QueryParseFailure, Option[Int]] = Right(value = None)
Similarly, a required parameter will fail in the fourth case but an optional parameter will succeed with None
.
required.parse(Map())
// res14: Either[QueryParseFailure, Int] = Left(
// value = NoParameterWithName(name = "id")
// )
optional.parse(Map())
// res15: Either[QueryParseFailure, Option[Int]] = Right(value = None)
Params
There are a small number of predefined Param
instances on the
Param companion object. Constructing your own instances can
be done in several ways.
The imap
method transforms a Param[A]
into a Param[B]
by providing
functions A => B
and B => A
. This example constructs a Param[Int]
from the
built-in Param[String]
.
val intParam = Param.string.imap(_.toInt)(_.toString)
intParam.parse("100")
// res16: Either[ParamParseFailure, Int] = Right(value = 100)
A Param.One[A]
can be lifted to a Param.All[Seq[A]]
that uses the given
Param.One
for every element in the Seq
.
val intParams = Param.lift(intParam)
intParams.unparse(Seq(1, 2, 3))
// res17: Seq[String] = List("1", "2", "3")
The mkString
method can be used for a Param.All
that constructs a String
containing elements separated by a separator. For example, to accumulate a
sub-path we could use the following.
val subPath = Param.mkString("/")
subPath.parse(Vector("assets", "css"))
// res18: Either[ParamParseFailure, String] = Right(value = "assets/css")
subPath.unparse("assets/css")
// res19: Seq[String] = ArraySeq("assets", "css")
Finally, you can directly call the constructors for Param.One
and Param.All
.
Param Names
Params
have a String
name. This is, by convention, some indication of the type written within angle brackets. For example "<String>"
for a Param[String]
.
Param.string.name
// res20: String = "<String>"
The name is mosty used in development mode, to output useful debugging information. You can change the name of a Param
using the withName
method. It's good practice to set the name whenever you create a new Param
. For example, if deriving a new Param
from an existing one you should consider changing the name.
// Bad, as the name doesn't reflect the underlying type.
intParam.name
// res21: String = "<String>"
// Better, as the name has been changed appropriately.
intParam.withName("<Int>").name
// res22: String = "<Int>"