From 9fcdec156abd88d5e865977d25f6a351034bb086 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 4 Jan 2024 16:02:14 +0800 Subject: [PATCH 1/2] . --- cask/src/cask/endpoints/JsonEndpoint.scala | 2 ++ cask/src/cask/endpoints/ParamReader.scala | 20 +++++++++++++++++++ cask/src/cask/endpoints/WebEndpoints.scala | 9 ++------- cask/src/cask/main/Main.scala | 6 +++++- cask/src/cask/model/Params.scala | 1 + cask/src/cask/package.scala | 2 ++ cask/src/cask/router/Misc.scala | 1 + .../1 - Cask: a Scala HTTP micro-framework.md | 10 +++------- .../formJsonPost/app/src/FormJsonPost.scala | 9 +++++++++ .../app/test/src/ExampleTests.scala | 6 ++++++ .../app/src/VariableRoutes.scala | 16 +++++++-------- .../app/test/src/ExampleTests.scala | 2 -- 12 files changed, 59 insertions(+), 25 deletions(-) diff --git a/cask/src/cask/endpoints/JsonEndpoint.scala b/cask/src/cask/endpoints/JsonEndpoint.scala index fbab2fa8e8..5c421cac65 100644 --- a/cask/src/cask/endpoints/JsonEndpoint.scala +++ b/cask/src/cask/endpoints/JsonEndpoint.scala @@ -24,6 +24,8 @@ object JsReader{ implicit def paramReader[T: ParamReader]: JsReader[T] = new JsReader[T] { override def arity = 0 + override def unknownQueryParams: Boolean = implicitly[ParamReader[T]].unknownQueryParams + override def remainingPathSegments: Boolean = implicitly[ParamReader[T]].remainingPathSegments override def read(ctx: cask.model.Request, label: String, v: ujson.Value) = { implicitly[ParamReader[T]].read(ctx, label, Nil) } diff --git a/cask/src/cask/endpoints/ParamReader.scala b/cask/src/cask/endpoints/ParamReader.scala index 4ac34f086b..4e6face9a6 100644 --- a/cask/src/cask/endpoints/ParamReader.scala +++ b/cask/src/cask/endpoints/ParamReader.scala @@ -25,4 +25,24 @@ object ParamReader{ implicit object CookieParam extends NilParam[Cookie]((ctx, label) => Cookie.fromUndertow(ctx.exchange.getRequestCookies().get(label)) ) + + implicit object QueryParams extends ParamReader[cask.model.QueryParams] { + def arity: Int = 0 + + override def unknownQueryParams = true + + def read(ctx: cask.model.Request, label: String, v: Unit) = { + cask.model.QueryParams(ctx.queryParams) + } + } + + implicit object RemainingPathSegments extends ParamReader[cask.model.RemainingPathSegments] { + def arity: Int = 0 + + override def remainingPathSegments = true + + def read(ctx: cask.model.Request, label: String, v: Unit) = { + cask.model.RemainingPathSegments(ctx.remainingPathSegments) + } + } } diff --git a/cask/src/cask/endpoints/WebEndpoints.scala b/cask/src/cask/endpoints/WebEndpoints.scala index e115739ade..b2b9731a62 100644 --- a/cask/src/cask/endpoints/WebEndpoints.scala +++ b/cask/src/cask/endpoints/WebEndpoints.scala @@ -55,15 +55,8 @@ 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) @@ -92,6 +85,8 @@ object QueryParamReader{ implicit def paramReader[T: ParamReader]: QueryParamReader[T] = new QueryParamReader[T] { override def arity = 0 + override def unknownQueryParams: Boolean = implicitly[ParamReader[T]].unknownQueryParams + override def remainingPathSegments: Boolean = implicitly[ParamReader[T]].remainingPathSegments override def read(ctx: cask.model.Request, label: String, v: Seq[String]) = { implicitly[ParamReader[T]].read(ctx, label, v) } diff --git a/cask/src/cask/main/Main.scala b/cask/src/cask/main/Main.scala index 9bf71328b7..31fd1522b2 100644 --- a/cask/src/cask/main/Main.scala +++ b/cask/src/cask/main/Main.scala @@ -150,7 +150,11 @@ object Main{ val segments = Util.splitPath(metadata.endpoint.path) val methods = metadata.endpoint.methods.map(_ -> (routes, metadata: EndpointMetadata[_])) val methodMap = methods.toMap[String, (Routes, EndpointMetadata[_])] - (segments, methodMap, metadata.endpoint.subpath) + val subpath = + metadata.endpoint.subpath || + metadata.entryPoint.argSignatures.exists(_.exists(_.reads.remainingPathSegments)) + + (segments, methodMap, subpath) } val dispatchInputs = flattenedRoutes.groupBy(_._1).map { case (segments, values) => diff --git a/cask/src/cask/model/Params.scala b/cask/src/cask/model/Params.scala index 7be7f1bc88..56a7135bc9 100644 --- a/cask/src/cask/model/Params.scala +++ b/cask/src/cask/model/Params.scala @@ -7,6 +7,7 @@ import io.undertow.server.HttpServerExchange import io.undertow.server.handlers.CookieImpl case class QueryParams(value: Map[String, collection.Seq[String]]) +case class RemainingPathSegments(value: Seq[String]) case class Request(exchange: HttpServerExchange, remainingPathSegments: Seq[String]) extends geny.ByteData with geny.Readable { diff --git a/cask/src/cask/package.scala b/cask/src/cask/package.scala index 83822d7132..8ef0a1b81d 100644 --- a/cask/src/cask/package.scala +++ b/cask/src/cask/package.scala @@ -20,6 +20,8 @@ package object cask { val Request = model.Request type QueryParams = model.QueryParams val QueryParams = model.QueryParams + type RemainingPathSegments = model.RemainingPathSegments + val RemainingPathSegments = model.RemainingPathSegments // endpoints type websocket = endpoints.websocket diff --git a/cask/src/cask/router/Misc.scala b/cask/src/cask/router/Misc.scala index 286655e8cf..e3de551859 100644 --- a/cask/src/cask/router/Misc.scala +++ b/cask/src/cask/router/Misc.scala @@ -20,5 +20,6 @@ case class ArgSig[I, -T, +V, -C](name: String, trait ArgReader[I, +T, -C]{ def arity: Int def unknownQueryParams: Boolean = false + def remainingPathSegments: 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 cc0cc0a970..1cf920af53 100644 --- a/docs/pages/1 - Cask: a Scala HTTP micro-framework.md +++ b/docs/pages/1 - Cask: a Scala HTTP micro-framework.md @@ -151,17 +151,13 @@ take 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 +* `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 -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 -matter). This will make the route match any sub-path of the prefix given to the -`@cask` decorator, and give you the remainder to use in your endpoint logic -as `request.remainingPathSegments` - ## Multi-method Routes $$$httpMethods diff --git a/example/formJsonPost/app/src/FormJsonPost.scala b/example/formJsonPost/app/src/FormJsonPost.scala index 9a02f5e336..70d7a2644c 100644 --- a/example/formJsonPost/app/src/FormJsonPost.scala +++ b/example/formJsonPost/app/src/FormJsonPost.scala @@ -31,5 +31,14 @@ object FormJsonPost extends cask.MainRoutes{ image.fileName } + + @cask.postJson("/json-extra") + def jsonEndpointExtra(value1: ujson.Value, + value2: Seq[Int], + params: cask.QueryParams, + segments: cask.RemainingPathSegments) = { + "OK " + value1 + " " + value2 + " " + params.value + " " + segments.value + } + initialize() } diff --git a/example/formJsonPost/app/test/src/ExampleTests.scala b/example/formJsonPost/app/test/src/ExampleTests.scala index 67e273482c..4d1a1cfbe0 100644 --- a/example/formJsonPost/app/test/src/ExampleTests.scala +++ b/example/formJsonPost/app/test/src/ExampleTests.scala @@ -52,6 +52,12 @@ object ExampleTests extends TestSuite{ ) ) response5.text() ==> "my-best-image.txt" + + + val response6 = requests.post( + s"$host/json-extra/omg/wtf/bbq?iam=cow&hearme=moo", + data = """{"value1": true, "value2": [3]}""" + ) } } } diff --git a/example/variableRoutes/app/src/VariableRoutes.scala b/example/variableRoutes/app/src/VariableRoutes.scala index 826c5b82c6..82b7dcde8c 100644 --- a/example/variableRoutes/app/src/VariableRoutes.scala +++ b/example/variableRoutes/app/src/VariableRoutes.scala @@ -31,18 +31,18 @@ object VariableRoutes extends cask.MainRoutes{ } @cask.get("/user2/:userName") // allow unknown query params - def getUserProfileAllowUnknown(userName: String, queryParams: cask.QueryParams) = { - s"User $userName " + queryParams.value + def getUserProfileAllowUnknown(userName: String, params: cask.QueryParams) = { + s"User $userName " + params.value } - @cask.get("/path", subpath = true) - def getSubpath(request: cask.Request) = { - s"Subpath ${request.remainingPathSegments}" + @cask.get("/path") + def getSubpath(remainingPathSegments: cask.RemainingPathSegments) = { + s"Subpath ${remainingPathSegments.value}" } - @cask.post("/path", subpath = true) - def postArticleSubpath(request: cask.Request) = { - s"POST Subpath ${request.remainingPathSegments}" + @cask.post("/path") + def postArticleSubpath(remainingPathSegments: cask.RemainingPathSegments) = { + s"POST Subpath ${remainingPathSegments.value}" } initialize() diff --git a/example/variableRoutes/app/test/src/ExampleTests.scala b/example/variableRoutes/app/test/src/ExampleTests.scala index b8a33356df..bdcb907180 100644 --- a/example/variableRoutes/app/test/src/ExampleTests.scala +++ b/example/variableRoutes/app/test/src/ExampleTests.scala @@ -112,8 +112,6 @@ object ExampleTests extends TestSuite{ res3 == "User lihaoyi Map(unknown1 -> WrappedArray(123), unknown2 -> WrappedArray(abc))" || res3 == "User lihaoyi Map(unknown1 -> ArraySeq(123), unknown2 -> ArraySeq(abc))" ) - } - } } From 9c53449d8e07d6171ed2dad9e5cfbd5b7340185e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 4 Jan 2024 16:29:25 +0800 Subject: [PATCH 2/2] . --- cask/src/cask/endpoints/FormEndpoint.scala | 3 +++ example/formJsonPost/app/src/FormJsonPost.scala | 8 ++++++++ .../app/test/src/ExampleTests.scala | 17 +++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/cask/src/cask/endpoints/FormEndpoint.scala b/cask/src/cask/endpoints/FormEndpoint.scala index 0dd7c1e48b..956e09130b 100644 --- a/cask/src/cask/endpoints/FormEndpoint.scala +++ b/cask/src/cask/endpoints/FormEndpoint.scala @@ -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)) } diff --git a/example/formJsonPost/app/src/FormJsonPost.scala b/example/formJsonPost/app/src/FormJsonPost.scala index 70d7a2644c..e43968c927 100644 --- a/example/formJsonPost/app/src/FormJsonPost.scala +++ b/example/formJsonPost/app/src/FormJsonPost.scala @@ -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() } diff --git a/example/formJsonPost/app/test/src/ExampleTests.scala b/example/formJsonPost/app/test/src/ExampleTests.scala index 4d1a1cfbe0..adb2ee1b76 100644 --- a/example/formJsonPost/app/test/src/ExampleTests.scala +++ b/example/formJsonPost/app/test/src/ExampleTests.scala @@ -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)" + ) } } }