From a7cec9465105a8b983bac7ac721827f578f35166 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sun, 12 Aug 2018 23:29:31 +0800 Subject: tweak docgen --- docs/build.sc | 20 +- .../1 - Cask: a Scala HTTP micro-framework .md | 625 +-------------------- 2 files changed, 36 insertions(+), 609 deletions(-) (limited to 'docs') 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( - "" + 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 ------------------ -- cgit v1.2.3