From 0e7ee84ecb93c173a6d0bce3eb98d57117066cd7 Mon Sep 17 00:00:00 2001 From: megri Date: Mon, 15 Nov 2021 13:31:01 +0100 Subject: [PATCH] Rework routeDispatchTries + endpoint lookup for more intuitive 404/405 responses. (#52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent cask from responding with 405 for an undefined route. Fixes #51. Prior to this commit, `prepareRouteTries` created a mapping from method-name to DispatchTrie (Map[String, DispatchTrie[…]]). This commit instead creates a DispatchTrie[Map[String, …]], basically an inversion of the previous result. The updated tests in minimalApplication and minimalApplication2 have been updated to cover the differences. --- .gitignore | 3 + .mill-version | 2 + build.sc | 2 +- cask/src/cask/main/Main.scala | 79 +++++++++---------- example/compress/build.sc | 2 +- example/compress2/build.sc | 2 +- example/compress3/build.sc | 2 +- example/cookies/build.sc | 2 +- example/decorated/build.sc | 2 +- example/decorated2/build.sc | 2 +- example/endpoints/build.sc | 2 +- example/formJsonPost/build.sc | 2 +- example/httpMethods/build.sc | 2 +- .../app/test/src/ExampleTests.scala | 2 +- example/minimalApplication/build.sc | 2 +- .../app/test/src/ExampleTests.scala | 2 +- example/minimalApplication2/build.sc | 2 +- example/redirectAbort/build.sc | 2 +- example/scalatags/build.sc | 2 +- example/staticFiles/build.sc | 2 +- example/staticFiles2/build.sc | 2 +- example/todo/build.sc | 2 +- example/todoApi/build.sc | 2 +- example/todoDb/build.sc | 2 +- example/twirl/build.sc | 2 +- example/variableRoutes/build.sc | 2 +- example/websockets/build.sc | 2 +- example/websockets2/build.sc | 2 +- example/websockets3/build.sc | 2 +- example/websockets4/build.sc | 2 +- 30 files changed, 70 insertions(+), 68 deletions(-) create mode 100644 .mill-version diff --git a/.gitignore b/.gitignore index 8d3ee552d6..e992878ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.bloop/ +.metals/ +.vscode/ target/ *.iml .idea diff --git a/.mill-version b/.mill-version new file mode 100644 index 0000000000..13f88c27db --- /dev/null +++ b/.mill-version @@ -0,0 +1,2 @@ +0.9.7 + diff --git a/build.sc b/build.sc index 611920a2bf..792946bc07 100644 --- a/build.sc +++ b/build.sc @@ -62,7 +62,7 @@ class CaskMainModule(val crossScalaVersion: String) extends CaskModule { def scalacPluginIvyDeps = T{ if (!isDotty) Agg(ivy"com.lihaoyi::acyclic:0.2.0") else Agg() } object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", ivy"com.lihaoyi::requests::0.6.9" diff --git a/cask/src/cask/main/Main.scala b/cask/src/cask/main/Main.scala index 9a02f1167c..3e53c18504 100644 --- a/cask/src/cask/main/Main.scala +++ b/cask/src/cask/main/Main.scala @@ -44,10 +44,10 @@ abstract class Main{ implicit def log: cask.util.Logger = new cask.util.Logger.Console() - def routeTries = Main.prepareRouteTries(allRoutes) + def dispatchTrie = Main.prepareDispatchTrie(allRoutes) def defaultHandler = new BlockingHandler( - new Main.DefaultHandler(routeTries, mainDecorators, debugMode, handleNotFound, handleMethodNotAllowed, handleEndpointError) + new Main.DefaultHandler(dispatchTrie, mainDecorators, debugMode, handleNotFound, handleMethodNotAllowed, handleEndpointError) ) def handleNotFound() = Main.defaultHandleNotFound() @@ -72,7 +72,7 @@ abstract class Main{ } object Main{ - class DefaultHandler(routeTries: Map[String, DispatchTrie[(Routes, EndpointMetadata[_])]], + class DefaultHandler(dispatchTrie: DispatchTrie[Map[String, (Routes, EndpointMetadata[_])]], mainDecorators: Seq[Decorator[_, _, _]], debugMode: Boolean, handleNotFound: () => Response.Raw, @@ -99,35 +99,30 @@ object Main{ (r: Any) => Main.writeResponse(exchange, r.asInstanceOf[Response.Raw]) ) - val dispatchTrie: DispatchTrie[(Routes, EndpointMetadata[_])] = routeTries.get(effectiveMethod) match { - case None => - Main.writeResponse(exchange, handleMethodNotAllowed()) - return - case Some(trie) => trie - } - dispatchTrie.lookup(Util.splitPath(exchange.getRequestPath).toList, Map()) match { case None => Main.writeResponse(exchange, handleNotFound()) - case Some(((routes, metadata), routeBindings, remaining)) => - Decorator.invoke( - Request(exchange, remaining), - metadata.endpoint, - metadata.entryPoint.asInstanceOf[EntryPoint[Routes, _]], - routes, - routeBindings, - (mainDecorators ++ routes.decorators ++ metadata.decorators).toList, - Nil - ) match{ - case Result.Success(res) => runner(res) - case e: Result.Error => - Main.writeResponse( - exchange, - handleError(routes, metadata, e) - ) - None + case Some((methodMap, routeBindings, remaining)) => + methodMap.get(effectiveMethod) match { + case None => Main.writeResponse(exchange, handleMethodNotAllowed()) + case Some((routes, metadata)) => + Decorator.invoke( + Request(exchange, remaining), + metadata.endpoint, + metadata.entryPoint.asInstanceOf[EntryPoint[Routes, _]], + routes, + routeBindings, + (mainDecorators ++ routes.decorators ++ metadata.decorators).toList, + Nil + ) match { + case Result.Success(res) => runner(res) + case e: Result.Error => + Main.writeResponse( + exchange, + handleError(routes, metadata, e) + ) + } } } - // println("Completed Request: " + exchange.getRequestPath) }catch{case e: Throwable => e.printStackTrace() } @@ -147,23 +142,25 @@ object Main{ ) } - def prepareRouteTries(allRoutes: Seq[Routes]): Map[String, DispatchTrie[(Routes, EndpointMetadata[_])]] = { - val routeList = for{ + def prepareDispatchTrie(allRoutes: Seq[Routes]): DispatchTrie[Map[String, (Routes, EndpointMetadata[_])]] = { + val flattenedRoutes = for { routes <- allRoutes - route <- routes.caskMetadata.value.map(x => x: EndpointMetadata[_]) - } yield (routes, route) + metadata <- routes.caskMetadata.value + } yield { + val segments = Util.splitPath(metadata.endpoint.path) + val methodMap = metadata.endpoint.methods.map(_ -> (routes, metadata: EndpointMetadata[_])).toMap + (segments, methodMap, metadata.endpoint.subpath) + } - val allMethods: Set[String] = - routeList.flatMap(_._2.endpoint.methods).map(_.toLowerCase).toSet + val dispatchInputs = flattenedRoutes.groupBy(_._1).map { case (segments, values) => + val methodMap = values.map(_._2).flatten.toMap + val hasSubpath = values.map(_._3).contains(true) + (segments, methodMap, hasSubpath) + }.toSeq - allMethods - .map { method => - method -> DispatchTrie.construct[(Routes, EndpointMetadata[_])](0, - for ((route, metadata) <- routeList if metadata.endpoint.methods.contains(method)) - yield (Util.splitPath(metadata.endpoint.path): collection.IndexedSeq[String], (route, metadata), metadata.endpoint.subpath) - ) - }.toMap + DispatchTrie.construct(0, dispatchInputs) } + def writeResponse(exchange: HttpServerExchange, response: Response.Raw) = { response.data.headers.foreach{case (k, v) => exchange.getResponseHeaders.put(new HttpString(k), v) diff --git a/example/compress/build.sc b/example/compress/build.sc index 23660c667b..0d6016b1d1 100644 --- a/example/compress/build.sc +++ b/example/compress/build.sc @@ -7,7 +7,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/compress2/build.sc b/example/compress2/build.sc index 46a9034788..f19e434305 100644 --- a/example/compress2/build.sc +++ b/example/compress2/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/compress3/build.sc b/example/compress3/build.sc index 46a9034788..f19e434305 100644 --- a/example/compress3/build.sc +++ b/example/compress3/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/cookies/build.sc b/example/cookies/build.sc index 46a9034788..f19e434305 100644 --- a/example/cookies/build.sc +++ b/example/cookies/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/decorated/build.sc b/example/decorated/build.sc index 46a9034788..f19e434305 100644 --- a/example/decorated/build.sc +++ b/example/decorated/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/decorated2/build.sc b/example/decorated2/build.sc index 46a9034788..f19e434305 100644 --- a/example/decorated2/build.sc +++ b/example/decorated2/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/endpoints/build.sc b/example/endpoints/build.sc index 46a9034788..f19e434305 100644 --- a/example/endpoints/build.sc +++ b/example/endpoints/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/formJsonPost/build.sc b/example/formJsonPost/build.sc index 78b60c17a4..a7264cea6f 100644 --- a/example/formJsonPost/build.sc +++ b/example/formJsonPost/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/httpMethods/build.sc b/example/httpMethods/build.sc index 46a9034788..f19e434305 100644 --- a/example/httpMethods/build.sc +++ b/example/httpMethods/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/minimalApplication/app/test/src/ExampleTests.scala b/example/minimalApplication/app/test/src/ExampleTests.scala index 986fc617b3..cd04898c05 100644 --- a/example/minimalApplication/app/test/src/ExampleTests.scala +++ b/example/minimalApplication/app/test/src/ExampleTests.scala @@ -27,7 +27,7 @@ object ExampleTests extends TestSuite{ requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh" - requests.get(s"$host/do-thing", check = false).statusCode ==> 404 + requests.delete(s"$host/do-thing", check = false).statusCode ==> 405 } } } diff --git a/example/minimalApplication/build.sc b/example/minimalApplication/build.sc index 46a9034788..f19e434305 100644 --- a/example/minimalApplication/build.sc +++ b/example/minimalApplication/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/minimalApplication2/app/test/src/ExampleTests.scala b/example/minimalApplication2/app/test/src/ExampleTests.scala index 5bbee09e9f..7d5ad2873e 100644 --- a/example/minimalApplication2/app/test/src/ExampleTests.scala +++ b/example/minimalApplication2/app/test/src/ExampleTests.scala @@ -27,7 +27,7 @@ object ExampleTests extends TestSuite{ requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh" - requests.get(s"$host/do-thing", check = false).statusCode ==> 404 + requests.delete(s"$host/do-thing", check = false).statusCode ==> 405 } } } diff --git a/example/minimalApplication2/build.sc b/example/minimalApplication2/build.sc index 46a9034788..f19e434305 100644 --- a/example/minimalApplication2/build.sc +++ b/example/minimalApplication2/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/redirectAbort/build.sc b/example/redirectAbort/build.sc index 46a9034788..f19e434305 100644 --- a/example/redirectAbort/build.sc +++ b/example/redirectAbort/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/scalatags/build.sc b/example/scalatags/build.sc index 5f6953cd7d..62d5b7f7bc 100644 --- a/example/scalatags/build.sc +++ b/example/scalatags/build.sc @@ -8,7 +8,7 @@ trait AppModule extends CrossScalaModule{ ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/staticFiles/build.sc b/example/staticFiles/build.sc index 2e99d1d4ab..e3a97174c3 100644 --- a/example/staticFiles/build.sc +++ b/example/staticFiles/build.sc @@ -8,7 +8,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/staticFiles2/build.sc b/example/staticFiles2/build.sc index 2e99d1d4ab..e3a97174c3 100644 --- a/example/staticFiles2/build.sc +++ b/example/staticFiles2/build.sc @@ -8,7 +8,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/todo/build.sc b/example/todo/build.sc index 7da6daecb1..aa367b29ce 100644 --- a/example/todo/build.sc +++ b/example/todo/build.sc @@ -10,7 +10,7 @@ trait AppModule extends CrossScalaModule{ ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/todoApi/build.sc b/example/todoApi/build.sc index 46a9034788..f19e434305 100644 --- a/example/todoApi/build.sc +++ b/example/todoApi/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/todoDb/build.sc b/example/todoDb/build.sc index 13d1191ead..f5bf4c92dc 100644 --- a/example/todoDb/build.sc +++ b/example/todoDb/build.sc @@ -9,7 +9,7 @@ trait AppModule extends CrossScalaModule{ ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/twirl/build.sc b/example/twirl/build.sc index b42194622a..d140d39832 100644 --- a/example/twirl/build.sc +++ b/example/twirl/build.sc @@ -12,7 +12,7 @@ trait AppModule extends CrossScalaModule with mill.twirllib.TwirlModule{ ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/variableRoutes/build.sc b/example/variableRoutes/build.sc index 46a9034788..f19e434305 100644 --- a/example/variableRoutes/build.sc +++ b/example/variableRoutes/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/websockets/build.sc b/example/websockets/build.sc index f2968378a4..8677052576 100644 --- a/example/websockets/build.sc +++ b/example/websockets/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/websockets2/build.sc b/example/websockets2/build.sc index f2968378a4..8677052576 100644 --- a/example/websockets2/build.sc +++ b/example/websockets2/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/websockets3/build.sc b/example/websockets3/build.sc index f2968378a4..8677052576 100644 --- a/example/websockets3/build.sc +++ b/example/websockets3/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10", diff --git a/example/websockets4/build.sc b/example/websockets4/build.sc index f2968378a4..8677052576 100644 --- a/example/websockets4/build.sc +++ b/example/websockets4/build.sc @@ -6,7 +6,7 @@ trait AppModule extends CrossScalaModule{ def ivyDeps = Agg[Dep]( ) object test extends Tests{ - def testFrameworks = Seq("utest.runner.Framework") + def testFramework = "utest.runner.Framework" def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.7.10",