From c0f39d743fbdf07544a6f5b6284d7123e5c36296 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 12 Aug 2018 22:42:47 +0800 Subject: auto publishing of docs w/ example downloads works --- readme.md | 872 +------------------------------------------------------------- 1 file changed, 1 insertion(+), 871 deletions(-) (limited to 'readme.md') diff --git a/readme.md b/readme.md index 4add926..a672287 100644 --- a/readme.md +++ b/readme.md @@ -22,875 +22,5 @@ Cask is a simple Scala web framework inspired by Python's 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 [XXX](XXX) 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" -``` - -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() -} -``` - -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) -``` - -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() -} -``` - -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¶m=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() -} -``` - -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 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() -} -``` - -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() -} -``` - -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() -} -``` - -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() -} -``` - -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() -} - -``` - -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) -``` - -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()) -} -``` - -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() -} -``` - -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() -} - -``` - -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( - "" + 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() -} -``` - -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 +- [Documentation](http://www.lihaoyi.com/cask/) \ No newline at end of file -- cgit v1.2.3