diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2018-07-28 20:35:51 +0800 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2018-07-28 21:03:15 +0800 |
commit | 229ab52fe36c0882bac8aa84ae484a12d339242a (patch) | |
tree | e9f8f5de1309cec044b6eb8c83f3fc5a86e5beda | |
parent | 8b14cd206e008b4001f9b257f48870c8d40e8498 (diff) | |
download | cask-229ab52fe36c0882bac8aa84ae484a12d339242a.tar.gz cask-229ab52fe36c0882bac8aa84ae484a12d339242a.tar.bz2 cask-229ab52fe36c0882bac8aa84ae484a12d339242a.zip |
wip more readme
-rw-r--r-- | cask/src/cask/endpoints/FormEndpoint.scala | 4 | ||||
-rw-r--r-- | cask/src/cask/endpoints/JsonEndpoint.scala | 2 | ||||
-rw-r--r-- | cask/src/cask/endpoints/StaticEndpoints.scala | 4 | ||||
-rw-r--r-- | cask/src/cask/endpoints/WebEndpoints.scala | 10 | ||||
-rw-r--r-- | cask/src/cask/internal/Router.scala | 4 | ||||
-rw-r--r-- | cask/src/cask/main/Decorators.scala | 11 | ||||
-rw-r--r-- | cask/src/cask/main/Main.scala | 17 | ||||
-rw-r--r-- | cask/src/cask/package.scala | 2 | ||||
-rw-r--r-- | cask/test/src/test/cask/Decorated.scala | 11 | ||||
-rw-r--r-- | cask/test/src/test/cask/FailureTests.scala | 15 | ||||
-rw-r--r-- | cask/test/src/test/cask/FormJsonPost.scala | 1 | ||||
-rw-r--r-- | readme.md | 48 |
12 files changed, 93 insertions, 36 deletions
diff --git a/cask/src/cask/endpoints/FormEndpoint.scala b/cask/src/cask/endpoints/FormEndpoint.scala index 715c803..525dfde 100644 --- a/cask/src/cask/endpoints/FormEndpoint.scala +++ b/cask/src/cask/endpoints/FormEndpoint.scala @@ -56,13 +56,13 @@ class postForm(val path: String, override val subpath: Boolean = false) extends "Unable to parse form data: " + e + "\n" + Util.stackTraceString(e) ))} } yield { - val formDataBindings = + cask.main.Decor( formData .iterator() .asScala .map(k => (k, formData.get(k).asScala.map(FormEntry.fromUndertow).toSeq)) .toMap - formDataBindings + ) } } def wrapPathSegment(s: String): Input = Seq(FormValue(s, new io.undertow.util.HeaderMap)) diff --git a/cask/src/cask/endpoints/JsonEndpoint.scala b/cask/src/cask/endpoints/JsonEndpoint.scala index 425f942..853e07d 100644 --- a/cask/src/cask/endpoints/JsonEndpoint.scala +++ b/cask/src/cask/endpoints/JsonEndpoint.scala @@ -43,7 +43,7 @@ class postJson(val path: String, override val subpath: Boolean = false) extends obj <- try Right(json.obj) catch {case e: Throwable => Left(cask.model.Response("Input JSON must be a dictionary"))} - } yield obj.toMap + } yield cask.main.Decor(obj.toMap) } def wrapPathSegment(s: String): Input = ujson.Js.Str(s) } diff --git a/cask/src/cask/endpoints/StaticEndpoints.scala b/cask/src/cask/endpoints/StaticEndpoints.scala index 436fd25..7e1e6dd 100644 --- a/cask/src/cask/endpoints/StaticEndpoints.scala +++ b/cask/src/cask/endpoints/StaticEndpoints.scala @@ -14,6 +14,8 @@ class static(val path: String) extends Endpoint[String] { Router.Result.Success(cask.model.Static(t + "/" + ctx.remaining.mkString("/"))) } - def getRawParams(ctx: ParamContext) = Right(Map()) + def getRawParams(ctx: ParamContext) = Right( + cask.main.Decor(Map()) + ) def wrapPathSegment(s: String): Input = Seq(s) } diff --git a/cask/src/cask/endpoints/WebEndpoints.scala b/cask/src/cask/endpoints/WebEndpoints.scala index d0ac46d..a5b2d02 100644 --- a/cask/src/cask/endpoints/WebEndpoints.scala +++ b/cask/src/cask/endpoints/WebEndpoints.scala @@ -11,10 +11,12 @@ trait WebEndpoint extends Endpoint[BaseResponse]{ type Input = Seq[String] type InputParser[T] = QueryParamReader[T] def getRawParams(ctx: ParamContext) = Right( - ctx.exchange.getQueryParameters - .asScala - .map{case (k, vs) => (k, vs.asScala.toArray.toSeq)} - .toMap + cask.main.Decor( + ctx.exchange.getQueryParameters + .asScala + .map{case (k, vs) => (k, vs.asScala.toArray.toSeq)} + .toMap + ) ) def wrapPathSegment(s: String) = Seq(s) } diff --git a/cask/src/cask/internal/Router.scala b/cask/src/cask/internal/Router.scala index 4615bd3..c831240 100644 --- a/cask/src/cask/internal/Router.scala +++ b/cask/src/cask/internal/Router.scala @@ -217,10 +217,10 @@ class Router[C <: Context](val c: C) { val argValuesSymbol = q"${c.fresh[TermName]("argValues")}" val argSigsSymbol = q"${c.fresh[TermName]("argSigs")}" val ctxSymbol = q"${c.fresh[TermName]("ctx")}" - if (method.paramLists.length != argReaders.length) c.abort( + if (method.paramLists.length > argReaders.length) c.abort( method.pos, s"Endpoint ${method.name}'s number of parameter lists (${method.paramLists.length}) " + - s"doesn't match number of decorators (${argReaders.length})" + s"cannot be more than the number of decorators (${argReaders.length})" ) val argData = for(argListIndex <- method.paramLists.indices) yield{ val annotDeserializeType = annotDeserializeTypes(argListIndex) diff --git a/cask/src/cask/main/Decorators.scala b/cask/src/cask/main/Decorators.scala index 91cf92f..b262039 100644 --- a/cask/src/cask/main/Decorators.scala +++ b/cask/src/cask/main/Decorators.scala @@ -33,9 +33,18 @@ trait Endpoint[R] extends BaseDecorator{ trait BaseDecorator{ type Input type InputParser[T] <: ArgReader[Input, T, ParamContext] - def getRawParams(ctx: ParamContext): Either[cask.model.Response, Map[String, Input]] + def getRawParams(ctx: ParamContext): Either[cask.model.Response, Decor[Input]] def getParamParser[T](implicit p: InputParser[T]) = p +} +object Decor{ + def apply[Input](params: (String, Input)*) = new Decor(params.toMap, () => ()) + def apply[Input](params: TraversableOnce[(String, Input)], cleanup: () => Unit = () => ()) = { + new Decor(params.toMap, cleanup) + } +} +class Decor[Input](val params: Map[String, Input], val cleanup: () => Unit){ + def withCleanup(newCleanUp: () => Unit) = new Decor(params, newCleanUp) } trait Decorator extends BaseDecorator { diff --git a/cask/src/cask/main/Main.scala b/cask/src/cask/main/Main.scala index 9ac0022..fb07e77 100644 --- a/cask/src/cask/main/Main.scala +++ b/cask/src/cask/main/Main.scala @@ -57,18 +57,23 @@ abstract class BaseMain{ case None => writeResponse(exchange, handleError(404)) case Some(((routes, metadata), bindings, remaining)) => val params = for{ - endpointParams <- metadata.endpoint.getRawParams(ParamContext(exchange, remaining)) - decoratorParams <- Util.sequenceEither( + decoratorParams <- Util.sequenceEither[Response, cask.main.Decor[_], Seq]( metadata.decorators.map(e => e.getRawParams(ParamContext(exchange, remaining))) ) - } yield (endpointParams ++ bindings.mapValues(metadata.endpoint.wrapPathSegment)) +: decoratorParams + endpointParams <- metadata.endpoint.getRawParams(ParamContext(exchange, remaining)) + } yield ( + (endpointParams.params ++ bindings.mapValues(metadata.endpoint.wrapPathSegment)) +: + decoratorParams.map(_.params), + () => {endpointParams.cleanup(); decoratorParams.foreach(_.cleanup())} + ) val result = params match{ case Left(resp) => resp - case Right(paramValues) => - metadata.entryPoint + case Right((paramValues, cleanup)) => + try metadata.entryPoint .asInstanceOf[EntryPoint[cask.main.Routes, cask.model.ParamContext]] .invoke(routes, ParamContext(exchange, remaining), paramValues) + finally cleanup() } @@ -86,8 +91,6 @@ abstract class BaseMain{ statusCode = 500) ) } - - } } } diff --git a/cask/src/cask/package.scala b/cask/src/cask/package.scala index 24a0a20..b7f1478 100644 --- a/cask/src/cask/package.scala +++ b/cask/src/cask/package.scala @@ -36,5 +36,7 @@ package object cask { type Decorator = main.Decorator type Endpoint[R] = main.Endpoint[R] type BaseDecorator = main.BaseDecorator + type Decor[T] = main.Decor[T] + val Decor = main.Decor } diff --git a/cask/test/src/test/cask/Decorated.scala b/cask/test/src/test/cask/Decorated.scala index ed377cf..3925bf1 100644 --- a/cask/test/src/test/cask/Decorated.scala +++ b/cask/test/src/test/cask/Decorated.scala @@ -7,10 +7,10 @@ object Decorated extends cask.MainRoutes{ override def toString = "[haoyi]" } class loggedIn extends cask.Decorator { - def getRawParams(ctx: ParamContext) = Right(Map("user" -> new User())) + def getRawParams(ctx: ParamContext) = Right(cask.Decor("user" -> new User())) } class withExtra extends cask.Decorator { - def getRawParams(ctx: ParamContext) = Right(Map("extra" -> 31337)) + def getRawParams(ctx: ParamContext) = Right(cask.Decor("extra" -> 31337)) } @withExtra() @@ -32,5 +32,12 @@ object Decorated extends cask.MainRoutes{ world + user + extra } + @withExtra() + @loggedIn() + @cask.get("/ignore-extra/:world") + def ignoreExtra(world: String)(user: User) = { + world + user + } + initialize() } diff --git a/cask/test/src/test/cask/FailureTests.scala b/cask/test/src/test/cask/FailureTests.scala index dd6914a..3ed4249 100644 --- a/cask/test/src/test/cask/FailureTests.scala +++ b/cask/test/src/test/cask/FailureTests.scala @@ -5,7 +5,7 @@ import utest._ object FailureTests extends TestSuite { class myDecorator extends cask.Decorator { - def getRawParams(ctx: ParamContext) = Right(Map("extra" -> 31337)) + def getRawParams(ctx: ParamContext) = Right(cask.Decor("extra" -> 31337)) } val tests = Tests{ @@ -17,17 +17,7 @@ object FailureTests extends TestSuite { initialize() } """).msg ==> - "Endpoint hello's number of parameter lists (2) doesn't match number of decorators (1)" - - utest.compileError(""" - object Decorated extends cask.MainRoutes{ - @myDecorator() - @cask.get("/hello/:world") - def hello(world: String)= world - initialize() - } - """).msg ==> - "Endpoint hello's number of parameter lists (1) doesn't match number of decorators (2)" + "Endpoint hello's number of parameter lists (2) cannot be more than the number of decorators (1)" utest.compileError(""" object Decorated extends cask.MainRoutes{ @@ -51,3 +41,4 @@ object FailureTests extends TestSuite { } } } + diff --git a/cask/test/src/test/cask/FormJsonPost.scala b/cask/test/src/test/cask/FormJsonPost.scala index 9db3d24..05a8761 100644 --- a/cask/test/src/test/cask/FormJsonPost.scala +++ b/cask/test/src/test/cask/FormJsonPost.scala @@ -18,4 +18,3 @@ object FormJsonPost extends cask.MainRoutes{ initialize() } - @@ -281,6 +281,7 @@ the relevant headers or status code for you. Extending Endpoints with Decorators ----------------------------------- + ```scala import cask.model.ParamContext @@ -289,10 +290,10 @@ object Decorated extends cask.MainRoutes{ override def toString = "[haoyi]" } class loggedIn extends cask.Decorator { - def getRawParams(ctx: ParamContext) = Right(Map("user" -> new User())) + def getRawParams(ctx: ParamContext) = Right(cask.Decor("user" -> new User())) } class withExtra extends cask.Decorator { - def getRawParams(ctx: ParamContext) = Right(Map("extra" -> 31337)) + def getRawParams(ctx: ParamContext) = Right(cask.Decor("extra" -> 31337)) } @withExtra() @@ -314,7 +315,48 @@ object Decorated extends cask.MainRoutes{ world + user + extra } + @withExtra() + @loggedIn() + @cask.get("/ignore-extra/:world") + def ignoreExtra(world: String)(user: User) = { + world + user + } + initialize() } +``` + +You can write extra decorator annotations that stack on top of the existing +`@cask.get`/`@cask.post` to provide additional arguments or validation. This is +done by implementing the `cask.Decorator` interface and it's `getRawParams` +function. `getRawParams`: + +- Receives a `ParamContext`, which basically gives you full access to the + underlying undertow HTTP connection so you can pick out whatever data you + would like + +- Returns an `Either[Response, cask.Decor[Any]]`. Returning a `Left` lets you + bail out early with a fixed `cask.Response`, avoiding further processing. + Returning a `Right` provides a map of parameter names and values that will + then get passed to the endpoint function in consecutive parameter lists (shown + above), as well as an optional cleanup function that is run after the endpoint + terminates. + +Each additional decorator is responsible for one additional parameter list to +the right of the existing parameter lists, each of which can contain any number +of parameters. + +Decorators are useful for things like: + +- Making an endpoint return a HTTP 403 if the user isn't logged in, but if they are + logged in providing the `: User` object to the body of the endpoint function + +- Rate-limiting users by returning early with a HTTP 429 if a user tries to + access an endpoint too many times too quickly + +- Providing request-scoped values to the endpoint function: perhaps a database + transaction that commits when the function succeeds (and rolls-back if it + fails), or access to some system resource that needs to be released. -```
\ No newline at end of file +Writing Custom Endpoints +------------------------
\ No newline at end of file |