diff --git a/cask/src/cask/internal/DispatchTrie.scala b/cask/src/cask/internal/DispatchTrie.scala index cc9f7e7252..ab9d610b4a 100644 --- a/cask/src/cask/internal/DispatchTrie.scala +++ b/cask/src/cask/internal/DispatchTrie.scala @@ -4,28 +4,44 @@ object DispatchTrie{ def construct[T, V](index: Int, inputs: collection.Seq[(collection.IndexedSeq[String], T, Boolean)]) (validationGroups: T => Seq[V]): DispatchTrie[T] = { - val continuations = mutable.Map.empty[String, mutable.Buffer[(collection.IndexedSeq[String], T, Boolean)]] - + val continuations = mutable.Map.empty[String, (String, mutable.Buffer[(collection.IndexedSeq[String], T, Boolean)])] val terminals = mutable.Buffer.empty[(collection.IndexedSeq[String], T, Boolean)] for((path, endPoint, allowSubpath) <- inputs) { if (path.length < index) () // do nothing - else if (path.length == index) { - terminals.append((path, endPoint, allowSubpath)) - } else if (path.length > index){ - val buf = continuations.getOrElseUpdate(path(index), mutable.Buffer.empty) - buf.append((path, endPoint, allowSubpath)) + else if (path.length == index) terminals.append((path, endPoint, allowSubpath)) + else if (path.length > index){ + val segment = path(index) + val buf = continuations.getOrElseUpdate( + if (segment.startsWith(":")) ":" else segment, + (segment, mutable.Buffer.empty) + ) + buf._2.append((path, endPoint, allowSubpath)) } } - for(group <- inputs.flatMap(t => validationGroups(t._2)).distinct) { - val groupTerminals = terminals.flatMap{case (path, v, allowSubpath) => + validateGroups(inputs, terminals, continuations.map{case (k, (k2, v)) => (k, v)})(validationGroups) + + DispatchTrie[T]( + current = terminals.headOption.map(x => x._2 -> x._3), + children = continuations + .map{ case (k, (k2, vs)) => (k, (k2, construct(index + 1, vs)(validationGroups)))} + .toMap + ) + } + + def validateGroups[T, V](inputs: collection.Seq[(collection.IndexedSeq[String], T, Boolean)], + terminals0: collection.Seq[(collection.IndexedSeq[String], T, Boolean)], + continuations0: collection.Map[String, collection.Seq[(collection.IndexedSeq[String], T, Boolean)]]) + (validationGroups: T => Seq[V]) = { + for (group <- inputs.flatMap(t => validationGroups(t._2)).distinct) { + val terminals = terminals0.flatMap { case (path, v, allowSubpath) => validationGroups(v) .filter(_ == group) - .map{group => (path, v, allowSubpath, group)} + .map { group => (path, v, allowSubpath, group) } } - val groupContinuations = continuations + val continuations = continuations0 .map { case (k, vs) => k -> vs.flatMap { case (path, v, allowSubpath) => validationGroups(v) @@ -35,50 +51,35 @@ object DispatchTrie{ } .filter(_._2.nonEmpty) - validateGroup(groupTerminals, groupContinuations) - } - - DispatchTrie[T]( - current = terminals.headOption.map(x => x._2 -> x._3), - children = continuations - .map{ case (k, vs) => (k, construct(index + 1, vs)(validationGroups))} - .toMap - ) - } - - def validateGroup[T, V](terminals: collection.Seq[(collection.Seq[String], T, Boolean, V)], - continuations: mutable.Map[String, mutable.Buffer[(collection.IndexedSeq[String], T, Boolean, V)]]) = { - val wildcards = continuations.filter(_._1(0) == ':') - - def renderTerminals = terminals - .map{case (path, v, allowSubpath, group) => s"$group${renderPath(path)}"} - .mkString(", ") + val wildcards = continuations.filter(_._1(0) == ':') - def renderContinuations = continuations.toSeq - .flatMap(_._2) - .map{case (path, v, allowSubpath, group) => s"$group${renderPath(path)}"} + def render(values: collection.Seq[(collection.IndexedSeq[String], T, Boolean, V)]) = values + .map { case (path, v, allowSubpath, group) => s"$group /" + path.mkString("/") } + .sorted .mkString(", ") - if (terminals.length > 1) { - throw new Exception( - s"More than one endpoint has the same path: $renderTerminals" - ) - } + def renderTerminals = render(terminals) + def renderContinuations = render(continuations.toSeq.flatMap(_._2)) - if (wildcards.size >= 1 && continuations.size > 1) { - throw new Exception( - s"Routes overlap with wildcards: $renderContinuations" - ) - } + if (terminals.length > 1) { + throw new Exception( + s"More than one endpoint has the same path: $renderTerminals" + ) + } + + if (wildcards.size >= 1 && continuations.size > 1) { + throw new Exception( + s"Routes overlap with wildcards: $renderContinuations" + ) + } - if (terminals.headOption.exists(_._3) && continuations.size == 1) { - throw new Exception( - s"Routes overlap with subpath capture: $renderTerminals, $renderContinuations" - ) + if (terminals.headOption.exists(_._3) && continuations.size == 1) { + throw new Exception( + s"Routes overlap with subpath capture: $renderTerminals, $renderContinuations" + ) + } } } - - def renderPath(p: collection.Seq[String]) = " /" + p.mkString("/") } /** @@ -90,7 +91,7 @@ object DispatchTrie{ * segments) */ case class DispatchTrie[T](current: Option[(T, Boolean)], - children: Map[String, DispatchTrie[T]]){ + children: Map[String, (String, DispatchTrie[T])]){ final def lookup(remainingInput: List[String], bindings: Map[String, String]) : Option[(T, Map[String, String], Seq[String])] = { @@ -101,11 +102,12 @@ case class DispatchTrie[T](current: Option[(T, Boolean)], current.map(x => (x._1, bindings, head :: rest)) case head :: rest => if (children.size == 1 && children.keys.head.startsWith(":")){ - children.values.head.lookup(rest, bindings + (children.keys.head.drop(1) -> head)) + val (k, (k2, v)) = children.head + v.lookup(rest, bindings + (k2.drop(1) -> head)) }else{ children.get(head) match{ case None => None - case Some(continuation) => continuation.lookup(rest, bindings) + case Some((k2, continuation)) => continuation.lookup(rest, bindings) } } @@ -114,6 +116,6 @@ case class DispatchTrie[T](current: Option[(T, Boolean)], def map[V](f: T => V): DispatchTrie[V] = DispatchTrie( current.map{case (t, v) => (f(t), v)}, - children.map { case (k, v) => (k, v.map(f))} + children.map { case (k, (k2, v)) => (k, (k2, v.map(f)))} ) } diff --git a/cask/test/src/test/cask/DispatchTrieTests.scala b/cask/test/src/test/cask/DispatchTrieTests.scala index 1da0df3286..7654f0e7ef 100644 --- a/cask/test/src/test/cask/DispatchTrieTests.scala +++ b/cask/test/src/test/cask/DispatchTrieTests.scala @@ -5,166 +5,245 @@ import utest._ object DispatchTrieTests extends TestSuite { val tests = Tests{ - "hello" - { + test("hello") { val x = DispatchTrie.construct(0, - Seq((Vector("hello"), 1, false)) + Seq((Vector("hello"), ("GET", "fooImpl"), false)) )(Seq(_)) - assert( - x.lookup(List("hello"), Map()) == Some((1, Map(), Nil)), - x.lookup(List("hello", "world"), Map()) == None, - x.lookup(List("world"), Map()) == None - ) + x.lookup(List("hello"), Map()) ==> Some((("GET", "fooImpl"), Map(), Nil)) + + x.lookup(List("hello", "world"), Map()) ==> None + x.lookup(List("world"), Map()) ==> None } - "nested" - { + test("nested") { val x = DispatchTrie.construct(0, Seq( - (Vector("hello", "world"), 1, false), - (Vector("hello", "cow"), 2, false) + (Vector("hello", "world"), ("GET", "fooImpl"), false), + (Vector("hello", "cow"), ("GET", "barImpl"), false) ) )(Seq(_)) - assert( - x.lookup(List("hello", "world"), Map()) == Some((1, Map(), Nil)), - x.lookup(List("hello", "cow"), Map()) == Some((2, Map(), Nil)), - x.lookup(List("hello"), Map()) == None, - x.lookup(List("hello", "moo"), Map()) == None, - x.lookup(List("hello", "world", "moo"), Map()) == None - ) + + x.lookup(List("hello", "world"), Map()) ==> Some((("GET", "fooImpl"), Map(), Nil)) + x.lookup(List("hello", "cow"), Map()) ==> Some((("GET", "barImpl"), Map(), Nil)) + + x.lookup(List("hello"), Map()) ==> None + x.lookup(List("hello", "moo"), Map()) ==> None + x.lookup(List("hello", "world", "moo"), Map()) ==> None + } - "bindings" - { + test("bindings") { val x = DispatchTrie.construct(0, - Seq((Vector(":hello", ":world"), 1, false)) + Seq((Vector(":hello", ":world"), ("GET", "fooImpl"), false)) )(Seq(_)) - assert( - x.lookup(List("hello", "world"), Map()) == Some((1, Map("hello" -> "hello", "world" -> "world"), Nil)), - x.lookup(List("world", "hello"), Map()) == Some((1, Map("hello" -> "world", "world" -> "hello"), Nil)), - x.lookup(List("hello", "world", "cow"), Map()) == None, - x.lookup(List("hello"), Map()) == None - ) + x.lookup(List("hello", "world"), Map()) ==> Some((("GET", "fooImpl"), Map("hello" -> "hello", "world" -> "world"), Nil)) + x.lookup(List("world", "hello"), Map()) ==> Some((("GET", "fooImpl"), Map("hello" -> "world", "world" -> "hello"), Nil)) + + x.lookup(List("hello", "world", "cow"), Map()) ==> None + x.lookup(List("hello"), Map()) ==> None + } - "path" - { + test("path") { val x = DispatchTrie.construct(0, - Seq((Vector("hello"), 1, true)) + Seq((Vector("hello"), ("GET", "fooImpl"), true)) )(Seq(_)) - assert( - x.lookup(List("hello", "world"), Map()) == Some((1,Map(), Seq("world"))), - x.lookup(List("hello", "world", "cow"), Map()) == Some((1,Map(), Seq("world", "cow"))), - x.lookup(List("hello"), Map()) == Some((1,Map(), Seq())), - x.lookup(List(), Map()) == None - ) + x.lookup(List("hello", "world"), Map()) ==> Some((("GET", "fooImpl"), Map(), Seq("world"))) + x.lookup(List("hello", "world", "cow"), Map()) ==> Some((("GET", "fooImpl"), Map(), Seq("world", "cow"))) + x.lookup(List("hello"), Map()) ==> Some((("GET", "fooImpl"), Map(), Seq())) + + x.lookup(List(), Map()) == None } - "errors" - { - test - { + test("partialOverlap") { + test("wildcardAndFixedWildcard"){ + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello"), ("GET", "fooImpl"), false), + (Vector("hello", ":world"), ("GET", "barImpl"), false) + ) + )(Seq(_)) + + x.lookup(List("hello", "world"), Map()) ==> Some(("GET", Map("hello" -> "hello", "world" -> "world"), Nil)) + x.lookup(List("world", "hello"), Map()) ==> Some(("GET", Map("hello" -> "world", "world" -> "hello"), Nil)) + + x.lookup(List("hello", "world", "cow"), Map()) ==> None + x.lookup(List("hello"), Map()) ==> None + } + + + test("wildcardAndSameWildcardFixed") { + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello"), ("GET", "fooImpl"), false), + (Vector(":hello", "world"), ("GET", "barImpl"), false) + ) + )(Seq(_)) + + x.lookup(List("hello"), Map()) ==> Some((("GET", "fooImpl"), Map("hello" -> "hello"), Nil)) + x.lookup(List("hello", "world"), Map()) ==> Some((("GET", "barImpl"), Map("hello" -> "hello"), Nil)) + + x.lookup(List("world", "hello"), Map()) ==> None + x.lookup(List("hello", "world", "cow"), Map()) ==> None + } + + test("wildcardAndDifferingWildcardFixed") { + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello"), "GET", false), + (Vector(":world", "world"), "GET", false) + ) + )(Seq(_)) + + x.lookup(List("hello", "world"), Map()) ==> Some(("GET", Map("hello" -> "hello"), Nil)) + x.lookup(List("hello"), Map()) ==> Some(("GET", Map("hello" -> "hello"), Nil)) + + x.lookup(List("world", "hello"), Map()) ==> None + x.lookup(List("hello", "world", "cow"), Map()) ==> None + } + + test("sameWildcardDifferingFixed"){ + val x = DispatchTrie.construct(0, + Seq( + (Vector(":var", "foo"), ("GET", "fooImpl"), false), + (Vector(":var", "bar"), ("GET", "barImpl"), false) + ) + )(t => Seq(t._1)) + + x.lookup(List("hello", "foo"), Map()) ==> Some((("GET", "fooImpl"), Map("var" -> "hello"), Nil)) + x.lookup(List("world", "bar"), Map()) ==> Some((("GET", "barImpl"), Map("var" -> "world"), Nil)) + + x.lookup(List("hello", "world", "cow"), Map()) ==> None + x.lookup(List("hello"), Map()) ==> None + } + + test("differingWildcardDifferingFixed") { + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello", "foo"), ("GET", "fooImpl"), false), + (Vector(":world", "bar"), ("GET", "barImpl"), false) + ) + )(Seq(_)) + + x.lookup(List("hello", "foo"), Map()) ==> Some((("GET", "fooImpl"), Map("hello" -> "hello"), Nil)) + x.lookup(List("world", "bar"), Map()) ==> Some((("GET", "barImpl"), Map("world" -> "world"), Nil)) + + x.lookup(List("hello", "world", "cow"), Map()) ==> None + x.lookup(List("hello"), Map()) ==> None + } + + } + + + test("errors") { + test("wildcardAndFixed") { DispatchTrie.construct(0, Seq( - (Vector("hello", ":world"), 1, false), - (Vector("hello", "world"), 2, false) + (Vector("hello", ":world"), "GET", false), + (Vector("hello", "world"), "POST", false) ) )(Seq(_)) val ex = intercept[Exception]{ DispatchTrie.construct(0, Seq( - (Vector("hello", ":world"), 1, false), - (Vector("hello", "world"), 1, false) + (Vector("hello", ":world"), "GET", false), + (Vector("hello", "world"), "GET", false) ) )(Seq(_)) } assert( ex.getMessage == - "Routes overlap with wildcards: 1 /hello/:world, 1 /hello/world" + "Routes overlap with wildcards: GET /hello/:world, GET /hello/world" ) } - test - { + test("subpathCapture") { DispatchTrie.construct(0, Seq( - (Vector("hello", ":world"), 1, false), - (Vector("hello", "world", "omg"), 2, false) + (Vector("hello"), "GET", true), + (Vector("hello", "cow", "omg"), "POST", false) ) )(Seq(_)) val ex = intercept[Exception]{ DispatchTrie.construct(0, Seq( - (Vector("hello", ":world"), 1, false), - (Vector("hello", "world", "omg"), 1, false) + (Vector("hello"), "GET", true), + (Vector("hello", "cow", "omg"), "GET", false) ) )(Seq(_)) } assert( ex.getMessage == - "Routes overlap with wildcards: 1 /hello/:world, 1 /hello/world/omg" + "Routes overlap with subpath capture: GET /hello, GET /hello/cow/omg" ) } - test - { + test("wildcardAndWildcard") { DispatchTrie.construct(0, Seq( - (Vector("hello"), 1, true), - (Vector("hello", "cow", "omg"), 2, false) + (Vector("hello", ":world"), "GET", false), + (Vector("hello", ":cow"), "POST", false) ) )(Seq(_)) val ex = intercept[Exception]{ DispatchTrie.construct(0, Seq( - (Vector("hello"), 1, true), - (Vector("hello", "cow", "omg"), 1, false) + (Vector("hello", ":world"), "GET", false), + (Vector("hello", ":cow"), "GET", false) ) )(Seq(_)) } assert( ex.getMessage == - "Routes overlap with subpath capture: 1 /hello, 1 /hello/cow/omg" + "More than one endpoint has the same path: GET /hello/:cow, GET /hello/:world" ) } - test - { + test("wildcardAndWildcardPrefix") { DispatchTrie.construct(0, Seq( - (Vector("hello", ":world"), 1, false), - (Vector("hello", ":cow"), 2, false) + (Vector(":world", "hello"), "GET", false), + (Vector(":cow", "hello"), "POST", false) ) )(Seq(_)) val ex = intercept[Exception]{ DispatchTrie.construct(0, Seq( - (Vector("hello", ":world"), 1, false), - (Vector("hello", ":cow"), 1, false) + (Vector(":world", "hello"), "GET", false), + (Vector(":cow", "hello"), "GET", false) ) )(Seq(_)) } assert( ex.getMessage == - "Routes overlap with wildcards: 1 /hello/:world, 1 /hello/:cow" + "More than one endpoint has the same path: GET /:cow/hello, GET /:world/hello" ) } - test - { + test("fixedAndFixed") { DispatchTrie.construct(0, Seq( - (Vector("hello", "world"), 1, false), - (Vector("hello", "world"), 2, false) + (Vector("hello", "world"), "GET", false), + (Vector("hello", "world"), "POST", false) ) )(Seq(_)) val ex = intercept[Exception]{ DispatchTrie.construct(0, Seq( - (Vector("hello", "world"), 1, false), - (Vector("hello", "world"), 1, false) + (Vector("hello", "world"), "GET", false), + (Vector("hello", "world"), "GET", false) ) )(Seq(_)) } assert( ex.getMessage == - "More than one endpoint has the same path: 1 /hello/world, 1 /hello/world" + "More than one endpoint has the same path: GET /hello/world, GET /hello/world" ) } } diff --git a/cask/test/src/test/cask/FailureTests.scala b/cask/test/src/test/cask/FailureTests.scala index 94dd9846d2..eb554a624a 100644 --- a/cask/test/src/test/cask/FailureTests.scala +++ b/cask/test/src/test/cask/FailureTests.scala @@ -11,7 +11,7 @@ object FailureTests extends TestSuite { } val tests = Tests{ - "mismatchedDecorators" - { + test("mismatchedDecorators") { val m = utest.compileError(""" object Decorated extends cask.MainRoutes{ @myDecorator @@ -23,7 +23,7 @@ object FailureTests extends TestSuite { assert(m.contains("required: cask.router.Decorator[_, cask.endpoints.WebsocketResult, _]")) } - "noEndpoint" - { + test("noEndpoint") { utest.compileError(""" object Decorated extends cask.MainRoutes{ @cask.get("/hello/:world") @@ -35,7 +35,7 @@ object FailureTests extends TestSuite { "Last annotation applied to a function must be an instance of Endpoint, not test.cask.FailureTests.myDecorator" } - "tooManyEndpoint" - { + test("tooManyEndpoint") { utest.compileError(""" object Decorated extends cask.MainRoutes{ @cask.get("/hello/:world") diff --git a/cask/test/src/test/cask/UtilTests.scala b/cask/test/src/test/cask/UtilTests.scala index 3148c9e9ab..0c887f1553 100644 --- a/cask/test/src/test/cask/UtilTests.scala +++ b/cask/test/src/test/cask/UtilTests.scala @@ -4,7 +4,7 @@ import utest._ object UtilTests extends TestSuite { val tests = Tests{ - "splitPath" - { + test("splitPath") { cask.internal.Util.splitPath("") ==> Seq() cask.internal.Util.splitPath("/") ==> Seq() cask.internal.Util.splitPath("////") ==> Seq()