Skip to content

Commit

Permalink
Add cask.QueryParams type to allow route methods to take arbitrary …
Browse files Browse the repository at this point in the history
…query parameters (#108)

Fixes #99
  • Loading branch information
lihaoyi authored Jan 4, 2024
1 parent db650ab commit b808a2c
Show file tree
Hide file tree
Showing 9 changed files with 60 additions and 16 deletions.
4 changes: 2 additions & 2 deletions cask/src-3/cask/router/Macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,12 @@ object Macros {
Runtime.makeReadCall(
args,
ctx,
(sig.default match {
sig.default match {
case None => None
case Some(getter) =>
val value = getter.asInstanceOf[Cls => Any](clazz)
Some(value)
}),
},
sig
)
}
Expand Down
10 changes: 10 additions & 0 deletions cask/src/cask/endpoints/WebEndpoints.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class delete(val path: String, override val subpath: Boolean = false) extends We
val methods = Seq("delete")
}
class route(val path: String, val methods: Seq[String], override val subpath: Boolean = false) extends WebEndpoint

class options(val path: String, override val subpath: Boolean = false) extends WebEndpoint{
val methods = Seq("options")
}
Expand All @@ -54,6 +55,15 @@ abstract class QueryParamReader[T]
def read(ctx: cask.model.Request, label: String, v: Seq[String]): T
}
object QueryParamReader{
implicit object QueryParams extends QueryParamReader[cask.model.QueryParams]{
def arity: Int = 0

override def unknownQueryParams = true
def read(ctx: cask.model.Request, label: String, v: Seq[String]) = {
cask.model.QueryParams(ctx.queryParams)
}

}
class SimpleParam[T](f: String => T) extends QueryParamReader[T]{
def arity = 1
def read(ctx: cask.model.Request, label: String, v: Seq[String]): T = f(v.head)
Expand Down
2 changes: 2 additions & 0 deletions cask/src/cask/model/Params.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import cask.internal.Util
import io.undertow.server.HttpServerExchange
import io.undertow.server.handlers.CookieImpl

case class QueryParams(value: Map[String, collection.Seq[String]])

case class Request(exchange: HttpServerExchange, remainingPathSegments: Seq[String])
extends geny.ByteData with geny.Readable {
import collection.JavaConverters._
Expand Down
2 changes: 2 additions & 0 deletions cask/src/cask/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package object cask {
val Cookie = model.Cookie
type Request = model.Request
val Request = model.Request
type QueryParams = model.QueryParams
val QueryParams = model.QueryParams

// endpoints
type websocket = endpoints.websocket
Expand Down
7 changes: 4 additions & 3 deletions cask/src/cask/router/EntryPoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ case class EntryPoint[T, C](name: String,
for(k <- firstArgs.keys) {
if (!paramLists.head.contains(k)) {
val as = firstArgs(k)
if (as.reads.arity != 0 && as.default.isEmpty) missing.append(as)
if (as.reads.arity > 0 && as.default.isEmpty) missing.append(as)
}
}

if (missing.nonEmpty || unknown.nonEmpty) Result.Error.MismatchedArguments(missing.toSeq, unknown.toSeq)
else {
if (missing.nonEmpty || (!argSignatures.exists(_.exists(_.reads.unknownQueryParams)) && unknown.nonEmpty)) {
Result.Error.MismatchedArguments(missing.toSeq, unknown.toSeq)
} else {
try invoke0(
target,
ctx,
Expand Down
1 change: 1 addition & 0 deletions cask/src/cask/router/Misc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ case class ArgSig[I, -T, +V, -C](name: String,

trait ArgReader[I, +T, -C]{
def arity: Int
def unknownQueryParams: Boolean = false
def read(ctx: C, label: String, input: I): T
}
10 changes: 8 additions & 2 deletions docs/pages/1 - Cask: a Scala HTTP micro-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,22 @@ $$$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 `param` take
or from query-parameters of the same name (e.g. `param` above). You can make your route
take
* `param: String` to match `?param=hello`
* `param: Int` for `?param=123`
* `param: Int` for `?param=123`. Other valid types include `Boolean`, `Byte`, `Short`, `Long`,
`Float`, `Double`
* `param: Option[T] = None` or `param: String = "DEFAULT VALUE"` for cases where the
`?param=hello` is optional.
* `param: Seq[T]` for repeated params such as `?param=hello&param=world` with at
least one value
* `param: Seq[T] = Nil` for repeated params such as `?param=hello&param=world` allowing
zero values
* `queryParams: cask.QueryParams` if you want your route to be able to handle arbitrary
query params without needing to list them out as separate arguments
* `request: cask.Request` which provides lower level access to the things that the HTTP
request provides
If you need to capture the entire sub-path of the request, you can set the flag
`subpath=true` and ask for a `request: cask.Request` (the name of the param doesn't
Expand Down
7 changes: 6 additions & 1 deletion example/variableRoutes/app/src/VariableRoutes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ object VariableRoutes extends cask.MainRoutes{
}

@cask.get("/article3/:articleId") // Optional query param with default
def getArticleDefault(articleId: Int, param: String = "DEFAULT VALUE") = {
def getArticleDefault(articleId: Int, param: String = "DEFAULT VALUE") = {
s"Article $articleId $param"
}

Expand All @@ -30,6 +30,11 @@ object VariableRoutes extends cask.MainRoutes{
s"Article $articleId $param"
}

@cask.get("/user2/:userName") // allow unknown query params
def getUserProfileAllowUnknown(userName: String, queryParams: cask.QueryParams) = {
s"User $userName " + queryParams.value
}

@cask.get("/path", subpath = true)
def getSubpath(request: cask.Request) = {
s"Subpath ${request.remainingPathSegments}"
Expand Down
33 changes: 25 additions & 8 deletions example/variableRoutes/app/test/src/ExampleTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,10 @@ object ExampleTests extends TestSuite{
)


val res1 = requests.get(s"$host/article4/123?param=xyz&param=abc").text()
assert(
requests.get(s"$host/article4/123?param=xyz&param=abc").text() ==
"Article 123 ArraySeq(xyz, abc)" ||
requests.get(s"$host/article4/123?param=xyz&param=abc").text() ==
"Article 123 ArrayBuffer(xyz, abc)"
res1 == "Article 123 ArraySeq(xyz, abc)" ||
res1 == "Article 123 ArrayBuffer(xyz, abc)"
)

requests.get(s"$host/article4/123", check = false).text() ==>
Expand All @@ -81,11 +80,10 @@ object ExampleTests extends TestSuite{
|
|""".stripMargin

val res2 = requests.get(s"$host/article5/123?param=xyz&param=abc").text()
assert(
requests.get(s"$host/article5/123?param=xyz&param=abc").text() ==
"Article 123 ArraySeq(xyz, abc)" ||
requests.get(s"$host/article5/123?param=xyz&param=abc").text() ==
"Article 123 ArrayBuffer(xyz, abc)"
res2 == "Article 123 ArraySeq(xyz, abc)" ||
res2 == "Article 123 ArrayBuffer(xyz, abc)"
)
assert(
requests.get(s"$host/article5/123").text() == "Article 123 List()"
Expand All @@ -96,6 +94,25 @@ object ExampleTests extends TestSuite{

requests.post(s"$host/path/one/two/three").text() ==>
"POST Subpath List(one, two, three)"

requests.get(s"$host/user/lihaoyi?unknown1=123&unknown2=abc", check = false).text() ==>
"""Unknown arguments: "unknown1" "unknown2"
|
|Arguments provided did not match expected signature:
|
|getUserProfile
| 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))"
)

}

}
Expand Down

0 comments on commit b808a2c

Please sign in to comment.