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

Add cask.QueryParams type to allow route methods to take arbitrary query parameters #108

Merged
merged 9 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
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