summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-08-12 23:29:31 +0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-08-12 23:29:31 +0800
commita7cec9465105a8b983bac7ac721827f578f35166 (patch)
tree17da60565b02a51152862827136c5eb29087f2fd /docs
parent11f8fa2faaef68684a91716f6331a4e6e1463b3b (diff)
downloadcask-a7cec9465105a8b983bac7ac721827f578f35166.tar.gz
cask-a7cec9465105a8b983bac7ac721827f578f35166.tar.bz2
cask-a7cec9465105a8b983bac7ac721827f578f35166.zip
tweak docgen
Diffstat (limited to 'docs')
-rw-r--r--docs/build.sc20
-rw-r--r--docs/pages/1 - Cask: a Scala HTTP micro-framework .md625
2 files changed, 36 insertions, 609 deletions
diff --git a/docs/build.sc b/docs/build.sc
index 48fd3e7..28598dc 100644
--- a/docs/build.sc
+++ b/docs/build.sc
@@ -33,10 +33,22 @@ val posts = {
Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.AUTOLINKS
)
- val txt = read(path)
- .replaceAll(
- """\$\$\$([a-zA-Z_0-9]+)""",
- s"[example project](https://github.com/lihaoyi/cask/releases/download/$releaseTag/$label.$$1)"
+ val txt =
+ """\$\$\$([a-zA-Z_0-9]+)""".r.replaceAllIn(
+ read(path),
+ m => {
+ val g = m.group(1)
+ val txt = read(ls(pwd/up/'example/g/'app/'src).head).replace("$", "\\$")
+ val downloadLink =
+ s"https://github.com/lihaoyi/cask/releases/download/$releaseTag/$label.$g"
+
+ s"""
+ |```scala
+ |$txt
+ |```
+ |
+ |- [example project]($downloadLink)""".stripMargin
+ }
)
val ast = processor.parseMarkdown(txt.toArray)
diff --git a/docs/pages/1 - Cask: a Scala HTTP micro-framework .md b/docs/pages/1 - Cask: a Scala HTTP micro-framework .md
index 0b45154..f151612 100644
--- a/docs/pages/1 - Cask: a Scala HTTP micro-framework .md
+++ b/docs/pages/1 - Cask: a Scala HTTP micro-framework .md
@@ -1,18 +1,4 @@
-```scala
-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()
-}
-```
+$$$minimalApplication
[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
@@ -26,8 +12,10 @@ 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:
+
+- Unzip one of the example projects available on this page (e.g. above) into a
+ folder. This should give you the following files:
+
```text
build.sc
app/src/MinimalExample.scala
@@ -83,23 +71,7 @@ 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.readAllBytes()).reverse
- }
-
- initialize()
-}
-```
-
-- $$$minimalApplication
+$$$minimalApplication
The rough outline of how the minimal example works should be easy to understand:
@@ -127,54 +99,14 @@ 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.readAllBytes()).reverse
- }
+$$$minimalApplication2
- 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
+$$$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),
@@ -190,29 +122,7 @@ matter). This will make the route match any sub-path of the prefix given to the
### 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
+$$$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,
@@ -237,35 +147,7 @@ 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
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
@@ -275,21 +157,7 @@ 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
+$$$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
@@ -298,23 +166,7 @@ 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
+$$$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
@@ -323,53 +175,7 @@ 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
+$$$decorated
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
@@ -408,46 +214,7 @@ 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
+$$$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
@@ -456,21 +223,7 @@ 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
+$$$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
@@ -480,87 +233,16 @@ 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
+$$$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
+$$$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.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
+$$$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/).
@@ -572,93 +254,7 @@ etc.. Those can be managed via the normal mechanism for
### 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.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
+$$$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
@@ -693,188 +289,7 @@ 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.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
+$$$todo
Main Customization
------------------