summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-08-12 22:42:47 +0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-08-12 22:55:53 +0800
commitc0f39d743fbdf07544a6f5b6284d7123e5c36296 (patch)
tree64470286acb80c61e711299eded0b67fd516a8b8
parentfd9c399db8c1c0d86cc65d5e1c41968b42a813d1 (diff)
downloadcask-c0f39d743fbdf07544a6f5b6284d7123e5c36296.tar.gz
cask-c0f39d743fbdf07544a6f5b6284d7123e5c36296.tar.bz2
cask-c0f39d743fbdf07544a6f5b6284d7123e5c36296.zip
auto publishing of docs w/ example downloads works
-rw-r--r--build.sc41
-rw-r--r--docs/build.sc198
-rw-r--r--docs/favicon.pngbin0 -> 206 bytes
-rw-r--r--docs/logo-white.svg1
-rw-r--r--docs/pageStyles.sc130
-rw-r--r--docs/pages.sc192
-rw-r--r--docs/pages/1 - Cask: a Scala HTTP micro-framework .md (renamed from docs/readme.md)91
-rw-r--r--readme.md872
-rw-r--r--version.sc37
9 files changed, 624 insertions, 938 deletions
diff --git a/build.sc b/build.sc
index f1fa42f..9114815 100644
--- a/build.sc
+++ b/build.sc
@@ -1,6 +1,6 @@
import mill._, scalalib._
import ammonite.ops._, ujson.Js
-import $file.upload
+import $file.upload, $file.version
import $file.example.compress.build
import $file.example.compress2.build
import $file.example.compress3.build
@@ -21,7 +21,7 @@ import $file.example.variableRoutes.build
object cask extends ScalaModule{
def scalaVersion = "2.12.6"
def ivyDeps = Agg(
- ivy"org.scala-lang:scala-reflect:$scalaVersion",
+ ivy"org.scala-lang:scala-reflect:${scalaVersion()}",
ivy"io.undertow:undertow-core:2.0.11.Final",
ivy"com.lihaoyi::upickle:0.6.6",
ivy"com.lihaoyi::scalatags:0.6.7",
@@ -68,41 +68,8 @@ object example extends Module{
object variableRoutes extends $file.example.variableRoutes.build.AppModule with LocalModule
}
-val isMasterCommit = {
- sys.env.get("TRAVIS_PULL_REQUEST") == Some("false") &&
- (sys.env.get("TRAVIS_BRANCH") == Some("master") || sys.env("TRAVIS_TAG") != "")
-}
-
-def gitHead = T.input{
- sys.env.get("TRAVIS_COMMIT").getOrElse(
- %%('git, "rev-parse", "HEAD")(pwd).out.string.trim()
- )
-}
-
-
-def publishVersion = T.input{
- val tag =
- try Option(
- %%('git, 'describe, "--exact-match", "--tags", "--always", gitHead())(pwd).out.string.trim()
- )
- catch{case e => None}
-
- val dirtySuffix = %%('git, 'diff)(pwd).out.string.trim() match{
- case "" => ""
- case s => "-DIRTY" + Integer.toHexString(s.hashCode)
- }
-
- tag match{
- case Some(t) => (t, t)
- case None =>
- val latestTaggedVersion = %%('git, 'describe, "--abbrev=0", "--always", "--tags")(pwd).out.trim
-
- val commitsSinceLastTag =
- %%('git, "rev-list", gitHead(), "--not", latestTaggedVersion, "--count")(pwd).out.trim.toInt
-
- (latestTaggedVersion, s"$latestTaggedVersion-$commitsSinceLastTag-${gitHead().take(6)}$dirtySuffix")
- }
-}
+def publishVersion = T.input($file.version.publishVersion)
+def gitHead = T.input($file.version.gitHead)
def uploadToGithub(authKey: String) = T.command{
val (releaseTag, label) = publishVersion()
diff --git a/docs/build.sc b/docs/build.sc
new file mode 100644
index 0000000..48fd3e7
--- /dev/null
+++ b/docs/build.sc
@@ -0,0 +1,198 @@
+// Load dependencies
+import $ivy.{`org.pegdown:pegdown:1.6.0`, `com.lihaoyi::scalatags:0.6.5`}
+import $file.pageStyles, pageStyles._
+import $file.pages, pages._
+import scalatags.Text.all._
+import $file.^.version
+import ammonite.ops._
+import collection.JavaConverters._
+import org.pegdown.{PegDownProcessor, ToHtmlSerializer, LinkRenderer, Extensions}
+import org.pegdown.ast.{VerbatimNode, ExpImageNode, HeaderNode, TextNode, SimpleNode, TableNode}
+
+val (releaseTag, label) = version.publishVersion
+
+val postsFolder = cwd/'pages
+
+interp.watch(postsFolder)
+
+val targetFolder = cwd/'target
+
+
+val (markdownFiles, otherFiles) = ls! postsFolder partition (_.ext == "md")
+markdownFiles.foreach(println)
+// Walk the posts/ folder and parse out the name, full- and first-paragraph-
+// HTML of each post to be used on their respective pages and on the index
+
+val posts = {
+ val split = for(path <- markdownFiles) yield {
+ val Array(number, name) = path.last.split(" - ", 2)
+ (number, name.stripSuffix(".md"), path)
+ }
+ for ((index, name, path) <- split.sortBy(_._1.toInt)) yield {
+ val processor = new PegDownProcessor(
+ 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 ast = processor.parseMarkdown(txt.toArray)
+ val headers = collection.mutable.Buffer.empty[(String, Int)]
+ class Serializer extends ToHtmlSerializer(new LinkRenderer){
+ override def printImageTag(rendering: LinkRenderer.Rendering) {
+ printer.print("<div style=\"text-align: center\"><img")
+ printAttribute("src", rendering.href)
+ // shouldn't include the alt attribute if its empty
+ if(!rendering.text.equals("")){
+ printAttribute("alt", rendering.text)
+ }
+ import collection.JavaConversions._
+ for (attr <- rendering.attributes) {
+ printAttribute(attr.name, attr.value)
+ }
+ printer.print(" style=\"max-width: 100%; max-height: 500px\"")
+ printer.print(" /></div>")
+ }
+ override def visit(node: HeaderNode) = {
+ val tag = "h" + node.getLevel()
+
+
+ val id =
+ node
+ .getChildren
+ .asScala
+ .collect{case t: TextNode => t.getText}
+ .mkString
+
+ headers.append(id -> node.getLevel())
+
+
+ val setId = s"id=${'"'+sanitize(id)+'"'}"
+ printer.print(s"""<$tag $setId class="${Styles.hoverBox.name}">""")
+ visitChildren(node)
+ printer.print(
+ a(href := ("#" + sanitize(id)), Styles.hoverLink)(
+ i(cls := "fa fa-link", aria.hidden := true)
+ ).render
+ )
+ printer.print(s"</$tag>")
+ }
+
+ override def visit(node: VerbatimNode) = {
+ printer.println().print(
+ """<pre style="background-color: #f8f8f8">""" +
+ s"""<code style="white-space:pre; background-color: #f8f8f8" class="${node.getType()}">"""
+ )
+
+ var text = node.getText()
+ // print HTML breaks for all initial newlines
+ while(text.charAt(0) == '\n') {
+ printer.print("\n")
+ text = text.substring(1)
+ }
+ printer.printEncoded(text)
+ printer.print("</code></pre>")
+ }
+ override def visit(node: TableNode) = {
+ currentTableNode = node
+ printer.print("<table class=\"table table-bordered\">")
+ visitChildren(node)
+ printer.print("</table>")
+ currentTableNode = null
+ }
+ }
+
+ val postlude = Seq[Frag](
+ hr,
+
+ p(
+ b("About the Author:"),
+ i(
+ " Haoyi is a software engineer, an early contributor to ",
+ a(href:="http://www.scala-js.org/")("Scala.js"),
+ ", and the author of many open-source Scala tools such as Mill, the ",
+ a(href:="lihaoyi.com/Ammonite", "Ammonite REPL"), " and ",
+ a(href:="https://github.com/lihaoyi/fastparse", "FastParse"), ". "
+ )
+ ),
+ p(
+ i(
+ "If you've enjoy using Mill, or enjoyed using Haoyi's other open ",
+ "source libraries, please chip in (or get your Company to chip in!) via ",
+ a(href:="https://www.patreon.com/lihaoyi", "Patreon"), " so he can ", "continue his open-source work"
+ )
+ ),
+ hr
+ ).render
+
+ val rawHtmlContent = new Serializer().toHtml(ast) + postlude
+ PostInfo(name, headers, rawHtmlContent)
+ }
+}
+
+def formatRssDate(date: java.time.LocalDate) = {
+ date
+ .atTime(0, 0)
+ .atZone(java.time.ZoneId.of("UTC"))
+ .format(java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME)
+}
+
+@main
+def main(publish: Boolean = false) = {
+
+ rm! targetFolder
+
+ mkdir! targetFolder/'page
+ for(otherFile <- otherFiles){
+ cp(otherFile, targetFolder/'page/(otherFile relativeTo postsFolder))
+ }
+
+ cp(pwd/"favicon.png", targetFolder/"favicon.ico")
+ cp(pwd/"logo-white.svg", targetFolder/"logo-white.svg")
+
+ for(i <- posts.indices){
+ val post = posts(i)
+
+ val adjacentLinks = div(display.flex, flexDirection.row, justifyContent.spaceBetween)(
+ for((j, isNext) <- Seq(i-1 -> false, i+1 -> true))
+ yield posts.lift(j) match{
+ case None => div()
+ case Some(dest) =>
+ renderAdjacentLink(
+ isNext,
+ dest.name,
+ (i == 0, j == 0) match {
+ case (true, true) => s"index.html"
+ case (true, false) => s"page/${sanitize(dest.name)}.html"
+ case (false, true) => s"../index.html"
+ case (false, false) => s"${sanitize(dest.name)}.html"
+ }
+ )
+ }
+ )
+
+
+ write(
+ if (i == 0) targetFolder / "index.html"
+ else targetFolder/'page/s"${sanitize(post.name)}.html",
+ postContent(
+ i == 0,
+ post,
+ adjacentLinks,
+ posts.map(_.name)
+ )
+ )
+ }
+
+ if (publish){
+ implicit val wd = cwd/'target
+ %git 'init
+ %git('add, "-A", ".")
+ %git('commit, "-am", "first commit")
+ %git('remote, 'add, 'origin, "git@github.com:lihaoyi/cask.git")
+ %git('push, "-uf", 'origin, "master:gh-pages")
+ }
+} \ No newline at end of file
diff --git a/docs/favicon.png b/docs/favicon.png
new file mode 100644
index 0000000..82430a7
--- /dev/null
+++ b/docs/favicon.png
Binary files differ
diff --git a/docs/logo-white.svg b/docs/logo-white.svg
new file mode 100644
index 0000000..a681aa9
--- /dev/null
+++ b/docs/logo-white.svg
@@ -0,0 +1 @@
+<!DOCTYPE html><svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><polyline points="24,-44.0 0,4.0 0,20.0 24,-28.0" fill="#f8f8f8"></polyline><polyline points="24,-20.0 0,28.0 0,44.0 24,-4.0" fill="#f8f8f8"></polyline><polyline points="24,4.0 0,52.0 0,68.0 24,20.0" fill="#f8f8f8"></polyline></svg> \ No newline at end of file
diff --git a/docs/pageStyles.sc b/docs/pageStyles.sc
new file mode 100644
index 0000000..06651b1
--- /dev/null
+++ b/docs/pageStyles.sc
@@ -0,0 +1,130 @@
+import $ivy.`com.lihaoyi::scalatags:0.6.5`
+
+import scalatags.stylesheet._
+import scalatags.Text.all._
+
+
+val marginWidth = "25%"
+object WideStyles extends StyleSheet{
+ initStyleSheet()
+ override def customSheetName = Some("WideStyles")
+ def header = cls(
+ position.fixed,
+ top := 0,
+ bottom := 0,
+ width := marginWidth,
+ justifyContent.center,
+ display.flex,
+ flexDirection.column
+ )
+ def tableOfContentsItem = cls(
+ // We have to use inline-block and verticalAlign.middle and width: 100%
+ // here, instead of simply using display.block, because display.block items
+ // with overflow.hidden seem to misbehave and render badly in different ways
+ // between firefox (renders correctly), chrome (body of list item is offset
+ // one row from the bullet) and safari (bullet is entirely missing)
+ display.`inline-block`,
+ width := "100%",
+ verticalAlign.middle,
+ overflow.hidden,
+ textOverflow.ellipsis
+
+ )
+ def tableOfContents = cls(
+ display.flex,
+ flexDirection.column,
+ flexGrow := 1,
+ flexShrink := 1,
+ minHeight := 0,
+ width := "100%"
+
+ )
+ def content = cls(
+ padding := "2em 3em 0",
+ padding := 48,
+ marginLeft := marginWidth,
+ boxSizing.`border-box`
+ )
+ def footer = cls(
+ position.fixed,
+ bottom := 0,
+ height := 50,
+ width := marginWidth
+ )
+ def marginLeftZero = cls(
+ marginLeft := 0
+ )
+}
+object NarrowStyles extends StyleSheet{
+ initStyleSheet()
+ override def customSheetName = Some("NarrowStyles")
+ def header = cls(
+ marginBottom := 10
+ )
+ def content = cls(
+ padding := 16
+ )
+ def headerContent = cls(
+ flexDirection.row,
+ width := "100%",
+ display.flex,
+ alignItems.center
+ )
+
+ def flexFont = cls(
+ fontSize := "4vw"
+ )
+ def disappear = cls(
+ display.none
+ )
+ def floatLeft = cls(
+ float.left,
+ marginLeft := 30
+ )
+}
+object Styles extends CascadingStyleSheet{
+ initStyleSheet()
+ override def customSheetName = Some("Styles")
+ def hoverBox = cls(
+ display.flex,
+ flexDirection.row,
+ alignItems.center,
+ justifyContent.spaceBetween,
+ &hover(
+ hoverLink(
+ opacity := 0.5
+ )
+ )
+ )
+ def hoverLink = cls(
+ opacity := 0.1,
+ &hover(
+ opacity := 1.0
+ )
+ )
+ def headerStyle = cls(
+ backgroundColor := "rgb(61, 79, 93)",
+ display.flex,
+ boxSizing.`border-box`
+ )
+ def headerLinkBox = cls(
+ flex := 1,
+ display.flex,
+ flexDirection.column,
+ )
+ def headerLink = cls(
+ flex := 1,
+ display.flex,
+ justifyContent.center,
+ alignItems.center,
+ padding := "10px 10px"
+ )
+ def footerStyle = cls(
+ display.flex,
+ justifyContent.center,
+ color := "rgb(158, 167, 174)"
+ )
+ def subtleLink = cls(
+ textDecoration.none
+ )
+} \ No newline at end of file
diff --git a/docs/pages.sc b/docs/pages.sc
new file mode 100644
index 0000000..a6c05c0
--- /dev/null
+++ b/docs/pages.sc
@@ -0,0 +1,192 @@
+import $ivy.`com.lihaoyi::scalatags:0.6.5`
+import scalatags.Text.all._, scalatags.Text.tags2
+import java.time.LocalDate
+import $file.pageStyles, pageStyles._
+
+case class PostInfo(name: String,
+ headers: Seq[(String, Int)],
+ rawHtmlContent: String)
+
+
+def sanitize(s: String): String = {
+ s.split(" |-", -1).map(_.filter(_.isLetterOrDigit)).mkString("-").toLowerCase
+}
+def pageChrome(titleText: Option[String],
+ homePage: Boolean,
+ contents: Frag,
+ contentHeaders: Seq[(String, Int)],
+ pageHeaders: Seq[String]): String = {
+ val pageTitle = titleText.getOrElse("Mill")
+ val sheets = Seq(
+ "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css",
+ "https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css",
+ "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.1.0/styles/github-gist.min.css"
+ )
+
+
+ html(
+ head(
+ meta(charset := "utf-8"),
+ for(sheet <- sheets)
+ yield link(href := sheet, rel := "stylesheet", `type` := "text/css" ),
+ tags2.title(pageTitle),
+ tags2.style(s"@media (min-width: 60em) {${WideStyles.styleSheetText}}"),
+ tags2.style(s"@media (max-width: 60em) {${NarrowStyles.styleSheetText}}"),
+ tags2.style(Styles.styleSheetText),
+ script(src:="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.1.0/highlight.min.js"),
+ script(src:="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.1.0/languages/scala.min.js"),
+ script(raw("hljs.initHighlightingOnLoad();")),
+ // This makes media queries work on iphone (???)
+ // http://stackoverflow.com/questions/13002731/responsive-design-media-query-not-working-on-iphone
+ meta(name:="viewport", content:="initial-scale = 1.0,maximum-scale = 1.0")
+ ),
+ body(margin := 0, backgroundColor := "#f8f8f8")(
+ navBar(homePage, contentHeaders, pageHeaders),
+ div(
+ WideStyles.content,
+ NarrowStyles.content,
+ maxWidth := 900,
+ titleText.map(h1(_)),
+ contents
+ ),
+ if (contentHeaders.nonEmpty) frag()
+ else div(
+ WideStyles.footer,
+ Styles.footerStyle,
+ "Last published ", currentTimeText
+ )
+
+ )
+ ).render
+}
+
+def navBar(homePage: Boolean, contentHeaders: Seq[(String, Int)], pageHeaders: Seq[String]) = {
+ def navList(navLabel: String, frags: Frag, narrowHide: Boolean) = {
+ div(
+ WideStyles.tableOfContents,
+ if(narrowHide) NarrowStyles.disappear else frag(),
+ color := "#f8f8f8"
+ )(
+ div(paddingLeft := 40, NarrowStyles.disappear)(
+ b(navLabel)
+ ),
+ div(overflowY.auto, flexShrink := 1, minHeight := 0)(
+ ul(
+ overflow.hidden,
+ textAlign.start,
+ marginTop := 10,
+ whiteSpace.nowrap,
+ textOverflow.ellipsis,
+ marginRight := 10
+ )(
+ frags
+ )
+ )
+ )
+ }
+ val pageList = navList(
+ "Pages",
+ for((header, i) <- pageHeaders.zipWithIndex) yield li(
+ WideStyles.marginLeftZero,
+ NarrowStyles.floatLeft
+ )(
+ a(
+ color := "#f8f8f8",
+ WideStyles.tableOfContentsItem,
+ href := {
+ (homePage, i == 0) match {
+ case (true, true) => s"index.html"
+ case (true, false) => s"page/${sanitize(header)}.html"
+ case (false, true) => s"../index.html"
+ case (false, false) => s"${sanitize(header)}.html"
+ }
+ }
+ )(
+ header
+ )
+ ),
+ narrowHide = false
+ )
+
+ val headerBox = div(
+ NarrowStyles.headerContent,
+ h1(
+ textAlign.center,
+ a(
+ img(
+ src := {homePage match{
+ case false => s"../logo-white.svg"
+ case true => "logo-white.svg"
+ }},
+ height := 30,
+ marginTop := -5
+ ),
+ color := "#f8f8f8",
+ " Mill",
+ href := (if (homePage) "" else ".."),
+ Styles.subtleLink,
+ NarrowStyles.flexFont,
+ fontWeight.bold
+ ),
+ padding := "30px 30px",
+ margin := 0
+ ),
+ div(
+ Styles.headerLinkBox,
+ pageList
+ )
+ )
+
+
+ val tableOfContents = navList(
+ "Table of Contents",
+ for {
+ (header, indent) <- contentHeaders
+ offset <- indent match{
+ case 2 => Some(0)
+ case 3 => Some(20)
+ case _ => None
+ }
+ } yield li(marginLeft := offset)(
+ a(
+ color := "#f8f8f8",
+ WideStyles.tableOfContentsItem,
+ href := s"#${sanitize(header)}"
+ )(
+ header
+ )
+ ),
+ narrowHide = true
+ )
+
+ div(
+ WideStyles.header,
+ NarrowStyles.header,
+ Styles.headerStyle,
+ headerBox,
+ hr(NarrowStyles.disappear, backgroundColor := "#f8f8f8", width := "80%"),
+ tableOfContents
+ )
+}
+
+
+val currentTimeText = LocalDate.now.toString
+
+
+def renderAdjacentLink(next: Boolean, name: String, url: String) = {
+ a(href := url)(
+ if(next) frag(name, " ", i(cls:="fa fa-arrow-right" , aria.hidden:=true))
+ else frag(i(cls:="fa fa-arrow-left" , aria.hidden:=true), " ", name)
+ )
+}
+def postContent(homePage: Boolean, post: PostInfo, adjacentLinks: Frag, posts: Seq[String]) = pageChrome(
+ Some(post.name),
+ homePage,
+ Seq[Frag](
+ div(adjacentLinks, marginBottom := 10),
+ raw(post.rawHtmlContent),
+ adjacentLinks
+ ),
+ post.headers,
+ pageHeaders = posts
+) \ No newline at end of file
diff --git a/docs/readme.md b/docs/pages/1 - Cask: a Scala HTTP micro-framework .md
index 4add926..22d879e 100644
--- a/docs/readme.md
+++ b/docs/pages/1 - Cask: a Scala HTTP micro-framework .md
@@ -1,6 +1,3 @@
-Cask: a Scala HTTP micro-framework
-==================================
-
```scala
object MinimalApplication extends cask.MainRoutes{
@cask.get("/")
@@ -17,10 +14,10 @@ object MinimalApplication extends cask.MainRoutes{
}
```
-Cask is a simple Scala web framework inspired by Python's
-[Flask](http://flask.pocoo.org/docs/1.0/) project. It aims to bring simplicity,
-flexibility and ease-of-use to Scala webservers, avoiding cryptic DSLs or
-complicated asynchrony.
+[Cask](https://github.com/lihaoyi/cask) is a simple Scala web framework inspired
+by Python's [Flask](http://flask.pocoo.org/docs/1.0/) project. It aims to bring
+simplicity, flexibility and ease-of-use to Scala webservers, avoiding cryptic
+DSLs or complicated asynchrony.
Getting Started
---------------
@@ -29,7 +26,8 @@ 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:
+- Unzip the $$$minimalApplication into a folder. This should
+ give you the following files:
```text
build.sc
app/src/MinimalExample.scala
@@ -80,8 +78,11 @@ ivy"com.lihaoyi::cask:0.1.0"
"com.lihaoyi" %% "cask" % "0.1.0"
```
-Minimal Example
----------------
+Example Projects
+----------------
+
+### Minimal Example
+
```scala
object MinimalApplication extends cask.MainRoutes{
@cask.get("/")
@@ -98,6 +99,8 @@ object MinimalApplication extends cask.MainRoutes{
}
```
+- $$$minimalApplication
+
The rough outline of how the minimal example works should be easy to understand:
- You define an object that inherits from `cask.MainRoutes`
@@ -142,11 +145,13 @@ object MinimalRoutes extends cask.Routes{
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
----------------
+### Variable Routes
+
```scala
object VariableRoutes extends cask.MainRoutes{
@@ -169,6 +174,8 @@ object VariableRoutes extends cask.MainRoutes{
}
```
+- $$$variableRoutes
+
You can bind variables to endpoints by declaring them as parameters: these are
either taken from a path-segment matcher of the same name (e.g. `postId` above),
or from query-parameters of the same name (e.g. `param` above). You can make
@@ -181,8 +188,8 @@ If you need to capture the entire sub-path of the request, you can set the flag
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
------------------------------------
+### Receiving Form-encoded or JSON data
+
```scala
object FormJsonPost extends cask.MainRoutes{
@@ -205,6 +212,8 @@ object FormJsonPost extends cask.MainRoutes{
}
```
+- $$$formJsonPost
+
If you need to handle a JSON-encoded POST request, you can use the
`@cast.postJson` decorator. This assumes the posted request body is a JSON dict,
and uses its keys to populate the endpoint's parameters, either as raw
@@ -226,8 +235,8 @@ deserialization into Scala data-types fails, a 400 response is returned
automatically with a helpful error message.
-Processing Cookies
-------------------
+### Processing Cookies
+
```scala
object Cookies extends cask.MainRoutes{
@@ -256,14 +265,16 @@ object Cookies extends cask.MainRoutes{
}
```
+- $$$cookies
+
Cookies are most easily read by declaring a `: cask.Cookie` parameter; the
parameter name is used to fetch the cookie you are interested in. Cookies can be
stored by setting the `cookie` attribute in the response, and deleted simply by
setting `expires = java.time.Instant.EPOCH` (i.e. to have expired a long time
ago)
-Serving Static Files
---------------------
+### Serving Static Files
+
```scala
object StaticFiles extends cask.MainRoutes{
@cask.get("/")
@@ -278,13 +289,15 @@ object StaticFiles extends cask.MainRoutes{
}
```
+- $$$staticFiles
+
You can ask Cask to serve static files by defining a `@cask.static` endpoint.
This will match any subpath of the value returned by the endpoint (e.g. above
`/static/file.txt`, `/static/folder/file.txt`, etc.) and return the file
contents from the corresponding file on disk (and 404 otherwise).
-Redirects or Aborts
--------------------
+### Redirects or Aborts
+
```scala
object RedirectAbort extends cask.MainRoutes{
@cask.get("/")
@@ -301,12 +314,14 @@ object RedirectAbort extends cask.MainRoutes{
}
```
+- $$$redirectAbort
+
Cask provides some convenient helpers `cask.Redirect` and `cask.Abort` which you
can return; these are simple wrappers around `cask.Request`, and simply set up
the relevant headers or status code for you.
-Extending Endpoints with Decorators
------------------------------------
+### Extending Endpoints with Decorators
+
```scala
object Decorated extends cask.MainRoutes{
@@ -354,6 +369,8 @@ object Decorated extends cask.MainRoutes{
}
```
+- $$$decorateds
+
You can write extra decorator annotations that stack on top of the existing
`@cask.get`/`@cask.post` to provide additional arguments or validation. This is
done by implementing the `cask.Decorator` interface and it's `getRawParams`
@@ -430,12 +447,14 @@ object Decorated2 extends cask.MainRoutes{
}
```
+- $$$decorated2
+
This is convenient for cases where you want a set of decorators to apply broadly
across your web application, and do not want to repeat them over and over at
every single endpoint.
-Gzip & Deflated Responses
--------------------------
+### Gzip & Deflated Responses
+
```scala
object Compress extends cask.MainRoutes{
@@ -451,6 +470,8 @@ object Compress extends cask.MainRoutes{
```
+- $$$compress
+
Cask provides a useful `@cask.decorators.compress` decorator that gzips or
deflates a response body if possible. This is useful if you don't have a proxy
like Nginx or similar in front of your server to perform the compression for
@@ -474,6 +495,8 @@ object Compress2 extends cask.Routes{
object Compress2Main extends cask.Main(Compress2)
```
+- $$$compress2
+
Or globally, in your `cask.Main`:
```scala
@@ -492,8 +515,10 @@ object Compress3Main extends cask.Main(Compress3){
}
```
-TodoMVC Api Server
-------------------
+- $$$compress3
+
+### TodoMVC Api Server
+
```scala
object TodoMvcApi extends cask.MainRoutes{
@@ -535,6 +560,8 @@ object TodoMvcApi extends cask.MainRoutes{
}
```
+- $$$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/).
@@ -543,8 +570,8 @@ etc.. Those can be managed via the normal mechanism for
[Serving Static Files](#serving-static-files).
-TodoMVC Database Integration
-----------------------------
+### TodoMVC Database Integration
+
```scala
import cask.internal.Router
import com.typesafe.config.ConfigFactory
@@ -631,6 +658,8 @@ object TodoMvcDb extends cask.MainRoutes{
```
+- $$$todoDb
+
This example demonstrates how to use Cask to write a TodoMVC API server that
persists it's state in a database rather than in memory. We use the
[Quill](http://getquill.io/) database access library to write a `@transactional`
@@ -648,8 +677,8 @@ 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
-----------------------
+### 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
@@ -845,6 +874,8 @@ object Server extends cask.MainRoutes{
}
```
+- $$$todo
+
Main Customization
------------------
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&param=world`, or
-`: Option[T]` for cases where the `?param=hello` is optional.
-
-If you need to capture the entire sub-path of the request, you can set the flag
-`subpath=true` and ask for a `: cask.Subpath` (the name of the param doesn't
-matter). This will make the route match any sub-path of the prefix given to the
-`@cask` decorator, and give you the remainder to use in your endpoint logic.
-
-Receiving Form-encoded or JSON data
------------------------------------
-
-```scala
-object FormJsonPost extends cask.MainRoutes{
- @cask.postJson("/json")
- def jsonEndpoint(value1: ujson.Js.Value, value2: Seq[Int]) = {
- "OK " + value1 + " " + value2
- }
-
- @cask.postForm("/form")
- def formEndpoint(value1: cask.FormValue, value2: Seq[Int]) = {
- "OK " + value1 + " " + value2
- }
-
- @cask.postForm("/upload")
- def uploadFile(image: cask.FormFile) = {
- image.fileName
- }
-
- initialize()
-}
-```
-
-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(
- "<!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()
-}
-```
-
-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
diff --git a/version.sc b/version.sc
new file mode 100644
index 0000000..4e73530
--- /dev/null
+++ b/version.sc
@@ -0,0 +1,37 @@
+import ammonite.ops.{%%, pwd}
+
+val isMasterCommit = {
+ sys.env.get("TRAVIS_PULL_REQUEST") == Some("false") &&
+ (sys.env.get("TRAVIS_BRANCH") == Some("master") || sys.env("TRAVIS_TAG") != "")
+}
+
+def gitHead =
+ sys.env.get("TRAVIS_COMMIT").getOrElse(
+ %%('git, "rev-parse", "HEAD")(pwd).out.string.trim()
+ )
+
+
+
+def publishVersion = {
+ val tag =
+ try Option(
+ %%('git, 'describe, "--exact-match", "--tags", "--always", gitHead)(pwd).out.string.trim()
+ )
+ catch{case e => None}
+
+ val dirtySuffix = %%('git, 'diff)(pwd).out.string.trim() match{
+ case "" => ""
+ case s => "-DIRTY" + Integer.toHexString(s.hashCode)
+ }
+
+ tag match{
+ case Some(t) => (t, t)
+ case None =>
+ val latestTaggedVersion = %%('git, 'describe, "--abbrev=0", "--always", "--tags")(pwd).out.trim
+
+ val commitsSinceLastTag =
+ %%('git, "rev-list", gitHead, "--not", latestTaggedVersion, "--count")(pwd).out.trim.toInt
+
+ (latestTaggedVersion, s"$latestTaggedVersion-$commitsSinceLastTag-${gitHead.take(6)}$dirtySuffix")
+ }
+}