Skip to content

Instantly share code, notes, and snippets.

@jroper
Created July 2, 2014 07:50
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jroper/f98de7f01740bd555a74 to your computer and use it in GitHub Desktop.
Save jroper/f98de7f01740bd555a74 to your computer and use it in GitHub Desktop.
Simple Play routing DSL with string interpolation
import java.util.regex.Pattern
import play.core.Routes
import play.api.mvc._
object Router extends Routes {
def routes = {
// Static paths
case Route("GET", p"") => controllers.Application.index
case Route("GET", p"/items") => controllers.Items.list
// A simple parameter
case Route("GET", p"/item/$id") => controllers.Items.get(id)
case Route("DELETE", p"/item/$id") => controllers.Items.delete(id)
// Non string parameters?
case Route("GET", p"/item/$id/child/${Id(child)}") => controllers.Items.getChild(id, child: Long)
// Regexes
case Route("GET", p"/item/$id/part/$part<[A-Z]>") => controllers.Items.getPart(id, part)
// multiple path part parameters
case Route("GET", p"/assets/$file*") => controllers.Assets.versioned(path = "/public", file: Asset)
}
// The magic is implemented here...
// A path extracting String interpolator
implicit class PathContext(sc: StringContext) {
val p = {
// "parse" the path
sc.parts.tail.map { part =>
if (part.startsWith("*")) {
// It's a .* matcher
"(.*)" + Pattern.quote(part.drop(1))
} else if (part.startsWith("<") && part.contains(">")) {
// It's a regex matcher
val splitted = part.split(">", 2)
val regex = splitted(0).drop(1)
"(" + regex + ")" + Pattern.quote(splitted(1))
} else {
// It's an ordinary path part matcher
"([^/]*)" + Pattern.quote(part)
}
}.mkString(Pattern.quote(sc.parts.head), "", "/?").r
}
}
// Extractor for routes
object Route {
def unapply(rh: RequestHeader) = {
if (rh.path.startsWith(prefix) {
Some(rh.method -> rh.path.drop(prefix.length))
} else None
}
}
// Extractor for Long ids
object Id {
def unapply(s: String) = try {
Some(s.toLong)
} catch {
case e: NumberFormatException => None
}
}
// routes boiler plate fluff
private var _prefix = ""
def prefix = _prefix
def setPrefix(prefix: String) = _prefix = prefix
def documentation = Nil
}
@aloiscochard
Copy link

@jroper how are gonna you support reverse routing with a routing table which is not statically inspect-able?

In other words, why not using a datastructure you can inspect instead of a partial function...

Also, here is a nice REST DSL, where resources are decoupled from the routing, might be of inspiration:
http://silkapp.github.io/rest/tutorial.html

Cheers

@adriaanm
Copy link

adriaanm commented Jul 2, 2014

Neat! Would it make sense to move the child: Long type ascription into the pattern? case Route("GET", p"/item/$id/child/${Id(child: Long)}").

A macro or a virtualized match could reify the match to generate the inverse.

@xcarpentier
Copy link

How do you implement conneg ?

@aloiscochard
Copy link

@adriaanm it feel a bit overkill to use meta-programming when you could actually avoid it :-)

@LeifW
Copy link

LeifW commented Dec 4, 2014

That routing reminds me of Finagle's. I like that you can pass in params to the actions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment