summaryrefslogtreecommitdiff
path: root/cask
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-08-08 15:53:37 +0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-08-08 18:24:18 +0800
commitd85fd093539bdd7d8d432b058c2e2225eaa1ee2b (patch)
tree1b10e0cdcea08a51255152e259e446a75ec57ead /cask
parenta5320694193fd86b639c53a91fa24fb7f8ea914e (diff)
downloadcask-d85fd093539bdd7d8d432b058c2e2225eaa1ee2b.tar.gz
cask-d85fd093539bdd7d8d432b058c2e2225eaa1ee2b.tar.bz2
cask-d85fd093539bdd7d8d432b058c2e2225eaa1ee2b.zip
Properly roll back transactions when endpoints fail in TodoMvcDb
Diffstat (limited to 'cask')
-rw-r--r--cask/src/cask/endpoints/FormEndpoint.scala5
-rw-r--r--cask/src/cask/endpoints/JsonEndpoint.scala5
-rw-r--r--cask/src/cask/endpoints/StaticEndpoints.scala8
-rw-r--r--cask/src/cask/endpoints/WebEndpoints.scala5
-rw-r--r--cask/src/cask/internal/Router.scala4
-rw-r--r--cask/src/cask/main/Decorators.scala67
-rw-r--r--cask/src/cask/main/Main.scala29
-rw-r--r--cask/src/cask/main/Routes.scala8
-rw-r--r--cask/src/cask/package.scala4
-rw-r--r--cask/test/src/test/cask/Decorated.scala9
-rw-r--r--cask/test/src/test/cask/ExampleTests.scala86
-rw-r--r--cask/test/src/test/cask/FailureTests.scala3
-rw-r--r--cask/test/src/test/cask/MinimalApplication2.scala16
-rw-r--r--cask/test/src/test/cask/TodoMvcDb.scala42
14 files changed, 175 insertions, 116 deletions
diff --git a/cask/src/cask/endpoints/FormEndpoint.scala b/cask/src/cask/endpoints/FormEndpoint.scala
index 4e8feb3..48190ce 100644
--- a/cask/src/cask/endpoints/FormEndpoint.scala
+++ b/cask/src/cask/endpoints/FormEndpoint.scala
@@ -43,12 +43,13 @@ object FormReader{
def read(ctx: ParamContext, label: String, input: Seq[FormEntry]) = input.map(_.asInstanceOf[FormFile])
}
}
-class postForm(val path: String, override val subpath: Boolean = false) extends Endpoint[Response]{
+class postForm(val path: String, override val subpath: Boolean = false) extends Endpoint{
+ type Output = Response
val methods = Seq("post")
type Input = Seq[FormEntry]
type InputParser[T] = FormReader[T]
- def wrapMethodOutput(ctx: ParamContext,
+ def wrapFunction(ctx: ParamContext,
delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = {
try {
val formData = FormParserFactory.builder().build().createParser(ctx.exchange).parseBlocking()
diff --git a/cask/src/cask/endpoints/JsonEndpoint.scala b/cask/src/cask/endpoints/JsonEndpoint.scala
index 3c960d2..fdbbbec 100644
--- a/cask/src/cask/endpoints/JsonEndpoint.scala
+++ b/cask/src/cask/endpoints/JsonEndpoint.scala
@@ -24,12 +24,13 @@ object JsReader{
}
}
}
-class postJson(val path: String, override val subpath: Boolean = false) extends Endpoint[Response]{
+class postJson(val path: String, override val subpath: Boolean = false) extends Endpoint{
+ type Output = Response
val methods = Seq("post")
type Input = ujson.Js.Value
type InputParser[T] = JsReader[T]
- def wrapMethodOutput(ctx: ParamContext,
+ def wrapFunction(ctx: ParamContext,
delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = {
val obj = for{
str <-
diff --git a/cask/src/cask/endpoints/StaticEndpoints.scala b/cask/src/cask/endpoints/StaticEndpoints.scala
index 173cdac..048f89a 100644
--- a/cask/src/cask/endpoints/StaticEndpoints.scala
+++ b/cask/src/cask/endpoints/StaticEndpoints.scala
@@ -4,15 +4,13 @@ import cask.internal.Router
import cask.main.Endpoint
import cask.model.{Response, ParamContext}
-class static(val path: String) extends Endpoint[String] {
+class static(val path: String) extends Endpoint {
+ type Output = String
val methods = Seq("get")
type Input = Seq[String]
type InputParser[T] = QueryParamReader[T]
override def subpath = true
- def wrapOutput(t: String) = t
-
- def wrapMethodOutput(ctx: ParamContext,
- delegate: Map[String, Input] => Router.Result[String]): Router.Result[Response] = {
+ def wrapFunction(ctx: ParamContext, delegate: Delegate): Returned = {
delegate(Map()) match{
case Router.Result.Success(t) => Router.Result.Success(cask.model.Static(t + "/" + ctx.remaining.mkString("/")))
case e: Router.Result.Error => e
diff --git a/cask/src/cask/endpoints/WebEndpoints.scala b/cask/src/cask/endpoints/WebEndpoints.scala
index c37a73a..70d16e0 100644
--- a/cask/src/cask/endpoints/WebEndpoints.scala
+++ b/cask/src/cask/endpoints/WebEndpoints.scala
@@ -7,10 +7,11 @@ import cask.model.{Response, ParamContext}
import collection.JavaConverters._
-trait WebEndpoint extends Endpoint[Response]{
+trait WebEndpoint extends Endpoint{
+ type Output = Response
type Input = Seq[String]
type InputParser[T] = QueryParamReader[T]
- def wrapMethodOutput(ctx: ParamContext,
+ def wrapFunction(ctx: ParamContext,
delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = {
delegate(
ctx.exchange.getQueryParameters
diff --git a/cask/src/cask/internal/Router.scala b/cask/src/cask/internal/Router.scala
index c831240..7ad0c18 100644
--- a/cask/src/cask/internal/Router.scala
+++ b/cask/src/cask/internal/Router.scala
@@ -197,7 +197,7 @@ class Router[C <: Context](val c: C) {
def extractMethod(method: MethodSymbol,
curCls: c.universe.Type,
- wrapOutput: (c.Tree, c.Tree) => c.Tree,
+ convertToResultType: c.Tree,
ctx: c.Type,
argReaders: Seq[c.Tree],
annotDeserializeTypes: Seq[c.Tree]): c.universe.Tree = {
@@ -323,7 +323,7 @@ class Router[C <: Context](val c: C) {
) =>
cask.internal.Router.validate(Seq(..${readArgs.flatten.toList})) match{
case cask.internal.Router.Result.Success(Seq(..${argNames.flatten.toList})) =>
- ${wrapOutput(ctxSymbol, methodCall)}
+ cask.internal.Router.Result.Success($convertToResultType($methodCall))
case x: cask.internal.Router.Result.Error => x
}
)
diff --git a/cask/src/cask/main/Decorators.scala b/cask/src/cask/main/Decorators.scala
index 462a369..1bd8867 100644
--- a/cask/src/cask/main/Decorators.scala
+++ b/cask/src/cask/main/Decorators.scala
@@ -4,48 +4,65 @@ import cask.internal.Router
import cask.internal.Router.ArgReader
import cask.model.{Response, ParamContext}
-
-trait Endpoint[R] extends BaseDecorator{
-
- type Output = R
+/**
+ * Used to annotate a single Cask endpoint function; similar to a [[Decorator]]
+ * but with additional metadata and capabilities.
+ */
+trait Endpoint extends BaseDecorator{
+ /**
+ * What is the path that this particular endpoint matches?
+ */
val path: String
+ /**
+ * Which HTTP methods does this endpoint support? POST? GET? PUT? Or some
+ * combination of those?
+ */
val methods: Seq[String]
+
+ /**
+ * Whether or not this endpoint allows matching on sub-paths: does
+ * `@endpoint("/foo")` capture the path "/foo/bar/baz"? Useful to e.g. have
+ * an endpoint match URLs with paths in a filesystem (real or virtual) to
+ * serve files
+ */
def subpath: Boolean = false
- def wrapMethodOutput0(ctx: ParamContext, t: R): cask.internal.Router.Result[Any] = {
- cask.internal.Router.Result.Success(t)
- }
- def wrapMethodOutput(ctx: ParamContext,
- delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response]
+ def convertToResultType(t: Output): Output = t
+ /**
+ * [[Endpoint]]s are unique among decorators in that they alone can bind
+ * path segments to parameters, e.g. binding `/hello/:world` to `(world: Int)`.
+ * In order to do so, we need to box up the path segment strings into an
+ * [[Input]] so they can later be parsed by [[getParamParser]] into an
+ * instance of the appropriate type.
+ */
def wrapPathSegment(s: String): Input
}
-/**
- * The core interface of decorator annotations: the decorator provides "raw"
- * values to the annotated function via `getRawParams`, which then get
- * processed by `getParamParser` into the correct argument types before
- * being passed to the function.
- *
- * For a trivial "provide value" decorator, `getRawParams` would return the
- * final param value and `getParamParser` would return a no-op parser. For
- * a decorator that takes its input as query-params, JSON, or similar,
- * `getRawParams` would provide raw query/JSON/etc. values and
- * `getParamParser` would be responsible for processing those into the
- * correct parameter types.
- */
trait BaseDecorator{
type Input
type InputParser[T] <: ArgReader[Input, T, ParamContext]
type Output
- def wrapMethodOutput(ctx: ParamContext,
- delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response]
+ type Delegate = Map[String, Input] => Router.Result[Output]
+ type Returned = Router.Result[Response]
+ def wrapFunction(ctx: ParamContext, delegate: Delegate): Returned
def getParamParser[T](implicit p: InputParser[T]) = p
}
-
+/**
+ * A decorator allows you to annotate a function to wrap it, via
+ * `wrapFunction`. You can use this to perform additional validation before or
+ * after the function runs, provide an additional parameter list of params,
+ * open/commit/rollback database transactions before/after the function runs,
+ * or even retrying the wrapped function if it fails.
+ *
+ * Calls to the wrapped function are done on the `delegate` parameter passed
+ * to `wrapFunction`, which takes a `Map` representing any additional argument
+ * lists (if any).
+ */
trait Decorator extends BaseDecorator {
+
type Input = Any
type Output = Response
type InputParser[T] = NoOpParser[Input, T]
diff --git a/cask/src/cask/main/Main.scala b/cask/src/cask/main/Main.scala
index 65655fc..5558a08 100644
--- a/cask/src/cask/main/Main.scala
+++ b/cask/src/cask/main/Main.scala
@@ -58,17 +58,25 @@ abstract class BaseMain{
case Some(((routes, metadata), extBindings, remaining)) =>
val ctx = ParamContext(exchange, remaining)
def rec(remaining: List[Decorator],
- bindings: List[Map[String, Any]]): Router.Result[Response] = remaining match{
- case head :: rest => head.wrapMethodOutput(ctx, args => rec(rest, args :: bindings))
- case Nil =>
- metadata.endpoint.wrapMethodOutput(ctx, epBindings =>
- metadata.entryPoint
- .asInstanceOf[EntryPoint[cask.main.Routes, cask.model.ParamContext]]
- .invoke(routes, ctx, (epBindings ++ extBindings.mapValues(metadata.endpoint.wrapPathSegment)) :: bindings.reverse)
- .asInstanceOf[Router.Result[Nothing]]
- )
+ bindings: List[Map[String, Any]]): Router.Result[Response] = try {
+ remaining match {
+ case head :: rest =>
+ head.wrapFunction(ctx, args => rec(rest, args :: bindings))
+
+ case Nil =>
+ metadata.endpoint.wrapFunction(ctx, epBindings =>
+ metadata.entryPoint
+ .asInstanceOf[EntryPoint[cask.main.Routes, cask.model.ParamContext]]
+ .invoke(routes, ctx, (epBindings ++ extBindings.mapValues(metadata.endpoint.wrapPathSegment)) :: bindings.reverse)
+ .asInstanceOf[Router.Result[Nothing]]
+ )
+
+ }
+ // Make sure we wrap any exceptions that bubble up from decorator
+ // bodies, so outer decorators do not need to worry about their
+ // delegate throwing on them
+ }catch{case e: Throwable => Router.Result.Error.Exception(e) }
- }
rec(metadata.decorators.toList, Nil)match{
case Router.Result.Success(response: Response) => writeResponse(exchange, response)
case e: Router.Result.Error =>
@@ -88,6 +96,7 @@ abstract class BaseMain{
}
}
+
def main(args: Array[String]): Unit = {
val server = Undertow.builder
.addHttpListener(port, host)
diff --git a/cask/src/cask/main/Routes.scala b/cask/src/cask/main/Routes.scala
index d26641e..0dea657 100644
--- a/cask/src/cask/main/Routes.scala
+++ b/cask/src/cask/main/Routes.scala
@@ -8,7 +8,7 @@ import language.experimental.macros
object Routes{
case class EndpointMetadata[T](decorators: Seq[Decorator],
- endpoint: Endpoint[_],
+ endpoint: Endpoint,
entryPoint: EntryPoint[T, ParamContext])
case class RoutesEndpointsMetadata[T](value: EndpointMetadata[T]*)
object RoutesEndpointsMetadata{
@@ -22,12 +22,12 @@ object Routes{
val annotations = m.annotations.filter(_.tree.tpe <:< c.weakTypeOf[BaseDecorator]).reverse
if annotations.nonEmpty
} yield {
- if(!(annotations.head.tree.tpe <:< weakTypeOf[Endpoint[_]])) c.abort(
+ if(!(annotations.head.tree.tpe <:< weakTypeOf[Endpoint])) c.abort(
annotations.head.tree.pos,
s"Last annotation applied to a function must be an instance of Endpoint, " +
s"not ${annotations.head.tree.tpe}"
)
- val allEndpoints = annotations.filter(_.tree.tpe <:< weakTypeOf[Endpoint[_]])
+ val allEndpoints = annotations.filter(_.tree.tpe <:< weakTypeOf[Endpoint])
if(allEndpoints.length > 1) c.abort(
annotations.head.tree.pos,
s"You can only apply one Endpoint annotation to a function, not " +
@@ -42,7 +42,7 @@ object Routes{
val route = router.extractMethod(
m.asInstanceOf[MethodSymbol],
weakTypeOf[T],
- (ctx: c.Tree, t: c.Tree) => q"${annotObjectSyms.head}.wrapMethodOutput0($ctx, $t)",
+ q"${annotObjectSyms.head}.convertToResultType",
c.weakTypeOf[ParamContext],
annotObjectSyms.map(annotObjectSym => q"$annotObjectSym.getParamParser"),
annotObjectSyms.map(annotObjectSym => tq"$annotObjectSym.Input")
diff --git a/cask/src/cask/package.scala b/cask/src/cask/package.scala
index 24a0a20..19dc675 100644
--- a/cask/src/cask/package.scala
+++ b/cask/src/cask/package.scala
@@ -18,6 +18,8 @@ package object cask {
val Subpath = model.Subpath
type Request = model.Request
val Request = model.Request
+ type ParamContext = model.ParamContext
+ val ParamContext = model.ParamContext
// endpoints
type get = endpoints.get
@@ -34,7 +36,7 @@ package object cask {
val Routes = main.Routes
type Main = main.Main
type Decorator = main.Decorator
- type Endpoint[R] = main.Endpoint[R]
+ type Endpoint = main.Endpoint
type BaseDecorator = main.BaseDecorator
}
diff --git a/cask/test/src/test/cask/Decorated.scala b/cask/test/src/test/cask/Decorated.scala
index 9fac78a..d7cb6b8 100644
--- a/cask/test/src/test/cask/Decorated.scala
+++ b/cask/test/src/test/cask/Decorated.scala
@@ -1,21 +1,16 @@
package test.cask
-import cask.internal.Router
-import cask.model.{ParamContext, Response}
-
object Decorated extends cask.MainRoutes{
class User{
override def toString = "[haoyi]"
}
class loggedIn extends cask.Decorator {
- def wrapMethodOutput(ctx: ParamContext,
- delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = {
+ def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = {
delegate(Map("user" -> new User()))
}
}
class withExtra extends cask.Decorator {
- def wrapMethodOutput(ctx: ParamContext,
- delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = {
+ def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = {
delegate(Map("extra" -> 31337))
}
}
diff --git a/cask/test/src/test/cask/ExampleTests.scala b/cask/test/src/test/cask/ExampleTests.scala
index 6858051..36e0387 100644
--- a/cask/test/src/test/cask/ExampleTests.scala
+++ b/cask/test/src/test/cask/ExampleTests.scala
@@ -4,7 +4,7 @@ import io.undertow.server.handlers.BlockingHandler
import utest._
object ExampleTests extends TestSuite{
- def test[T](example: cask.main.MainRoutes)(f: String => T): T = {
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
val server = Undertow.builder
.addHttpListener(8080, "localhost")
.setHandler(new BlockingHandler(example.defaultHandler))
@@ -23,25 +23,37 @@ object ExampleTests extends TestSuite{
success.text() ==> "Hello World!"
success.statusCode ==> 200
- requests.get(host + "/doesnt-exist").statusCode ==> 404
+ requests.get(s"$host/doesnt-exist").statusCode ==> 404
- requests.post(host + "/do-thing", data = "hello").text() ==> "olleh"
+ requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh"
- requests.get(host + "/do-thing").statusCode ==> 404
+ requests.get(s"$host/do-thing").statusCode ==> 404
+ }
+ 'MinimalApplication2 - test(MinimalMain){ host =>
+ val success = requests.get(host)
+
+ success.text() ==> "Hello World!"
+ success.statusCode ==> 200
+
+ requests.get(s"$host/doesnt-exist").statusCode ==> 404
+
+ requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh"
+
+ requests.get(s"$host/do-thing").statusCode ==> 404
}
'VariableRoutes - test(VariableRoutes){ host =>
val noIndexPage = requests.get(host)
noIndexPage.statusCode ==> 404
- requests.get(host + "/user/lihaoyi").text() ==> "User lihaoyi"
+ requests.get(s"$host/user/lihaoyi").text() ==> "User lihaoyi"
- requests.get(host + "/user").statusCode ==> 404
+ requests.get(s"$host/user").statusCode ==> 404
- requests.get(host + "/post/123?param=xyz&param=abc").text() ==>
+ requests.get(s"$host/post/123?param=xyz&param=abc").text() ==>
"Post 123 ArrayBuffer(xyz, abc)"
- requests.get(host + "/post/123").text() ==>
+ requests.get(s"$host/post/123").text() ==>
"""Missing argument: (param: Seq[String])
|
|Arguments provided did not match expected signature:
@@ -52,33 +64,33 @@ object ExampleTests extends TestSuite{
|
|""".stripMargin
- requests.get(host + "/path/one/two/three").text() ==>
+ requests.get(s"$host/path/one/two/three").text() ==>
"Subpath List(one, two, three)"
}
'StaticFiles - test(StaticFiles){ host =>
- requests.get(host + "/static/example.txt").text() ==>
+ requests.get(s"$host/static/example.txt").text() ==>
"the quick brown fox jumps over the lazy dog"
}
'RedirectAbort - test(RedirectAbort){ host =>
- val resp = requests.get(host + "/")
+ val resp = requests.get(s"$host/")
resp.statusCode ==> 401
resp.history.get.statusCode ==> 301
}
'FormJsonPost - test(FormJsonPost){ host =>
- requests.post(host + "/json", data = """{"value1": true, "value2": [3]}""").text() ==>
+ requests.post(s"$host/json", data = """{"value1": true, "value2": [3]}""").text() ==>
"OK true Vector(3)"
requests.post(
- host + "/form",
+ s"$host/form",
data = Seq("value1" -> "hello", "value2" -> "1", "value2" -> "2")
).text() ==>
"OK FormValue(hello,null) List(1, 2)"
val resp = requests.post(
- host + "/upload",
+ s"$host/upload",
data = requests.MultiPart(
requests.MultiItem("image", "...", "my-best-image.txt")
)
@@ -86,61 +98,61 @@ object ExampleTests extends TestSuite{
resp.text() ==> "my-best-image.txt"
}
'Decorated - test(Decorated){ host =>
- requests.get(host + "/hello/woo").text() ==> "woo31337"
- requests.get(host + "/internal/boo").text() ==> "boo[haoyi]"
- requests.get(host + "/internal-extra/goo").text() ==> "goo[haoyi]31337"
+ requests.get(s"$host/hello/woo").text() ==> "woo31337"
+ requests.get(s"$host/internal/boo").text() ==> "boo[haoyi]"
+ requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337"
}
'TodoMvcApi - test(TodoMvcApi){ host =>
- requests.get(host + "/list/all").text() ==>
+ requests.get(s"$host/list/all").text() ==>
"""[{"checked":true,"text":"Get started with Cask"},{"checked":false,"text":"Profit!"}]"""
- requests.get(host + "/list/active").text() ==>
+ requests.get(s"$host/list/active").text() ==>
"""[{"checked":false,"text":"Profit!"}]"""
- requests.get(host + "/list/completed").text() ==>
+ requests.get(s"$host/list/completed").text() ==>
"""[{"checked":true,"text":"Get started with Cask"}]"""
- requests.post(host + "/toggle/1")
+ requests.post(s"$host/toggle/1")
- requests.get(host + "/list/all").text() ==>
+ requests.get(s"$host/list/all").text() ==>
"""[{"checked":true,"text":"Get started with Cask"},{"checked":true,"text":"Profit!"}]"""
- requests.get(host + "/list/active").text() ==>
+ requests.get(s"$host/list/active").text() ==>
"""[]"""
- requests.post(host + "/add", data = "new Task")
+ requests.post(s"$host/add", data = "new Task")
- requests.get(host + "/list/active").text() ==>
+ requests.get(s"$host/list/active").text() ==>
"""[{"checked":false,"text":"new Task"}]"""
- requests.post(host + "/delete/0")
+ requests.post(s"$host/delete/0")
- requests.get(host + "/list/active").text() ==>
+ requests.get(s"$host/list/active").text() ==>
"""[]"""
}
'TodoMvcDb - test(TodoMvcDb){ host =>
- requests.get(host + "/list/all").text() ==>
+ requests.get(s"$host/list/all").text() ==>
"""[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":false,"text":"Profit!"}]"""
- requests.get(host + "/list/active").text() ==>
+ requests.get(s"$host/list/active").text() ==>
"""[{"id":2,"checked":false,"text":"Profit!"}]"""
- requests.get(host + "/list/completed").text() ==>
+ requests.get(s"$host/list/completed").text() ==>
"""[{"id":1,"checked":true,"text":"Get started with Cask"}]"""
- requests.post(host + "/toggle/2")
+ requests.post(s"$host/toggle/2")
- requests.get(host + "/list/all").text() ==>
+ requests.get(s"$host/list/all").text() ==>
"""[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":true,"text":"Profit!"}]"""
- requests.get(host + "/list/active").text() ==>
+ requests.get(s"$host/list/active").text() ==>
"""[]"""
- requests.post(host + "/add", data = "new Task")
+ requests.post(s"$host/add", data = "new Task")
- requests.get(host + "/list/active").text() ==>
+ requests.get(s"$host/list/active").text() ==>
"""[{"id":3,"checked":false,"text":"new Task"}]"""
- requests.post(host + "/delete/3")
+ requests.post(s"$host/delete/3")
- requests.get(host + "/list/active").text() ==>
+ requests.get(s"$host/list/active").text() ==>
"""[]"""
}
}
diff --git a/cask/test/src/test/cask/FailureTests.scala b/cask/test/src/test/cask/FailureTests.scala
index de3b438..9e28c0b 100644
--- a/cask/test/src/test/cask/FailureTests.scala
+++ b/cask/test/src/test/cask/FailureTests.scala
@@ -6,8 +6,7 @@ import utest._
object FailureTests extends TestSuite {
class myDecorator extends cask.Decorator {
- def wrapMethodOutput(ctx: ParamContext,
- delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = {
+ def wrapFunction(ctx: ParamContext, delegate: Delegate): Returned = {
delegate(Map("extra" -> 31337))
}
}
diff --git a/cask/test/src/test/cask/MinimalApplication2.scala b/cask/test/src/test/cask/MinimalApplication2.scala
new file mode 100644
index 0000000..924b00f
--- /dev/null
+++ b/cask/test/src/test/cask/MinimalApplication2.scala
@@ -0,0 +1,16 @@
+package test.cask
+
+object MinimalRoutes extends cask.Routes{
+ @cask.get("/")
+ def hello() = {
+ "Hello World!"
+ }
+
+ @cask.post("/do-thing")
+ def doThing(request: cask.Request) = {
+ new String(request.data.readAllBytes()).reverse
+ }
+
+ initialize()
+}
+object MinimalMain extends cask.Main(MinimalRoutes) \ No newline at end of file
diff --git a/cask/test/src/test/cask/TodoMvcDb.scala b/cask/test/src/test/cask/TodoMvcDb.scala
index c6f8191..b352d1a 100644
--- a/cask/test/src/test/cask/TodoMvcDb.scala
+++ b/cask/test/src/test/cask/TodoMvcDb.scala
@@ -1,32 +1,39 @@
package test.cask
+
import cask.internal.Router
-import cask.model.{ParamContext, Response}
import com.typesafe.config.ConfigFactory
-import io.getquill._
+import io.getquill.{SqliteJdbcContext, SnakeCase}
object TodoMvcDb extends cask.MainRoutes{
- case class Todo(id: Int, checked: Boolean, text: String)
- object Todo{
- implicit def todoRW = upickle.default.macroRW[Todo]
- }
+ val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite")
+
object ctx extends SqliteJdbcContext(
SnakeCase,
ConfigFactory.parseString(
s"""{"driverClassName":"org.sqlite.JDBC","jdbcUrl":"jdbc:sqlite:$tmpDb/file.db"}"""
)
)
- val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite")
-
- import ctx._
class transactional extends cask.Decorator{
- def wrapMethodOutput(pctx: ParamContext,
- delegate: Map[String, Input] => Router.Result[Response]): Router.Result[Response] = {
- ctx.transaction(delegate(Map("ctx" -> ctx)))
+ class TransactionFailed(val value: Router.Result.Error) extends Exception
+ def wrapFunction(pctx: cask.ParamContext, delegate: Delegate): Returned = {
+ try ctx.transaction(
+ delegate(Map()) match{
+ case Router.Result.Success(t) => Router.Result.Success(t)
+ case e: Router.Result.Error => throw new TransactionFailed(e)
+ }
+ )
+ catch{case e: TransactionFailed => e.value}
+
}
}
+ case class Todo(id: Int, checked: Boolean, text: String)
+ object Todo{
+ implicit def todoRW = upickle.default.macroRW[Todo]
+ }
+
ctx.executeAction(
"""CREATE TABLE todo (
| id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -42,9 +49,11 @@ object TodoMvcDb extends cask.MainRoutes{
|""".stripMargin
)
+ import ctx._
+
@transactional
@cask.get("/list/:state")
- def list(state: String)(ctx: SqliteJdbcContext[_]) = {
+ def list(state: String) = {
val filteredTodos = state match{
case "all" => run(query[Todo])
case "active" => run(query[Todo].filter(!_.checked))
@@ -55,22 +64,21 @@ object TodoMvcDb extends cask.MainRoutes{
@transactional
@cask.post("/add")
- def add(request: cask.Request)(ctx: SqliteJdbcContext[_]) = {
+ def add(request: cask.Request) = {
val body = new String(request.data.readAllBytes())
run(query[Todo].insert(_.checked -> lift(false), _.text -> lift(body)).returning(_.id))
}
@transactional
@cask.post("/toggle/:index")
- def toggle(index: Int)(ctx: SqliteJdbcContext[_]) = {
+ def toggle(index: Int) = {
run(query[Todo].filter(_.id == lift(index)).update(p => p.checked -> !p.checked))
}
@transactional
@cask.post("/delete/:index")
- def delete(index: Int)(ctx: SqliteJdbcContext[_]) = {
+ def delete(index: Int) = {
run(query[Todo].filter(_.id == lift(index)).delete)
-
}
initialize()