-
Notifications
You must be signed in to change notification settings - Fork 1k
Add Cask tutorials #3056
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Cask tutorials #3056
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -9,11 +9,9 @@ next-page: web-server-query-parameters | |||||
|
||||||
{% include markdown.html path="_markdown/install-cask.md" %} | ||||||
|
||||||
## Basic example | ||||||
## Serving dynamically generated content | ||||||
|
||||||
To create an endpoint returning dynamically generated content, use `@cask.get` annotation. | ||||||
|
||||||
For example, create an endpoint that returns the current date and time. | ||||||
You can create an endpoint returning dynamically generated content with `@cask.get` annotation. | ||||||
|
||||||
{% tabs web-server-dynamic-1 class=tabs-scala-version %} | ||||||
{% tab 'Scala 2' %} | ||||||
|
@@ -22,7 +20,7 @@ import java.time.ZonedDateTime | |||||
|
||||||
object MyApp extends cask.MainRoutes { | ||||||
@cask.get("/time") | ||||||
def dynamic() = s"Current date is: ${ZonedDateTime.now()}" | ||||||
def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}" | ||||||
|
||||||
initialize() | ||||||
} | ||||||
|
@@ -34,21 +32,29 @@ import java.time.ZonedDateTime | |||||
|
||||||
object MyApp extends cask.MainRoutes: | ||||||
@cask.get("/time") | ||||||
def dynamic() = s"Current date is: ${ZonedDateTime.now()}" | ||||||
def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}" | ||||||
|
||||||
initialize() | ||||||
``` | ||||||
{% endtab %} | ||||||
{% endtabs %} | ||||||
|
||||||
The example above creates an endpoint returning the current date and time available at `/time`. The exact response will be | ||||||
recreated every time you refresh the webpage. | ||||||
|
||||||
Since the endpoint method has `String` output type, the result will be sent with `text/plain` [content type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type). | ||||||
If you want an HTML output being interpreted by the browser, you else need to set the `Content-Type` header manually | ||||||
or [use the Scalatags templating library](/toolkit/web-server-dynamic.html#using-html-templates), supported by Cask. | ||||||
|
||||||
### Running the example | ||||||
|
||||||
Run the example the same way as before (assuming you use the same project structure). | ||||||
Run the example the same way as before, assuming you use the same project structure as described in [the static file tutorial](/toolkit/web-server-static.html). | ||||||
|
||||||
{% tabs web-server-dynamic-2 class=tabs-build-tool %} | ||||||
{% tab 'Scala CLI' %} | ||||||
In the terminal, the following command will start the server: | ||||||
``` | ||||||
scala-cli run Example.sc | ||||||
scala-cli run Example.scala | ||||||
``` | ||||||
{% endtab %} | ||||||
{% tab 'sbt' %} | ||||||
|
@@ -65,17 +71,17 @@ In the terminal, the following command will start the server: | |||||
{% endtab %} | ||||||
{% endtabs %} | ||||||
|
||||||
Access the endpoint at [http://localhost:8080/time](http://localhost:8080/time). You should see a result similar to the | ||||||
one below. | ||||||
Access [the endpoint](http://localhost:8080/time). You should see a result similar to the one below. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO the previous text, with the link appearing in the text, was better here. We should actively show the user that we're sending them to a |
||||||
|
||||||
``` | ||||||
Current date is: 2024-07-22T09:07:05.752534+02:00[Europe/Warsaw] | ||||||
``` | ||||||
|
||||||
## Using path segments | ||||||
|
||||||
You can use path segments to specify the returned data more precisely. Building on the example above, add the `:city` | ||||||
segment to get the current time in a city of choice. | ||||||
Cask gives you the ability to access segments of the URL path withing the endpoint function. | ||||||
Building on the example above, you can add a segment to specify that the endpoint should return the date and time | ||||||
in a given city. | ||||||
|
||||||
{% tabs web-server-dynamic-3 class=tabs-scala-version %} | ||||||
{% tab 'Scala 2' %} | ||||||
|
@@ -90,7 +96,7 @@ object MyApp extends cask.MainRoutes { | |||||
} | ||||||
|
||||||
@cask.get("/time/:city") | ||||||
def dynamicWithCity(city: String) = { | ||||||
def dynamicWithCity(city: String): String = { | ||||||
getZoneIdForCity(city) match { | ||||||
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" | ||||||
case None => s"Couldn't find time zone for city $city" | ||||||
|
@@ -112,7 +118,7 @@ object MyApp extends cask.MainRoutes: | |||||
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) | ||||||
|
||||||
@cask.get("/time/:city") | ||||||
def dynamicWithCity(city: String) = | ||||||
def dynamicWithCity(city: String): String = | ||||||
getZoneIdForCity(city) match | ||||||
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" | ||||||
case None => s"Couldn't find time zone for city $city" | ||||||
|
@@ -122,23 +128,22 @@ object MyApp extends cask.MainRoutes: | |||||
{% endtab %} | ||||||
{% endtabs %} | ||||||
|
||||||
Accessing the endpoint at [http://localhost:8080/time/Paris](http://localhost:8080/time/Paris) will result with: | ||||||
``` | ||||||
Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris] | ||||||
``` | ||||||
In the example above, the `:city` segment in `/time/:city` is available through the `city` argument of the endpoint method. | ||||||
The name of the argument must be identical to the segment name. The `getZoneIdForCity` helper method find the timezone for | ||||||
a given city and then the current date and time is translated to that timezone. | ||||||
|
||||||
and at [http://localhost:8080/time/Tokyo](http://localhost:8080/time/Tokyo) you will see: | ||||||
Accessing [the endpoint](http://localhost:8080/time/Paris) (notice the `Paris` segment in the URL) will result with: | ||||||
``` | ||||||
Current date is: 2024-07-22T16:08:41.137563+09:00[Asia/Tokyo] | ||||||
Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris] | ||||||
``` | ||||||
|
||||||
Cask endpoints can handle either fixed or arbitrary number of path segments. Please consult the | ||||||
[documentation](https://com-lihaoyi.github.io/cask/index.html#variable-routes) for more details. | ||||||
You can use more than one path segment in an endpoint by adding more arguments to the endpoint method. It's also possible to use paths | ||||||
with an unspecified number of segments (for example `/path/foo/bar/baz/`) by giving the endpoint method an argument with `cask.RemainingPathSegments` type. | ||||||
Consult the [documentation](https://com-lihaoyi.github.io/cask/index.html#variable-routes) for more details. | ||||||
|
||||||
## Using HTML templates | ||||||
|
||||||
You can combine Cask code with a templating library like [Scalatags](https://com-lihaoyi.github.io/scalatags/) to | ||||||
build an HTML response. | ||||||
To create an HTML response, you can combine Cask code with the [Scalatags](https://com-lihaoyi.github.io/scalatags/) templating library. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
Import the Scalatags library: | ||||||
|
||||||
|
@@ -163,9 +168,10 @@ ivy"com.lihaoyi::scalatags::0.13.1" | |||||
{% endtab %} | ||||||
{% endtabs %} | ||||||
|
||||||
Now the example above can be rewritten to use a template. Cask will build a response out of the `doctype` automatically. | ||||||
Now the example above can be rewritten to use a template. Cask will build a response out of the `doctype` automatically, | ||||||
setting the `Content-Type` header to `text/html`. | ||||||
|
||||||
{% tabs web-server-dynamic-3 class=tabs-scala-version %} | ||||||
{% tabs web-server-dynamic-5 class=tabs-scala-version %} | ||||||
{% tab 'Scala 2' %} | ||||||
```scala | ||||||
import java.time.{ZoneId, ZonedDateTime} | ||||||
|
@@ -179,7 +185,7 @@ object MyApp extends cask.MainRoutes { | |||||
} | ||||||
|
||||||
@cask.get("/time/:city") | ||||||
def dynamicWithCity(city: String) = { | ||||||
def dynamicWithCity(city: String): doctype = { | ||||||
val text = getZoneIdForCity(city) match { | ||||||
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" | ||||||
case None => s"Couldn't find time zone for city $city" | ||||||
|
@@ -188,7 +194,7 @@ object MyApp extends cask.MainRoutes { | |||||
doctype("html")( | ||||||
html( | ||||||
body( | ||||||
h1(text) | ||||||
p(text) | ||||||
) | ||||||
) | ||||||
) | ||||||
|
@@ -210,19 +216,22 @@ object MyApp extends cask.MainRoutes: | |||||
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) | ||||||
|
||||||
@cask.get("/time/:city") | ||||||
def dynamicWithCity(city: String) = | ||||||
def dynamicWithCity(city: String): doctype = | ||||||
val text = getZoneIdForCity(city) match | ||||||
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" | ||||||
case None => s"Couldn't find time zone for city $city" | ||||||
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" | ||||||
case None => s"Couldn't find time zone for city $city" | ||||||
doctype("html")( | ||||||
html( | ||||||
body( | ||||||
h1(text) | ||||||
p(text) | ||||||
) | ||||||
) | ||||||
) | ||||||
|
||||||
initialize() | ||||||
``` | ||||||
{% endtab %} | ||||||
{% endtabs %} | ||||||
{% endtabs %} | ||||||
|
||||||
Here we get the text of response and wrap it in a Scalatags template. Notice that the return type changed from `String` | ||||||
to `doctype`. |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -11,23 +11,16 @@ next-page: web-server-websockets | |||||
|
||||||
## Handling form-encoded input | ||||||
|
||||||
Similarly to path segments and query parameters, form fields are read by using endpoint method arguments. Use `cask.postForm` | ||||||
annotation and set the HTML form method to `post`. | ||||||
|
||||||
In this example we create a form asking for name and surname of a user and then redirect to a greeting page. Notice the | ||||||
use of `cask.Response`. The default returned content type is `text/plain`, set it to `text/html` in order for browser to display | ||||||
the form correctly. | ||||||
|
||||||
The `formMethod` endpoint reads the form data using `name` and `surname` parameters. The names of parameters must | ||||||
be identical to the field names of the form. | ||||||
To create an endpoint that handles the data provided in an HTML form, use `cask.postForm` annotation, give the endpoint method arguments | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
with names corresponding to names of fields in the form and set the form method to `post`. | ||||||
|
||||||
{% tabs web-server-input-1 class=tabs-scala-version %} | ||||||
{% tab 'Scala 2' %} | ||||||
```scala | ||||||
object MyApp extends cask.MainRoutes { | ||||||
|
||||||
@cask.get("/form") | ||||||
def getForm() = { | ||||||
def getForm(): String = { | ||||||
val html = | ||||||
"""<!doctype html> | ||||||
|<html> | ||||||
|
@@ -46,7 +39,7 @@ object MyApp extends cask.MainRoutes { | |||||
} | ||||||
|
||||||
@cask.postForm("/form") | ||||||
def formEndpoint(name: String, surname: String) = | ||||||
def formEndpoint(name: String, surname: String): String = | ||||||
"Hello " + name + " " + surname | ||||||
|
||||||
initialize() | ||||||
|
@@ -58,7 +51,7 @@ object MyApp extends cask.MainRoutes { | |||||
object MyApp extends cask.MainRoutes: | ||||||
|
||||||
@cask.get("/form") | ||||||
def getForm() = | ||||||
def getForm(): String = | ||||||
val html = | ||||||
"""<!doctype html> | ||||||
|<html> | ||||||
|
@@ -76,27 +69,33 @@ object MyApp extends cask.MainRoutes: | |||||
cask.Response(data = html, headers = Seq("Content-Type" -> "text/html")) | ||||||
|
||||||
@cask.postForm("/form") | ||||||
def formEndpoint(name: String, surname: String) = | ||||||
def formEndpoint(name: String, surname: String): String = | ||||||
"Hello " + name + " " + surname | ||||||
|
||||||
initialize() | ||||||
``` | ||||||
{% endtab %} | ||||||
{% endtabs %} | ||||||
|
||||||
In this example we create a form asking for name and surname of a user and then redirect the user to a greeting page. Notice the | ||||||
use of `cask.Response`. The default returned content type in case of `String` returning endpoint method is `text/plain`, | ||||||
set it to `text/html` in order for browser to display the form correctly. | ||||||
|
||||||
The `formEndpoint` endpoint reads the form data using `name` and `surname` parameters. The names of parameters must | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
be identical to the field names of the form. | ||||||
|
||||||
## Handling JSON-encoded input | ||||||
|
||||||
JSON fields are handled in the same way as form fields, except that `cask.PostJson` annotation is used. The topmost fields | ||||||
will be read into the endpoint method arguments and if any of them is missing or has an incorrect type, an error message | ||||||
will be returned with 400 response code. | ||||||
JSON fields are handled in the same way as form fields, except that `cask.PostJson` annotation is used. The fields | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
will be read into the endpoint method arguments. | ||||||
|
||||||
{% tabs web-server-input-2 class=tabs-scala-version %} | ||||||
{% tab 'Scala 2' %} | ||||||
```scala | ||||||
object MyApp extends cask.MainRoutes { | ||||||
|
||||||
@cask.postJson("/json") | ||||||
def jsonEndpoint(name: String, surname: String) = | ||||||
def jsonEndpoint(name: String, surname: String): String = | ||||||
"Hello " + name + " " + surname | ||||||
|
||||||
initialize() | ||||||
|
@@ -107,8 +106,8 @@ object MyApp extends cask.MainRoutes { | |||||
```scala | ||||||
object MyApp extends cask.MainRoutes: | ||||||
|
||||||
@cask.postJson("/json") | ||||||
def jsonEndpoint(name: String, surname: String) = | ||||||
@cask.postJson("/json") | ||||||
def jsonEndpoint(name: String, surname: String): String = | ||||||
"Hello " + name + " " + surname | ||||||
|
||||||
initialize() | ||||||
|
@@ -129,15 +128,20 @@ The response will be: | |||||
Hello John Smith | ||||||
``` | ||||||
|
||||||
Deserialization is handled by uPickle JSON library. To deserialize an object, use `ujson.Value` type. | ||||||
The endpoint will accept JSONs that have only the fields with names specified as the endpoint method arguments. If there | ||||||
are more fields than expected, some fields are missing or have an incorrect data type, an error message | ||||||
will be returned with 400 response code. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
To handle the case when the fields of the JSON are not known in advance, you can use argument with the `ujson.Value` type | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
from uPickle library. | ||||||
|
||||||
{% tabs web-server-input-3 class=tabs-scala-version %} | ||||||
{% tab 'Scala 2' %} | ||||||
```scala | ||||||
object MyApp extends cask.MainRoutes { | ||||||
|
||||||
@cask.postJson("/json") | ||||||
def jsonEndpoint(value: ujson.Value) = | ||||||
def jsonEndpoint(value: ujson.Value): String = | ||||||
value.toString | ||||||
|
||||||
initialize() | ||||||
|
@@ -150,7 +154,7 @@ object MyApp extends cask.MainRoutes { | |||||
object MyApp extends cask.MainRoutes: | ||||||
|
||||||
@cask.postJson("/json") | ||||||
def jsonEndpoint(value: ujson.Value) = | ||||||
def jsonEndpoint(value: ujson.Value): String = | ||||||
value.toString | ||||||
|
||||||
initialize() | ||||||
|
@@ -159,27 +163,30 @@ object MyApp extends cask.MainRoutes: | |||||
{% endtab %} | ||||||
{% endtabs %} | ||||||
|
||||||
In this example the JSON is merely converted to `String`, check the [*uPickle tutorial*](/toolkit/json-introduction.html) for more information | ||||||
on what can be done with `ujson.Value` type. | ||||||
|
||||||
Send a POST request. | ||||||
```shell | ||||||
curl --header "Content-Type: application/json" \ | ||||||
--data '{"value":{"name":"John","surname":"Smith"}}' \ | ||||||
http://localhost:8080/json2 | ||||||
``` | ||||||
|
||||||
Server will respond with: | ||||||
The server will respond with: | ||||||
``` | ||||||
"{\"name\":\"John\",\"surname\":\"Smith\"}" | ||||||
``` | ||||||
|
||||||
## Handling JSON-encoded output | ||||||
|
||||||
Cask endpoint can return JSON objects returned by uPickle library functions. Cask will automatically handle the `ujson.Value` | ||||||
type and set the `Content-Type application/json` header. | ||||||
type and set the `Content-Type` header to `application/json`. | ||||||
|
||||||
In this example we use a simple `TimeData` case class to send information about the time zone and current time in a chosen | ||||||
location. To serialize a case class into JSON you need to define a serializer in its companion object. | ||||||
In this example `TimeData` case class stores the information about the time zone and current time in a chosen | ||||||
location. To serialize a case class into JSON, use type class derivation or define the serializer in its companion object in case of Scala 2. | ||||||
|
||||||
{% tabs web-server-input-3 class=tabs-scala-version %} | ||||||
{% tabs web-server-input-4 class=tabs-scala-version %} | ||||||
{% tab 'Scala 2' %} | ||||||
```scala | ||||||
object MyApp extends cask.MainRoutes { | ||||||
|
@@ -195,7 +202,7 @@ object MyApp extends cask.MainRoutes { | |||||
} | ||||||
|
||||||
@cask.get("/time_json/:city") | ||||||
def timeJSON(city: String) = { | ||||||
def timeJSON(city: String): ujson.Value = { | ||||||
val timezone = getZoneIdForCity(city) | ||||||
val time = timezone match { | ||||||
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" | ||||||
|
@@ -210,20 +217,18 @@ object MyApp extends cask.MainRoutes { | |||||
```scala | ||||||
object MyApp extends cask.MainRoutes { | ||||||
import upickle.default.{ReadWriter, macroRW, writeJs} | ||||||
case class TimeData(timezone: Option[String], time: String) | ||||||
object TimeData: | ||||||
given rw: ReadWriter[TimeData]= macroRW | ||||||
case class TimeData(timezone: Option[String], time: String) derives ReadWriter | ||||||
|
||||||
private def getZoneIdForCity(city: String): Option[ZoneId] = | ||||||
import scala.jdk.CollectionConverters.* | ||||||
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) | ||||||
|
||||||
@cask.get("/time_json/:city") | ||||||
def timeJSON(city: String) = { | ||||||
def timeJSON(city: String): ujson.Value = { | ||||||
val timezone = getZoneIdForCity(city) | ||||||
val time = timezone match | ||||||
case Some(zoneId)=> s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" | ||||||
case None => s"Couldn't find time zone for city $city" | ||||||
case Some(zoneId)=> s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" | ||||||
case None => s"Couldn't find time zone for city $city" | ||||||
writeJs(TimeData(timezone.map(_.toString), time)) | ||||||
} | ||||||
} | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.