diff --git a/cask/src-3/cask/router/Macros.scala b/cask/src-3/cask/router/Macros.scala index f6eaa496b5..78561ad1bc 100644 --- a/cask/src-3/cask/router/Macros.scala +++ b/cask/src-3/cask/router/Macros.scala @@ -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 ) } diff --git a/cask/src/cask/endpoints/WebEndpoints.scala b/cask/src/cask/endpoints/WebEndpoints.scala index 9951628288..e115739ade 100644 --- a/cask/src/cask/endpoints/WebEndpoints.scala +++ b/cask/src/cask/endpoints/WebEndpoints.scala @@ -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") } @@ -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) diff --git a/cask/src/cask/model/Params.scala b/cask/src/cask/model/Params.scala index 3d8d4ddbe8..7be7f1bc88 100644 --- a/cask/src/cask/model/Params.scala +++ b/cask/src/cask/model/Params.scala @@ -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._ diff --git a/cask/src/cask/package.scala b/cask/src/cask/package.scala index 1eca56f357..83822d7132 100644 --- a/cask/src/cask/package.scala +++ b/cask/src/cask/package.scala @@ -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 diff --git a/cask/src/cask/router/EntryPoint.scala b/cask/src/cask/router/EntryPoint.scala index 6fe44fc534..2d2b7e5d62 100644 --- a/cask/src/cask/router/EntryPoint.scala +++ b/cask/src/cask/router/EntryPoint.scala @@ -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, diff --git a/cask/src/cask/router/Misc.scala b/cask/src/cask/router/Misc.scala index 438ec439f0..286655e8cf 100644 --- a/cask/src/cask/router/Misc.scala +++ b/cask/src/cask/router/Misc.scala @@ -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 } 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 5cfeeb52fd..cc0cc0a970 100644 --- a/docs/pages/1 - Cask: a Scala HTTP micro-framework.md +++ b/docs/pages/1 - Cask: a Scala HTTP micro-framework.md @@ -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¶m=world` with at least one value * `param: Seq[T] = Nil` for repeated params such as `?param=hello¶m=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 diff --git a/example/variableRoutes/app/src/VariableRoutes.scala b/example/variableRoutes/app/src/VariableRoutes.scala index 480d047ad7..826c5b82c6 100644 --- a/example/variableRoutes/app/src/VariableRoutes.scala +++ b/example/variableRoutes/app/src/VariableRoutes.scala @@ -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" } @@ -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}" diff --git a/example/variableRoutes/app/test/src/ExampleTests.scala b/example/variableRoutes/app/test/src/ExampleTests.scala index 4494c68663..b8a33356df 100644 --- a/example/variableRoutes/app/test/src/ExampleTests.scala +++ b/example/variableRoutes/app/test/src/ExampleTests.scala @@ -63,11 +63,10 @@ object ExampleTests extends TestSuite{ ) + val res1 = requests.get(s"$host/article4/123?param=xyz¶m=abc").text() assert( - requests.get(s"$host/article4/123?param=xyz¶m=abc").text() == - "Article 123 ArraySeq(xyz, abc)" || - requests.get(s"$host/article4/123?param=xyz¶m=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() ==> @@ -81,11 +80,10 @@ object ExampleTests extends TestSuite{ | |""".stripMargin + val res2 = requests.get(s"$host/article5/123?param=xyz¶m=abc").text() assert( - requests.get(s"$host/article5/123?param=xyz¶m=abc").text() == - "Article 123 ArraySeq(xyz, abc)" || - requests.get(s"$host/article5/123?param=xyz¶m=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()" @@ -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))" + ) + } }