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:

  1. A parameter exists under the given name and the associated value can be parsed.
  2. A parameter exists under the given name and the associated value cannot be parsed.
  3. A parameter exists under the given name but there is no associated value.
  4. 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>"

Response→