Skip to content

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

Merged
merged 6 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _overviews/toolkit/OrderedListOfMdFiles
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ web-server-dynamic.md
web-server-query-parameters.md
web-server-input.md
web-server-websockets.md
web-server-cookies-and-decorators.md
web-server-cookies-and-decorators.md
75 changes: 42 additions & 33 deletions _overviews/toolkit/web-server-dynamic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
You can create an endpoint returning dynamically generated content with `@cask.get` annotation.
You can create an endpoint returning dynamically generated content with the `@cask.get` annotation.


{% tabs web-server-dynamic-1 class=tabs-scala-version %}
{% tab 'Scala 2' %}
Expand All @@ -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()
}
Expand All @@ -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' %}
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The 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 localhost URL.


```
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' %}
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To create an HTML response, you can combine Cask code with the [Scalatags](https://com-lihaoyi.github.io/scalatags/) templating library.
To create an HTML response, you can combine Cask with the [Scalatags](https://com-lihaoyi.github.io/scalatags/) templating library.


Import the Scalatags library:

Expand All @@ -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}
Expand All @@ -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"
Expand All @@ -188,7 +194,7 @@ object MyApp extends cask.MainRoutes {
doctype("html")(
html(
body(
h1(text)
p(text)
)
)
)
Expand All @@ -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`.
73 changes: 39 additions & 34 deletions _overviews/toolkit/web-server-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To create an endpoint that handles the data provided in an HTML form, use `cask.postForm` annotation, give the endpoint method arguments
To create an endpoint that handles the data provided in an HTML form, use the `@cask.postForm` annotation. Add arguments to the endpoint method

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>
Expand All @@ -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()
Expand All @@ -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>
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `formEndpoint` endpoint reads the form data using `name` and `surname` parameters. The names of parameters must
The `formEndpoint` endpoint reads the form data using the `name` and `surname` parameters. The names of parameters must

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
JSON fields are handled in the same way as form fields, except that `cask.PostJson` annotation is used. The fields
JSON fields are handled in the same way as form fields, except that we use the `@cask.PostJson` annotation. The fields

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()
Expand All @@ -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()
Expand All @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
will be returned with 400 response code.
will be returned with the response code 400.


To handle the case when the fields of the JSON are not known in advance, you can use argument with the `ujson.Value` type
Copy link
Member

Choose a reason for hiding this comment

The 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
To handle the case when the fields of the JSON are not known in advance, you can use an argument with the `ujson.Value` type,

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()
Expand All @@ -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()
Expand All @@ -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 {
Expand All @@ -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)}"
Expand All @@ -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))
}
}
Expand Down
Loading
Loading