Created
July 2, 2014 07:50
-
-
Save jroper/f98de7f01740bd555a74 to your computer and use it in GitHub Desktop.
Simple Play routing DSL with string interpolation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
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.
How do you implement conneg ?
@adriaanm it feel a bit overkill to use meta-programming when you could actually avoid it :-)
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
@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