From fe17f0a465a49433867ea4917fd4938a7d2b6609 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Thu, 9 Aug 2018 00:36:25 +0800 Subject: Add `@cask.decorators.compress` utility Allow for decorators to be applied across `cask.Routes` or `cask.Main` --- cask/src/cask/decorators/compress.scala | 43 ++++++++++++ cask/src/cask/internal/Router.scala | 9 +-- cask/src/cask/main/Decorators.scala | 1 + cask/src/cask/main/Main.scala | 3 +- cask/src/cask/main/Routes.scala | 2 + cask/test/src/test/cask/Compress.scala | 12 ++++ cask/test/src/test/cask/Compress2.scala | 14 ++++ cask/test/src/test/cask/Compress3.scala | 15 ++++ cask/test/src/test/cask/Decorated2.scala | 38 +++++++++++ cask/test/src/test/cask/ExampleTests.scala | 32 +++++++++ cask/test/src/test/cask/FailureTests.scala | 14 ++-- readme.md | 106 +++++++++++++++++++++++++++++ 12 files changed, 272 insertions(+), 17 deletions(-) create mode 100644 cask/src/cask/decorators/compress.scala create mode 100644 cask/test/src/test/cask/Compress.scala create mode 100644 cask/test/src/test/cask/Compress2.scala create mode 100644 cask/test/src/test/cask/Compress3.scala create mode 100644 cask/test/src/test/cask/Decorated2.scala diff --git a/cask/src/cask/decorators/compress.scala b/cask/src/cask/decorators/compress.scala new file mode 100644 index 0000000..fc67dd4 --- /dev/null +++ b/cask/src/cask/decorators/compress.scala @@ -0,0 +1,43 @@ +package cask.decorators +import java.io.{ByteArrayOutputStream, OutputStream} +import java.util.zip.{DeflaterOutputStream, GZIPOutputStream} + +import cask.internal.Router +import cask.model.{ParamContext, Response} + +import collection.JavaConverters._ +class compress extends cask.Decorator{ + def wrapFunction(ctx: ParamContext, delegate: Delegate) = { + val acceptEncodings = ctx.exchange.getRequestHeaders.get("Accept-Encoding").asScala.flatMap(_.split(", ")) + delegate(Map()) match{ + case Router.Result.Success(v) => + val (newData, newHeaders) = if (acceptEncodings.exists(_.toLowerCase == "gzip")) { + new Response.Data { + def write(out: OutputStream): Unit = { + val wrap = new GZIPOutputStream(out) + v.data.write(wrap) + wrap.flush() + wrap.close() + } + } -> Seq("Content-Encoding" -> "gzip") + }else if (acceptEncodings.exists(_.toLowerCase == "deflate")){ + new Response.Data { + def write(out: OutputStream): Unit = { + val wrap = new DeflaterOutputStream(out) + v.data.write(wrap) + wrap.flush() + } + } -> Seq("Content-Encoding" -> "deflate") + }else v.data -> Nil + Router.Result.Success( + Response( + newData, + v.statusCode, + v.headers ++ newHeaders, + v.cookies + ) + ) + case e: Router.Result.Error => e + } + } +} diff --git a/cask/src/cask/internal/Router.scala b/cask/src/cask/internal/Router.scala index 7ad0c18..c66e8d2 100644 --- a/cask/src/cask/internal/Router.scala +++ b/cask/src/cask/internal/Router.scala @@ -217,14 +217,9 @@ 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( - method.pos, - s"Endpoint ${method.name}'s number of parameter lists (${method.paramLists.length}) " + - s"cannot be more than the number of decorators (${argReaders.length})" - ) val argData = for(argListIndex <- method.paramLists.indices) yield{ - val annotDeserializeType = annotDeserializeTypes(argListIndex) - val argReader = argReaders(argListIndex) + val annotDeserializeType = annotDeserializeTypes.lift(argListIndex).getOrElse(tq"scala.Any") + val argReader = argReaders.lift(argListIndex).getOrElse(q"cask.main.NoOpParser.instanceAny") val flattenedArgLists = method.paramss(argListIndex) def hasDefault(i: Int) = { val defaultName = s"${method.name}$$default$$${i + 1}" diff --git a/cask/src/cask/main/Decorators.scala b/cask/src/cask/main/Decorators.scala index 1bd8867..73d8c19 100644 --- a/cask/src/cask/main/Decorators.scala +++ b/cask/src/cask/main/Decorators.scala @@ -75,4 +75,5 @@ class NoOpParser[Input, T] extends ArgReader[Input, T, ParamContext] { } object NoOpParser{ implicit def instance[Input, T] = new NoOpParser[Input, T] + implicit def instanceAny[T] = new NoOpParser[Any, T] } \ No newline at end of file diff --git a/cask/src/cask/main/Main.scala b/cask/src/cask/main/Main.scala index 5558a08..4a8182b 100644 --- a/cask/src/cask/main/Main.scala +++ b/cask/src/cask/main/Main.scala @@ -15,6 +15,7 @@ class Main(servers0: Routes*) extends BaseMain{ def allRoutes = servers0.toSeq } abstract class BaseMain{ + def mainDecorators = Seq.empty[cask.main.Decorator] def allRoutes: Seq[Routes] val port: Int = 8080 val host: String = "localhost" @@ -77,7 +78,7 @@ abstract class BaseMain{ // delegate throwing on them }catch{case e: Throwable => Router.Result.Error.Exception(e) } - rec(metadata.decorators.toList, Nil)match{ + rec((metadata.decorators ++ routes.decorators ++ mainDecorators).toList, Nil)match{ case Router.Result.Success(response: Response) => writeResponse(exchange, response) case e: Router.Result.Error => diff --git a/cask/src/cask/main/Routes.scala b/cask/src/cask/main/Routes.scala index 0dea657..7b47731 100644 --- a/cask/src/cask/main/Routes.scala +++ b/cask/src/cask/main/Routes.scala @@ -70,6 +70,8 @@ object Routes{ } trait Routes{ + + def decorators = Seq.empty[cask.main.Decorator] private[this] var metadata0: Routes.RoutesEndpointsMetadata[this.type] = null def caskMetadata = if (metadata0 != null) metadata0 diff --git a/cask/test/src/test/cask/Compress.scala b/cask/test/src/test/cask/Compress.scala new file mode 100644 index 0000000..1a027d6 --- /dev/null +++ b/cask/test/src/test/cask/Compress.scala @@ -0,0 +1,12 @@ +package test.cask + +object Compress extends cask.MainRoutes{ + + @cask.decorators.compress + @cask.get("/") + def hello() = { + "Hello World! Hello World! Hello World!" + } + + initialize() +} diff --git a/cask/test/src/test/cask/Compress2.scala b/cask/test/src/test/cask/Compress2.scala new file mode 100644 index 0000000..0f2d01f --- /dev/null +++ b/cask/test/src/test/cask/Compress2.scala @@ -0,0 +1,14 @@ +package test.cask + +object Compress2 extends cask.Routes{ + override def decorators = Seq(new cask.decorators.compress()) + + @cask.get("/") + def hello() = { + "Hello World! Hello World! Hello World!" + } + + initialize() +} + +object Compress2Main extends cask.Main(Compress2) \ No newline at end of file diff --git a/cask/test/src/test/cask/Compress3.scala b/cask/test/src/test/cask/Compress3.scala new file mode 100644 index 0000000..1c8da25 --- /dev/null +++ b/cask/test/src/test/cask/Compress3.scala @@ -0,0 +1,15 @@ +package test.cask + +object Compress3 extends cask.Routes{ + + @cask.get("/") + def hello() = { + "Hello World! Hello World! Hello World!" + } + + initialize() +} + +object Compress3Main extends cask.Main(Compress3){ + override def mainDecorators = Seq(new cask.decorators.compress()) +} \ No newline at end of file diff --git a/cask/test/src/test/cask/Decorated2.scala b/cask/test/src/test/cask/Decorated2.scala new file mode 100644 index 0000000..0d11952 --- /dev/null +++ b/cask/test/src/test/cask/Decorated2.scala @@ -0,0 +1,38 @@ +package test.cask + +object Decorated2 extends cask.MainRoutes{ + class User{ + override def toString = "[haoyi]" + } + class loggedIn extends cask.Decorator { + def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = { + delegate(Map("user" -> new User())) + } + } + class withExtra extends cask.Decorator { + def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = { + delegate(Map("extra" -> 31337)) + } + } + + override def decorators = Seq(new withExtra()) + + @cask.get("/hello/:world") + def hello(world: String)(extra: Int) = { + world + extra + } + + @loggedIn() + @cask.get("/internal-extra/:world") + def internalExtra(world: String)(user: User)(extra: Int) = { + world + user + extra + } + + @loggedIn() + @cask.get("/ignore-extra/:world") + def ignoreExtra(world: String)(user: User)(extra: Int) = { + world + user + } + + initialize() +} diff --git a/cask/test/src/test/cask/ExampleTests.scala b/cask/test/src/test/cask/ExampleTests.scala index 36e0387..6784b1e 100644 --- a/cask/test/src/test/cask/ExampleTests.scala +++ b/cask/test/src/test/cask/ExampleTests.scala @@ -102,6 +102,12 @@ object ExampleTests extends TestSuite{ requests.get(s"$host/internal/boo").text() ==> "boo[haoyi]" requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337" + } + 'Decorated2 - test(Decorated2){ host => + requests.get(s"$host/hello/woo").text() ==> "woo31337" + requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337" + requests.get(s"$host/ignore-extra/boo").text() ==> "boo[haoyi]" + } 'TodoMvcApi - test(TodoMvcApi){ host => requests.get(s"$host/list/all").text() ==> @@ -155,5 +161,31 @@ object ExampleTests extends TestSuite{ requests.get(s"$host/list/active").text() ==> """[]""" } + + 'Compress - test(Compress){ host => + val expected = "Hello World! Hello World! Hello World!" + requests.get(s"$host").text() ==> expected + assert( + requests.get(s"$host", autoDecompress = false).text().length < expected.length + ) + + } + + 'Compress2Main - test(Compress2Main) { host => + val expected = "Hello World! Hello World! Hello World!" + requests.get(s"$host").text() ==> expected + assert( + requests.get(s"$host", autoDecompress = false).text().length < expected.length + ) + } + + 'Compress3Main - test(Compress3Main){ host => + val expected = "Hello World! Hello World! Hello World!" + requests.get(s"$host").text() ==> expected + assert( + requests.get(s"$host", autoDecompress = false).text().length < expected.length + ) + + } } } diff --git a/cask/test/src/test/cask/FailureTests.scala b/cask/test/src/test/cask/FailureTests.scala index 9e28c0b..62eb946 100644 --- a/cask/test/src/test/cask/FailureTests.scala +++ b/cask/test/src/test/cask/FailureTests.scala @@ -11,16 +11,12 @@ object FailureTests extends TestSuite { } } val tests = Tests{ - 'mismatchedDecorators - { - utest.compileError(""" - object Decorated extends cask.MainRoutes{ - @cask.get("/hello/:world") - def hello(world: String)(extra: Int) = world + extra - initialize() - } - """).msg ==> - "Endpoint hello's number of parameter lists (2) cannot be more than the number of decorators (1)" + object Decorated extends cask.MainRoutes{ + @cask.get("/hello/:world") + def hello(world: String)(extra: Int) = world + extra + initialize() + } utest.compileError(""" object Decorated extends cask.MainRoutes{ diff --git a/readme.md b/readme.md index 4ed7322..88f2523 100644 --- a/readme.md +++ b/readme.md @@ -386,6 +386,112 @@ Decorators are useful for things like: transaction that commits when the function succeeds (and rolls-back if it fails), or access to some system resource that needs to be released. +For decorators that you wish to apply to multiple routes at once, you can define +them by overriding the `cask.Routes#decorators` field (to apply to every +endpoint in that routes object) or `cask.Main#mainDecorators` (to apply to every +endpoint, period): + +```scala +object Decorated2 extends cask.MainRoutes{ + class User{ + override def toString = "[haoyi]" + } + class loggedIn extends cask.Decorator { + def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = { + delegate(Map("user" -> new User())) + } + } + class withExtra extends cask.Decorator { + def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = { + delegate(Map("extra" -> 31337)) + } + } + + override def decorators = Seq(new withExtra()) + + @cask.get("/hello/:world") + def hello(world: String)(extra: Int) = { + world + extra + } + + @loggedIn() + @cask.get("/internal-extra/:world") + def internalExtra(world: String)(user: User)(extra: Int) = { + world + user + extra + } + + @loggedIn() + @cask.get("/ignore-extra/:world") + def ignoreExtra(world: String)(user: User) = { + world + user + } + + initialize() +} +``` + +This is convenient for cases where you want a set of decorators to apply broadly +across your web application, and do not want to repeat them over and over at +every single endpoint. + +Gzip & Deflated Responses +------------------------- + +```scala +object Compress extends cask.MainRoutes{ + + @cask.decorators.compress + @cask.get("/") + def hello() = { + "Hello World! Hello World! Hello World!" + } + + initialize() +} + +``` + +Cask provides a useful `@cask.decorators.compress` decorator that gzips or +deflates a response body if possible. This is useful if you don't have a proxy +like Nginx or similar in front of your server to perform the compression for +you. + +Like all decorators, `@cask.decorators.compress` can be defined on a level of a +set of `cask.Routes`: + +```scala +object Compress2 extends cask.Routes{ + override def decorators = Seq(new cask.decorators.compress()) + + @cask.get("/") + def hello() = { + "Hello World! Hello World! Hello World!" + } + + initialize() +} + +object Compress2Main extends cask.Main(Compress2) +``` + +Or globally, in your `cask.Main`: + +```scala +object Compress3 extends cask.Routes{ + + @cask.get("/") + def hello() = { + "Hello World! Hello World! Hello World!" + } + + initialize() +} + +object Compress3Main extends cask.Main(Compress3){ + override def decorators = Seq(new cask.decorators.compress()) +} +``` + TodoMVC Api Server ------------------ -- cgit v1.2.3