From f28fc52fdcf0a8c07916a3a585c59089ef606c81 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 5 Jan 2024 11:26:55 +0800 Subject: [PATCH 1/7] . --- cask/src/cask/internal/DispatchTrie.scala | 84 +++++++++---------- .../src/test/cask/DispatchTrieTests.scala | 16 ++++ 2 files changed, 58 insertions(+), 42 deletions(-) diff --git a/cask/src/cask/internal/DispatchTrie.scala b/cask/src/cask/internal/DispatchTrie.scala index cc9f7e7252..6ef1c1f245 100644 --- a/cask/src/cask/internal/DispatchTrie.scala +++ b/cask/src/cask/internal/DispatchTrie.scala @@ -5,27 +5,39 @@ object DispatchTrie{ 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 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){ + 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)) } } - for(group <- inputs.flatMap(t => validationGroups(t._2)).distinct) { - val groupTerminals = terminals.flatMap{case (path, v, allowSubpath) => + validateGroups(inputs, terminals, continuations)(validationGroups) + + 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 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,46 +47,34 @@ object DispatchTrie{ } .filter(_._2.nonEmpty) - validateGroup(groupTerminals, groupContinuations) - } + val wildcards = continuations.filter(_._1(0) == ':') - 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(", ") - - def renderContinuations = continuations.toSeq - .flatMap(_._2) + def renderTerminals = terminals .map{case (path, v, allowSubpath, group) => s"$group${renderPath(path)}"} .mkString(", ") - if (terminals.length > 1) { - throw new Exception( - s"More than one endpoint has the same path: $renderTerminals" - ) - } + def renderContinuations = continuations.toSeq + .flatMap(_._2) + .map{case (path, v, allowSubpath, group) => s"$group${renderPath(path)}"} + .mkString(", ") - 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 (terminals.headOption.exists(_._3) && continuations.size == 1) { - throw new Exception( - s"Routes overlap with subpath capture: $renderTerminals, $renderContinuations" - ) + 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" + ) + } } } diff --git a/cask/test/src/test/cask/DispatchTrieTests.scala b/cask/test/src/test/cask/DispatchTrieTests.scala index 1da0df3286..e5007eecba 100644 --- a/cask/test/src/test/cask/DispatchTrieTests.scala +++ b/cask/test/src/test/cask/DispatchTrieTests.scala @@ -57,6 +57,22 @@ object DispatchTrieTests extends TestSuite { ) } + "partialOverlap" - { + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello"), 1, false), + (Vector("hello", ":world"), 1, 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 + ) + } + "errors" - { test - { DispatchTrie.construct(0, From 02cfafd0f9006a655f1adc5356c9da5ebcb20501 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 5 Jan 2024 11:28:32 +0800 Subject: [PATCH 2/7] . --- cask/src/cask/internal/DispatchTrie.scala | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/cask/src/cask/internal/DispatchTrie.scala b/cask/src/cask/internal/DispatchTrie.scala index 6ef1c1f245..9a6b371087 100644 --- a/cask/src/cask/internal/DispatchTrie.scala +++ b/cask/src/cask/internal/DispatchTrie.scala @@ -49,14 +49,12 @@ object DispatchTrie{ val wildcards = continuations.filter(_._1(0) == ':') - def renderTerminals = terminals - .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${renderPath(path)}" } .mkString(", ") - def renderContinuations = continuations.toSeq - .flatMap(_._2) - .map{case (path, v, allowSubpath, group) => s"$group${renderPath(path)}"} - .mkString(", ") + def renderTerminals = render(terminals) + def renderContinuations = render(continuations.toSeq.flatMap(_._2)) if (terminals.length > 1) { throw new Exception( From ff8b39b7d1de2b4bac6715d948eafeea429aec57 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 5 Jan 2024 11:29:28 +0800 Subject: [PATCH 3/7] . --- cask/src/cask/internal/DispatchTrie.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cask/src/cask/internal/DispatchTrie.scala b/cask/src/cask/internal/DispatchTrie.scala index 9a6b371087..07e4f6e4f0 100644 --- a/cask/src/cask/internal/DispatchTrie.scala +++ b/cask/src/cask/internal/DispatchTrie.scala @@ -50,7 +50,7 @@ object DispatchTrie{ val wildcards = continuations.filter(_._1(0) == ':') def render(values: collection.Seq[(collection.IndexedSeq[String], T, Boolean, V)]) = values - .map { case (path, v, allowSubpath, group) => s"$group${renderPath(path)}" } + .map { case (path, v, allowSubpath, group) => s"$group /" + path.mkString("/") } .mkString(", ") def renderTerminals = render(terminals) @@ -75,8 +75,6 @@ object DispatchTrie{ } } } - - def renderPath(p: collection.Seq[String]) = " /" + p.mkString("/") } /** From 6764cd6fdbec8cd6b0184dc3996703339231afb5 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 5 Jan 2024 11:46:11 +0800 Subject: [PATCH 4/7] wip --- .../src/test/cask/DispatchTrieTests.scala | 192 ++++++++++++------ 1 file changed, 125 insertions(+), 67 deletions(-) diff --git a/cask/test/src/test/cask/DispatchTrieTests.scala b/cask/test/src/test/cask/DispatchTrieTests.scala index e5007eecba..dc46b7d151 100644 --- a/cask/test/src/test/cask/DispatchTrieTests.scala +++ b/cask/test/src/test/cask/DispatchTrieTests.scala @@ -7,180 +7,238 @@ object DispatchTrieTests extends TestSuite { "hello" - { val x = DispatchTrie.construct(0, - Seq((Vector("hello"), 1, false)) + Seq((Vector("hello"), "GET", 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", Map(), Nil)) + + x.lookup(List("hello", "world"), Map()) ==> None + x.lookup(List("world"), Map()) ==> None } "nested" - { val x = DispatchTrie.construct(0, Seq( - (Vector("hello", "world"), 1, false), - (Vector("hello", "cow"), 2, false) + (Vector("hello", "world"), "GET", false), + (Vector("hello", "cow"), "POST", 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", Map(), Nil)) + x.lookup(List("hello", "cow"), Map()) ==> Some(("POST", Map(), Nil)) + + x.lookup(List("hello"), Map()) ==> None + x.lookup(List("hello", "moo"), Map()) ==> None + x.lookup(List("hello", "world", "moo"), Map()) ==> None + } "bindings" - { val x = DispatchTrie.construct(0, - Seq((Vector(":hello", ":world"), 1, false)) + Seq((Vector(":hello", ":world"), "GET", 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", 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 + } "path" - { val x = DispatchTrie.construct(0, - Seq((Vector("hello"), 1, true)) + Seq((Vector("hello"), "GET", 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", Map(), Seq("world"))) + x.lookup(List("hello", "world", "cow"), Map()) ==> Some(("GET", Map(), Seq("world", "cow"))) + x.lookup(List("hello"), Map()) ==> Some(("GET", Map(), Seq())) + + x.lookup(List(), Map()) == None } "partialOverlap" - { val x = DispatchTrie.construct(0, Seq( - (Vector(":hello"), 1, false), - (Vector("hello", ":world"), 1, false) + (Vector(":hello"), "GET", false), + (Vector("hello", ":world"), "GET", 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", 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 + } + + "partialOverlap2" - { + 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 + } + + "partialOverlap3" - { + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello", "foo"), "GET", false), + (Vector(":world", "bar"), "GET", 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 + } + + "partialOverlap4" - { + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello"), "GET", false), + (Vector(":hello", "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 + } + + "partialOverlap5" - { + 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("world" -> "hello"), Nil)) + x.lookup(List("hello"), Map()) ==> Some(("GET", Map("world" -> "hello"), Nil)) + + x.lookup(List("world", "hello"), Map()) ==> None + x.lookup(List("hello", "world", "cow"), Map()) ==> None } "errors" - { - test - { + 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" + "Routes overlap with wildcards: GET /hello/:world, GET /hello/:cow" ) } - 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" + "Routes overlap with wildcards: GET /:world/hello, GET /:cow/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" ) } } From e368868562dbc560e6ff184fc37faf0155189e27 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 5 Jan 2024 12:16:21 +0800 Subject: [PATCH 5/7] tests --- .../src/test/cask/DispatchTrieTests.scala | 120 ++++++++---------- cask/test/src/test/cask/FailureTests.scala | 6 +- cask/test/src/test/cask/UtilTests.scala | 2 +- 3 files changed, 59 insertions(+), 69 deletions(-) diff --git a/cask/test/src/test/cask/DispatchTrieTests.scala b/cask/test/src/test/cask/DispatchTrieTests.scala index dc46b7d151..54d31b55fb 100644 --- a/cask/test/src/test/cask/DispatchTrieTests.scala +++ b/cask/test/src/test/cask/DispatchTrieTests.scala @@ -5,7 +5,7 @@ import utest._ object DispatchTrieTests extends TestSuite { val tests = Tests{ - "hello" - { + test("hello") { val x = DispatchTrie.construct(0, Seq((Vector("hello"), "GET", false)) )(Seq(_)) @@ -15,7 +15,7 @@ object DispatchTrieTests extends TestSuite { 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"), "GET", false), @@ -31,7 +31,7 @@ object DispatchTrieTests extends TestSuite { x.lookup(List("hello", "world", "moo"), Map()) ==> None } - "bindings" - { + test("bindings") { val x = DispatchTrie.construct(0, Seq((Vector(":hello", ":world"), "GET", false)) )(Seq(_)) @@ -44,7 +44,7 @@ object DispatchTrieTests extends TestSuite { } - "path" - { + test("path") { val x = DispatchTrie.construct(0, Seq((Vector("hello"), "GET", true)) )(Seq(_)) @@ -56,82 +56,72 @@ object DispatchTrieTests extends TestSuite { x.lookup(List(), Map()) == None } - "partialOverlap" - { - val x = DispatchTrie.construct(0, - Seq( - (Vector(":hello"), "GET", false), - (Vector("hello", ":world"), "GET", 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)) + test("partialOverlap") { + test("wildcardAndFixedWildcard"){ + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello"), "GET", false), + (Vector("hello", ":world"), "GET", false) + ) + )(Seq(_)) - x.lookup(List("hello", "world", "cow"), Map()) ==> None - x.lookup(List("hello"), Map()) ==> None - } + 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)) - "partialOverlap2" - { - 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", "world", "cow"), Map()) ==> None + x.lookup(List("hello"), Map()) ==> None + } - 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("wildcardAndWildcardFixed") { + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello"), "GET", false), + (Vector(":hello", "world"), "GET", false) + ) + )(Seq(_)) - "partialOverlap3" - { - val x = DispatchTrie.construct(0, - Seq( - (Vector(":hello", "foo"), "GET", false), - (Vector(":world", "bar"), "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("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("world", "hello"), Map()) ==> None + x.lookup(List("hello", "world", "cow"), Map()) ==> None + } - x.lookup(List("hello", "world", "cow"), Map()) ==> None - x.lookup(List("hello"), 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)) - "partialOverlap4" - { - val x = DispatchTrie.construct(0, - Seq( - (Vector(":hello"), "GET", false), - (Vector(":hello", "world"), "GET", false) - ) - )(Seq(_)) + 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"), Map()) ==> Some(("GET", Map("hello" -> "hello"), Nil)) - x.lookup(List("hello"), Map()) ==> Some(("GET", Map("hello" -> "hello"), Nil)) + x.lookup(List("hello", "world", "cow"), Map()) ==> None + x.lookup(List("hello"), Map()) ==> None + } - x.lookup(List("world", "hello"), Map()) ==> None - x.lookup(List("hello", "world", "cow"), Map()) ==> None - } + test("differingWildcardDifferingFixed") { + val x = DispatchTrie.construct(0, + Seq( + (Vector(":hello", "foo"), "GET", false), + (Vector(":world", "bar"), "GET", false) + ) + )(Seq(_)) - "partialOverlap5" - { - 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", "world" -> "world"), Nil)) + x.lookup(List("world", "hello"), Map()) ==> Some(("GET", Map("hello" -> "world", "world" -> "hello"), Nil)) - x.lookup(List("hello", "world"), Map()) ==> Some(("GET", Map("world" -> "hello"), Nil)) - x.lookup(List("hello"), Map()) ==> Some(("GET", Map("world" -> "hello"), Nil)) + x.lookup(List("hello", "world", "cow"), Map()) ==> None + x.lookup(List("hello"), Map()) ==> None + } - x.lookup(List("world", "hello"), Map()) ==> None - x.lookup(List("hello", "world", "cow"), Map()) ==> None } - "errors" - { + + test("errors") { test("wildcardAndFixed") { DispatchTrie.construct(0, Seq( 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() From 28d4537ef2ddc22872ee2d1584f8f117d22a1bbe Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 5 Jan 2024 12:17:24 +0800 Subject: [PATCH 6/7] . --- cask/test/src/test/cask/DispatchTrieTests.scala | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cask/test/src/test/cask/DispatchTrieTests.scala b/cask/test/src/test/cask/DispatchTrieTests.scala index 54d31b55fb..92893b24fd 100644 --- a/cask/test/src/test/cask/DispatchTrieTests.scala +++ b/cask/test/src/test/cask/DispatchTrieTests.scala @@ -73,7 +73,7 @@ object DispatchTrieTests extends TestSuite { } - test("wildcardAndWildcardFixed") { + test("wildcardAndSameWildcardFixed") { val x = DispatchTrie.construct(0, Seq( (Vector(":hello"), "GET", false), @@ -88,6 +88,21 @@ object DispatchTrieTests extends TestSuite { 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( From 437d1bef05c1dad7123fc828b4b2b80b9f2b592d Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Fri, 5 Jan 2024 13:01:06 +0800 Subject: [PATCH 7/7] tweaktests --- cask/src/cask/internal/DispatchTrie.scala | 24 +++++---- .../src/test/cask/DispatchTrieTests.scala | 50 +++++++++---------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/cask/src/cask/internal/DispatchTrie.scala b/cask/src/cask/internal/DispatchTrie.scala index 07e4f6e4f0..ab9d610b4a 100644 --- a/cask/src/cask/internal/DispatchTrie.scala +++ b/cask/src/cask/internal/DispatchTrie.scala @@ -4,24 +4,28 @@ 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)) + val segment = path(index) + val buf = continuations.getOrElseUpdate( + if (segment.startsWith(":")) ":" else segment, + (segment, mutable.Buffer.empty) + ) + buf._2.append((path, endPoint, allowSubpath)) } } - validateGroups(inputs, terminals, continuations)(validationGroups) + 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, vs) => (k, construct(index + 1, vs)(validationGroups))} + .map{ case (k, (k2, vs)) => (k, (k2, construct(index + 1, vs)(validationGroups)))} .toMap ) } @@ -51,6 +55,7 @@ object DispatchTrie{ def render(values: collection.Seq[(collection.IndexedSeq[String], T, Boolean, V)]) = values .map { case (path, v, allowSubpath, group) => s"$group /" + path.mkString("/") } + .sorted .mkString(", ") def renderTerminals = render(terminals) @@ -86,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])] = { @@ -97,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) } } @@ -110,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 92893b24fd..7654f0e7ef 100644 --- a/cask/test/src/test/cask/DispatchTrieTests.scala +++ b/cask/test/src/test/cask/DispatchTrieTests.scala @@ -7,10 +7,10 @@ object DispatchTrieTests extends TestSuite { test("hello") { val x = DispatchTrie.construct(0, - Seq((Vector("hello"), "GET", false)) + Seq((Vector("hello"), ("GET", "fooImpl"), false)) )(Seq(_)) - x.lookup(List("hello"), Map()) ==> Some(("GET", Map(), Nil)) + x.lookup(List("hello"), Map()) ==> Some((("GET", "fooImpl"), Map(), Nil)) x.lookup(List("hello", "world"), Map()) ==> None x.lookup(List("world"), Map()) ==> None @@ -18,13 +18,13 @@ object DispatchTrieTests extends TestSuite { test("nested") { val x = DispatchTrie.construct(0, Seq( - (Vector("hello", "world"), "GET", false), - (Vector("hello", "cow"), "POST", false) + (Vector("hello", "world"), ("GET", "fooImpl"), false), + (Vector("hello", "cow"), ("GET", "barImpl"), false) ) )(Seq(_)) - x.lookup(List("hello", "world"), Map()) ==> Some(("GET", Map(), Nil)) - x.lookup(List("hello", "cow"), Map()) ==> Some(("POST", Map(), Nil)) + 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 @@ -33,11 +33,11 @@ object DispatchTrieTests extends TestSuite { } test("bindings") { val x = DispatchTrie.construct(0, - Seq((Vector(":hello", ":world"), "GET", false)) + Seq((Vector(":hello", ":world"), ("GET", "fooImpl"), 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"), 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 @@ -46,12 +46,12 @@ object DispatchTrieTests extends TestSuite { test("path") { val x = DispatchTrie.construct(0, - Seq((Vector("hello"), "GET", true)) + Seq((Vector("hello"), ("GET", "fooImpl"), true)) )(Seq(_)) - x.lookup(List("hello", "world"), Map()) ==> Some(("GET", Map(), Seq("world"))) - x.lookup(List("hello", "world", "cow"), Map()) ==> Some(("GET", Map(), Seq("world", "cow"))) - x.lookup(List("hello"), Map()) ==> Some(("GET", Map(), Seq())) + 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 } @@ -60,8 +60,8 @@ object DispatchTrieTests extends TestSuite { test("wildcardAndFixedWildcard"){ val x = DispatchTrie.construct(0, Seq( - (Vector(":hello"), "GET", false), - (Vector("hello", ":world"), "GET", false) + (Vector(":hello"), ("GET", "fooImpl"), false), + (Vector("hello", ":world"), ("GET", "barImpl"), false) ) )(Seq(_)) @@ -76,13 +76,13 @@ object DispatchTrieTests extends TestSuite { test("wildcardAndSameWildcardFixed") { val x = DispatchTrie.construct(0, Seq( - (Vector(":hello"), "GET", false), - (Vector(":hello", "world"), "GET", false) + (Vector(":hello"), ("GET", "fooImpl"), false), + (Vector(":hello", "world"), ("GET", "barImpl"), 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("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 @@ -121,13 +121,13 @@ object DispatchTrieTests extends TestSuite { test("differingWildcardDifferingFixed") { val x = DispatchTrie.construct(0, Seq( - (Vector(":hello", "foo"), "GET", false), - (Vector(":world", "bar"), "GET", false) + (Vector(":hello", "foo"), ("GET", "fooImpl"), false), + (Vector(":world", "bar"), ("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", "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 @@ -200,7 +200,7 @@ object DispatchTrieTests extends TestSuite { assert( ex.getMessage == - "Routes overlap with wildcards: GET /hello/:world, GET /hello/:cow" + "More than one endpoint has the same path: GET /hello/:cow, GET /hello/:world" ) } test("wildcardAndWildcardPrefix") { @@ -222,7 +222,7 @@ object DispatchTrieTests extends TestSuite { assert( ex.getMessage == - "Routes overlap with wildcards: GET /:world/hello, GET /:cow/hello" + "More than one endpoint has the same path: GET /:cow/hello, GET /:world/hello" ) } test("fixedAndFixed") {