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:120)
// 	at krop.route.Path.$div(Path.scala:89)
// 	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$14506/0x0000000102fba840@215c404e,
//       unparse = krop.route.Param$$$Lambda$14507/0x0000000102fba040@28a18091
//     )
//   )
// )
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$14506/0x0000000102fba840@215c404e,
//       unparse = krop.route.Param$$$Lambda$14507/0x0000000102fba040@28a18091
//     )
//   )
// )
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>"