summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-08-08 15:22:02 +0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-08-08 15:22:02 +0800
commita5320694193fd86b639c53a91fa24fb7f8ea914e (patch)
tree84d5d94f1fa0a6aeee2b7dc81b1f0276e2f38994
parenta89ebd17dab5af6814d58f02d410acb1eb60e592 (diff)
downloadcask-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.sc4
-rw-r--r--cask/src/cask/endpoints/FormEndpoint.scala19
-rw-r--r--cask/src/cask/endpoints/JsonEndpoint.scala14
-rw-r--r--cask/src/cask/endpoints/StaticEndpoints.scala14
-rw-r--r--cask/src/cask/endpoints/WebEndpoints.scala11
-rw-r--r--cask/src/cask/main/Decorators.scala23
-rw-r--r--cask/src/cask/main/ErrorMsgs.scala2
-rw-r--r--cask/src/cask/main/Main.scala41
-rw-r--r--cask/src/cask/main/Routes.scala4
-rw-r--r--cask/src/cask/model/Response.scala31
-rw-r--r--cask/src/cask/package.scala2
-rw-r--r--cask/test/src/test/cask/Decorated.scala13
-rw-r--r--cask/test/src/test/cask/ExampleTests.scala26
-rw-r--r--cask/test/src/test/cask/FailureTests.scala8
-rw-r--r--cask/test/src/test/cask/TodoMvcDb.scala77
15 files changed, 204 insertions, 85 deletions
diff --git a/build.sc b/build.sc
index 36c4020..6fe21d6 100644
--- a/build.sc
+++ b/build.sc
@@ -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()
+}