From 76fe7513a4a339d223a9169f5420fd74e57ca50b Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 4 Jan 2024 20:06:05 +0800 Subject: [PATCH] Separate query param docs from variable routes (#112) --- build.sc | 5 + .../1 - Cask: a Scala HTTP micro-framework.md | 20 ++-- example/queryParams/app/src/QueryParams.scala | 35 +++++++ .../app/test/src/ExampleTests.scala | 95 +++++++++++++++++++ example/queryParams/build.sc | 14 +++ .../app/src/VariableRoutes.scala | 36 +------ .../app/test/src/ExampleTests.scala | 72 -------------- 7 files changed, 166 insertions(+), 111 deletions(-) create mode 100644 example/queryParams/app/src/QueryParams.scala create mode 100644 example/queryParams/app/test/src/ExampleTests.scala create mode 100644 example/queryParams/build.sc diff --git a/build.sc b/build.sc index 5aa5454ea3..c836fcbdf8 100644 --- a/build.sc +++ b/build.sc @@ -21,6 +21,7 @@ import $file.example.todoApi.build import $file.example.todoDb.build import $file.example.twirl.build import $file.example.variableRoutes.build +import $file.example.queryParams.build import $file.example.websockets.build import $file.example.websockets2.build import $file.example.websockets3.build @@ -172,6 +173,9 @@ object example extends Module{ trait VariableRoutesModule extends millbuild.example.variableRoutes.build.AppModule with LocalModule object variableRoutes extends Cross[VariableRoutesModule](scalaVersions) + trait QueryParamsModule extends millbuild.example.variableRoutes.build.AppModule with LocalModule + object queryParams extends Cross[QueryParamsModule](scalaVersions) + trait WebsocketsModule extends millbuild.example.websockets.build.AppModule with LocalModule object websockets extends Cross[WebsocketsModule](scalaVersions) @@ -228,6 +232,7 @@ def uploadToGithub() = T.command{ millbuild.example.todoDb.build.millSourcePath, millbuild.example.twirl.build.millSourcePath, millbuild.example.variableRoutes.build.millSourcePath, + millbuild.example.queryParams.build.millSourcePath, millbuild.example.websockets.build.millSourcePath, millbuild.example.websockets2.build.millSourcePath, millbuild.example.websockets3.build.millSourcePath, diff --git a/docs/pages/1 - Cask: a Scala HTTP micro-framework.md b/docs/pages/1 - Cask: a Scala HTTP micro-framework.md index 1cf920af53..1dad2cef95 100644 --- a/docs/pages/1 - Cask: a Scala HTTP micro-framework.md +++ b/docs/pages/1 - Cask: a Scala HTTP micro-framework.md @@ -137,10 +137,19 @@ and pass them all into `cask.Main`. $$$variableRoutes -You can bind variables to endpoints by declaring them as parameters: these are -either taken from a path-segment matcher of the same name (e.g. `postId` above), -or from query-parameters of the same name (e.g. `param` above). You can make your route -take +You can bind path segments to endpoint parameters by declaring them as parameters. these are +either: + +* A parameter of the same name as the variable path segment of the same name as you + (e.g. `postId` above), +* A parameter of type `segments: cask.RemainingPathSegments`, if you want to allow + the endpoint to handle arbitrary sub-paths of the given path + +## Query Params + +$$$queryParams + +You can bind query parameters to your endpoint method via parameters of the form: * `param: String` to match `?param=hello` * `param: Int` for `?param=123`. Other valid types include `Boolean`, `Byte`, `Short`, `Long`, @@ -153,8 +162,7 @@ take zero values * `params: cask.QueryParams` if you want your route to be able to handle arbitrary query params without needing to list them out as separate arguments -* `segments: cask.RemainingPathSegments` if you want to allow the endpoint to handle - arbitrary sub-paths of the given path + * `request: cask.Request` which provides lower level access to the things that the HTTP request provides diff --git a/example/queryParams/app/src/QueryParams.scala b/example/queryParams/app/src/QueryParams.scala new file mode 100644 index 0000000000..5e73574e82 --- /dev/null +++ b/example/queryParams/app/src/QueryParams.scala @@ -0,0 +1,35 @@ +package app +object QueryParams extends cask.MainRoutes{ + + @cask.get("/article/:articleId") // Mandatory query param, e.g. HOST/article/foo?param=bar + def getArticle(articleId: Int, param: String) = { + s"Article $articleId $param" + } + + @cask.get("/article2/:articleId") // Optional query param + def getArticleOptional(articleId: Int, param: Option[String] = None) = { + s"Article $articleId $param" + } + + @cask.get("/article3/:articleId") // Optional query param with default + def getArticleDefault(articleId: Int, param: String = "DEFAULT VALUE") = { + s"Article $articleId $param" + } + + @cask.get("/article4/:articleId") // 1-or-more param, e.g. HOST/article/foo?param=bar¶m=qux + def getArticleSeq(articleId: Int, param: Seq[String]) = { + s"Article $articleId $param" + } + + @cask.get("/article5/:articleId") // 0-or-more query param + def getArticleOptionalSeq(articleId: Int, param: Seq[String] = Nil) = { + s"Article $articleId $param" + } + + @cask.get("/user2/:userName") // allow unknown params, e.g. HOST/article/foo?foo=bar&qux=baz + def getUserProfileAllowUnknown(userName: String, params: cask.QueryParams) = { + s"User $userName " + params.value + } + + initialize() +} diff --git a/example/queryParams/app/test/src/ExampleTests.scala b/example/queryParams/app/test/src/ExampleTests.scala new file mode 100644 index 0000000000..c0f8466df1 --- /dev/null +++ b/example/queryParams/app/test/src/ExampleTests.scala @@ -0,0 +1,95 @@ +package app +import io.undertow.Undertow + +import utest._ + +object ExampleTests extends TestSuite{ + def withServer[T](example: cask.main.Main)(f: String => T): T = { + val server = Undertow.builder + .addHttpListener(8081, "localhost") + .setHandler(example.defaultHandler) + .build + server.start() + val res = + try f("http://localhost:8081") + finally server.stop() + res + } + + val tests = Tests{ + test("QueryParams") - withServer(QueryParams){ host => + val noIndexPage = requests.get(host, check = false) + noIndexPage.statusCode ==> 404 + + assert( + requests.get(s"$host/article/123?param=xyz").text() == + "Article 123 xyz" + ) + + requests.get(s"$host/article/123", check = false).text() ==> + """Missing argument: (param: String) + | + |Arguments provided did not match expected signature: + | + |getArticle + | articleId Int + | param String + | + |""".stripMargin + + assert( + requests.get(s"$host/article2/123?param=xyz").text() == + "Article 123 Some(xyz)" + ) + + assert( + requests.get(s"$host/article2/123").text() == + "Article 123 None" + ) + + assert( + requests.get(s"$host/article3/123?param=xyz").text() == + "Article 123 xyz" + ) + + assert( + requests.get(s"$host/article3/123").text() == + "Article 123 DEFAULT VALUE" + ) + + + val res1 = requests.get(s"$host/article4/123?param=xyz¶m=abc").text() + assert( + res1 == "Article 123 ArraySeq(xyz, abc)" || + res1 == "Article 123 ArrayBuffer(xyz, abc)" + ) + + requests.get(s"$host/article4/123", check = false).text() ==> + """Missing argument: (param: Seq[String]) + | + |Arguments provided did not match expected signature: + | + |getArticleSeq + | articleId Int + | param Seq[String] + | + |""".stripMargin + + val res2 = requests.get(s"$host/article5/123?param=xyz¶m=abc").text() + assert( + res2 == "Article 123 ArraySeq(xyz, abc)" || + res2 == "Article 123 ArrayBuffer(xyz, abc)" + ) + assert( + requests.get(s"$host/article5/123").text() == "Article 123 List()" + ) + + val res3 = requests.get(s"$host/user2/lihaoyi?unknown1=123&unknown2=abc", check = false).text() + assert( + res3 == "User lihaoyi Map(unknown1 -> ArrayBuffer(123), unknown2 -> ArrayBuffer(abc))" || + res3 == "User lihaoyi Map(unknown1 -> WrappedArray(123), unknown2 -> WrappedArray(abc))" || + res3 == "User lihaoyi Map(unknown1 -> ArraySeq(123), unknown2 -> ArraySeq(abc))" + ) + } + } +} diff --git a/example/queryParams/build.sc b/example/queryParams/build.sc new file mode 100644 index 0000000000..75de91ef2f --- /dev/null +++ b/example/queryParams/build.sc @@ -0,0 +1,14 @@ +import mill._, scalalib._ + +trait AppModule extends CrossScalaModule{ + + def ivyDeps = Agg[Dep]( + ) + object test extends ScalaTests with TestModule.Utest{ + + def ivyDeps = Agg( + ivy"com.lihaoyi::utest::0.8.1", + ivy"com.lihaoyi::requests::0.8.0", + ) + } +} diff --git a/example/variableRoutes/app/src/VariableRoutes.scala b/example/variableRoutes/app/src/VariableRoutes.scala index 82b7dcde8c..bcd9518cc1 100644 --- a/example/variableRoutes/app/src/VariableRoutes.scala +++ b/example/variableRoutes/app/src/VariableRoutes.scala @@ -1,46 +1,16 @@ package app object VariableRoutes extends cask.MainRoutes{ - @cask.get("/user/:userName") + @cask.get("/user/:userName") // variable path segment, e.g. HOST/user/lihaoyi def getUserProfile(userName: String) = { s"User $userName" } - @cask.get("/article/:articleId") - def getArticle(articleId: Int, param: String) = { // Mandatory query param - s"Article $articleId $param" - } - - @cask.get("/article2/:articleId") // Optional query param - def getArticleOptional(articleId: Int, param: Option[String] = None) = { - s"Article $articleId $param" - } - - @cask.get("/article3/:articleId") // Optional query param with default - def getArticleDefault(articleId: Int, param: String = "DEFAULT VALUE") = { - s"Article $articleId $param" - } - - @cask.get("/article4/:articleId") // 1-or-more query param - def getArticleSeq(articleId: Int, param: Seq[String]) = { - s"Article $articleId $param" - } - - @cask.get("/article5/:articleId") // 0-or-more query param - def getArticleOptionalSeq(articleId: Int, param: Seq[String] = Nil) = { - s"Article $articleId $param" - } - - @cask.get("/user2/:userName") // allow unknown query params - def getUserProfileAllowUnknown(userName: String, params: cask.QueryParams) = { - s"User $userName " + params.value - } - - @cask.get("/path") + @cask.get("/path") // GET allowing arbitrary sub-paths, e.g. HOST/path/foo/bar/baz def getSubpath(remainingPathSegments: cask.RemainingPathSegments) = { s"Subpath ${remainingPathSegments.value}" } - @cask.post("/path") + @cask.post("/path") // POST allowing arbitrary sub-paths, e.g. HOST/path/foo/bar/baz def postArticleSubpath(remainingPathSegments: cask.RemainingPathSegments) = { s"POST Subpath ${remainingPathSegments.value}" } diff --git a/example/variableRoutes/app/test/src/ExampleTests.scala b/example/variableRoutes/app/test/src/ExampleTests.scala index bdcb907180..1d2d9378bf 100644 --- a/example/variableRoutes/app/test/src/ExampleTests.scala +++ b/example/variableRoutes/app/test/src/ExampleTests.scala @@ -25,70 +25,6 @@ object ExampleTests extends TestSuite{ requests.get(s"$host/user", check = false).statusCode ==> 404 - - assert( - requests.get(s"$host/article/123?param=xyz").text() == - "Article 123 xyz" - ) - - requests.get(s"$host/article/123", check = false).text() ==> - """Missing argument: (param: String) - | - |Arguments provided did not match expected signature: - | - |getArticle - | articleId Int - | param String - | - |""".stripMargin - - assert( - requests.get(s"$host/article2/123?param=xyz").text() == - "Article 123 Some(xyz)" - ) - - assert( - requests.get(s"$host/article2/123").text() == - "Article 123 None" - ) - - assert( - requests.get(s"$host/article3/123?param=xyz").text() == - "Article 123 xyz" - ) - - assert( - requests.get(s"$host/article3/123").text() == - "Article 123 DEFAULT VALUE" - ) - - - val res1 = requests.get(s"$host/article4/123?param=xyz¶m=abc").text() - assert( - res1 == "Article 123 ArraySeq(xyz, abc)" || - res1 == "Article 123 ArrayBuffer(xyz, abc)" - ) - - requests.get(s"$host/article4/123", check = false).text() ==> - """Missing argument: (param: Seq[String]) - | - |Arguments provided did not match expected signature: - | - |getArticleSeq - | articleId Int - | param Seq[String] - | - |""".stripMargin - - val res2 = requests.get(s"$host/article5/123?param=xyz¶m=abc").text() - assert( - res2 == "Article 123 ArraySeq(xyz, abc)" || - res2 == "Article 123 ArrayBuffer(xyz, abc)" - ) - assert( - requests.get(s"$host/article5/123").text() == "Article 123 List()" - ) - requests.get(s"$host/path/one/two/three").text() ==> "Subpath List(one, two, three)" @@ -104,14 +40,6 @@ object ExampleTests extends TestSuite{ | userName String | |""".stripMargin - - - val res3 = requests.get(s"$host/user2/lihaoyi?unknown1=123&unknown2=abc", check = false).text() - assert( - res3 == "User lihaoyi Map(unknown1 -> ArrayBuffer(123), unknown2 -> ArrayBuffer(abc))" || - res3 == "User lihaoyi Map(unknown1 -> WrappedArray(123), unknown2 -> WrappedArray(abc))" || - res3 == "User lihaoyi Map(unknown1 -> ArraySeq(123), unknown2 -> ArraySeq(abc))" - ) } } }