summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-08-12 22:18:39 +0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-08-12 22:18:39 +0800
commitfd9c399db8c1c0d86cc65d5e1c41968b42a813d1 (patch)
tree8e8fc2875cb1c26f309384a9ca0ad72e1fa893f3
parent9bf8c31fa9321558d7d02f6a5b687cd55a924e7f (diff)
downloadcask-fd9c399db8c1c0d86cc65d5e1c41968b42a813d1.tar.gz
cask-fd9c399db8c1c0d86cc65d5e1c41968b42a813d1.tar.bz2
cask-fd9c399db8c1c0d86cc65d5e1c41968b42a813d1.zip
auto-upload examples
-rw-r--r--build.sc129
-rw-r--r--cask/test/src/test/cask/ExampleTests.scala191
-rw-r--r--cask/test/src/test/cask/FailureTests.scala6
-rw-r--r--cask/test/src/test/cask/HttpMethods.scala13
-rwxr-xr-xci/publish-local.sh7
-rw-r--r--docs/readme.md896
-rw-r--r--example/compress/app/src/Compress.scala (renamed from cask/test/src/test/cask/Compress.scala)3
-rw-r--r--example/compress/app/test/src/ExampleTests.scala28
-rw-r--r--example/compress/build.sc18
-rw-r--r--example/compress2/app/src/Compress2.scala (renamed from cask/test/src/test/cask/Compress2.scala)2
-rw-r--r--example/compress2/app/test/src/ExampleTests.scala28
-rw-r--r--example/compress2/build.sc18
-rw-r--r--example/compress3/app/src/Compress3.scala (renamed from cask/test/src/test/cask/Compress3.scala)2
-rw-r--r--example/compress3/app/test/src/ExampleTests.scala29
-rw-r--r--example/compress3/build.sc18
-rw-r--r--example/cookies/app/src/Cookies.scala (renamed from cask/test/src/test/cask/Cookies.scala)4
-rw-r--r--example/cookies/app/test/src/ExampleTests.scala31
-rw-r--r--example/cookies/build.sc18
-rw-r--r--example/decorated/app/src/Decorated.scala (renamed from cask/test/src/test/cask/Decorated.scala)3
-rw-r--r--example/decorated/app/test/src/ExampleTests.scala27
-rw-r--r--example/decorated/build.sc18
-rw-r--r--example/decorated2/app/src/Decorated2.scala (renamed from cask/test/src/test/cask/Decorated2.scala)3
-rw-r--r--example/decorated2/app/test/src/ExampleTests.scala27
-rw-r--r--example/decorated2/build.sc18
-rw-r--r--example/formJsonPost/app/src/FormJsonPost.scala (renamed from cask/test/src/test/cask/FormJsonPost.scala)3
-rw-r--r--example/formJsonPost/app/test/src/ExampleTests.scala39
-rw-r--r--example/formJsonPost/build.sc18
-rw-r--r--example/httpMethods/app/src/HttpMethods.scala10
-rw-r--r--example/httpMethods/app/test/src/ExampleTests.scala25
-rw-r--r--example/httpMethods/build.sc18
-rw-r--r--example/minimalApplication/app/src/MinimalApplication.scala (renamed from cask/test/src/test/cask/MinimalApplication.scala)3
-rw-r--r--example/minimalApplication/app/test/src/ExampleTests.scala33
-rw-r--r--example/minimalApplication/build.sc18
-rw-r--r--example/minimalApplication2/app/src/MinimalApplication2.scala (renamed from cask/test/src/test/cask/MinimalApplication2.scala)2
-rw-r--r--example/minimalApplication2/app/test/src/ExampleTests.scala33
-rw-r--r--example/minimalApplication2/build.sc18
-rw-r--r--example/redirectAbort/app/src/RedirectAbort.scala (renamed from cask/test/src/test/cask/RedirectAbort.scala)4
-rw-r--r--example/redirectAbort/app/test/src/ExampleTests.scala27
-rw-r--r--example/redirectAbort/build.sc18
-rw-r--r--example/staticFiles/app/src/StaticFiles.scala (renamed from cask/test/src/test/cask/StaticFiles.scala)3
-rw-r--r--example/staticFiles/app/test/src/ExampleTests.scala27
-rw-r--r--example/staticFiles/build.sc18
-rw-r--r--example/todo/app/resources/todo/app.js (renamed from example/todo/resources/todo/app.js)0
-rw-r--r--example/todo/app/resources/todo/index.css (renamed from example/todo/resources/todo/index.css)0
-rw-r--r--example/todo/app/src/todo/TodoServer.scala (renamed from example/todo/src/todo/Server.scala)4
-rw-r--r--example/todo/app/test/src/todo/TodoTest.scala22
-rw-r--r--example/todo/build.sc20
-rw-r--r--example/todoApi/app/src/TodoMvcApi.scala (renamed from cask/test/src/test/cask/TodoMvcApi.scala)3
-rw-r--r--example/todoApi/app/test/src/ExampleTests.scala47
-rw-r--r--example/todoApi/build.sc18
-rw-r--r--example/todoDb/app/src/TodoMvcDb.scala (renamed from cask/test/src/test/cask/TodoMvcDb.scala)3
-rw-r--r--example/todoDb/app/test/src/ExampleTests.scala47
-rw-r--r--example/todoDb/build.sc20
-rw-r--r--example/variableRoutes/app/src/VariableRoutes.scala (renamed from cask/test/src/test/cask/VariableRoutes.scala)3
-rw-r--r--example/variableRoutes/app/test/src/ExampleTests.scala48
-rw-r--r--example/variableRoutes/build.sc18
-rw-r--r--upload.sc52
57 files changed, 1907 insertions, 252 deletions
diff --git a/build.sc b/build.sc
index 2fd3976..f1fa42f 100644
--- a/build.sc
+++ b/build.sc
@@ -1,4 +1,22 @@
import mill._, scalalib._
+import ammonite.ops._, ujson.Js
+import $file.upload
+import $file.example.compress.build
+import $file.example.compress2.build
+import $file.example.compress3.build
+import $file.example.cookies.build
+import $file.example.decorated.build
+import $file.example.decorated2.build
+import $file.example.formJsonPost.build
+import $file.example.httpMethods.build
+import $file.example.minimalApplication.build
+import $file.example.minimalApplication2.build
+import $file.example.redirectAbort.build
+import $file.example.staticFiles.build
+import $file.example.todo.build
+import $file.example.todoApi.build
+import $file.example.todoDb.build
+import $file.example.variableRoutes.build
object cask extends ScalaModule{
def scalaVersion = "2.12.6"
@@ -26,12 +44,111 @@ object cask extends ScalaModule{
}
}
object example extends Module{
- object todo extends ScalaModule{
- def scalaVersion = "2.12.6"
+ trait LocalModule extends ScalaModule{
+ def ivyDeps = super.ivyDeps().filter(_ != ivy"com.lihaoyi::cask:0.0.1")
+
+ override def millSourcePath = super.millSourcePath / "app"
def moduleDeps = Seq(cask)
- def ivyDeps = Agg(
- ivy"org.xerial:sqlite-jdbc:3.18.0",
- ivy"io.getquill::quill-jdbc:2.5.4"
+ }
+ object compress extends $file.example.compress.build.AppModule with LocalModule
+ object compress2 extends $file.example.compress2.build.AppModule with LocalModule
+ object compress3 extends $file.example.compress3.build.AppModule with LocalModule
+ object cookies extends $file.example.cookies.build.AppModule with LocalModule
+ object decorated extends $file.example.decorated.build.AppModule with LocalModule
+ object decorated2 extends $file.example.decorated2.build.AppModule with LocalModule
+ object formJsonPost extends $file.example.formJsonPost.build.AppModule with LocalModule
+ object httpMethods extends $file.example.httpMethods.build.AppModule with LocalModule
+ object minimalApplication extends $file.example.minimalApplication.build.AppModule with LocalModule
+ object minimalApplication2 extends $file.example.minimalApplication2.build.AppModule with LocalModule
+ object redirectAbort extends $file.example.redirectAbort.build.AppModule with LocalModule
+ object staticFiles extends $file.example.staticFiles.build.AppModule with LocalModule
+ object todo extends $file.example.todo.build.AppModule with LocalModule
+ object todoApi extends $file.example.todoApi.build.AppModule with LocalModule
+ object todoDb extends $file.example.todoDb.build.AppModule with LocalModule
+ 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)
}
-} \ No newline at end of file
+
+ 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 uploadToGithub(authKey: String) = T.command{
+ val (releaseTag, label) = publishVersion()
+
+ if (releaseTag == label){
+ scalaj.http.Http("https://api.github.com/repos/lihaoyi/cask/releases")
+ .postData(
+ ujson.write(
+ Js.Obj(
+ "tag_name" -> releaseTag,
+ "name" -> releaseTag
+ )
+ )
+ )
+ .header("Authorization", "token " + authKey)
+ .asString
+ }
+
+ val examples = Seq(
+ $file.example.compress.build.millSourcePath,
+ $file.example.compress2.build.millSourcePath,
+ $file.example.compress3.build.millSourcePath,
+ $file.example.cookies.build.millSourcePath,
+ $file.example.decorated.build.millSourcePath,
+ $file.example.decorated2.build.millSourcePath,
+ $file.example.formJsonPost.build.millSourcePath,
+ $file.example.httpMethods.build.millSourcePath,
+ $file.example.minimalApplication.build.millSourcePath,
+ $file.example.minimalApplication2.build.millSourcePath,
+ $file.example.redirectAbort.build.millSourcePath,
+ $file.example.staticFiles.build.millSourcePath,
+ $file.example.todo.build.millSourcePath,
+ $file.example.todoApi.build.millSourcePath,
+ $file.example.todoDb.build.millSourcePath,
+ $file.example.variableRoutes.build.millSourcePath,
+ )
+ for(example <- examples){
+ val f = tmp.dir()
+ cp(example, f/'folder)
+ write.over(
+ f/'folder/"build.sc",
+ read(f/'folder/"build.sc").replace("trait AppModule ", "object app ")
+ )
+
+ %%("zip", "-r", f/"out.zip", f/'folder)(T.ctx().dest)
+ upload.apply(f/"out.zip", releaseTag, label + "/" + example.last, authKey)
+ }
+}
+
diff --git a/cask/test/src/test/cask/ExampleTests.scala b/cask/test/src/test/cask/ExampleTests.scala
deleted file mode 100644
index 6784b1e..0000000
--- a/cask/test/src/test/cask/ExampleTests.scala
+++ /dev/null
@@ -1,191 +0,0 @@
-package test.cask
-import io.undertow.Undertow
-import io.undertow.server.handlers.BlockingHandler
-import utest._
-
-object ExampleTests extends TestSuite{
- def test[T](example: cask.main.BaseMain)(f: String => T): T = {
- val server = Undertow.builder
- .addHttpListener(8080, "localhost")
- .setHandler(new BlockingHandler(example.defaultHandler))
- .build
- server.start()
- val res =
- try f("http://localhost:8080")
- finally server.stop()
- res
- }
-
- val tests = Tests{
- 'MinimalApplication - test(MinimalApplication){ host =>
- val success = requests.get(host)
-
- success.text() ==> "Hello World!"
- success.statusCode ==> 200
-
- requests.get(s"$host/doesnt-exist").statusCode ==> 404
-
- requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh"
-
- requests.get(s"$host/do-thing").statusCode ==> 404
- }
- 'MinimalApplication2 - test(MinimalMain){ host =>
- val success = requests.get(host)
-
- success.text() ==> "Hello World!"
- success.statusCode ==> 200
-
- requests.get(s"$host/doesnt-exist").statusCode ==> 404
-
- requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh"
-
- requests.get(s"$host/do-thing").statusCode ==> 404
- }
- 'VariableRoutes - test(VariableRoutes){ host =>
- val noIndexPage = requests.get(host)
- noIndexPage.statusCode ==> 404
-
- requests.get(s"$host/user/lihaoyi").text() ==> "User lihaoyi"
-
- requests.get(s"$host/user").statusCode ==> 404
-
-
- requests.get(s"$host/post/123?param=xyz&param=abc").text() ==>
- "Post 123 ArrayBuffer(xyz, abc)"
-
- requests.get(s"$host/post/123").text() ==>
- """Missing argument: (param: Seq[String])
- |
- |Arguments provided did not match expected signature:
- |
- |showPost
- | postId Int
- | param Seq[String]
- |
- |""".stripMargin
-
- requests.get(s"$host/path/one/two/three").text() ==>
- "Subpath List(one, two, three)"
- }
-
- 'StaticFiles - test(StaticFiles){ host =>
- requests.get(s"$host/static/example.txt").text() ==>
- "the quick brown fox jumps over the lazy dog"
- }
-
- 'RedirectAbort - test(RedirectAbort){ host =>
- val resp = requests.get(s"$host/")
- resp.statusCode ==> 401
- resp.history.get.statusCode ==> 301
- }
-
- 'FormJsonPost - test(FormJsonPost){ host =>
- requests.post(s"$host/json", data = """{"value1": true, "value2": [3]}""").text() ==>
- "OK true Vector(3)"
-
- requests.post(
- s"$host/form",
- data = Seq("value1" -> "hello", "value2" -> "1", "value2" -> "2")
- ).text() ==>
- "OK FormValue(hello,null) List(1, 2)"
-
- val resp = requests.post(
- s"$host/upload",
- data = requests.MultiPart(
- requests.MultiItem("image", "...", "my-best-image.txt")
- )
- )
- resp.text() ==> "my-best-image.txt"
- }
- 'Decorated - test(Decorated){ host =>
- requests.get(s"$host/hello/woo").text() ==> "woo31337"
- requests.get(s"$host/internal/boo").text() ==> "boo[haoyi]"
- requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337"
-
- }
- 'Decorated2 - test(Decorated2){ host =>
- requests.get(s"$host/hello/woo").text() ==> "woo31337"
- requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337"
- requests.get(s"$host/ignore-extra/boo").text() ==> "boo[haoyi]"
-
- }
- 'TodoMvcApi - test(TodoMvcApi){ host =>
- requests.get(s"$host/list/all").text() ==>
- """[{"checked":true,"text":"Get started with Cask"},{"checked":false,"text":"Profit!"}]"""
- requests.get(s"$host/list/active").text() ==>
- """[{"checked":false,"text":"Profit!"}]"""
- requests.get(s"$host/list/completed").text() ==>
- """[{"checked":true,"text":"Get started with Cask"}]"""
-
- requests.post(s"$host/toggle/1")
-
- requests.get(s"$host/list/all").text() ==>
- """[{"checked":true,"text":"Get started with Cask"},{"checked":true,"text":"Profit!"}]"""
-
- requests.get(s"$host/list/active").text() ==>
- """[]"""
-
- requests.post(s"$host/add", data = "new Task")
-
- requests.get(s"$host/list/active").text() ==>
- """[{"checked":false,"text":"new Task"}]"""
-
- requests.post(s"$host/delete/0")
-
- requests.get(s"$host/list/active").text() ==>
- """[]"""
- }
- 'TodoMvcDb - test(TodoMvcDb){ host =>
- requests.get(s"$host/list/all").text() ==>
- """[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":false,"text":"Profit!"}]"""
- requests.get(s"$host/list/active").text() ==>
- """[{"id":2,"checked":false,"text":"Profit!"}]"""
- requests.get(s"$host/list/completed").text() ==>
- """[{"id":1,"checked":true,"text":"Get started with Cask"}]"""
-
- requests.post(s"$host/toggle/2")
-
- requests.get(s"$host/list/all").text() ==>
- """[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":true,"text":"Profit!"}]"""
-
- requests.get(s"$host/list/active").text() ==>
- """[]"""
-
- requests.post(s"$host/add", data = "new Task")
-
- requests.get(s"$host/list/active").text() ==>
- """[{"id":3,"checked":false,"text":"new Task"}]"""
-
- requests.post(s"$host/delete/3")
-
- requests.get(s"$host/list/active").text() ==>
- """[]"""
- }
-
- 'Compress - test(Compress){ host =>
- val expected = "Hello World! Hello World! Hello World!"
- requests.get(s"$host").text() ==> expected
- assert(
- requests.get(s"$host", autoDecompress = false).text().length < expected.length
- )
-
- }
-
- 'Compress2Main - test(Compress2Main) { host =>
- val expected = "Hello World! Hello World! Hello World!"
- requests.get(s"$host").text() ==> expected
- assert(
- requests.get(s"$host", autoDecompress = false).text().length < expected.length
- )
- }
-
- 'Compress3Main - test(Compress3Main){ host =>
- val expected = "Hello World! Hello World! Hello World!"
- requests.get(s"$host").text() ==> expected
- assert(
- requests.get(s"$host", autoDecompress = false).text().length < expected.length
- )
-
- }
- }
-}
diff --git a/cask/test/src/test/cask/FailureTests.scala b/cask/test/src/test/cask/FailureTests.scala
index 62eb946..fed56e5 100644
--- a/cask/test/src/test/cask/FailureTests.scala
+++ b/cask/test/src/test/cask/FailureTests.scala
@@ -12,12 +12,6 @@ object FailureTests extends TestSuite {
}
val tests = Tests{
'mismatchedDecorators - {
- object Decorated extends cask.MainRoutes{
- @cask.get("/hello/:world")
- def hello(world: String)(extra: Int) = world + extra
- initialize()
- }
-
utest.compileError("""
object Decorated extends cask.MainRoutes{
@cask.get("/hello/:world")
diff --git a/cask/test/src/test/cask/HttpMethods.scala b/cask/test/src/test/cask/HttpMethods.scala
deleted file mode 100644
index 7f2ab7c..0000000
--- a/cask/test/src/test/cask/HttpMethods.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-package test.cask
-
-import io.undertow.server.HttpServerExchange
-
-object HttpMethods extends cask.MainRoutes{
- @cask.route("/login", methods = Seq("GET", "POST"))
- def login(exchange: HttpServerExchange) = {
- if (exchange.getRequestMethod.equalToString("POST")) "do_the_login"
- else "show_the_login_form"
- }
-
- initialize()
-}
diff --git a/ci/publish-local.sh b/ci/publish-local.sh
deleted file mode 100755
index c137340..0000000
--- a/ci/publish-local.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-set -eux
-
-mill -i all __.publishLocal release
-
-mv out/release/dest/mill ~/mill-release
diff --git a/docs/readme.md b/docs/readme.md
new file mode 100644
index 0000000..4add926
--- /dev/null
+++ b/docs/readme.md
@@ -0,0 +1,896 @@
+Cask: a Scala HTTP micro-framework
+==================================
+
+```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()
+}
+```
+
+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
+---------------
+
+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
diff --git a/cask/test/src/test/cask/Compress.scala b/example/compress/app/src/Compress.scala
index 1a027d6..9c57494 100644
--- a/cask/test/src/test/cask/Compress.scala
+++ b/example/compress/app/src/Compress.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object Compress extends cask.MainRoutes{
@cask.decorators.compress
diff --git a/example/compress/app/test/src/ExampleTests.scala b/example/compress/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..5a4a5bf
--- /dev/null
+++ b/example/compress/app/test/src/ExampleTests.scala
@@ -0,0 +1,28 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'Compress - test(Compress){ host =>
+ val expected = "Hello World! Hello World! Hello World!"
+ requests.get(s"$host").text() ==> expected
+ assert(
+ requests.get(s"$host", autoDecompress = false).text().length < expected.length
+ )
+ }
+ }
+}
diff --git a/example/compress/build.sc b/example/compress/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/compress/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/Compress2.scala b/example/compress2/app/src/Compress2.scala
index 0f2d01f..1a4cf69 100644
--- a/cask/test/src/test/cask/Compress2.scala
+++ b/example/compress2/app/src/Compress2.scala
@@ -1,4 +1,4 @@
-package test.cask
+package app
object Compress2 extends cask.Routes{
override def decorators = Seq(new cask.decorators.compress())
diff --git a/example/compress2/app/test/src/ExampleTests.scala b/example/compress2/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..cbe3301
--- /dev/null
+++ b/example/compress2/app/test/src/ExampleTests.scala
@@ -0,0 +1,28 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'Compress2Main - test(Compress2Main) { host =>
+ val expected = "Hello World! Hello World! Hello World!"
+ requests.get(s"$host").text() ==> expected
+ assert(
+ requests.get(s"$host", autoDecompress = false).text().length < expected.length
+ )
+ }
+ }
+}
diff --git a/example/compress2/build.sc b/example/compress2/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/compress2/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/Compress3.scala b/example/compress3/app/src/Compress3.scala
index 1c8da25..4d4df99 100644
--- a/cask/test/src/test/cask/Compress3.scala
+++ b/example/compress3/app/src/Compress3.scala
@@ -1,4 +1,4 @@
-package test.cask
+package app
object Compress3 extends cask.Routes{
diff --git a/example/compress3/app/test/src/ExampleTests.scala b/example/compress3/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..3b013f8
--- /dev/null
+++ b/example/compress3/app/test/src/ExampleTests.scala
@@ -0,0 +1,29 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'Compress3Main - test(Compress3Main){ host =>
+ val expected = "Hello World! Hello World! Hello World!"
+ requests.get(s"$host").text() ==> expected
+ assert(
+ requests.get(s"$host", autoDecompress = false).text().length < expected.length
+ )
+
+ }
+ }
+}
diff --git a/example/compress3/build.sc b/example/compress3/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/compress3/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/Cookies.scala b/example/cookies/app/src/Cookies.scala
index ba9edce..c07e373 100644
--- a/cask/test/src/test/cask/Cookies.scala
+++ b/example/cookies/app/src/Cookies.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object Cookies extends cask.MainRoutes{
@cask.get("/read-cookie")
def readCookies(username: cask.Cookie) = {
@@ -24,4 +23,3 @@ object Cookies extends cask.MainRoutes{
initialize()
}
-
diff --git a/example/cookies/app/test/src/ExampleTests.scala b/example/cookies/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..951728b
--- /dev/null
+++ b/example/cookies/app/test/src/ExampleTests.scala
@@ -0,0 +1,31 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'Cookies - test(Cookies){ host =>
+ val sess = requests.Session()
+ sess.get(s"$host/read-cookie").statusCode ==> 400
+ sess.get(s"$host/store-cookie")
+ sess.get(s"$host/read-cookie").text() ==> "the username"
+ sess.get(s"$host/read-cookie").statusCode ==> 200
+ sess.get(s"$host/delete-cookie")
+ sess.get(s"$host/read-cookie").statusCode ==> 400
+
+ }
+ }
+}
diff --git a/example/cookies/build.sc b/example/cookies/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/cookies/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/Decorated.scala b/example/decorated/app/src/Decorated.scala
index d7cb6b8..77f9133 100644
--- a/cask/test/src/test/cask/Decorated.scala
+++ b/example/decorated/app/src/Decorated.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object Decorated extends cask.MainRoutes{
class User{
override def toString = "[haoyi]"
diff --git a/example/decorated/app/test/src/ExampleTests.scala b/example/decorated/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..9aea3bc
--- /dev/null
+++ b/example/decorated/app/test/src/ExampleTests.scala
@@ -0,0 +1,27 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'Decorated - test(Decorated){ host =>
+ requests.get(s"$host/hello/woo").text() ==> "woo31337"
+ requests.get(s"$host/internal/boo").text() ==> "boo[haoyi]"
+ requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337"
+
+ }
+ }
+}
diff --git a/example/decorated/build.sc b/example/decorated/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/decorated/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/Decorated2.scala b/example/decorated2/app/src/Decorated2.scala
index 0d11952..014965e 100644
--- a/cask/test/src/test/cask/Decorated2.scala
+++ b/example/decorated2/app/src/Decorated2.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object Decorated2 extends cask.MainRoutes{
class User{
override def toString = "[haoyi]"
diff --git a/example/decorated2/app/test/src/ExampleTests.scala b/example/decorated2/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..7fec82a
--- /dev/null
+++ b/example/decorated2/app/test/src/ExampleTests.scala
@@ -0,0 +1,27 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'Decorated2 - test(Decorated2){ host =>
+ requests.get(s"$host/hello/woo").text() ==> "woo31337"
+ requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337"
+ requests.get(s"$host/ignore-extra/boo").text() ==> "boo[haoyi]"
+
+ }
+ }
+}
diff --git a/example/decorated2/build.sc b/example/decorated2/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/decorated2/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/FormJsonPost.scala b/example/formJsonPost/app/src/FormJsonPost.scala
index 05a8761..3714f39 100644
--- a/cask/test/src/test/cask/FormJsonPost.scala
+++ b/example/formJsonPost/app/src/FormJsonPost.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object FormJsonPost extends cask.MainRoutes{
@cask.postJson("/json")
def jsonEndpoint(value1: ujson.Js.Value, value2: Seq[Int]) = {
diff --git a/example/formJsonPost/app/test/src/ExampleTests.scala b/example/formJsonPost/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..137a978
--- /dev/null
+++ b/example/formJsonPost/app/test/src/ExampleTests.scala
@@ -0,0 +1,39 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'FormJsonPost - test(FormJsonPost){ host =>
+ requests.post(s"$host/json", data = """{"value1": true, "value2": [3]}""").text() ==>
+ "OK true Vector(3)"
+
+ requests.post(
+ s"$host/form",
+ data = Seq("value1" -> "hello", "value2" -> "1", "value2" -> "2")
+ ).text() ==>
+ "OK FormValue(hello,null) List(1, 2)"
+
+ val resp = requests.post(
+ s"$host/upload",
+ data = requests.MultiPart(
+ requests.MultiItem("image", "...", "my-best-image.txt")
+ )
+ )
+ resp.text() ==> "my-best-image.txt"
+ }
+ }
+}
diff --git a/example/formJsonPost/build.sc b/example/formJsonPost/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/formJsonPost/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/example/httpMethods/app/src/HttpMethods.scala b/example/httpMethods/app/src/HttpMethods.scala
new file mode 100644
index 0000000..5fcbdca
--- /dev/null
+++ b/example/httpMethods/app/src/HttpMethods.scala
@@ -0,0 +1,10 @@
+package app
+object HttpMethods extends cask.MainRoutes{
+ @cask.route("/login", methods = Seq("get", "post"))
+ def login(request: cask.Request) = {
+ if (request.exchange.getRequestMethod.equalToString("post")) "do_the_login"
+ else "show_the_login_form"
+ }
+
+ initialize()
+}
diff --git a/example/httpMethods/app/test/src/ExampleTests.scala b/example/httpMethods/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..e14bcf5
--- /dev/null
+++ b/example/httpMethods/app/test/src/ExampleTests.scala
@@ -0,0 +1,25 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'HttpMethods - test(HttpMethods){ host =>
+ requests.post(s"$host/login").text() ==> "do_the_login"
+ requests.get(s"$host/login").text() ==> "show_the_login_form"
+ }
+ }
+}
diff --git a/example/httpMethods/build.sc b/example/httpMethods/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/httpMethods/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/MinimalApplication.scala b/example/minimalApplication/app/src/MinimalApplication.scala
index ec38891..5357e64 100644
--- a/cask/test/src/test/cask/MinimalApplication.scala
+++ b/example/minimalApplication/app/src/MinimalApplication.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object MinimalApplication extends cask.MainRoutes{
@cask.get("/")
def hello() = {
diff --git a/example/minimalApplication/app/test/src/ExampleTests.scala b/example/minimalApplication/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..8c8ecb2
--- /dev/null
+++ b/example/minimalApplication/app/test/src/ExampleTests.scala
@@ -0,0 +1,33 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests {
+ 'MinimalApplication - test(MinimalApplication) { host =>
+ val success = requests.get(host)
+
+ success.text() ==> "Hello World!"
+ success.statusCode ==> 200
+
+ requests.get(s"$host/doesnt-exist").statusCode ==> 404
+
+ requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh"
+
+ requests.get(s"$host/do-thing").statusCode ==> 404
+ }
+ }
+}
diff --git a/example/minimalApplication/build.sc b/example/minimalApplication/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/minimalApplication/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/MinimalApplication2.scala b/example/minimalApplication2/app/src/MinimalApplication2.scala
index 924b00f..01b4aa5 100644
--- a/cask/test/src/test/cask/MinimalApplication2.scala
+++ b/example/minimalApplication2/app/src/MinimalApplication2.scala
@@ -1,4 +1,4 @@
-package test.cask
+package app
object MinimalRoutes extends cask.Routes{
@cask.get("/")
diff --git a/example/minimalApplication2/app/test/src/ExampleTests.scala b/example/minimalApplication2/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..0d8f1bc
--- /dev/null
+++ b/example/minimalApplication2/app/test/src/ExampleTests.scala
@@ -0,0 +1,33 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'MinimalApplication2 - test(MinimalMain){ host =>
+ val success = requests.get(host)
+
+ success.text() ==> "Hello World!"
+ success.statusCode ==> 200
+
+ requests.get(s"$host/doesnt-exist").statusCode ==> 404
+
+ requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh"
+
+ requests.get(s"$host/do-thing").statusCode ==> 404
+ }
+ }
+}
diff --git a/example/minimalApplication2/build.sc b/example/minimalApplication2/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/minimalApplication2/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/RedirectAbort.scala b/example/redirectAbort/app/src/RedirectAbort.scala
index f2aa811..18ef2d8 100644
--- a/cask/test/src/test/cask/RedirectAbort.scala
+++ b/example/redirectAbort/app/src/RedirectAbort.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object RedirectAbort extends cask.MainRoutes{
@cask.get("/")
def index() = {
@@ -13,4 +12,3 @@ object RedirectAbort extends cask.MainRoutes{
initialize()
}
-
diff --git a/example/redirectAbort/app/test/src/ExampleTests.scala b/example/redirectAbort/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..f095517
--- /dev/null
+++ b/example/redirectAbort/app/test/src/ExampleTests.scala
@@ -0,0 +1,27 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+
+ 'RedirectAbort - test(RedirectAbort){ host =>
+ val resp = requests.get(s"$host/")
+ resp.statusCode ==> 401
+ resp.history.get.statusCode ==> 301
+ }
+ }
+}
diff --git a/example/redirectAbort/build.sc b/example/redirectAbort/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/redirectAbort/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/StaticFiles.scala b/example/staticFiles/app/src/StaticFiles.scala
index 8f4a8ef..0d3bebc 100644
--- a/cask/test/src/test/cask/StaticFiles.scala
+++ b/example/staticFiles/app/src/StaticFiles.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object StaticFiles extends cask.MainRoutes{
@cask.get("/")
def index() = {
diff --git a/example/staticFiles/app/test/src/ExampleTests.scala b/example/staticFiles/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..ddf7de3
--- /dev/null
+++ b/example/staticFiles/app/test/src/ExampleTests.scala
@@ -0,0 +1,27 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+
+ 'StaticFiles - test(StaticFiles){ host =>
+ requests.get(s"$host/static/example.txt").text() ==>
+ "the quick brown fox jumps over the lazy dog"
+ }
+
+ }
+}
diff --git a/example/staticFiles/build.sc b/example/staticFiles/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/staticFiles/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/example/todo/resources/todo/app.js b/example/todo/app/resources/todo/app.js
index b7b8437..b7b8437 100644
--- a/example/todo/resources/todo/app.js
+++ b/example/todo/app/resources/todo/app.js
diff --git a/example/todo/resources/todo/index.css b/example/todo/app/resources/todo/index.css
index 208a762..208a762 100644
--- a/example/todo/resources/todo/index.css
+++ b/example/todo/app/resources/todo/index.css
diff --git a/example/todo/src/todo/Server.scala b/example/todo/app/src/todo/TodoServer.scala
index 6f43c6d..0c66895 100644
--- a/example/todo/src/todo/Server.scala
+++ b/example/todo/app/src/todo/TodoServer.scala
@@ -1,10 +1,10 @@
-package todo
+package app
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{
+object TodoServer extends cask.MainRoutes{
val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite")
object ctx extends SqliteJdbcContext(
diff --git a/example/todo/app/test/src/todo/TodoTest.scala b/example/todo/app/test/src/todo/TodoTest.scala
new file mode 100644
index 0000000..8f38612
--- /dev/null
+++ b/example/todo/app/test/src/todo/TodoTest.scala
@@ -0,0 +1,22 @@
+package app
+import utest._
+object TodoTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = io.undertow.Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+ val tests = Tests{
+ 'TodoServer - test(TodoServer){ host =>
+ val page = requests.get(host).text()
+ assert(page.contains("What needs to be done?"))
+ }
+ }
+
+} \ No newline at end of file
diff --git a/example/todo/build.sc b/example/todo/build.sc
new file mode 100644
index 0000000..b570b3b
--- /dev/null
+++ b/example/todo/build.sc
@@ -0,0 +1,20 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ ivy"org.xerial:sqlite-jdbc:3.18.0",
+ ivy"io.getquill::quill-jdbc:2.5.4"
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/TodoMvcApi.scala b/example/todoApi/app/src/TodoMvcApi.scala
index 74a5b9b..3559f28 100644
--- a/cask/test/src/test/cask/TodoMvcApi.scala
+++ b/example/todoApi/app/src/TodoMvcApi.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object TodoMvcApi extends cask.MainRoutes{
case class Todo(checked: Boolean, text: String)
object Todo{
diff --git a/example/todoApi/app/test/src/ExampleTests.scala b/example/todoApi/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..5e9e11a
--- /dev/null
+++ b/example/todoApi/app/test/src/ExampleTests.scala
@@ -0,0 +1,47 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'TodoMvcApi - test(TodoMvcApi){ host =>
+ requests.get(s"$host/list/all").text() ==>
+ """[{"checked":true,"text":"Get started with Cask"},{"checked":false,"text":"Profit!"}]"""
+ requests.get(s"$host/list/active").text() ==>
+ """[{"checked":false,"text":"Profit!"}]"""
+ requests.get(s"$host/list/completed").text() ==>
+ """[{"checked":true,"text":"Get started with Cask"}]"""
+
+ requests.post(s"$host/toggle/1")
+
+ requests.get(s"$host/list/all").text() ==>
+ """[{"checked":true,"text":"Get started with Cask"},{"checked":true,"text":"Profit!"}]"""
+
+ requests.get(s"$host/list/active").text() ==>
+ """[]"""
+
+ requests.post(s"$host/add", data = "new Task")
+
+ requests.get(s"$host/list/active").text() ==>
+ """[{"checked":false,"text":"new Task"}]"""
+
+ requests.post(s"$host/delete/0")
+
+ requests.get(s"$host/list/active").text() ==>
+ """[]"""
+ }
+ }
+}
diff --git a/example/todoApi/build.sc b/example/todoApi/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/todoApi/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/TodoMvcDb.scala b/example/todoDb/app/src/TodoMvcDb.scala
index b352d1a..72e20bd 100644
--- a/cask/test/src/test/cask/TodoMvcDb.scala
+++ b/example/todoDb/app/src/TodoMvcDb.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
import cask.internal.Router
import com.typesafe.config.ConfigFactory
import io.getquill.{SqliteJdbcContext, SnakeCase}
diff --git a/example/todoDb/app/test/src/ExampleTests.scala b/example/todoDb/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..eccd913
--- /dev/null
+++ b/example/todoDb/app/test/src/ExampleTests.scala
@@ -0,0 +1,47 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'TodoMvcDb - test(TodoMvcDb){ host =>
+ requests.get(s"$host/list/all").text() ==>
+ """[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":false,"text":"Profit!"}]"""
+ requests.get(s"$host/list/active").text() ==>
+ """[{"id":2,"checked":false,"text":"Profit!"}]"""
+ requests.get(s"$host/list/completed").text() ==>
+ """[{"id":1,"checked":true,"text":"Get started with Cask"}]"""
+
+ requests.post(s"$host/toggle/2")
+
+ requests.get(s"$host/list/all").text() ==>
+ """[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":true,"text":"Profit!"}]"""
+
+ requests.get(s"$host/list/active").text() ==>
+ """[]"""
+
+ requests.post(s"$host/add", data = "new Task")
+
+ requests.get(s"$host/list/active").text() ==>
+ """[{"id":3,"checked":false,"text":"new Task"}]"""
+
+ requests.post(s"$host/delete/3")
+
+ requests.get(s"$host/list/active").text() ==>
+ """[]"""
+ }
+ }
+}
diff --git a/example/todoDb/build.sc b/example/todoDb/build.sc
new file mode 100644
index 0000000..b570b3b
--- /dev/null
+++ b/example/todoDb/build.sc
@@ -0,0 +1,20 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ ivy"org.xerial:sqlite-jdbc:3.18.0",
+ ivy"io.getquill::quill-jdbc:2.5.4"
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/cask/test/src/test/cask/VariableRoutes.scala b/example/variableRoutes/app/src/VariableRoutes.scala
index c997d39..760ab15 100644
--- a/cask/test/src/test/cask/VariableRoutes.scala
+++ b/example/variableRoutes/app/src/VariableRoutes.scala
@@ -1,5 +1,4 @@
-package test.cask
-
+package app
object VariableRoutes extends cask.MainRoutes{
@cask.get("/user/:userName")
def showUserProfile(userName: String) = {
diff --git a/example/variableRoutes/app/test/src/ExampleTests.scala b/example/variableRoutes/app/test/src/ExampleTests.scala
new file mode 100644
index 0000000..49c960b
--- /dev/null
+++ b/example/variableRoutes/app/test/src/ExampleTests.scala
@@ -0,0 +1,48 @@
+package app
+import io.undertow.Undertow
+
+import utest._
+
+object ExampleTests extends TestSuite{
+ def test[T](example: cask.main.BaseMain)(f: String => T): T = {
+ val server = Undertow.builder
+ .addHttpListener(8080, "localhost")
+ .setHandler(example.defaultHandler)
+ .build
+ server.start()
+ val res =
+ try f("http://localhost:8080")
+ finally server.stop()
+ res
+ }
+
+ val tests = Tests{
+ 'VariableRoutes - test(VariableRoutes){ host =>
+ val noIndexPage = requests.get(host)
+ noIndexPage.statusCode ==> 404
+
+ requests.get(s"$host/user/lihaoyi").text() ==> "User lihaoyi"
+
+ requests.get(s"$host/user").statusCode ==> 404
+
+
+ requests.get(s"$host/post/123?param=xyz&param=abc").text() ==>
+ "Post 123 ArrayBuffer(xyz, abc)"
+
+ requests.get(s"$host/post/123").text() ==>
+ """Missing argument: (param: Seq[String])
+ |
+ |Arguments provided did not match expected signature:
+ |
+ |showPost
+ | postId Int
+ | param Seq[String]
+ |
+ |""".stripMargin
+
+ requests.get(s"$host/path/one/two/three").text() ==>
+ "Subpath List(one, two, three)"
+ }
+
+ }
+}
diff --git a/example/variableRoutes/build.sc b/example/variableRoutes/build.sc
new file mode 100644
index 0000000..6b3ab3f
--- /dev/null
+++ b/example/variableRoutes/build.sc
@@ -0,0 +1,18 @@
+import mill._, scalalib._
+
+
+trait AppModule extends ScalaModule{
+ def scalaVersion = "2.12.6"
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::cask:0.0.1",
+ )
+
+ object test extends Tests{
+ def testFrameworks = Seq("utest.runner.Framework")
+ def forkArgs = Seq("--illegal-access=deny")
+ def ivyDeps = Agg(
+ ivy"com.lihaoyi::utest::0.6.3",
+ ivy"com.lihaoyi::requests::0.1.2",
+ )
+ }
+} \ No newline at end of file
diff --git a/upload.sc b/upload.sc
new file mode 100644
index 0000000..6c47295
--- /dev/null
+++ b/upload.sc
@@ -0,0 +1,52 @@
+#!/usr/bin/env amm
+import ammonite.ops._
+import scalaj.http._
+
+@main
+def shorten(longUrl: String) = {
+ println("shorten longUrl " + longUrl)
+ val shortUrl = Http("https://git.io")
+ .postForm(Seq("url" -> longUrl))
+ .asString
+ .headers("Location")
+ .head
+ println("shorten shortUrl " + shortUrl)
+ shortUrl
+}
+@main
+def apply(uploadedFile: Path,
+ tagName: String,
+ uploadName: String,
+ authKey: String): String = {
+ val body = Http("https://api.github.com/repos/lihaoyi/cask/releases/tags/" + tagName)
+ .header("Authorization", "token " + authKey)
+ .asString.body
+
+ val parsed = ujson.read(body)
+
+ println(body)
+
+ val snapshotReleaseId = parsed("id").num.toInt
+
+
+ val uploadUrl =
+ s"https://uploads.github.com/repos/lihaoyi/cask/releases/" +
+ s"$snapshotReleaseId/assets?name=$uploadName"
+
+ val res = Http(uploadUrl)
+ .header("Content-Type", "application/octet-stream")
+ .header("Authorization", "token " + authKey)
+ .timeout(connTimeoutMs = 5000, readTimeoutMs = 60000)
+ .postData(read.bytes! uploadedFile)
+ .asString
+
+ println(res.body)
+ val longUrl = ujson.read(res.body)("browser_download_url").str.toString
+
+ println("Long Url " + longUrl)
+
+ val shortUrl = shorten(longUrl)
+
+ println("Short Url " + shortUrl)
+ shortUrl
+}