January 2021

Trail User Manual

Tim Nieradzik

sparse.tech

Abstract: Routing library for the Scala platform

Introduction

Trail is a routing library for Scala. It allows defining type-safe routes, generating URLs and performing pattern matching.

Installation

Add the following dependencies to your build configuration:

libraryDependencies += "tech.sparse" %%  "trail" % "0.3.1"  // Scala
libraryDependencies += "tech.sparse" %%% "trail" % "0.3.1"  // Scala.js, Scala Native

Usage

First, import Trail's DSL and define a type-safe route:

import trail._
val details = Root / "details" / Arg[Int]

To fill the route's placeholders, call the url() or apply() functions:

println(details.url(1))
println(details(1))
Output:
/details/1
/details/1

When parsing an URL, Trail maps the values onto Scala types. The result will also contain any unmatched path elements, arguments or the fragment:

println(details.parse("/details/42"))
println(details.parse("/details/42/sub-page?name=value"))
println(details.parse("/details/42/sub-page?name=value#frag"))
Output:
Some((42,Path(,List(),None)))
Some((42,Path(sub-page,List((name,value)),None)))
Some((42,Path(sub-page,List((name,value)),Some(frag))))

We will now define a route with one query parameter. Here, we are only interested in the arguments and exact path matches. For this use case, Trail provides the function parseArgs():

val route = Root / "details" & Param[Boolean]("show")
println(route.parseArgs("/details/sub-page"))
println(route.parseArgs("/details?show=false"))
println(route.parseArgs("/details?show=false&a=b"))
println(route.parseArgs("/details#frag"))
Output:
None
Some(false)
Some(false)
None

The output shows that additional arguments are still permitted. If this is undesired, you can call parseArgsStrict() to parse a route more strictly:

println(route.parseArgsStrict("/details/sub-page"))
println(route.parseArgsStrict("/details?show=false"))
println(route.parseArgsStrict("/details?show=false&a=b"))
println(route.parseArgsStrict("/details#frag"))
Output:
None
Some(false)
None
None

Routes may specify optional query parameters:

val routeParamsOpt = Root / "details" & Param[Int]("id") & Param[Option[Boolean]]("show")
println(routeParamsOpt.parseArgs("/details?id=42"))
Output:
Some((42,None))

You can match fragments, too:

val routeFragment = Root $ Fragment[Int]
println(routeFragment.parseArgs("/#42"))
Output:
Some(42)

Since parseArgs() disallows additional path elements, you can match them only on specific routes using Elems. It should be the last DSL combinator in the route definition:

val routeAdditionalElems = Root / "a" / Elems
println(routeAdditionalElems.parseArgs("/a/b/c"))
Output:
Some(List(b, c))

Similarly, additional parameters can be matched with Params:

val routeAdditionalParams = Root / Arg[String] & Params
println(routeAdditionalParams.parseArgs("/a?param1=value1&param2=value2"))
Output:
Some((a,List((param1,value1), (param2,value2))))

Routing tables can be expressed with pattern matching:

val userInfo = Root / "user" / Arg[String] & Param[Boolean]("show")

val result = "/user/hello?show=false" match {
  case details (a)      => s"details: $a"
  case userInfo((u, s)) => s"user: $u, show: $s"
}
println(result)
Output:
user: hello, show: false

The underlying unapply() function calls parseArgs() instead of parse(). Therefore, the ordering of routes does not impact precedence.

You may populate the trail.Path() data structure yourself and use it in place of an URL. This is useful if an HTTP server already provides the path and arguments of requests:

val (requestPath, requestParams) = ("/user/hello", List("show" -> "false"))
val result2 = trail.Path(requestPath, requestParams) match {
  case details (a)      => s"details: $a"
  case userInfo((u, s)) => s"user: $u, show: $s"
}
println(result2)
Output:
user: hello, show: false

Trail defines codecs for common Scala types. You can define a custom codec for any type. These codecs can be used in arguments, parameters and fragments:

import scala.util.Try
implicit case object IntSetCodec extends Codec[Set[Int]] {
  override def encode(s: Set[Int]): Option[String] = Some(s.mkString(","))
  override def decode(s: Option[String]): Option[Set[Int]] =
    s.flatMap(value =>
      if (value.isEmpty) Some(Set())
      else Try(value.split(',').map(_.toInt).toSet).toOption)
}

val export = Root / "export" / Arg[Set[Int]]
println(export.url(Set(1, 2, 3)))
Output:
/export/1,2,3

It is possible to define a custom path element type, too:

case class Foo(bar: String)
implicit object FooElement extends StaticElement[Foo](_.bar)

println((Root / Foo("asdf")).url(()))
Output:
/asdf

Trail provides helper utilities, for example to encode and decode URI values:

val encoded = URI.encode("äöü")
println(encoded)
println(URI.decode(encoded))
Output:
%C3%A4%C3%B6%C3%BC
äöü

Edit chapter ⤴

Development

Manual

Run the following command to generate the listings for the manual:

$ sbt manual/run

The manual was generated using Instructor. Follow its installation instructions, then run the following command:

instructor manual.toml
Edit chapter ⤴

Support

Bugs/feature requests

Please use our issue tracker to file bugs and feature requests.

Discussions

For discussions and general questions, please join our Gitter channel instead:

Edit chapter ⤴

Generated with Instructor v0.1-SNAPSHOT