olafurpg / tiny-router

Library to map ADTs to urls and urls back to ADTs

GitHub

tiny-router Join the chat at https://gitter.im/olafurpg/tiny-router Build Status

A small library (100 LOC, zero dependencies) to map an ADT to urls and urls back to ADTs. Works on Scala, Scala.js, 2.10 and 2.11.

libraryDependencies += "com.geirsson" %%% "tiny-router" % "latest.integration"

Example

It requires a bit of boilerplate to provide the implementation for each direction.

sealed abstract class Page
case object Dashboard extends Page
case class Edit(id: Int) extends Page
case class Update(from: Int, to: Float) extends Page

val router = {
  import tinyrouter.TinyRouter._
  tinyrouter.Router[Page](
    dynamic[Edit](x => s"edit/${x.id}") {
      case url"edit/${int(i)}" => Edit(i)
    },
    dynamic[Update](x => s"update/${x.from}/${x.to}") {
      case url"update/${int(from)}/${float(to)}" => Update(from, to)
    },
    static(Dashboard, "dashboard")
  )
}
val url  = router.toUrl(Edit(2))    // Some("edit/2")
val edit = router.fromUrl("edit/2") // Some(Edit(2))

You should only define one route per class of the ADT. The following will not work.

val brokenRouter = tinyrouter.Router[Page](
  dynamic[Edit](x => s"edit/${x.id}") {
    case url"edit/${int(i)}" => Edit(i)
  },
  dynamic[Edit](x => s"banana/${x.id}") {
    case url"banana/${int(i)}" => Edit(i)
  }
)

Testing

Use scalacheck to test that your router is well-behaved. Optionally, use scalacheck-shapeless to automatically generate arbitrary instances of your page ADT. Example,

// in build.sbt: libraryDependencies += "com.github.alexarchambault" %%% "scalacheck-shapeless_1.13" % "VERSION" % "test"
import org.scalacheck.Prop.forAll
import org.scalacheck.Properties
import org.scalacheck.Shapeless._

object RouterProperties extends Properties("Router") {
  property("router is comprehensive") = forAll { page: Page =>
    router.toUrl(page).isDefined
  }
  property("routes are bijective") = forAll { page: Page =>
    val url = router.toUrl(page).get
    val pageFromUrl = router.fromUrl(url).get
    page == pageFromUrl
  }
}

Alternatives

Credits

The url extractor implementation is mostly borrowed and adapted from the awesome Playframework String Interpolating Routing DSL.