Paths
import krop.all.{*, given}
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"
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[Int]("id")
Multiple parameters can be captured. This example captures an Int
and String
.
Path / "user" :? Query[Int]("id").and[String]("name")
There can be multiple parameters with the same name. How this is handled depends on the underlying QueryParam. A QueryParam
that captures only a single element, such as QueryParam.int
or QueryParam.string
, will only capture the first of multiple parameters. A QueryParam
that captures multiple elements, created with Query.all
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.all[Seq[String]]("name")
A parameter can be optional, which we can create with Query.optional
. Optional parameters don't cause a route to fail to match if the parameter is missing.
Path / "user" :? Query.optional[String]("name") // Returns Option[String]
To collect all the query parameters as a Map[String, List[String]]
use Query.everything
.
val everything = Query.everything
We can also construct a QueryParam
directly, which requires a name and a type parameter, similarly to working with Query
. The type parameter is used to find a given instance of StringCodec or SeqStringCodec, depending on the kind of QueryParam
that is being constructed. See Codecs for more on the codec types.
val param = QueryParam.one[Int]("id") // Looks for StringCodec
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 decoded.
- A parameter exists under the given name and the associated value cannot be decoded.
- 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.
import krop.all.*
val required = QueryParam.one[Int]("id")
val optional = QueryParam.optional[Int]("id")
required.decode(Map("id" -> List("1")))
optional.decode(Map("id" -> List("1")))
In the second case both required and optional query parameters fail.
required.decode(Map("id" -> List("abc")))
optional.decode(Map("id" -> List("abc")))
A required parameter will fail in the third case, but an optional parameter will succeed with None
.
required.decode(Map("id" -> List()))
optional.decode(Map("id" -> List()))
Similarly, a required parameter will fail in the fourth case but an optional parameter will succeed with None
.
required.decode(Map())
optional.decode(Map())
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.decode("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.all[Int]
intParams.encode(Seq(1, 2, 3))
The separatedString
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.separatedString("/")
subPath.decode(Vector("assets", "css"))
subPath.encode("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
The name is mostly 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
// Better, as the name has been changed appropriately.
intParam.withName("<Int>").name