diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2018-08-12 22:18:39 +0800 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2018-08-12 22:18:39 +0800 |
commit | fd9c399db8c1c0d86cc65d5e1c41968b42a813d1 (patch) | |
tree | 8e8fc2875cb1c26f309384a9ca0ad72e1fa893f3 | |
parent | 9bf8c31fa9321558d7d02f6a5b687cd55a924e7f (diff) | |
download | cask-fd9c399db8c1c0d86cc65d5e1c41968b42a813d1.tar.gz cask-fd9c399db8c1c0d86cc65d5e1c41968b42a813d1.tar.bz2 cask-fd9c399db8c1c0d86cc65d5e1c41968b42a813d1.zip |
auto-upload examples
57 files changed, 1907 insertions, 252 deletions
@@ -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¶m=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¶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 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¶m=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 +} |