Cask


Table of Contents

Cask: a Scala HTTP micro-framework

Main Customization

Discord Chat
Patreon

package app
object MinimalApplication extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    "Hello World!"
  }

  @cask.post("/do-thing")
  def doThing(request: cask.Request) = {
    request.text().reverse
  }

  initialize()
}

Cask is a simple Scala web framework inspired by Python's Flask project. It aims to bring simplicity, flexibility and ease-of-use to Scala webservers, avoiding cryptic DSLs or complicated asynchrony.

Getting Started

The easiest way to begin using Cask is by downloading the example project above.

Unzip one of the example projects available on this page (e.g. above) into a folder. This should give you the following files:

build.sc
app/src/MinimalExample.scala
app/test/src/ExampleTests.scala
./mill -w app.runBackground

This will server up the Cask application on http://localhost:8080. You can immediately start interacting with it either via the browser, or programmatically via curl or a HTTP client like Requests-Scala:

val host = "http://localhost:8080"

val success = requests.get(host)

success.text() ==> "Hello World!"
success.statusCode ==> 200

requests.get(host + "/doesnt-exist").statusCode ==> 404

requests.post(host + "/do-thing", data = "hello").text() ==> "olleh"

requests.get(host + "/do-thing").statusCode ==> 404

These HTTP calls are part of the test suite for the example project, which you can run using:

./mill -w app.test

To configure your Cask application to work with IntelliJ, you can use:

./mill mill.scalalib.GenIdea/idea

This will need to be re-run when you re-configure your build.sc file, e.g. when adding additional modules or third-party dependencies.

Cask is just a Scala library, and you can use Cask in any existing Scala project via the following coordinates:

// Mill
ivy"com.lihaoyi::cask:0.9.2"

// SBT
"com.lihaoyi" %% "cask" % "0.9.2"

The ./mill command is just a wrapper around the Mill build tool; the build.sc files you see in all examples are Mill build files, and you can use your own installation of Mill instead of ./mill if you wish. All normal Mill commands and functionality works for ./mill.

The following examples will walk you through how to use Cask to accomplish tasks common to anyone writing a web application. Each example comes with a downloadable example project with code and unit tests, which you can use via the same ./mill -w app.runBackground or ./mill -w app.test workflows we saw above.

Minimal Example

package app
object MinimalApplication extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    "Hello World!"
  }

  @cask.post("/do-thing")
  def doThing(request: cask.Request) = {
    request.text().reverse
  }

  initialize()
}

The rough outline of how the minimal example works should be easy to understand:

In most cases, Cask provides convenient helpers to extract exactly the data from the incoming HTTP request that you need, while also de-serializing it into the data type you need and returning meaningful errors if they are missing. Thus, although you can always get all the data necessary through cask.Request, it is often more convenient to use another way, which will go into below.

As your application grows, you will likely want to split up the routes into separate files, themselves separate from any configuration of the Main entrypoint (e.g. overriding the port, host, default error handlers, etc.). You can do this by splitting it up into cask.Routes and cask.Main objects:

package app

case class MinimalRoutes()(implicit cc: castor.Context,
                           log: cask.Logger) extends cask.Routes{
  @cask.get("/")
  def hello() = {
    "Hello World!"
  }

  @cask.post("/do-thing")
  def doThing(request: cask.Request) = {
    request.text().reverse
  }

  initialize()
}
object MinimalRoutesMain extends cask.Main{
  val allRoutes = Seq(MinimalRoutes())
}

You can split up your routes into separate cask.Routes objects as makes sense and pass them all into cask.Main.

Variable Routes

package app
object VariableRoutes extends cask.MainRoutes{
  @cask.get("/user/:userName") // variable path segment, e.g. HOST/user/lihaoyi
  def getUserProfile(userName: String) = {
    s"User $userName"
  }

  @cask.get("/path") // GET allowing arbitrary sub-paths, e.g. HOST/path/foo/bar/baz
  def getSubpath(segments: cask.RemainingPathSegments) = {
    s"Subpath ${segments.value}"
  }

  @cask.post("/path") // POST allowing arbitrary sub-paths, e.g. HOST/path/foo/bar/baz
  def postArticleSubpath(segments: cask.RemainingPathSegments) = {
    s"POST Subpath ${segments.value}"
  }

  initialize()
}

You can bind path segments to endpoint parameters by declaring them as parameters. These are either:

Query Params

package app
object QueryParams extends cask.MainRoutes{

  @cask.get("/article/:articleId") // Mandatory query param, e.g. HOST/article/foo?param=bar
  def getArticle(articleId: Int, param: String) = { 
    s"Article $articleId $param"
  }

  @cask.get("/article2/:articleId") // Optional query param
  def getArticleOptional(articleId: Int, param: Option[String] = None) = {
    s"Article $articleId $param"
  }

  @cask.get("/article3/:articleId") // Optional query param with default
  def getArticleDefault(articleId: Int, param: String = "DEFAULT VALUE") = {
    s"Article $articleId $param"
  }

  @cask.get("/article4/:articleId") // 1-or-more param, e.g. HOST/article/foo?param=bar&param=qux
  def getArticleSeq(articleId: Int, param: Seq[String]) = {
    s"Article $articleId $param"
  }

  @cask.get("/article5/:articleId") // 0-or-more query param
  def getArticleOptionalSeq(articleId: Int, param: Seq[String] = Nil) = {
    s"Article $articleId $param"
  }

  @cask.get("/user2/:userName") // allow unknown params, e.g. HOST/article/foo?foo=bar&qux=baz
  def getUserProfileAllowUnknown(userName: String, params: cask.QueryParams) = {
    s"User $userName " + params.value
  }

  initialize()
}

You can bind query parameters to your endpoint method via parameters of the form:

Multi-method Routes

package app
object HttpMethods extends cask.MainRoutes{
  @cask.route("/login", methods = Seq("get", "post"))
  def login(request: cask.Request) = {
    if (request.exchange.getRequestMethod.equalToString("post")) "do_the_login"
    else "show_the_login_form"
  }

  @cask.route("/session", methods = Seq("delete"))
  def session(request: cask.Request) = {
    "delete_the_session"
  }

  @cask.route("/session", methods = Seq("secretmethod"))
  def admin(request: cask.Request) = {
    "security_by_obscurity"
  }

  @cask.route("/api", methods = Seq("options"))
  def cors(request: cask.Request) = {
    "allow_cors"
  }


  initialize()
}

Sometimes, you may want to handle multiple kinds of HTTP requests in the same endpoint function, e.g. with code that can accept both GETs and POSTs and decide what to do in each case. You can use the @cask.route annotation to do so

Receiving Form-encoded or JSON data

package app
object FormJsonPost extends cask.MainRoutes{
  @cask.postJson("/json")
  def jsonEndpoint(value1: ujson.Value, value2: Seq[Int]) = {
    "OK " + value1 + " " + value2
  }

  @cask.postJson("/json-obj")
  def jsonEndpointObj(value1: ujson.Value, value2: Seq[Int]) = {
    ujson.Obj(
      "value1" -> value1,
      "value2" -> value2
    )
  }

  @cask.postForm("/form")
  def formEndpoint(value1: cask.FormValue, value2: Seq[Int]) = {
    "OK " + value1 + " " + value2
  }

  @cask.postForm("/form-obj")
  def formEndpointObj(value1: cask.FormValue, value2: Seq[Int]) = {
    ujson.Obj(
      "value1" -> value1.value,
      "value2" -> value2
    )
  }

  @cask.postForm("/upload")
  def uploadFile(image: cask.FormFile) = {
    image.fileName
  }


  @cask.postJson("/json-extra")
  def jsonEndpointExtra(value1: ujson.Value,
                        value2: Seq[Int],
                        params: cask.QueryParams,
                        segments: cask.RemainingPathSegments) = {
    "OK " + value1 + " " + value2 + " " + params.value + " " + segments.value
  }

  @cask.postForm("/form-extra")
  def formEndpointExtra(value1: cask.FormValue, 
                        value2: Seq[Int],
                        params: cask.QueryParams,
                        segments: cask.RemainingPathSegments) = {
    "OK " + value1 + " " + value2 + " " + params.value + " " + segments.value
  }
  
  initialize()
}

If you need to handle a JSON-encoded POST request, you can use the @cask.postJson decorator. This assumes the posted request body is a JSON dict, and uses its keys to populate the endpoint's parameters, either as raw ujson.Values or deserialized into Seq[Int]s or other things. Deserialization is handled using the uPickle JSON library, though you could write your own version of postJson to work with any other JSON library of your choice.

Similarly, you can mark endpoints as @cask.postForm, in which case the endpoints params will be taken from the form-encoded POST body either raw (as cask.FormValues) or deserialized into simple data structures. Use cask.FormFile if you want the given form value to be a file upload.

Both normal forms and multipart forms are handled the same way.

If the necessary keys are not present in the JSON/form-encoded POST body, or the deserialization into Scala data-types fails, a 400 response is returned automatically with a helpful error message.

Processing Cookies

package app
object Cookies extends cask.MainRoutes{
  @cask.get("/read-cookie")
  def readCookies(username: cask.Cookie) = {
    username.value
  }

  @cask.get("/store-cookie")
  def storeCookies() = {
    cask.Response(
      "Cookies Set!",
      cookies = Seq(cask.Cookie("username", "the_username"))
    )
  }

  @cask.get("/delete-cookie")
  def deleteCookie() = {
    cask.Response(
      "Cookies Deleted!",
      cookies = Seq(cask.Cookie("username", "", expires = java.time.Instant.EPOCH))
    )
  }

  initialize()
}

Cookies are most easily read by declaring a : cask.Cookie parameter; the parameter name is used to fetch the cookie you are interested in. Cookies can be stored by setting the cookie attribute in the response, and deleted simply by setting expires = java.time.Instant.EPOCH (i.e. to have expired a long time ago)

Serving Static Files

package app
object StaticFiles extends cask.MainRoutes{
  @cask.get("/")
  def index() = {
    "Hello!"
  }

  @cask.staticFiles("/static/file")
  def staticFileRoutes() = "app/resources/cask"

  @cask.staticResources("/static/resource")
  def staticResourceRoutes() = "cask"

  @cask.staticResources("/static/resource2")
  def staticResourceRoutes2() = "."

  initialize()
}

You can ask Cask to serve static files by defining a @cask.staticFiles endpoint. This will match any subpath of the value returned by the endpoint (e.g. above /static/file.txt, /static/folder/file.txt, etc.) and return the file contents from the corresponding file on disk (and 404 otherwise).

Similarly, @cask.staticResources attempts to serve a request based on the JVM resource path, returning the data if a resource is present and a 404 otherwise.

You can also configure the headers you wish to return to static file requests, or use @cask.decorators.compress to compress the responses:

package app
object StaticFiles2 extends cask.MainRoutes{
  @cask.get("/")
  def index() = {
    "Hello!"
  }

  @cask.staticFiles("/static/file", headers = Seq("Cache-Control" -> "max-age=31536000"))
  def staticFileRoutes() = "app/resources/cask"

  @cask.decorators.compress
  @cask.staticResources("/static/resource")
  def staticResourceRoutes() = "cask"

  @cask.staticResources("/static/resource2")
  def staticResourceRoutes2() = "."

  initialize()
}

Redirects or Aborts

package app
object RedirectAbort extends cask.MainRoutes{
  @cask.get("/")
  def index() = {
    cask.Redirect("/login")
  }

  @cask.get("/login")
  def login() = {
    cask.Abort(401)
  }

  initialize()
}

Cask provides some convenient helpers cask.Redirect and cask.Abort which you can return; these are simple wrappers around cask.Request, and simply set up the relevant headers or status code for you.

HTML Rendering

Cask doesn't come bundled with HTML templating functionality, but it makes it really easy to use community-standard libraries like Scalatags to render your HTML. Simply adding the relevant ivy"com.lihaoyi::scalatags:0.9.1" dependency to your build.sc file is enough to render Scalatags templates:

package app
import scalatags.Text.all._
object Scalatags extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    doctype("html")(
      html(
        body(
          h1("Hello World"),
          p("I am cow")
        )
      )
    )
  }

  initialize()
}

If you prefer to use the Twirl templating engine, you can use that too:

package app
object Twirl extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    "<!doctype html>" + html.hello("Hello World")
  }

  initialize()
}

With the following app/views/hello.scala.html:

@(titleTxt: String)
<html>
    <body>
        <h1>@titleTxt</h1>
        <p>I am cow</p>
    </body>
</html>

Extending Endpoints with Decorators

package app
object Decorated extends cask.MainRoutes{
  class User{
    override def toString = "[haoyi]"
  }
  class loggedIn extends cask.RawDecorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate) = {
      delegate(Map("user" -> new User()))
    }
  }
  class withExtra extends cask.RawDecorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate) = {
      delegate(Map("extra" -> 31337))
    }
  }

  @withExtra()
  @cask.get("/hello/:world")
  def hello(world: String)(extra: Int) = {
    world + extra
  }

  @loggedIn()
  @cask.get("/internal/:world")
  def internal(world: String)(user: User) = {
    world + user
  }

  @withExtra()
  @loggedIn()
  @cask.get("/internal-extra/:world")
  def internalExtra(world: String)(user: User)(extra: Int) = {
    world + user + extra
  }

  @withExtra()
  @loggedIn()
  @cask.get("/ignore-extra/:world")
  def ignoreExtra(world: String)(user: User) = {
    world + user
  }

  @loggedIn()
  @cask.get("/hello-default")
  def defaults(world: String = "world")(user: User) = {
    world + user
  }
  initialize()
}

You can write extra decorator annotations that stack on top of the existing @cask.get/@cask.post to provide additional arguments or validation. This is done by implementing the cask.Decorator interface and it's getRawParams function. getRawParams:

Each additional decorator is responsible for one additional parameter list to the right of the existing parameter lists, each of which can contain any number of parameters.

Decorators are useful for things like:

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):

package app
object Decorated2 extends cask.MainRoutes{
  class User{
    override def toString = "[haoyi]"
  }
  class loggedIn extends cask.RawDecorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate) = {
      delegate(Map("user" -> new User()))
    }
  }
  class withExtra extends cask.RawDecorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate) = {
      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()
}

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.

Custom Endpoints

package app

class custom(val path: String, val methods: Seq[String])
  extends cask.HttpEndpoint[Int, Seq[String]]{
  def wrapFunction(ctx: cask.Request, delegate: Delegate) = {
    delegate(Map()).map{num =>
      cask.Response("Echo " + num, statusCode = num)
    }
  }

  def wrapPathSegment(s: String) = Seq(s)

  type InputParser[T] = cask.endpoints.QueryParamReader[T]
}

object Endpoints extends cask.MainRoutes{


  @custom("/echo/:status", methods = Seq("get"))
  def echoStatus(status: String) = {
    status.toInt
  }

  initialize()
}

When you need more flexibility than decorators allow, you can define your own custom cask.Endpoints to replace the default set that Cask provides. This allows you to

Generally you should not be writing custom cask.Endpoints every day, but if you find yourself trying to standardize on a way of doing things across your web application, it might make sense to write a custom endpoint decorator: to DRY things up , separate business logic (inside the annotated function) from plumbing (in the endpoint function and decorators), and enforcing a standard of how endpoint functions are written.

Gzip & Deflated Responses

package app
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:

package app

case class Compress2()(implicit cc: castor.Context,
                       log: cask.Logger) 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{
  val allRoutes = Seq(Compress2())
}

Or globally, in your cask.Main:

package app

case class Compress3()(implicit cc: castor.Context,
                       log: cask.Logger) extends cask.Routes{

  @cask.get("/")
  def hello() = {
    "Hello World! Hello World! Hello World!"
  }

  initialize()
}

object Compress3Main extends cask.Main{
  override def mainDecorators = Seq(new cask.decorators.compress())
  val allRoutes = Seq(Compress3())
}

Websockets

package app

object Websockets extends cask.MainRoutes{
  @cask.websocket("/connect/:userName")
  def showUserProfile(userName: String): cask.WebsocketResult = {
    if (userName != "haoyi") cask.Response("", statusCode = 403)
    else cask.WsHandler { channel =>
      cask.WsActor {
        case cask.Ws.Text("") => channel.send(cask.Ws.Close())
        case cask.Ws.Text(data) =>
          channel.send(cask.Ws.Text(userName + " " + data))
      }
    }
  }

  initialize()
}

Cask's Websocket endpoints are very similar to Cask's HTTP endpoints. Annotated with @cask.websocket instead of @cask.get or @cask.post, the primary difference is that instead of only returning a cask.Response, you now have an option of returning a cask.WsHandler.

The cask.WsHandler allows you to pro-actively start sending websocket messages once a connection has been made, via the channel: WsChannelActor it exposes, and lets you react to messages via the cask.WsActor you create. You can use these two APIs to perform full bi-directional, asynchronous communications, as websockets are intended to be used for. Note that all messages received on a each individual Websocket connection by your cask.WsActor are handled in a single-threaded fashion by default: this means you can work with local mutable state in your @cask.websocket endpoint without worrying about race conditions or multithreading. If you want further parallelism, you can explicitly spin off scala.concurrent.Futures or other cask.BatchActors to perform that parallel processing.

Returning a cask.Response immediately closes the websocket connection, and is useful if you want to e.g. return a 404 or 403 due to the initial request being invalid.

Cask also provides a lower-lever websocket interface, which allows you directly work with the underlying io.undertow.websockets.WebSocketConnectionCallback:

package app

import io.undertow.websockets.WebSocketConnectionCallback
import io.undertow.websockets.core.{AbstractReceiveListener, BufferedTextMessage, WebSocketChannel, WebSockets}
import io.undertow.websockets.spi.WebSocketHttpExchange

object Websockets2 extends cask.MainRoutes{
  @cask.websocket("/connect/:userName")
  def showUserProfile(userName: String): cask.WebsocketResult = {
    if (userName != "haoyi") cask.Response("", statusCode = 403)
    else new WebSocketConnectionCallback() {
      override def onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel): Unit = {
        channel.getReceiveSetter.set(
          new AbstractReceiveListener() {
            override def onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) = {
              message.getData match{
                case "" => channel.close()
                case data => WebSockets.sendTextBlocking(userName + " " + data, channel)
              }
            }
          }
        )
        channel.resumeReceives()
      }
    }
  }

  initialize()
}

It leaves it up to you to manage open channels, react to incoming messages, or pro-actively send them out, mostly using the underlying Undertow webserver interface. While Cask does not model streams, backpressure, iteratees, or provide any higher level API, it should not be difficult to take the Cask API and build whatever higher-level abstractions you prefer to use.

If you are separating your cask.Routes from your cask.Main, you need to inject in a cask.Logger to handle errors reported when handling websocket requests:

package app

case class Websockets3()(implicit cc: castor.Context,
                         log: cask.Logger) extends cask.Routes{
  @cask.websocket("/connect/:userName")
  def showUserProfile(userName: String): cask.WebsocketResult = {
    if (userName != "haoyi") cask.Response("", statusCode = 403)
    else cask.WsHandler { channel =>
      cask.WsActor {
        case cask.Ws.Text("") => channel.send(cask.Ws.Close())
        case cask.Ws.Text(data) =>
          channel.send(cask.Ws.Text(userName + " " + data))
      }
    }
  }

  initialize()
}

object Websockets3Main extends cask.Main{
  val allRoutes = Seq(Websockets3())
}

TodoMVC Api Server

package app
object TodoMvcApi extends cask.MainRoutes{
  case class Todo(checked: Boolean, text: String)
  object Todo{
    implicit def todoRW: upickle.default.ReadWriter[Todo] = upickle.default.macroRW[Todo]
  }
  var todos = Seq(
    Todo(true, "Get started with Cask"),
    Todo(false, "Profit!")
  )

  @cask.get("/list/:state")
  def list(state: String) = {
    val filteredTodos = state match{
      case "all" => todos
      case "active" => todos.filter(!_.checked)
      case "completed" => todos.filter(_.checked)
    }
    upickle.default.write(filteredTodos)
  }

  @cask.post("/add")
  def add(request: cask.Request) = {
    todos = Seq(Todo(false, request.text())) ++ todos
  }

  @cask.post("/toggle/:index")
  def toggle(index: Int) = {
    todos = todos.updated(index, todos(index).copy(checked = !todos(index).checked))
  }

  @cask.post("/delete/:index")
  def delete(index: Int) = {
    todos = todos.patch(index, Nil, 1)
  }

  initialize()
}

This is a simple self-contained example of using Cask to write an in-memory API server for the common TodoMVC example app.

This minimal example intentionally does not contain javascript, HTML, styles, etc.. Those can be managed via the normal mechanism for Serving Static Files.

TodoMVC Database Integration

package app
import scalasql.DbApi.Txn
import scalasql.Sc
import scalasql.SqliteDialect._

object TodoMvcDb extends cask.MainRoutes{
  val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite")
  val sqliteDataSource = new org.sqlite.SQLiteDataSource()
  sqliteDataSource.setUrl(s"jdbc:sqlite:$tmpDb/file.db")
  lazy val sqliteClient = new scalasql.DbClient.DataSource(
    sqliteDataSource,
    config = new scalasql.Config {}
  )

  class transactional extends cask.RawDecorator{
    def wrapFunction(pctx: cask.Request, delegate: Delegate) = {
      sqliteClient.transaction { txn =>
        val res = delegate(Map("txn" -> txn))
        if (res.isInstanceOf[cask.router.Result.Error]) txn.rollback()
        res
      }
    }
  }

  case class Todo[T[_]](id: T[Int], checked: T[Boolean], text: T[String])
  object Todo extends scalasql.Table[Todo]{
    implicit def todoRW = upickle.default.macroRW[Todo[Sc]]
  }

  sqliteClient.getAutoCommitClientConnection.updateRaw(
    """CREATE TABLE todo (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  checked BOOLEAN,
  text TEXT
);

INSERT INTO todo (checked, text) VALUES
(1, 'Get started with Cask'),
(0, 'Profit!');
""".stripMargin
  )

  @transactional
  @cask.get("/list/:state")
  def list(state: String)(txn: Txn) = {
    val filteredTodos = state match{
      case "all" => txn.run(Todo.select)
      case "active" => txn.run(Todo.select.filter(!_.checked))
      case "completed" => txn.run(Todo.select.filter(_.checked))
    }
    upickle.default.write(filteredTodos)
  }

  @transactional
  @cask.post("/add")
  def add(request: cask.Request)(txn: Txn) = {
    val body = request.text()
    txn.run(
      Todo
        .insert
        .columns(_.checked := false, _.text := body)
        .returning(_.id)
        .single
    )

    if (body == "FORCE FAILURE") throw new Exception("FORCE FAILURE BODY")
  }

  @transactional
  @cask.post("/toggle/:index")
  def toggle(index: Int)(txn: Txn) = {
    txn.run(Todo.update(_.id === index).set(p => p.checked := !p.checked))
  }

  @transactional
  @cask.post("/delete/:index")
  def delete(index: Int)(txn: Txn) = {
    txn.run(Todo.delete(_.id === index))
  }

  initialize()
}

This example demonstrates how to use Cask to write a TodoMVC API server that persists it's state in a database rather than in memory. We use the ScalaSql database access library to write a @transactional decorator that automatically opens one transaction per call to an endpoint, ensuring that database queries are properly committed on success or rolled-back on error. Note that because the default database connector propagates its transaction context in a thread-local, @transactional does not need to pass the ctx object into each endpoint as an additional parameter list, and so we simply leave it out.

While this example is specific to ScalaSql, you can easily modify the @transactional decorator to make it work with whatever database access library you happen to be using. For libraries which need an implicit transaction, it can be passed into each endpoint function as an additional parameter list as described in Extending Endpoints with Decorators. work with whatever database access library you happen to be using. For libraries which need an implicit transaction, it can be passed into each endpoint function as an additional parameter list as described in Extending Endpoints with Decorators.

TodoMVC Full Stack Web

The following code snippet is the complete code for a full-stack TodoMVC implementation: including HTML generation for the web UI via Scalatags, Javascript for the interactivity, static file serving, and database integration via ScalaSql. While slightly long, this example should give you a tour of all the things you need to know to use Cask.

Note that this is a "boring" server-side-rendered webapp with Ajax interactions, without any complex front-end frameworks or libraries: it's purpose is to demonstrate a simple working web application of using Cask end-to-end, which you can build upon to create your own Cask web application architected however you would like.

package app
import scalasql.DbApi.Txn
import scalasql.Sc
import scalasql.SqliteDialect._
import scalatags.Text.all._
import scalatags.Text.tags2

object TodoServer extends cask.MainRoutes{
  val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite")

  val sqliteDataSource = new org.sqlite.SQLiteDataSource()
  sqliteDataSource.setUrl(s"jdbc:sqlite:$tmpDb/file.db")
  lazy val sqliteClient = new scalasql.DbClient.DataSource(
    sqliteDataSource,
    config = new scalasql.Config {}
  )

  class transactional extends cask.RawDecorator{
    def wrapFunction(pctx: cask.Request, delegate: Delegate) = {
      sqliteClient.transaction { txn =>
        val res = delegate(Map("txn" -> txn))
        if (res.isInstanceOf[cask.router.Result.Error]) txn.rollback()
        res
      }
    }
  }

  case class Todo[T[_]](id: T[Int], checked: T[Boolean], text: T[String])
  object Todo extends scalasql.Table[Todo]

  sqliteClient.getAutoCommitClientConnection.updateRaw(
    """CREATE TABLE todo (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  checked BOOLEAN,
  text TEXT
);

INSERT INTO todo (checked, text) VALUES
(1, 'Get started with Cask'),
(0, 'Profit!');
""".stripMargin
  )

  @transactional
  @cask.post("/list/:state")
  def list(state: String)(txn: Txn) = renderBody(state)(txn).render

  @transactional
  @cask.post("/add/:state")
  def add(state: String, request: cask.Request)(implicit txn: Txn) = {
    val body = request.text()
    txn.run(Todo.insert.columns(_.checked := false, _.text := body))
    renderBody(state).render
  }

  @transactional
  @cask.post("/delete/:state/:index")
  def delete(state: String, index: Int)(implicit txn: Txn) = {
    txn.run(Todo.delete(_.id === index))
    renderBody(state).render
  }

  @transactional
  @cask.post("/toggle/:state/:index")
  def toggle(state: String, index: Int)(implicit txn: Txn) = {
    txn.run(Todo.update(_.id === index).set(p => p.checked := !p.checked))
    renderBody(state).render
  }

  @transactional
  @cask.post("/clear-completed/:state")
  def clearCompleted(state: String)(implicit txn: Txn) = {
    txn.run(Todo.delete(_.checked))
    renderBody(state).render
  }

  @transactional
  @cask.post("/toggle-all/:state")
  def toggleAll(state: String)(implicit txn: Txn) = {
    val next = txn.run(Todo.select.filter(_.checked).size) != 0
    txn.run(Todo.update(_ => true).set(_.checked := !next))
    renderBody(state).render
  }

  def renderBody(state: String)(implicit txn: Txn) = {
    val filteredTodos = state match{
      case "all" => txn.run(Todo.select).sortBy(-_.id)
      case "active" => txn.run(Todo.select.filter(!_.checked)).sortBy(-_.id)
      case "completed" => txn.run(Todo.select.filter(_.checked)).sortBy(-_.id)
    }
    frag(
      header(cls := "header",
        h1("todos"),
        input(cls := "new-todo", placeholder := "What needs to be done?", autofocus := "")
      ),
      tags2.section(cls := "main",
        input(
          id := "toggle-all",
          cls := "toggle-all",
          `type` := "checkbox",
          if (txn.run(Todo.select.filter(_.checked).size !== 0)) checked else ()
        ),
        label(`for` := "toggle-all","Mark all as complete"),
        ul(cls := "todo-list",
          for(todo <- filteredTodos) yield li(
            if (todo.checked) cls := "completed" else (),
            div(cls := "view",
              input(
                cls := "toggle",
                `type` := "checkbox",
                if (todo.checked) checked else (),
                data("todo-index") := todo.id
              ),
              label(todo.text),
              button(cls := "destroy", data("todo-index") := todo.id)
            ),
            input(cls := "edit", value := todo.text)
          )
        )
      ),
      footer(cls := "footer",
        span(cls := "todo-count",
          strong(txn.run(Todo.select.filter(!_.checked).size).toInt),
          " items left"
        ),
        ul(cls := "filters",
          li(cls := "todo-all",
            a(if (state == "all") cls := "selected" else (), "All")
          ),
          li(cls := "todo-active",
            a(if (state == "active") cls := "selected" else (), "Active")
          ),
          li(cls := "todo-completed",
            a(if (state == "completed") cls := "selected" else (), "Completed")
          )
        ),
        button(cls := "clear-completed","Clear completed")
      )
    )
  }

  @transactional
  @cask.get("/")
  def index()(implicit txn: Txn) = {
    doctype("html")(
      html(lang := "en",
        head(
          meta(charset := "utf-8"),
          meta(name := "viewport", content := "width=device-width, initial-scale=1"),
          tags2.title("Template • TodoMVC"),
          link(rel := "stylesheet", href := "/static/index.css")
        ),
        body(
          tags2.section(cls := "todoapp", renderBody("all")),
          footer(cls := "info",
            p("Double-click to edit a todo"),
            p("Created by ",
              a(href := "http://todomvc.com","Li Haoyi")
            ),
            p("Part of ",
              a(href := "http://todomvc.com","TodoMVC")
            )
          ),
          script(src := "/static/app.js")
        )
      )
    )
  }

  @cask.staticResources("/static")
  def static() = "todo"

  initialize()
}


About the Author: Haoyi is a software engineer, an early contributor to Scala.js, and the author of many open-source Scala tools such as Cask, the Ammonite REPL and FastParse.

If you've enjoy using Cask, or enjoyed using Haoyi's other open source libraries, please chip in (or get your Company to chip in!) via Patreon so he can continue his open-source work


Main Customization