Skip to content

Commit

Permalink
Greedy routers
Browse files Browse the repository at this point in the history
  • Loading branch information
vkostyukov committed Jan 29, 2015
1 parent 3f341bf commit a4daac6
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 23 deletions.
39 changes: 25 additions & 14 deletions core/src/main/scala/io/finch/route/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ package object route {
def apply(req: Req): Future[Rep] = {
val path = requestToRoute(req)
r(path) match {
case Some((_, service)) => service(req)
case Some((Nil, service)) => service(req)
case _ => RouteNotFound(req.path).toFutureException
}
}
Expand Down Expand Up @@ -174,10 +174,20 @@ package object route {
/**
* Sequentially composes this router with the given `that` router. The resulting
* router will succeed if either this or `that` routers are succeed.
*
* Router composition via `orElse` operator happens in a _greedy_ manner: it
* minimizes the output route tail. Thus, if both of the routers can handle
* the given `route` the router is being chosen is that which eats more.
*/
def orElse[B >: A](that: RouterN[B]): RouterN[B] = new RouterN[B] {
def apply(route: Route): Option[(Route, B)] =
self(route) orElse that(route)
def apply(route: Route): Option[(Route, B)] = (self(route), that(route)) match {
case (None, None) => None
case (a @ Some(_), None) => a
case (None, b @ Some(_)) => b
case (aa @ Some((a, _)), bb @ Some((b, _))) =>
if (a.length < b.length) aa else bb
}

override def toString = s"(${self.toString}|${that.toString})"
}

Expand Down Expand Up @@ -257,10 +267,20 @@ package object route {
/**
* Sequentially composes this router with the given `that` router. The resulting
* router will succeed if either this or `that` routers are succeed.
*
* Router composition via `orElse` operator happens in a _greedy_ manner: it
* minimizes the output route tail. Thus, if both of the routers can handle
* the given `route` the router is being chosen is that which eats more.
*/
def orElse(that: Router0): Router0 = new Router0 {
def apply(route: Route): Option[Route] =
self(route) orElse that(route)
def apply(route: Route): Option[Route] = (self(route), that(route)) match {
case (None, None) => None
case (a @ Some(_), None) => a
case (None, b @ Some(_)) => b
case (aa @ Some(a), bb @ Some(b)) =>
if (a.length < b.length) aa else bb
}

override def toString = s"(${self.toString}|${that.toString})"
}

Expand Down Expand Up @@ -355,15 +375,6 @@ package object route {
override def toString = "*"
}

/**
* A [[Router0]] that matches the end of the route.
*/
object $ extends Router0 {
def apply(route: Route): Option[Route] =
if (route.isEmpty) Some(Nil) else None
override def toString = ""
}

/**
* A router that extract an integer from the route.
*/
Expand Down
37 changes: 28 additions & 9 deletions core/src/test/scala/io/finch/route/RouterSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,15 @@ class RouterSpec extends FlatSpec with Matchers {
}

it should "be able to match the whole route" in {
val r1 = Get / * / $
val r2 = Get / * / * / * / * / $
r1(route) shouldBe None
val r1 = Get / *
val r2 = Get / * / * / * / *
r1(route) shouldBe Some(route.drop(2))
r2(route) shouldBe Some(Nil)
}

it should "support DSL for string and int extractors" in {
val r1 = Get / "a" / int / string
val r2 = Get / "a" / int("1") / "b" / int("2") / $
val r2 = Get / "a" / int("1") / "b" / int("2")

r1(route) shouldBe Some((route.drop(4), /(1, "b")))
r2(route) shouldBe Some((Nil, /(1, 2)))
Expand Down Expand Up @@ -149,17 +149,17 @@ class RouterSpec extends FlatSpec with Matchers {
}

it should "converts into a string" in {
val r1 = Get / $
val r1 = Get
val r2 = Get / "a" / true / 1
val r3 = Get / ("a" | "b") / int / long / string
val r4 = Get / string("name") / int("id") / boolean("flag") / "foo"
val r5 = Post / * / $
val r5 = Post / *

r1.toString shouldBe "GET/"
r1.toString shouldBe "GET"
r2.toString shouldBe "GET/a/true/1"
r3.toString shouldBe "GET/(a|b)/:int/:long/:string"
r4.toString shouldBe "GET/:name/:id/:flag/foo"
r5.toString shouldBe "POST/*/"
r5.toString shouldBe "POST/*"
}

it should "support the for-comprehension syntax" in {
Expand All @@ -169,7 +169,7 @@ class RouterSpec extends FlatSpec with Matchers {

r1(route) shouldBe Some((route.drop(3), "a1"))
r2(route) shouldBe Some((Nil, "b21"))
r3(route) shouldBe r1(route)
r3(route) shouldBe r2(route)
}

it should "be implicitly convertible into service from future" in {
Expand All @@ -181,4 +181,23 @@ class RouterSpec extends FlatSpec with Matchers {
Await.result(s(httpx.Request("/bar"))).contentString shouldBe "foo"
a [RouteNotFound] should be thrownBy Await.result(s(httpx.Request("/baz")))
}

it should "be greedy" in {
val a = List(PathToken("a"), PathToken("10"))
val b = List(PathToken("a"))

val r1 = "a" | ("a" / 10)
val r2 = ("a" / 10) | "a"
val r3 = ("a" / int) | ("a" /> 20)
val r4 = ("a" /> 20) | ("a" / int)

r1(a) shouldBe Some(Nil)
r1(b) shouldBe Some(Nil)
r2(a) shouldBe Some(Nil)
r2(b) shouldBe Some(Nil)
r3(a) shouldBe Some((Nil, 10))
r3(b) shouldBe Some((Nil, 20))
r4(a) shouldBe Some((Nil, 10))
r4(b) shouldBe Some((Nil, 20))
}
}

0 comments on commit a4daac6

Please sign in to comment.