diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2018-08-08 15:22:02 +0800 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2018-08-08 15:22:02 +0800 |
commit | a5320694193fd86b639c53a91fa24fb7f8ea914e (patch) | |
tree | 84d5d94f1fa0a6aeee2b7dc81b1f0276e2f38994 | |
parent | a89ebd17dab5af6814d58f02d410acb1eb60e592 (diff) | |
download | cask-a5320694193fd86b639c53a91fa24fb7f8ea914e.tar.gz cask-a5320694193fd86b639c53a91fa24fb7f8ea914e.tar.bz2 cask-a5320694193fd86b639c53a91fa24fb7f8ea914e.zip |
Refactor decorators into a more traditional delegation model, and use that to implement endpoint-scoped transactions using Quill
-rw-r--r-- | build.sc | 4 | ||||
-rw-r--r-- | cask/src/cask/endpoints/FormEndpoint.scala | 19 | ||||
-rw-r--r-- | cask/src/cask/endpoints/JsonEndpoint.scala | 14 | ||||
-rw-r--r-- | cask/src/cask/endpoints/StaticEndpoints.scala | 14 | ||||
-rw-r--r-- | cask/src/cask/endpoints/WebEndpoints.scala | 11 | ||||
-rw-r--r-- | cask/src/cask/main/Decorators.scala | 23 | ||||
-rw-r--r-- | cask/src/cask/main/ErrorMsgs.scala | 2 | ||||
-rw-r--r-- | cask/src/cask/main/Main.scala | 41 | ||||
-rw-r--r-- | cask/src/cask/main/Routes.scala | 4 | ||||
-rw-r--r-- | cask/src/cask/model/Response.scala | 31 | ||||
-rw-r--r-- | cask/src/cask/package.scala | 2 | ||||
-rw-r--r-- | cask/test/src/test/cask/Decorated.scala | 13 | ||||
-rw-r--r-- | cask/test/src/test/cask/ExampleTests.scala | 26 | ||||
-rw-r--r-- | cask/test/src/test/cask/FailureTests.scala | 8 | ||||
-rw-r--r-- | cask/test/src/test/cask/TodoMvcDb.scala | 77 |
15 files changed, 204 insertions, 85 deletions
@@ -20,7 +20,9 @@ object cask extends ScalaModule{ def testFrameworks = Seq("utest.runner.Framework") def ivyDeps = Agg( ivy"com.lihaoyi::utest::0.6.3", - ivy"com.lihaoyi::requests::0.1.2" + ivy"com.lihaoyi::requests::0.1.2", + ivy"org.xerial:sqlite-jdbc:3.18.0", + ivy"io.getquill::quill-jdbc:2.5.4" ) } } diff --git a/cask/src/cask/endpoints/FormEndpoint.scala b/cask/src/cask/endpoints/FormEndpoint.scala index 525dfde..4e8feb3 100644 --- a/cask/src/cask/endpoints/FormEndpoint.scala +++ b/cask/src/cask/endpoints/FormEndpoint.scala @@ -48,23 +48,24 @@ class postForm(val path: String, override val subpath: Boolean = false) extends val methods = Seq("post") type Input = Seq[FormEntry] type InputParser[T] = FormReader[T] - def getRawParams(ctx: ParamContext) = { - for{ - formData <- - try Right(FormParserFactory.builder().build().createParser(ctx.exchange).parseBlocking()) - catch{case e: Exception => Left(cask.model.Response( - "Unable to parse form data: " + e + "\n" + Util.stackTraceString(e) - ))} - } yield { - cask.main.Decor( + def wrapMethodOutput(ctx: ParamContext, + delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = { + try { + val formData = FormParserFactory.builder().build().createParser(ctx.exchange).parseBlocking() + delegate( formData .iterator() .asScala .map(k => (k, formData.get(k).asScala.map(FormEntry.fromUndertow).toSeq)) .toMap ) + } catch{case e: Exception => + Router.Result.Success(cask.model.Response( + "Unable to parse form data: " + e + "\n" + Util.stackTraceString(e) + )) } } + 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 853e07d..3c960d2 100644 --- a/cask/src/cask/endpoints/JsonEndpoint.scala +++ b/cask/src/cask/endpoints/JsonEndpoint.scala @@ -3,7 +3,7 @@ package cask.endpoints import cask.internal.{Router, Util} import cask.internal.Router.EntryPoint import cask.main.{Endpoint, Routes} -import cask.model.{ParamContext, Response} +import cask.model.{Response, ParamContext} sealed trait JsReader[T] extends Router.ArgReader[ujson.Js.Value, T, cask.model.ParamContext] @@ -28,8 +28,10 @@ class postJson(val path: String, override val subpath: Boolean = false) extends val methods = Seq("post") type Input = ujson.Js.Value type InputParser[T] = JsReader[T] - def getRawParams(ctx: ParamContext) = { - for{ + + def wrapMethodOutput(ctx: ParamContext, + delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = { + val obj = for{ str <- try Right(new String(ctx.exchange.getInputStream.readAllBytes())) catch{case e: Throwable => Left(cask.model.Response( @@ -43,7 +45,11 @@ 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 cask.main.Decor(obj.toMap) + } yield obj.toMap + obj match{ + case Left(r) => Router.Result.Success(r) + case Right(params) => delegate(params) + } } 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 7e1e6dd..173cdac 100644 --- a/cask/src/cask/endpoints/StaticEndpoints.scala +++ b/cask/src/cask/endpoints/StaticEndpoints.scala @@ -2,7 +2,7 @@ package cask.endpoints import cask.internal.Router import cask.main.Endpoint -import cask.model.ParamContext +import cask.model.{Response, ParamContext} class static(val path: String) extends Endpoint[String] { val methods = Seq("get") @@ -10,12 +10,14 @@ class static(val path: String) extends Endpoint[String] { type InputParser[T] = QueryParamReader[T] override def subpath = true def wrapOutput(t: String) = t - override def wrapMethodOutput(ctx: ParamContext, t: String) = { - Router.Result.Success(cask.model.Static(t + "/" + ctx.remaining.mkString("/"))) + + def wrapMethodOutput(ctx: ParamContext, + delegate: Map[String, Input] => Router.Result[String]): Router.Result[Response] = { + delegate(Map()) match{ + case Router.Result.Success(t) => Router.Result.Success(cask.model.Static(t + "/" + ctx.remaining.mkString("/"))) + case e: Router.Result.Error => e + } } - 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 a5b2d02..c37a73a 100644 --- a/cask/src/cask/endpoints/WebEndpoints.scala +++ b/cask/src/cask/endpoints/WebEndpoints.scala @@ -2,22 +2,23 @@ package cask.endpoints import cask.internal.Router import cask.main.Endpoint -import cask.model.{BaseResponse, ParamContext} +import cask.model.{Response, ParamContext} import collection.JavaConverters._ -trait WebEndpoint extends Endpoint[BaseResponse]{ +trait WebEndpoint extends Endpoint[Response]{ type Input = Seq[String] type InputParser[T] = QueryParamReader[T] - def getRawParams(ctx: ParamContext) = Right( - cask.main.Decor( + def wrapMethodOutput(ctx: ParamContext, + delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = { + delegate( ctx.exchange.getQueryParameters .asScala .map{case (k, vs) => (k, vs.asScala.toArray.toSeq)} .toMap ) - ) + } def wrapPathSegment(s: String) = Seq(s) } class get(val path: String, override val subpath: Boolean = false) extends WebEndpoint{ diff --git a/cask/src/cask/main/Decorators.scala b/cask/src/cask/main/Decorators.scala index b262039..462a369 100644 --- a/cask/src/cask/main/Decorators.scala +++ b/cask/src/cask/main/Decorators.scala @@ -1,17 +1,22 @@ package cask.main +import cask.internal.Router import cask.internal.Router.ArgReader -import cask.model.ParamContext +import cask.model.{Response, ParamContext} trait Endpoint[R] extends BaseDecorator{ + type Output = R val path: String val methods: Seq[String] def subpath: Boolean = false - def wrapMethodOutput(ctx: ParamContext,t: R): cask.internal.Router.Result[Any] = { + + 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 wrapPathSegment(s: String): Input @@ -33,22 +38,16 @@ trait Endpoint[R] extends BaseDecorator{ trait BaseDecorator{ type Input type InputParser[T] <: ArgReader[Input, T, ParamContext] - def getRawParams(ctx: ParamContext): Either[cask.model.Response, Decor[Input]] + type Output + def wrapMethodOutput(ctx: ParamContext, + delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] 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 { type Input = Any + type Output = Response type InputParser[T] = NoOpParser[Input, T] } diff --git a/cask/src/cask/main/ErrorMsgs.scala b/cask/src/cask/main/ErrorMsgs.scala index e54ea88..f5d9cc7 100644 --- a/cask/src/cask/main/ErrorMsgs.scala +++ b/cask/src/cask/main/ErrorMsgs.scala @@ -64,7 +64,7 @@ object ErrorMsgs { def expectedMsg = formatMainMethodSignature(base: T, route, 0, 0) x match{ - case Router.Result.Error.Exception(x) => ??? + case Router.Result.Error.Exception(x) => Util.stackTraceString(x) case Router.Result.Error.MismatchedArguments(missing, unknown) => val missingStr = if (missing.isEmpty) "" diff --git a/cask/src/cask/main/Main.scala b/cask/src/cask/main/Main.scala index fb07e77..65655fc 100644 --- a/cask/src/cask/main/Main.scala +++ b/cask/src/cask/main/Main.scala @@ -33,7 +33,7 @@ abstract class BaseMain{ ) }.toMap - def writeResponse(exchange: HttpServerExchange, response: BaseResponse) = { + def writeResponse(exchange: HttpServerExchange, response: Response) = { response.headers.foreach{case (k, v) => exchange.getResponseHeaders.put(new HttpString(k), v) } @@ -55,30 +55,22 @@ abstract class BaseMain{ def handleRequest(exchange: HttpServerExchange): Unit = { routeTries(exchange.getRequestMethod.toString.toLowerCase()).lookup(Util.splitPath(exchange.getRequestPath).toList, Map()) match{ case None => writeResponse(exchange, handleError(404)) - case Some(((routes, metadata), bindings, remaining)) => - val params = for{ - decoratorParams <- Util.sequenceEither[Response, cask.main.Decor[_], Seq]( - metadata.decorators.map(e => e.getRawParams(ParamContext(exchange, remaining))) - ) - endpointParams <- metadata.endpoint.getRawParams(ParamContext(exchange, remaining)) - } yield ( - (endpointParams.params ++ bindings.mapValues(metadata.endpoint.wrapPathSegment)) +: - decoratorParams.map(_.params), - () => {endpointParams.cleanup(); decoratorParams.foreach(_.cleanup())} - ) + 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]] + ) - val result = params match{ - case Left(resp) => resp - case Right((paramValues, cleanup)) => - try metadata.entryPoint - .asInstanceOf[EntryPoint[cask.main.Routes, cask.model.ParamContext]] - .invoke(routes, ParamContext(exchange, remaining), paramValues) - finally cleanup() } - - - result match{ - case Router.Result.Success(response: BaseResponse) => writeResponse(exchange, response) + rec(metadata.decorators.toList, Nil)match{ + case Router.Result.Success(response: Response) => writeResponse(exchange, response) case e: Router.Result.Error => writeResponse(exchange, @@ -88,7 +80,8 @@ abstract class BaseMain{ metadata.entryPoint.asInstanceOf[EntryPoint[cask.main.Routes, _]], e ), - statusCode = 500) + statusCode = 500 + ) ) } } diff --git a/cask/src/cask/main/Routes.scala b/cask/src/cask/main/Routes.scala index 1c49e42..d26641e 100644 --- a/cask/src/cask/main/Routes.scala +++ b/cask/src/cask/main/Routes.scala @@ -7,7 +7,7 @@ import scala.reflect.macros.blackbox.Context import language.experimental.macros object Routes{ - case class EndpointMetadata[T](decorators: Seq[BaseDecorator], + case class EndpointMetadata[T](decorators: Seq[Decorator], endpoint: Endpoint[_], entryPoint: EntryPoint[T, ParamContext]) case class RoutesEndpointsMetadata[T](value: EndpointMetadata[T]*) @@ -42,7 +42,7 @@ object Routes{ val route = router.extractMethod( m.asInstanceOf[MethodSymbol], weakTypeOf[T], - (ctx: c.Tree, t: c.Tree) => q"${annotObjectSyms.head}.wrapMethodOutput($ctx, $t)", + (ctx: c.Tree, t: c.Tree) => q"${annotObjectSyms.head}.wrapMethodOutput0($ctx, $t)", c.weakTypeOf[ParamContext], annotObjectSyms.map(annotObjectSym => q"$annotObjectSym.getParamParser"), annotObjectSyms.map(annotObjectSym => tq"$annotObjectSym.Input") diff --git a/cask/src/cask/model/Response.scala b/cask/src/cask/model/Response.scala index 23e029b..15f46a7 100644 --- a/cask/src/cask/model/Response.scala +++ b/cask/src/cask/model/Response.scala @@ -2,16 +2,23 @@ package cask.model import java.io.{InputStream, OutputStream, OutputStreamWriter} -import io.undertow.server.HttpServerExchange - -trait BaseResponse{ - def data: BaseResponse.Data +trait Response{ + def data: Response.Data def statusCode: Int def headers: Seq[(String, String)] def cookies: Seq[Cookie] } -object BaseResponse{ +object Response{ + def apply(data: Data, + statusCode: Int = 200, + headers: Seq[(String, String)] = Nil, + cookies: Seq[Cookie] = Nil) = Simple(data, statusCode, headers, cookies) + case class Simple(data: Data, + statusCode: Int = 200, + headers: Seq[(String, String)] = Nil, + cookies: Seq[Cookie] = Nil) extends Response + implicit def dataResponse[T](t: T)(implicit c: T => Data) = Response(t) trait Data{ def write(out: OutputStream): Unit @@ -34,7 +41,7 @@ object BaseResponse{ } } } -case class Redirect(url: String) extends BaseResponse{ +case class Redirect(url: String) extends Response{ override def data = "" override def statusCode = 301 @@ -43,7 +50,7 @@ case class Redirect(url: String) extends BaseResponse{ override def cookies = Nil } -case class Abort(code: Int) extends BaseResponse { +case class Abort(code: Int) extends Response { override def data = "" override def statusCode = code @@ -53,13 +60,13 @@ case class Abort(code: Int) extends BaseResponse { override def cookies = Nil } -case class Static(path: String) extends BaseResponse { +case class Static(path: String) extends Response { val relPath = java.nio.file.Paths.get(path) val (data0, statusCode0) = if (java.nio.file.Files.exists(relPath) && java.nio.file.Files.isRegularFile(relPath)){ - (java.nio.file.Files.newInputStream(relPath): BaseResponse.Data, 200) + (java.nio.file.Files.newInputStream(relPath): Response.Data, 200) }else{ - ("": BaseResponse.Data, 404) + ("": Response.Data, 404) } override def data = data0 @@ -73,8 +80,4 @@ case class Static(path: String) extends BaseResponse { -case class Response(data: BaseResponse.Data, - statusCode: Int = 200, - headers: Seq[(String, String)] = Nil, - cookies: Seq[Cookie] = Nil) extends BaseResponse diff --git a/cask/src/cask/package.scala b/cask/src/cask/package.scala index b7f1478..24a0a20 100644 --- a/cask/src/cask/package.scala +++ b/cask/src/cask/package.scala @@ -36,7 +36,5 @@ 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 3925bf1..9fac78a 100644 --- a/cask/test/src/test/cask/Decorated.scala +++ b/cask/test/src/test/cask/Decorated.scala @@ -1,16 +1,23 @@ package test.cask -import cask.model.ParamContext +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 getRawParams(ctx: ParamContext) = Right(cask.Decor("user" -> new User())) + def wrapMethodOutput(ctx: ParamContext, + delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = { + delegate(Map("user" -> new User())) + } } class withExtra extends cask.Decorator { - def getRawParams(ctx: ParamContext) = Right(cask.Decor("extra" -> 31337)) + def wrapMethodOutput(ctx: ParamContext, + delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = { + delegate(Map("extra" -> 31337)) + } } @withExtra() diff --git a/cask/test/src/test/cask/ExampleTests.scala b/cask/test/src/test/cask/ExampleTests.scala index 1196122..6858051 100644 --- a/cask/test/src/test/cask/ExampleTests.scala +++ b/cask/test/src/test/cask/ExampleTests.scala @@ -117,5 +117,31 @@ object ExampleTests extends TestSuite{ requests.get(host + "/list/active").text() ==> """[]""" } + 'TodoMvcDb - test(TodoMvcDb){ host => + requests.get(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() ==> + """[{"id":2,"checked":false,"text":"Profit!"}]""" + requests.get(host + "/list/completed").text() ==> + """[{"id":1,"checked":true,"text":"Get started with Cask"}]""" + + requests.post(host + "/toggle/2") + + requests.get(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.post(host + "/add", data = "new Task") + + requests.get(host + "/list/active").text() ==> + """[{"id":3,"checked":false,"text":"new Task"}]""" + + requests.post(host + "/delete/3") + + requests.get(host + "/list/active").text() ==> + """[]""" + } } } diff --git a/cask/test/src/test/cask/FailureTests.scala b/cask/test/src/test/cask/FailureTests.scala index 3ed4249..de3b438 100644 --- a/cask/test/src/test/cask/FailureTests.scala +++ b/cask/test/src/test/cask/FailureTests.scala @@ -1,11 +1,15 @@ package test.cask -import cask.model.ParamContext +import cask.internal.Router +import cask.model.{ParamContext, Response} import utest._ object FailureTests extends TestSuite { class myDecorator extends cask.Decorator { - def getRawParams(ctx: ParamContext) = Right(cask.Decor("extra" -> 31337)) + def wrapMethodOutput(ctx: ParamContext, + delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = { + delegate(Map("extra" -> 31337)) + } } val tests = Tests{ diff --git a/cask/test/src/test/cask/TodoMvcDb.scala b/cask/test/src/test/cask/TodoMvcDb.scala new file mode 100644 index 0000000..c6f8191 --- /dev/null +++ b/cask/test/src/test/cask/TodoMvcDb.scala @@ -0,0 +1,77 @@ +package test.cask +import cask.internal.Router +import cask.model.{ParamContext, Response} +import com.typesafe.config.ConfigFactory +import io.getquill._ + + +object TodoMvcDb extends cask.MainRoutes{ + case class Todo(id: Int, checked: Boolean, text: String) + object Todo{ + implicit def todoRW = upickle.default.macroRW[Todo] + } + 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))) + } + } + + ctx.executeAction( + """CREATE TABLE todo ( + | id INTEGER PRIMARY KEY AUTOINCREMENT, + | checked BOOLEAN, + | text TEXT + |); + |""".stripMargin + ) + ctx.executeAction( + """INSERT INTO todo (checked, text) VALUES + |(1, 'Get started with Cask'), + |(0, 'Profit!'); + |""".stripMargin + ) + + @transactional + @cask.get("/list/:state") + def list(state: String)(ctx: SqliteJdbcContext[_]) = { + val filteredTodos = state match{ + case "all" => run(query[Todo]) + case "active" => run(query[Todo].filter(!_.checked)) + case "completed" => run(query[Todo].filter(_.checked)) + } + upickle.default.write(filteredTodos) + } + + @transactional + @cask.post("/add") + def add(request: cask.Request)(ctx: SqliteJdbcContext[_]) = { + 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[_]) = { + run(query[Todo].filter(_.id == lift(index)).update(p => p.checked -> !p.checked)) + } + + @transactional + @cask.post("/delete/:index") + def delete(index: Int)(ctx: SqliteJdbcContext[_]) = { + run(query[Todo].filter(_.id == lift(index)).delete) + + } + + initialize() +} |