summaryrefslogtreecommitdiff
path: root/cask/src
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/src
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/src')
-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
9 files changed, 82 insertions, 53 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
}