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:119)
// at krop.route.Path.$div(Path.scala:88)
// 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[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.
val required = QueryParam.one[Int]("id")
val optional = QueryParam.optional[Int]("id")
required.decode(Map("id" -> List("1")))
// res9: Either[QueryParseFailure, Int] = Right(1)
optional.decode(Map("id" -> List("1")))
// res10: Either[QueryParseFailure, Option[Int]] = Right(Some(1))
In the second case both required and optional query parameters fail.
required.decode(Map("id" -> List("abc")))
// res11: Either[QueryParseFailure, Int] = Left(
// ValueParsingFailed(name = "id", value = "abc", description = "<Int>")
// )
optional.decode(Map("id" -> List("abc")))
// res12: Either[QueryParseFailure, Option[Int]] = Left(
// ValueParsingFailed(name = "id", value = "abc", description = "<Int>")
// )
A required parameter will fail in the third case, but an optional parameter will succeed with None.
required.decode(Map("id" -> List()))
// res13: Either[QueryParseFailure, Int] = Left(NoValuesForName("id"))
optional.decode(Map("id" -> List()))
// res14: Either[QueryParseFailure, Option[Int]] = Right(None)
Similarly, a required parameter will fail in the fourth case but an optional parameter will succeed with None.
required.decode(Map())
// res15: Either[QueryParseFailure, Int] = Left(NoParameterWithName("id"))
optional.decode(Map())
// res16: Either[QueryParseFailure, Option[Int]] = Right(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.decode("100")
// res17: Either[DecodeFailure, Int] = Right(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))
// res18: Seq[String] = List("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"))
// res19: Either[DecodeFailure, String] = Right("assets/css")
subPath.encode("assets/css")
// res20: 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
// res21: String = "<String>"
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
// res22: String = "<String>"
// Better, as the name has been changed appropriately.
intParam.withName("<Int>").name
// res23: String = "<Int>"