summaryrefslogtreecommitdiff
path: root/docs/pages/1 - Cask: a Scala HTTP micro-framework .md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/pages/1 - Cask: a Scala HTTP micro-framework .md')
-rw-r--r--docs/pages/1 - Cask: a Scala HTTP micro-framework .md927
1 files changed, 927 insertions, 0 deletions
diff --git a/docs/pages/1 - Cask: a Scala HTTP micro-framework .md b/docs/pages/1 - Cask: a Scala HTTP micro-framework .md
new file mode 100644
index 0000000..22d879e
--- /dev/null
+++ b/docs/pages/1 - Cask: a Scala HTTP micro-framework .md
@@ -0,0 +1,927 @@
+```scala
+object MinimalApplication extends cask.MainRoutes{
+ @cask.get("/")
+ def hello() = {
+ "Hello World!"
+ }
+
+ @cask.post("/do-thing")
+ def doThing(request: cask.Request) = {
+ new String(request.data.readAllBytes()).reverse
+ }
+
+ initialize()
+}
+```
+
+[Cask](https://github.com/lihaoyi/cask) is a simple Scala web framework inspired
+by Python's [Flask](http://flask.pocoo.org/docs/1.0/) 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
+[Mill](http://www.lihaoyi.com/mill/) example project:
+
+- Install [Mill](http://www.lihaoyi.com/mill/)
+- Unzip the $$$minimalApplication into a folder. This should
+ give you the following files:
+```text
+build.sc
+app/src/MinimalExample.scala
+app/test/src/ExampleTests.scala
+```
+
+- `cd` into the folder, and run
+
+```bash
+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](https://github.com/lihaoyi/requests-scala):
+
+```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:
+
+```bash
+mill -w app.test
+```
+
+Cask is just a Scala library, and you can use Cask in any existing Scala project
+via the following coordinates:
+
+```scala
+// Mill
+ivy"com.lihaoyi::cask:0.1.0"
+
+// SBT
+"com.lihaoyi" %% "cask" % "0.1.0"
+```
+
+Example Projects
+----------------
+
+### Minimal Example
+
+```scala
+object MinimalApplication extends cask.MainRoutes{
+ @cask.get("/")
+ def hello() = {
+ "Hello World!"
+ }
+
+ @cask.post("/do-thing")
+ def doThing(request: cask.Request) = {
+ new String(request.data.readAllBytes()).reverse
+ }
+
+ initialize()
+}
+```
+
+- $$$minimalApplication
+
+The rough outline of how the minimal example works should be easy to understand:
+
+- You define an object that inherits from `cask.MainRoutes`
+
+- Define endpoints using annotated functions, using `@cask.get` or `@cask.post`
+ with the route they should match
+
+- Each function can return the data you want in the response, or a
+ `cask.Response` if you want further customization: response code, headers,
+ etc.
+
+- Your function can tale an optional `cask.Request`, which exposes the entire
+ incoming HTTP request if necessary. In the above example, we use it to read
+ the request body into a string and return it reversed.
+
+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:
+
+```scala
+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)
+```
+
+- $$$minimalApplication2
+-
+You can split up your routes into separate `cask.Routes` objects as makes sense
+and pass them all into `cask.Main`.
+
+### Variable Routes
+
+
+```scala
+object VariableRoutes extends cask.MainRoutes{
+ @cask.get("/user/:userName")
+ def showUserProfile(userName: String) = {
+ s"User $userName"
+ }
+
+ @cask.get("/post/:postId")
+ def showPost(postId: Int, param: Seq[String]) = {
+ s"Post $postId $param"
+ }
+
+ @cask.get("/path", subpath = true)
+ def showSubpath(subPath: cask.Subpath) = {
+ s"Subpath ${subPath.value}"
+ }
+
+ initialize()
+}
+```
+
+- $$$variableRoutes
+
+You can bind variables to endpoints by declaring them as parameters: these are
+either taken from a path-segment matcher of the same name (e.g. `postId` above),
+or from query-parameters of the same name (e.g. `param` above). You can make
+`param` take a `: String` to match `?param=hello`, an `: Int` for `?param=123` a
+`Seq[T]` (as above) for repeated params such as `?param=hello&param=world`, or
+`: Option[T]` for cases where the `?param=hello` is optional.
+
+If you need to capture the entire sub-path of the request, you can set the flag
+`subpath=true` and ask for a `: cask.Subpath` (the name of the param doesn't
+matter). This will make the route match any sub-path of the prefix given to the
+`@cask` decorator, and give you the remainder to use in your endpoint logic.
+
+### Receiving Form-encoded or JSON data
+
+
+```scala
+object FormJsonPost extends cask.MainRoutes{
+ @cask.postJson("/json")
+ def jsonEndpoint(value1: ujson.Js.Value, value2: Seq[Int]) = {
+ "OK " + value1 + " " + value2
+ }
+
+ @cask.postForm("/form")
+ def formEndpoint(value1: cask.FormValue, value2: Seq[Int]) = {
+ "OK " + value1 + " " + value2
+ }
+
+ @cask.postForm("/upload")
+ def uploadFile(image: cask.FormFile) = {
+ image.fileName
+ }
+
+ initialize()
+}
+```
+
+- $$$formJsonPost
+
+If you need to handle a JSON-encoded POST request, you can use the
+`@cast.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.Js.Value`s or deserialized into `Seq[Int]`s or other things.
+Deserialization is handled using the
+[uPickle](https://github.com/lihaoyi/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.FormValue`s) 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
+
+
+```scala
+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
+
+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
+
+```scala
+object StaticFiles extends cask.MainRoutes{
+ @cask.get("/")
+ def index() = {
+ "Hello!"
+ }
+
+ @cask.static("/static")
+ def staticRoutes() = "cask/resources/cask"
+
+ initialize()
+}
+```
+
+- $$$staticFiles
+
+You can ask Cask to serve static files by defining a `@cask.static` 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).
+
+### Redirects or Aborts
+
+```scala
+object RedirectAbort extends cask.MainRoutes{
+ @cask.get("/")
+ def index() = {
+ cask.Redirect("/login")
+ }
+
+ @cask.get("/login")
+ def login() = {
+ cask.Abort(401)
+ }
+
+ initialize()
+}
+```
+
+- $$$redirectAbort
+
+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.
+
+### Extending Endpoints with Decorators
+
+
+```scala
+object Decorated extends cask.MainRoutes{
+ class User{
+ override def toString = "[haoyi]"
+ }
+ class loggedIn extends cask.Decorator {
+ def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = {
+ delegate(Map("user" -> new User()))
+ }
+ }
+ class withExtra extends cask.Decorator {
+ def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = {
+ 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
+ }
+
+ initialize()
+}
+```
+
+- $$$decorateds
+
+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`:
+
+- Receives a `ParamContext`, which basically gives you full access to the
+ underlying undertow HTTP connection so you can pick out whatever data you
+ would like
+
+- Returns an `Either[Response, cask.Decor[Any]]`. Returning a `Left` lets you
+ bail out early with a fixed `cask.Response`, avoiding further processing.
+ Returning a `Right` provides a map of parameter names and values that will
+ then get passed to the endpoint function in consecutive parameter lists (shown
+ above), as well as an optional cleanup function that is run after the endpoint
+ terminates.
+
+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:
+
+- Making an endpoint return a HTTP 403 if the user isn't logged in, but if they are
+ logged in providing the `: User` object to the body of the endpoint function
+
+- Rate-limiting users by returning early with a HTTP 429 if a user tries to
+ access an endpoint too many times too quickly
+
+- Providing request-scoped values to the endpoint function: perhaps a database
+ transaction that commits when the function succeeds (and rolls-back if it
+ fails), or access to some system resource that needs to be released.
+
+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):
+
+```scala
+object Decorated2 extends cask.MainRoutes{
+ class User{
+ override def toString = "[haoyi]"
+ }
+ class loggedIn extends cask.Decorator {
+ def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = {
+ delegate(Map("user" -> new User()))
+ }
+ }
+ class withExtra extends cask.Decorator {
+ def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = {
+ 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) = {
+ world + user
+ }
+
+ initialize()
+}
+```
+
+- $$$decorated2
+
+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.
+
+### Gzip & Deflated Responses
+
+
+```scala
+object Compress extends cask.MainRoutes{
+
+ @cask.decorators.compress
+ @cask.get("/")
+ def hello() = {
+ "Hello World! Hello World! Hello World!"
+ }
+
+ initialize()
+}
+
+```
+
+- $$$compress
+
+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`:
+
+```scala
+object Compress2 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(Compress2)
+```
+
+- $$$compress2
+
+Or globally, in your `cask.Main`:
+
+```scala
+object Compress3 extends cask.Routes{
+
+ @cask.get("/")
+ def hello() = {
+ "Hello World! Hello World! Hello World!"
+ }
+
+ initialize()
+}
+
+object Compress3Main extends cask.Main(Compress3){
+ override def decorators = Seq(new cask.decorators.compress())
+}
+```
+
+- $$$compress3
+
+### TodoMVC Api Server
+
+
+```scala
+object TodoMvcApi extends cask.MainRoutes{
+ case class Todo(checked: Boolean, text: String)
+ object Todo{
+ implicit def todoRW = 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, new String(request.data.readAllBytes()))) ++ 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()
+}
+```
+
+- $$$todoApi
+
+This is a simple self-contained example of using Cask to write an in-memory API
+server for the common [TodoMVC example app](http://todomvc.com/).
+
+This minimal example intentionally does not contain javascript, HTML, styles,
+etc.. Those can be managed via the normal mechanism for
+[Serving Static Files](#serving-static-files).
+
+
+### TodoMVC Database Integration
+
+```scala
+import cask.internal.Router
+import com.typesafe.config.ConfigFactory
+import io.getquill.{SqliteJdbcContext, SnakeCase}
+
+object TodoMvcDb extends cask.MainRoutes{
+ 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"}"""
+ )
+ )
+
+ class transactional extends cask.Decorator{
+ 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,
+ | checked BOOLEAN,
+ | text TEXT
+ |);
+ |""".stripMargin
+ )
+ ctx.executeAction(
+ """INSERT INTO todo (checked, text) VALUES
+ |(1, 'Get started with Cask'),
+ |(0, 'Profit!');
+ |""".stripMargin
+ )
+
+ import ctx._
+
+ @transactional
+ @cask.get("/list/:state")
+ def list(state: String) = {
+ 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) = {
+ 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) = {
+ run(query[Todo].filter(_.id == lift(index)).update(p => p.checked -> !p.checked))
+ }
+
+ @transactional
+ @cask.post("/delete/:index")
+ def delete(index: Int) = {
+ run(query[Todo].filter(_.id == lift(index)).delete)
+ }
+
+ initialize()
+}
+
+```
+
+- $$$todoDb
+
+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
+[Quill](http://getquill.io/) 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 Quill, 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](#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](https://github.com/lihaoyi/scalatags), Javascript for the
+interactivity, static file serving, and database integration via
+[Quill](https://github.com/getquill/quill). 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.
+
+```scala
+import cask.internal.Router
+import com.typesafe.config.ConfigFactory
+import io.getquill.{SnakeCase, SqliteJdbcContext}
+import scalatags.Text.all._
+import scalatags.Text.tags2
+object Server extends cask.MainRoutes{
+ 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"}"""
+ )
+ )
+
+ class transactional extends cask.Decorator{
+ 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)
+
+ 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
+ )
+
+ import ctx._
+
+ @transactional
+ @cask.post("/list/:state")
+ def list(state: String) = renderBody(state).render
+
+ @transactional
+ @cask.post("/add/:state")
+ def add(state: String, request: cask.Request) = {
+ val body = new String(request.data.readAllBytes())
+ run(query[Todo].insert(_.checked -> lift(false), _.text -> lift(body)).returning(_.id))
+ renderBody(state).render
+ }
+
+ @transactional
+ @cask.post("/delete/:state/:index")
+ def delete(state: String, index: Int) = {
+ run(query[Todo].filter(_.id == lift(index)).delete)
+ renderBody(state).render
+ }
+
+ @transactional
+ @cask.post("/toggle/:state/:index")
+ def toggle(state: String, index: Int) = {
+ run(query[Todo].filter(_.id == lift(index)).update(p => p.checked -> !p.checked))
+ renderBody(state).render
+ }
+
+ @transactional
+ @cask.post("/clear-completed/:state")
+ def clearCompleted(state: String) = {
+ run(query[Todo].filter(_.checked).delete)
+ renderBody(state).render
+ }
+
+ @transactional
+ @cask.post("/toggle-all/:state")
+ def toggleAll(state: String) = {
+ val next = run(query[Todo].filter(_.checked).size) != 0
+ run(query[Todo].update(_.checked -> !lift(next)))
+ renderBody(state).render
+ }
+
+ def renderBody(state: String) = {
+ val filteredTodos = state match{
+ case "all" => run(query[Todo]).sortBy(-_.id)
+ case "active" => run(query[Todo].filter(!_.checked)).sortBy(-_.id)
+ case "completed" => run(query[Todo].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 (run(query[Todo].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(run(query[Todo].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() = {
+ cask.Response(
+ "<!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.static("/static")
+ def static() = "example/todo/resources/todo"
+
+ initialize()
+}
+```
+
+- $$$todo
+
+Main Customization
+------------------
+
+Apart from the code used to configure and define your routes and endpoints, Cask
+also allows global configuration for things that apply to the entire web server.
+This can be done by overriding the following methods on `cask.Main` or
+`cask.MainRoutes`:
+
+### def debugMode: Boolean = true
+
+Makes the Cask report verbose error messages and stack traces if an endpoint
+fails; useful for debugging, should be disabled for production.
+
+### def main
+
+The cask program entrypoint. By default just spins up a webserver, but you can
+override it to do whatever you like before or after the webserver runs.
+
+### def defaultHandler
+
+Cask is built on top of the [Undertow](http://undertow.io/) web server. If you
+need some low-level functionality not exposed by the Cask API, you can override
+`defaultHandler` to make use of Undertow's own
+[handler API](http://undertow.io/undertow-docs/undertow-docs-2.0.0/index.html#built-in-handlers)
+for customizing your webserver. This allows for things that Cask itself doesn't
+internally support: asynchronous requests & response,
+[Websockets](http://undertow.io/undertow-docs/undertow-docs-2.0.0/index.html#websockets),
+etc.
+
+### def port: Int = 8080, def host: String = "localhost"
+
+The host & port to attach your webserver to.
+
+### def handleNotFound
+
+The response to serve when the incoming request does not match any of the routes
+or endpoints; defaults to a typical 404
+
+### def handleEndpointError
+
+The response to serve when the incoming request matches a route and endpoint,
+but then fails for other reasons. Defaults to 400 for mismatched or invalid
+endpoint arguments and 500 for exceptions in the endpoint body, and provides
+useful stack traces or metadata for debugging if `debugMode = true`.
+
+### def mainDecorators
+
+Any `cask.Decorator`s that you want to apply to all routes and all endpoints in
+the entire web application \ No newline at end of file