summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--build.sbt7
-rw-r--r--src/main/scala/router.scala136
-rw-r--r--src/test/scala/basic.scala143
4 files changed, 288 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..007798c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+/project
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..bba2cfb
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,7 @@
+name := "scala-path-router"
+
+version := "0.01"
+
+scalaVersion := "2.10.0"
+
+libraryDependencies += "org.scalatest" % "scalatest_2.10" % "1.9.1" % "test"
diff --git a/src/main/scala/router.scala b/src/main/scala/router.scala
new file mode 100644
index 0000000..f1fbfa7
--- /dev/null
+++ b/src/main/scala/router.scala
@@ -0,0 +1,136 @@
+package router
+
+import scala.collection.mutable.ArrayBuffer
+import scala.util.matching.Regex
+
+class Router[T] {
+ val routes = new ArrayBuffer[Route[T]]()
+
+ def addRoute (
+ path: String,
+ target: T,
+ defaults: Map[String, String] = Map(),
+ validations: Map[String, Regex] = Map()
+ ) {
+ routes += new Route(path, defaults, validations, target)
+ }
+
+ def insertRoute (
+ path: String,
+ target: T,
+ defaults: Map[String, String] = Map(),
+ validations: Map[String, Regex] = Map(),
+ at: Int = 0
+ ) {
+ routes insert (
+ at min routes.length,
+ new Route(path, defaults, validations, target)
+ )
+ }
+
+ def route (path: String): Option[Match[T]] = {
+ def _route (
+ components: Seq[String],
+ routes: List[Route[T]]
+ ): Option[Match[T]] = routes match {
+ case r :: rs => r.route(components) match {
+ case Some(found) => Some(found)
+ case None => _route(components, rs)
+ }
+ case _ => None
+ }
+ _route(path.split("/"), routes.toList)
+ }
+
+ def uriFor (mapping: Map[String, String]): String = {
+ throw new Error("unimplemented")
+ }
+}
+
+class Route[T] (
+ val path: String,
+ val defaults: Map[String, String],
+ val validations: Map[String, Regex],
+ val target: T
+) {
+ def route(
+ parts: Seq[String],
+ components: Seq[String] = components,
+ mapping: Map[String, String] = defaults
+ ): Option[Match[T]] = {
+ if (components.length == 0 && parts.length == 0) {
+ Some(new Match[T](path, mapping, target))
+ }
+ else if (components.length == 0 || parts.length == 0) {
+ None
+ }
+ else {
+ components.head match {
+ case Optional(name) => {
+ throw new Error("unsupported")
+ }
+ case Variable(name) => {
+ if (validate(name, parts.head)) {
+ route(parts.tail, components.tail, mapping + (name -> parts.head))
+ }
+ else {
+ None
+ }
+ }
+ case literal => parts.head match {
+ case `literal` => route(parts.tail, components.tail, mapping)
+ case _ => None
+ }
+ }
+ }
+ }
+
+ override def toString = path
+
+ private lazy val components =
+ path.split("/").filter(_.length > 0)
+
+ private lazy val length =
+ components.length
+
+ private lazy val lengthWithoutOptionals =
+ components.filter(!isOptional(_)).length
+
+ private lazy val requiredVariableComponentNames =
+ for (c <- components if isVariable(c) && !isOptional(c))
+ yield getComponentName(c)
+
+ private lazy val optionalVariableComponentNames =
+ for (c <- components if isVariable(c) && isOptional(c))
+ yield getComponentName(c)
+
+ private val Optional = """^\?:(.*)$""".r
+ private val Variable = """^\??:(.*)$""".r
+
+ private def isOptional (component: String): Boolean = component match {
+ case Optional(_) => true
+ case _ => false
+ }
+
+ private def isVariable (component: String): Boolean = component match {
+ case Variable(_) => true
+ case _ => false
+ }
+
+ private def getComponentName (component: String) = component match {
+ case Variable(name) => Some(name)
+ case _ => None
+ }
+
+ private def validate (name: String, component: String): Boolean =
+ validations get name match {
+ case Some(rx) => rx.findFirstIn(component).nonEmpty
+ case None => true
+ }
+}
+
+class Match[T] (
+ val path: String,
+ val mapping: Map[String, String],
+ val target: T
+)
diff --git a/src/test/scala/basic.scala b/src/test/scala/basic.scala
new file mode 100644
index 0000000..15280e5
--- /dev/null
+++ b/src/test/scala/basic.scala
@@ -0,0 +1,143 @@
+import org.scalatest.FunSuite
+// import router.test._
+
+import router.Router
+
+class Basic extends FunSuite {
+ val yearRx = """\d{4}""".r
+ val monthRx = """\d|10|11|12""".r
+ val dayRx = """\d|[12]\d|30|31""".r
+
+ val router = new Router[Boolean]
+ router addRoute (
+ "blog/:year/:month/:day",
+ true,
+ defaults = Map(
+ "controller" -> "blog",
+ "action" -> "show_date"
+ ),
+ validations = Map(
+ "year" -> yearRx,
+ "month" -> monthRx,
+ "day" -> dayRx
+ )
+ )
+
+ router insertRoute (
+ "blog",
+ true,
+ defaults = Map(
+ "controller" -> "blog",
+ "action" -> "index"
+ )
+ )
+
+ router insertRoute (
+ "blog/:action/:id",
+ true,
+ at = 2,
+ defaults = Map(
+ "controller" -> "blog"
+ ),
+ validations = Map(
+ "action" -> """\D+""".r,
+ "id" -> """\d+""".r
+ )
+ )
+
+ router insertRoute (
+ "test/?:x/?:y",
+ true,
+ at = 1000000,
+ defaults = Map(
+ "controller" -> "test",
+ "x" -> "x",
+ "y" -> "y"
+ )
+ )
+
+ def testRoute (router: Router[Boolean], path: String, mapping: Map[String, String]) {
+ val om = router.route(path)
+ assert(om.isDefined)
+ val m = om.get
+ assert(m.mapping.size == mapping.size)
+ assert(m.mapping.forall { case (k, v) => mapping(k) == v })
+ assert(m.target === true)
+ }
+
+ test ("routes are created in the correct order") {
+ assert(router.routes(0).path === "blog")
+ assert(router.routes(2).path === "blog/:action/:id")
+ assert(router.routes(3).path === "test/?:x/?:y")
+ }
+
+ test ("routes match properly") {
+ testRoute(
+ router, "blog", Map(
+ "controller" -> "blog",
+ "action" -> "index"
+ )
+ )
+
+ testRoute(
+ router, "blog/2006/12/5", Map(
+ "controller" -> "blog",
+ "action" -> "show_date",
+ "year" -> "2006",
+ "month" -> "12",
+ "day" -> "5"
+ )
+ )
+
+ testRoute(
+ router, "blog/1920/12/10", Map(
+ "controller" -> "blog",
+ "action" -> "show_date",
+ "year" -> "1920",
+ "month" -> "12",
+ "day" -> "10"
+ )
+ )
+
+ testRoute(
+ router, "blog/edit/5", Map(
+ "controller" -> "blog",
+ "action" -> "edit",
+ "id" -> "5"
+ )
+ )
+
+ testRoute(
+ router, "blog/show/123", Map(
+ "controller" -> "blog",
+ "action" -> "show",
+ "id" -> "123"
+ )
+ )
+
+ testRoute(
+ router, "blog/some_crazy_long_winded_action_name/12356789101112131151", Map(
+ "controller" -> "blog",
+ "action" -> "some_crazy_long_winded_action_name",
+ "id" -> "12356789101112131151"
+ )
+ )
+
+ testRoute(
+ router, "blog/delete/5", Map(
+ "controller" -> "blog",
+ "action" -> "delete",
+ "id" -> "5"
+ )
+ )
+
+ // TODO support optionals
+ // testRoute(
+ // router, "test/x1", Map(
+ // "controller" -> "test",
+ // "x" -> "x1",
+ // "y" -> "y"
+ // )
+ // )
+ }
+}