Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate query param docs from variable routes #112

Merged
merged 7 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions cask/src/cask/endpoints/FormEndpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ object FormReader{
implicit def paramFormReader[T: QueryParamReader]: FormReader[T] = new FormReader[T]{
def arity = implicitly[QueryParamReader[T]].arity

override def unknownQueryParams: Boolean = implicitly[QueryParamReader[T]].unknownQueryParams

override def remainingPathSegments: Boolean = implicitly[QueryParamReader[T]].remainingPathSegments
def read(ctx: Request, label: String, input: Seq[FormEntry]) = {
implicitly[QueryParamReader[T]].read(ctx, label, if (input == null) null else input.map(_.valueOrFileName))
}
Expand Down
3 changes: 2 additions & 1 deletion cask/src/cask/endpoints/StaticEndpoints.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import cask.router.{HttpEndpoint, Result}
import cask.model.Request
object StaticUtil{
def makePathAndContentType(t: String, ctx: Request) = {
val path = (cask.internal.Util.splitPath(t) ++ ctx.remainingPathSegments)
val leadingSlash = if (t.startsWith("/")) "/" else ""
val path = leadingSlash + (cask.internal.Util.splitPath(t) ++ ctx.remainingPathSegments)
.filter(s => s != "." && s != "..")
.mkString("/")
val contentType = java.nio.file.Files.probeContentType(java.nio.file.Paths.get(path))
Expand Down
20 changes: 14 additions & 6 deletions docs/pages/1 - Cask: a Scala HTTP micro-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions example/formJsonPost/app/src/FormJsonPost.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,13 @@ object FormJsonPost extends cask.MainRoutes{
"OK " + value1 + " " + value2 + " " + params.value + " " + segments.value
}

@cask.postForm("/form-extra")
def formEndpointExtra(value1: cask.FormValue,
value2: Seq[Int],
params: cask.QueryParams,
segments: cask.RemainingPathSegments) = {
"OK " + value1 + " " + value2 + " " + params.value + " " + segments.value
}

initialize()
}
17 changes: 17 additions & 0 deletions example/formJsonPost/app/test/src/ExampleTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ object ExampleTests extends TestSuite{
s"$host/json-extra/omg/wtf/bbq?iam=cow&hearme=moo",
data = """{"value1": true, "value2": [3]}"""
)

val text6 = response6.text()
assert(
text6 == "\"OK true List(3) Map(hearme -> ArraySeq(moo), iam -> ArraySeq(cow)) List(omg, wtf, bbq)\"" ||
text6 == "\"OK true Vector(3) Map(hearme -> WrappedArray(moo), iam -> WrappedArray(cow)) List(omg, wtf, bbq)\""
)

val response7 = requests.post(
s"$host/form-extra/omg/wtf/bbq?iam=cow&hearme=moo",
data = Seq("value1" -> "hello", "value2" -> "1", "value2" -> "2")
)

val text7 = response7.text()
assert(
text7 == "OK FormValue(hello,null) List(1, 2) Map(hearme -> ArraySeq(moo), iam -> ArraySeq(cow)) List(omg, wtf, bbq)" ||
text7 == "OK FormValue(hello,null) List(1, 2) Map(hearme -> WrappedArray(moo), iam -> WrappedArray(cow)) List(omg, wtf, bbq)"
)
}
}
}
35 changes: 35 additions & 0 deletions example/queryParams/app/src/QueryParams.scala
Original file line number Diff line number Diff line change
@@ -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&param=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()
}
95 changes: 95 additions & 0 deletions example/queryParams/app/test/src/ExampleTests.scala
Original file line number Diff line number Diff line change
@@ -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&param=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&param=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))"
)
}
}
}
14 changes: 14 additions & 0 deletions example/queryParams/build.sc
Original file line number Diff line number Diff line change
@@ -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",
)
}
}
36 changes: 3 additions & 33 deletions example/variableRoutes/app/src/VariableRoutes.scala
Original file line number Diff line number Diff line change
@@ -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}"
}
Expand Down
Loading