summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-08-09 00:36:25 +0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-08-09 00:36:25 +0800
commitfe17f0a465a49433867ea4917fd4938a7d2b6609 (patch)
tree04f04f15df5c33325d7da24fad8085587c465895
parentb1969928a179bfa833cab528d544a1f77cf24987 (diff)
downloadcask-fe17f0a465a49433867ea4917fd4938a7d2b6609.tar.gz
cask-fe17f0a465a49433867ea4917fd4938a7d2b6609.tar.bz2
cask-fe17f0a465a49433867ea4917fd4938a7d2b6609.zip
Add `@cask.decorators.compress` utility
Allow for decorators to be applied across `cask.Routes` or `cask.Main`
-rw-r--r--cask/src/cask/decorators/compress.scala43
-rw-r--r--cask/src/cask/internal/Router.scala9
-rw-r--r--cask/src/cask/main/Decorators.scala1
-rw-r--r--cask/src/cask/main/Main.scala3
-rw-r--r--cask/src/cask/main/Routes.scala2
-rw-r--r--cask/test/src/test/cask/Compress.scala12
-rw-r--r--cask/test/src/test/cask/Compress2.scala14
-rw-r--r--cask/test/src/test/cask/Compress3.scala15
-rw-r--r--cask/test/src/test/cask/Decorated2.scala38
-rw-r--r--cask/test/src/test/cask/ExampleTests.scala32
-rw-r--r--cask/test/src/test/cask/FailureTests.scala14
-rw-r--r--readme.md106
12 files changed, 272 insertions, 17 deletions
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
@@ -103,6 +103,12 @@ object ExampleTests extends TestSuite{
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() ==>
"""[{"checked":true,"text":"Get started with Cask"},{"checked":false,"text":"Profit!"}]"""
@@ -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
------------------