diff options
-rw-r--r-- | build.sc | 41 | ||||
-rw-r--r-- | docs/build.sc | 198 | ||||
-rw-r--r-- | docs/favicon.png | bin | 0 -> 206 bytes | |||
-rw-r--r-- | docs/logo-white.svg | 1 | ||||
-rw-r--r-- | docs/pageStyles.sc | 130 | ||||
-rw-r--r-- | docs/pages.sc | 192 | ||||
-rw-r--r-- | docs/pages/1 - Cask: a Scala HTTP micro-framework .md (renamed from docs/readme.md) | 91 | ||||
-rw-r--r-- | readme.md | 872 | ||||
-rw-r--r-- | version.sc | 37 |
9 files changed, 624 insertions, 938 deletions
@@ -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 Binary files differnew file mode 100644 index 0000000..82430a7 --- /dev/null +++ b/docs/favicon.png 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 ------------------ @@ -22,875 +22,5 @@ Cask is a simple Scala web framework inspired by Python's flexibility and ease-of-use to Scala webservers, avoiding cryptic DSLs or complicated asynchrony. -Getting Started ---------------- -The easiest way to begin using Cask is by downloading the -[Mill](http://www.lihaoyi.com/mill/) example project: - -- Install [Mill](http://www.lihaoyi.com/mill/) -- Unzip [XXX](XXX) into a folder. This should give you the following files: -```text -build.sc -app/src/MinimalExample.scala -app/test/src/ExampleTests.scala -``` - -- `cd` into the folder, and run - -```bash -mill -w app.runBackground -``` - -This will server up the Cask application on `http://localhost:8080`. You can -immediately start interacting with it either via the browser, or -programmatically via `curl` or a HTTP client like -[Requests-Scala](https://github.com/lihaoyi/requests-scala): - -```scala -val host = "http://localhost:8080" - -val success = requests.get(host) - -success.text() ==> "Hello World!" -success.statusCode ==> 200 - -requests.get(host + "/doesnt-exist").statusCode ==> 404 - -requests.post(host + "/do-thing", data = "hello").text() ==> "olleh" - -requests.get(host + "/do-thing").statusCode ==> 404 -``` - -These HTTP calls are part of the test suite for the example project, which you -can run using: - -```bash -mill -w app.test -``` - -Cask is just a Scala library, and you can use Cask in any existing Scala project -via the following coordinates: - -```scala -// Mill -ivy"com.lihaoyi::cask:0.1.0" - -// SBT -"com.lihaoyi" %% "cask" % "0.1.0" -``` - -Minimal Example ---------------- -```scala -object MinimalApplication extends cask.MainRoutes{ - @cask.get("/") - def hello() = { - "Hello World!" - } - - @cask.post("/do-thing") - def doThing(request: cask.Request) = { - new String(request.data.readAllBytes()).reverse - } - - initialize() -} -``` - -The rough outline of how the minimal example works should be easy to understand: - -- You define an object that inherits from `cask.MainRoutes` - -- Define endpoints using annotated functions, using `@cask.get` or `@cask.post` - with the route they should match - -- Each function can return the data you want in the response, or a - `cask.Response` if you want further customization: response code, headers, - etc. - -- Your function can tale an optional `cask.Request`, which exposes the entire - incoming HTTP request if necessary. In the above example, we use it to read - the request body into a string and return it reversed. - -In most cases, Cask provides convenient helpers to extract exactly the data from -the incoming HTTP request that you need, while also de-serializing it into the -data type you need and returning meaningful errors if they are missing. Thus, -although you can always get all the data necessary through `cask.Request`, it is -often more convenient to use another way, which will go into below. - -As your application grows, you will likely want to split up the routes into -separate files, themselves separate from any configuration of the Main -entrypoint (e.g. overriding the port, host, default error handlers, etc.). You -can do this by splitting it up into `cask.Routes` and `cask.Main` objects: - -```scala -object MinimalRoutes extends cask.Routes{ - @cask.get("/") - def hello() = { - "Hello World!" - } - - @cask.post("/do-thing") - def doThing(request: cask.Request) = { - new String(request.data.readAllBytes()).reverse - } - - initialize() -} - -object MinimalMain extends cask.Main(MinimalRoutes) -``` - -You can split up your routes into separate `cask.Routes` objects as makes sense -and pass them all into `cask.Main`. - -Variable Routes ---------------- - -```scala -object VariableRoutes extends cask.MainRoutes{ - @cask.get("/user/:userName") - def showUserProfile(userName: String) = { - s"User $userName" - } - - @cask.get("/post/:postId") - def showPost(postId: Int, param: Seq[String]) = { - s"Post $postId $param" - } - - @cask.get("/path", subpath = true) - def showSubpath(subPath: cask.Subpath) = { - s"Subpath ${subPath.value}" - } - - initialize() -} -``` - -You can bind variables to endpoints by declaring them as parameters: these are -either taken from a path-segment matcher of the same name (e.g. `postId` above), -or from query-parameters of the same name (e.g. `param` above). You can make -`param` take a `: String` to match `?param=hello`, an `: Int` for `?param=123` a -`Seq[T]` (as above) for repeated params such as `?param=hello¶m=world`, or -`: Option[T]` for cases where the `?param=hello` is optional. - -If you need to capture the entire sub-path of the request, you can set the flag -`subpath=true` and ask for a `: cask.Subpath` (the name of the param doesn't -matter). This will make the route match any sub-path of the prefix given to the -`@cask` decorator, and give you the remainder to use in your endpoint logic. - -Receiving Form-encoded or JSON data ------------------------------------ - -```scala -object FormJsonPost extends cask.MainRoutes{ - @cask.postJson("/json") - def jsonEndpoint(value1: ujson.Js.Value, value2: Seq[Int]) = { - "OK " + value1 + " " + value2 - } - - @cask.postForm("/form") - def formEndpoint(value1: cask.FormValue, value2: Seq[Int]) = { - "OK " + value1 + " " + value2 - } - - @cask.postForm("/upload") - def uploadFile(image: cask.FormFile) = { - image.fileName - } - - initialize() -} -``` - -If you need to handle a JSON-encoded POST request, you can use the -`@cast.postJson` decorator. This assumes the posted request body is a JSON dict, -and uses its keys to populate the endpoint's parameters, either as raw -`ujson.Js.Value`s or deserialized into `Seq[Int]`s or other things. -Deserialization is handled using the -[uPickle](https://github.com/lihaoyi/upickle) JSON library, though you could -write your own version of `postJson` to work with any other JSON library of your -choice. - -Similarly, you can mark endpoints as `@cask.postForm`, in which case the -endpoints params will be taken from the form-encoded POST body either raw (as -`cask.FormValue`s) or deserialized into simple data structures. Use -`cask.FormFile` if you want the given form value to be a file upload. - -Both normal forms and multipart forms are handled the same way. - -If the necessary keys are not present in the JSON/form-encoded POST body, or the -deserialization into Scala data-types fails, a 400 response is returned -automatically with a helpful error message. - - -Processing Cookies ------------------- - -```scala -object Cookies extends cask.MainRoutes{ - @cask.get("/read-cookie") - def readCookies(username: cask.Cookie) = { - username.value - } - - @cask.get("/store-cookie") - def storeCookies() = { - cask.Response( - "Cookies Set!", - cookies = Seq(cask.Cookie("username", "the username")) - ) - } - - @cask.get("/delete-cookie") - def deleteCookie() = { - cask.Response( - "Cookies Deleted!", - cookies = Seq(cask.Cookie("username", "", expires = java.time.Instant.EPOCH)) - ) - } - - initialize() -} -``` - -Cookies are most easily read by declaring a `: cask.Cookie` parameter; the -parameter name is used to fetch the cookie you are interested in. Cookies can be -stored by setting the `cookie` attribute in the response, and deleted simply by -setting `expires = java.time.Instant.EPOCH` (i.e. to have expired a long time -ago) - -Serving Static Files --------------------- -```scala -object StaticFiles extends cask.MainRoutes{ - @cask.get("/") - def index() = { - "Hello!" - } - - @cask.static("/static") - def staticRoutes() = "cask/resources/cask" - - initialize() -} -``` - -You can ask Cask to serve static files by defining a `@cask.static` endpoint. -This will match any subpath of the value returned by the endpoint (e.g. above -`/static/file.txt`, `/static/folder/file.txt`, etc.) and return the file -contents from the corresponding file on disk (and 404 otherwise). - -Redirects or Aborts -------------------- -```scala -object RedirectAbort extends cask.MainRoutes{ - @cask.get("/") - def index() = { - cask.Redirect("/login") - } - - @cask.get("/login") - def login() = { - cask.Abort(401) - } - - initialize() -} -``` - -Cask provides some convenient helpers `cask.Redirect` and `cask.Abort` which you -can return; these are simple wrappers around `cask.Request`, and simply set up -the relevant headers or status code for you. - -Extending Endpoints with Decorators ------------------------------------ - -```scala -object Decorated extends cask.MainRoutes{ - class User{ - override def toString = "[haoyi]" - } - class loggedIn extends cask.Decorator { - def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = { - delegate(Map("user" -> new User())) - } - } - class withExtra extends cask.Decorator { - def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = { - delegate(Map("extra" -> 31337)) - } - } - - @withExtra() - @cask.get("/hello/:world") - def hello(world: String)(extra: Int) = { - world + extra - } - - @loggedIn() - @cask.get("/internal/:world") - def internal(world: String)(user: User) = { - world + user - } - - @withExtra() - @loggedIn() - @cask.get("/internal-extra/:world") - def internalExtra(world: String)(user: User)(extra: Int) = { - world + user + extra - } - - @withExtra() - @loggedIn() - @cask.get("/ignore-extra/:world") - def ignoreExtra(world: String)(user: User) = { - world + user - } - - initialize() -} -``` - -You can write extra decorator annotations that stack on top of the existing -`@cask.get`/`@cask.post` to provide additional arguments or validation. This is -done by implementing the `cask.Decorator` interface and it's `getRawParams` -function. `getRawParams`: - -- Receives a `ParamContext`, which basically gives you full access to the - underlying undertow HTTP connection so you can pick out whatever data you - would like - -- Returns an `Either[Response, cask.Decor[Any]]`. Returning a `Left` lets you - bail out early with a fixed `cask.Response`, avoiding further processing. - Returning a `Right` provides a map of parameter names and values that will - then get passed to the endpoint function in consecutive parameter lists (shown - above), as well as an optional cleanup function that is run after the endpoint - terminates. - -Each additional decorator is responsible for one additional parameter list to -the right of the existing parameter lists, each of which can contain any number -of parameters. - -Decorators are useful for things like: - -- Making an endpoint return a HTTP 403 if the user isn't logged in, but if they are - logged in providing the `: User` object to the body of the endpoint function - -- Rate-limiting users by returning early with a HTTP 429 if a user tries to - access an endpoint too many times too quickly - -- Providing request-scoped values to the endpoint function: perhaps a database - transaction that commits when the function succeeds (and rolls-back if it - fails), or access to some system resource that needs to be released. - -For decorators that you wish to apply to multiple routes at once, you can define -them by overriding the `cask.Routes#decorators` field (to apply to every -endpoint in that routes object) or `cask.Main#mainDecorators` (to apply to every -endpoint, period): - -```scala -object Decorated2 extends cask.MainRoutes{ - class User{ - override def toString = "[haoyi]" - } - class loggedIn extends cask.Decorator { - def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = { - delegate(Map("user" -> new User())) - } - } - class withExtra extends cask.Decorator { - def wrapFunction(ctx: cask.ParamContext, delegate: Delegate): Returned = { - delegate(Map("extra" -> 31337)) - } - } - - override def decorators = Seq(new withExtra()) - - @cask.get("/hello/:world") - def hello(world: String)(extra: Int) = { - world + extra - } - - @loggedIn() - @cask.get("/internal-extra/:world") - def internalExtra(world: String)(user: User)(extra: Int) = { - world + user + extra - } - - @loggedIn() - @cask.get("/ignore-extra/:world") - def ignoreExtra(world: String)(user: User) = { - world + user - } - - initialize() -} -``` - -This is convenient for cases where you want a set of decorators to apply broadly -across your web application, and do not want to repeat them over and over at -every single endpoint. - -Gzip & Deflated Responses -------------------------- - -```scala -object Compress extends cask.MainRoutes{ - - @cask.decorators.compress - @cask.get("/") - def hello() = { - "Hello World! Hello World! Hello World!" - } - - initialize() -} - -``` - -Cask provides a useful `@cask.decorators.compress` decorator that gzips or -deflates a response body if possible. This is useful if you don't have a proxy -like Nginx or similar in front of your server to perform the compression for -you. - -Like all decorators, `@cask.decorators.compress` can be defined on a level of a -set of `cask.Routes`: - -```scala -object Compress2 extends cask.Routes{ - override def decorators = Seq(new cask.decorators.compress()) - - @cask.get("/") - def hello() = { - "Hello World! Hello World! Hello World!" - } - - initialize() -} - -object Compress2Main extends cask.Main(Compress2) -``` - -Or globally, in your `cask.Main`: - -```scala -object Compress3 extends cask.Routes{ - - @cask.get("/") - def hello() = { - "Hello World! Hello World! Hello World!" - } - - initialize() -} - -object Compress3Main extends cask.Main(Compress3){ - override def decorators = Seq(new cask.decorators.compress()) -} -``` - -TodoMVC Api Server ------------------- - -```scala -object TodoMvcApi extends cask.MainRoutes{ - case class Todo(checked: Boolean, text: String) - object Todo{ - implicit def todoRW = upickle.default.macroRW[Todo] - } - var todos = Seq( - Todo(true, "Get started with Cask"), - Todo(false, "Profit!") - ) - - @cask.get("/list/:state") - def list(state: String) = { - val filteredTodos = state match{ - case "all" => todos - case "active" => todos.filter(!_.checked) - case "completed" => todos.filter(_.checked) - } - upickle.default.write(filteredTodos) - } - - @cask.post("/add") - def add(request: cask.Request) = { - todos = Seq(Todo(false, new String(request.data.readAllBytes()))) ++ todos - } - - @cask.post("/toggle/:index") - def toggle(index: Int) = { - todos = todos.updated(index, todos(index).copy(checked = !todos(index).checked)) - } - - @cask.post("/delete/:index") - def delete(index: Int) = { - todos = todos.patch(index, Nil, 1) - } - - initialize() -} -``` - -This is a simple self-contained example of using Cask to write an in-memory API -server for the common [TodoMVC example app](http://todomvc.com/). - -This minimal example intentionally does not contain javascript, HTML, styles, -etc.. Those can be managed via the normal mechanism for -[Serving Static Files](#serving-static-files). - - -TodoMVC Database Integration ----------------------------- -```scala -import cask.internal.Router -import com.typesafe.config.ConfigFactory -import io.getquill.{SqliteJdbcContext, SnakeCase} - -object TodoMvcDb extends cask.MainRoutes{ - val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite") - - object ctx extends SqliteJdbcContext( - SnakeCase, - ConfigFactory.parseString( - s"""{"driverClassName":"org.sqlite.JDBC","jdbcUrl":"jdbc:sqlite:$tmpDb/file.db"}""" - ) - ) - - class transactional extends cask.Decorator{ - class TransactionFailed(val value: Router.Result.Error) extends Exception - def wrapFunction(pctx: cask.ParamContext, delegate: Delegate): Returned = { - try ctx.transaction( - delegate(Map()) match{ - case Router.Result.Success(t) => Router.Result.Success(t) - case e: Router.Result.Error => throw new TransactionFailed(e) - } - ) - catch{case e: TransactionFailed => e.value} - - } - } - - case class Todo(id: Int, checked: Boolean, text: String) - object Todo{ - implicit def todoRW = upickle.default.macroRW[Todo] - } - - ctx.executeAction( - """CREATE TABLE todo ( - | id INTEGER PRIMARY KEY AUTOINCREMENT, - | checked BOOLEAN, - | text TEXT - |); - |""".stripMargin - ) - ctx.executeAction( - """INSERT INTO todo (checked, text) VALUES - |(1, 'Get started with Cask'), - |(0, 'Profit!'); - |""".stripMargin - ) - - import ctx._ - - @transactional - @cask.get("/list/:state") - def list(state: String) = { - val filteredTodos = state match{ - case "all" => run(query[Todo]) - case "active" => run(query[Todo].filter(!_.checked)) - case "completed" => run(query[Todo].filter(_.checked)) - } - upickle.default.write(filteredTodos) - } - - @transactional - @cask.post("/add") - def add(request: cask.Request) = { - val body = new String(request.data.readAllBytes()) - run(query[Todo].insert(_.checked -> lift(false), _.text -> lift(body)).returning(_.id)) - } - - @transactional - @cask.post("/toggle/:index") - def toggle(index: Int) = { - run(query[Todo].filter(_.id == lift(index)).update(p => p.checked -> !p.checked)) - } - - @transactional - @cask.post("/delete/:index") - def delete(index: Int) = { - run(query[Todo].filter(_.id == lift(index)).delete) - } - - initialize() -} - -``` - -This example demonstrates how to use Cask to write a TodoMVC API server that -persists it's state in a database rather than in memory. We use the -[Quill](http://getquill.io/) database access library to write a `@transactional` -decorator that automatically opens one transaction per call to an endpoint, -ensuring that database queries are properly committed on success or rolled-back -on error. Note that because the default database connector propagates its -transaction context in a thread-local, `@transactional` does not need to pass -the `ctx` object into each endpoint as an additional parameter list, and so we -simply leave it out. - -While this example is specific to Quill, you can easily modify the -`@transactional` decorator to make it work with whatever database access library -you happen to be using. For libraries which need an implicit transaction, it can -be passed into each endpoint function as an additional parameter list as -described in -[Extending Endpoints with Decorators](#extending-endpoints-with-decorators). - -TodoMVC Full Stack Web ----------------------- - -The following code snippet is the complete code for a full-stack TodoMVC -implementation: including HTML generation for the web UI via -[Scalatags](https://github.com/lihaoyi/scalatags), Javascript for the -interactivity, static file serving, and database integration via -[Quill](https://github.com/getquill/quill). While slightly long, this example -should give you a tour of all the things you need to know to use Cask. - -Note that this is a "boring" server-side-rendered webapp with Ajax interactions, -without any complex front-end frameworks or libraries: it's purpose is to -demonstrate a simple working web application of using Cask end-to-end, which you -can build upon to create your own Cask web application architected however you -would like. - -```scala -import cask.internal.Router -import com.typesafe.config.ConfigFactory -import io.getquill.{SnakeCase, SqliteJdbcContext} -import scalatags.Text.all._ -import scalatags.Text.tags2 -object Server extends cask.MainRoutes{ - val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite") - - object ctx extends SqliteJdbcContext( - SnakeCase, - ConfigFactory.parseString( - s"""{"driverClassName":"org.sqlite.JDBC","jdbcUrl":"jdbc:sqlite:$tmpDb/file.db"}""" - ) - ) - - class transactional extends cask.Decorator{ - class TransactionFailed(val value: Router.Result.Error) extends Exception - def wrapFunction(pctx: cask.ParamContext, delegate: Delegate): Returned = { - try ctx.transaction( - delegate(Map()) match{ - case Router.Result.Success(t) => Router.Result.Success(t) - case e: Router.Result.Error => throw new TransactionFailed(e) - } - ) - catch{case e: TransactionFailed => e.value} - } - } - - case class Todo(id: Int, checked: Boolean, text: String) - - ctx.executeAction( - """CREATE TABLE todo ( - | id INTEGER PRIMARY KEY AUTOINCREMENT, - | checked BOOLEAN, - | text TEXT - |); - |""".stripMargin - ) - ctx.executeAction( - """INSERT INTO todo (checked, text) VALUES - |(1, 'Get started with Cask'), - |(0, 'Profit!'); - |""".stripMargin - ) - - import ctx._ - - @transactional - @cask.post("/list/:state") - def list(state: String) = renderBody(state).render - - @transactional - @cask.post("/add/:state") - def add(state: String, request: cask.Request) = { - val body = new String(request.data.readAllBytes()) - run(query[Todo].insert(_.checked -> lift(false), _.text -> lift(body)).returning(_.id)) - renderBody(state).render - } - - @transactional - @cask.post("/delete/:state/:index") - def delete(state: String, index: Int) = { - run(query[Todo].filter(_.id == lift(index)).delete) - renderBody(state).render - } - - @transactional - @cask.post("/toggle/:state/:index") - def toggle(state: String, index: Int) = { - run(query[Todo].filter(_.id == lift(index)).update(p => p.checked -> !p.checked)) - renderBody(state).render - } - - @transactional - @cask.post("/clear-completed/:state") - def clearCompleted(state: String) = { - run(query[Todo].filter(_.checked).delete) - renderBody(state).render - } - - @transactional - @cask.post("/toggle-all/:state") - def toggleAll(state: String) = { - val next = run(query[Todo].filter(_.checked).size) != 0 - run(query[Todo].update(_.checked -> !lift(next))) - renderBody(state).render - } - - def renderBody(state: String) = { - val filteredTodos = state match{ - case "all" => run(query[Todo]).sortBy(-_.id) - case "active" => run(query[Todo].filter(!_.checked)).sortBy(-_.id) - case "completed" => run(query[Todo].filter(_.checked)).sortBy(-_.id) - } - frag( - header(cls := "header", - h1("todos"), - input(cls := "new-todo", placeholder := "What needs to be done?", autofocus := "") - ), - tags2.section(cls := "main", - input( - id := "toggle-all", - cls := "toggle-all", - `type` := "checkbox", - if (run(query[Todo].filter(_.checked).size != 0)) checked else () - ), - label(`for` := "toggle-all","Mark all as complete"), - ul(cls := "todo-list", - for(todo <- filteredTodos) yield li( - if (todo.checked) cls := "completed" else (), - div(cls := "view", - input( - cls := "toggle", - `type` := "checkbox", - if (todo.checked) checked else (), - data("todo-index") := todo.id - ), - label(todo.text), - button(cls := "destroy", data("todo-index") := todo.id) - ), - input(cls := "edit", value := todo.text) - ) - ) - ), - footer(cls := "footer", - span(cls := "todo-count", - strong(run(query[Todo].filter(!_.checked).size).toInt), - " items left" - ), - ul(cls := "filters", - li(cls := "todo-all", - a(if (state == "all") cls := "selected" else (), "All") - ), - li(cls := "todo-active", - a(if (state == "active") cls := "selected" else (), "Active") - ), - li(cls := "todo-completed", - a(if (state == "completed") cls := "selected" else (), "Completed") - ) - ), - button(cls := "clear-completed","Clear completed") - ) - ) - } - - @transactional - @cask.get("/") - def index() = { - cask.Response( - "<!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") + } +} |