From 0a59ba0178b7980a5b68892a22544faad650ecda Mon Sep 17 00:00:00 2001 From: Ammonite Travis Bot Date: Sun, 17 Nov 2019 04:55:56 +0000 Subject: first commit --- favicon.ico | Bin 0 -> 206 bytes index.html | 1078 ++++++++++++++++++++++++++++++++++++++++++ logo-white.svg | 1 + page/about-cask.html | 154 ++++++ page/main-customization.html | 125 +++++ 5 files changed, 1358 insertions(+) create mode 100644 favicon.ico create mode 100644 index.html create mode 100644 logo-white.svg create mode 100644 page/about-cask.html create mode 100644 page/main-customization.html diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..82430a7 Binary files /dev/null and b/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..501a8db --- /dev/null +++ b/index.html @@ -0,0 +1,1078 @@ +Cask: a Scala HTTP micro-framework

Cask: a Scala HTTP micro-framework

Main Customization

Build Status
Gitter Chat
Patreon

+
package app
+object MinimalApplication extends cask.MainRoutes{
+  @cask.get("/")
+  def hello() = {
+    "Hello World!"
+  }
+
+  @cask.post("/do-thing")
+  def doThing(request: cask.Request) = {
+    new String(request.readAllBytes()).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.3.6"
+
+// SBT
+"com.lihaoyi" %% "cask" % "0.3.6"
+
+

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) = {
+    new String(request.readAllBytes()).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 val log: cask.Logger) extends cask.Routes{
+  @cask.get("/")
+  def hello() = {
+    "Hello World!"
+  }
+
+  @cask.post("/do-thing")
+  def doThing(request: cask.Request) = {
+    new String(request.readAllBytes()).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")
+  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(request: cask.Request) = {
+    s"Subpath ${request.remainingPathSegments}"
+  }
+
+  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&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.

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"
+  }
+
+  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.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 @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.7.0" 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
+  }
+
+  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 val 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 val 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 val 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.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.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.

+

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 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.RawDecorator{
+    class TransactionFailed(val value: cask.router.Result.Error) extends Exception
+    def wrapFunction(pctx: cask.Request, delegate: Delegate) = {
+      try ctx.transaction(
+        delegate(Map()) match{
+          case cask.router.Result.Success(t) => cask.router.Result.Success(t)
+          case e: cask.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.readAllBytes())
+    run(
+      query[Todo]
+        .insert(_.checked -> lift(false), _.text -> lift(body))
+        .returningGenerated(_.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 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. 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 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.

+
package app
+import com.typesafe.config.ConfigFactory
+import io.getquill.{SnakeCase, SqliteJdbcContext}
+import scalatags.Text.all._
+import scalatags.Text.tags2
+
+object TodoServer 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.RawDecorator{
+    class TransactionFailed(val value: cask.router.Result.Error) extends Exception
+    def wrapFunction(pctx: cask.Request, delegate: Delegate) = {
+      try ctx.transaction(
+        delegate(Map()) match{
+          case cask.router.Result.Success(t) => cask.router.Result.Success(t)
+          case e: cask.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.readAllBytes())
+    run(
+      query[Todo]
+        .insert(_.checked -> lift(false), _.text -> lift(body))
+        .returningGenerated(_.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.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
\ No newline at end of file diff --git a/logo-white.svg b/logo-white.svg new file mode 100644 index 0000000..a681aa9 --- /dev/null +++ b/logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/page/about-cask.html b/page/about-cask.html new file mode 100644 index 0000000..dc97d8f --- /dev/null +++ b/page/about-cask.html @@ -0,0 +1,154 @@ +About Cask

About Cask

Main Customization

Functions First

+

Inspired by Flask, Cask allows you to define your web applications endpoints using simple function defs that you already know and love, annotated with the minimal additional metadata necessary to work as HTTP endpoints.

+

It turns out that function defs already provide almost everything you need in a HTTP endpoint:

+ +

Cask extends these basics with annotations, providing:

+ +

While these annotations add a bit of complexity, they allow Cask to avoid needing custom DSLs for defining your HTTP routes, custom action-types, and many other things which you may be used to working with HTTP in Scala.

Extensible Annotations

+

Unlike most other annotation-based frameworks in Scala or Java, Cask's annotations are not magic markers, but self-contained classes containing all the logic they need to function. This has several benefits:

+ +

Overall, Cask annotations behave a lot more like Python decorators than "traditional" Java/Scala annotations: first-class, customizable, inspectable, and self-contained. This allows Cask to have the syntactic convenience of an annotation-based API, without the typical downsides of inflexibility and undiscoverability.

Simple First

+

Cask intentionally eskews many things that other, more enterprise-grade frameworks provide:

+ +

While these features all are valuable in specific cases, Cask aims for the 99% of code for which simple, boring code is perfectly fine. Cask's endpoints are synchronous by default, do not tie you to any underlying concurrency model, and should "just work" without any advanced knowledge apart from basic Scala and HTTP. Cask's websockets API is intentionally low-level, making it both simple to use and also simple to build on top of if you want to wrap it in your own concurrency-library-of-choice.

Thin Wrapper

+

Cask is implemented as a thin wrapper around the excellent Undertow HTTP server. If you need more advanced functionality, Cask lets you ask for the exchange: +HttpServerExchange in your endpoint, override defaultHandler and add your own Undertow handlers next to Cask's and avoid Cask's routing/endpoint system altogether, or override main if you want to change how the server is initialized.

+

Rather than trying to provide APIs for all conceivable functionality, Cask simply provides what it does best - simple routing for simple endpoints - and leaves the door wide open in case you need to drop down to the lower level Undertow APIs.

Community Libraries

+

Cask aims to re-use much of the excellent code that is already written and being used out in the Scala community, rather than trying to re-invent the wheel. Cask uses the Mill build tool, comes bundled with the uPickle JSON library, and makes it trivial to pull in libraries like Scalatags to render HTML or Quill for database access.

+

Each of these are stable, well-known, well-documented libraries you may already be familiar with, and Cask simply provides the HTTP/routing layer with the hooks necessary to tie everything together (e.g. into a TodoMVC webapp)


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
\ No newline at end of file diff --git a/page/main-customization.html b/page/main-customization.html new file mode 100644 index 0000000..f27fe7b --- /dev/null +++ b/page/main-customization.html @@ -0,0 +1,125 @@ +Main Customization

Main Customization

Cask: a Scala HTTP micro-frameworkAbout Cask

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 log

+

A logger that gets passed around the application. Used for convenient debug logging, as well as logging exceptions either to the terminal or to a centralized exception handler.

def defaultHandler

+

Cask is built on top of the Undertow 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 for customizing your webserver. This allows for things that Cask itself doesn't internally support.

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.Decorators that you want to apply to all routes and all endpoints in the entire web application. Useful for inserting application-wide instrumentation, logging, security-checks, and similar things.


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


Cask: a Scala HTTP micro-frameworkAbout Cask
\ No newline at end of file -- cgit v1.2.3