Skip to content

Commit 02c00cc

Browse files
authoredOct 1, 2024··
Add Cask tutorials (#3056)
1 parent c808a69 commit 02c00cc

11 files changed

+1091
-1
lines changed
 

‎_includes/_markdown/install-cask.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{% altDetails require-info-box 'Getting Cask' %}
2+
3+
{% tabs cask-install class=tabs-build-tool %}
4+
5+
{% tab 'Scala CLI' %}
6+
You can declare a dependency on Cask with the following `using` directive:
7+
```scala
8+
//> using dep "com.lihaoyi::cask::0.9.2"
9+
```
10+
{% endtab %}
11+
12+
{% tab 'sbt' %}
13+
In your `build.sbt`, you can add a dependency on Cask:
14+
```scala
15+
lazy val example = project.in(file("example"))
16+
.settings(
17+
scalaVersion := "3.4.2",
18+
libraryDependencies += "com.lihaoyi" %% "cask" % "0.9.2",
19+
fork := true
20+
)
21+
```
22+
{% endtab %}
23+
24+
{% tab 'Mill' %}
25+
In your `build.sc`, you can add a dependency on Cask:
26+
```scala
27+
object example extends RootModule with ScalaModule {
28+
def scalaVersion = "3.3.3"
29+
def ivyDeps = Agg(
30+
ivy"com.lihaoyi::cask::0.9.2"
31+
)
32+
}
33+
```
34+
{% endtab %}
35+
36+
{% endtabs %}
37+
{% endaltDetails %}

‎_overviews/toolkit/OrderedListOfMdFiles

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,10 @@ http-client-request-body.md
2727
http-client-json.md
2828
http-client-upload-file.md
2929
http-client-what-else.md
30+
web-server-intro.md
31+
web-server-static.md
32+
web-server-dynamic.md
33+
web-server-query-parameters.md
34+
web-server-input.md
35+
web-server-websockets.md
36+
web-server-cookies-and-decorators.md

‎_overviews/toolkit/http-client-what-else.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ type: section
44
description: An incomplete list of features of sttp
55
num: 29
66
previous-page: http-client-upload-file
7-
next-page:
7+
next-page: web-server-intro
88
---
99

1010
{% include markdown.html path="_markdown/install-upickle.md" %}

‎_overviews/toolkit/introduction.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ toolkit-index:
2222
description: Sending HTTP requests and uploading files with sttp.
2323
icon: "fa fa-globe"
2424
link: /toolkit/http-client-intro.html
25+
- title: Web servers
26+
description: Building web servers with Cask.
27+
icon: "fa fa-server"
28+
link: /toolkit/web-server-intro.html
2529
---
2630

2731
## What is the Scala Toolkit?
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
---
2+
title: How to use cookies and decorators?
3+
type: section
4+
description: Using cookies and decorators with Cask
5+
num: 36
6+
previous-page: web-server-websockets
7+
next-page:
8+
---
9+
10+
{% include markdown.html path="_markdown/install-cask.md" %}
11+
12+
## Using cookies
13+
14+
Cookies are saved by adding them to the `cookies` parameter of the `cask.Response` constructor.
15+
16+
In this example, we are building a rudimentary authentication service. The `getLogin` method provides a form where
17+
the user can enter their username and password. The `postLogin` method reads the credentials. If they match the expected ones, it generates a session
18+
identifier is generated, saves it in the application state, and sends back a cookie with the identifier.
19+
20+
Cookies can be read either with a method parameter of `cask.Cookie` type or by accessing the `cask.Request` directly.
21+
If using the former method, the names of parameters have to match the names of cookies. If a cookie with a matching name is not
22+
found, an error response will be returned. In the `checkLogin` function, the former method is used, as the cookie is not
23+
present before the user logs in.
24+
25+
To delete a cookie, set its `expires` parameter to an instant in the past, for example `Instant.EPOCH`.
26+
27+
{% tabs web-server-cookies-1 class=tabs-scala-version %}
28+
{% tab 'Scala 2' %}
29+
30+
```scala
31+
import java.util.UUID
32+
import java.util.concurrent.ConcurrentHashMap
33+
34+
object Example extends cask.MainRoutes {
35+
36+
val sessionIds = ConcurrentHashMap.newKeySet[String]()
37+
38+
@cask.get("/login")
39+
def getLogin(): cask.Response[String] = {
40+
val html =
41+
"""<!doctype html>
42+
|<html>
43+
|<body>
44+
|<form action="/login" method="post">
45+
| <label for="name">Username:</label><br>
46+
| <input type="text" name="name" value=""><br>
47+
| <label for="password">Password:</label><br>
48+
| <input type="text" name="password" value=""><br><br>
49+
| <input type="submit" value="Submit">
50+
|</form>
51+
|</body>
52+
|</html>""".stripMargin
53+
54+
cask.Response(data = html, headers = Seq("Content-Type" -> "text/html"))
55+
}
56+
57+
@cask.postForm("/login")
58+
def postLogin(name: String, password: String): cask.Response[String] = {
59+
if (name == "user" && password == "password") {
60+
val sessionId = UUID.randomUUID().toString
61+
sessionIds.add(sessionId)
62+
cask.Response(data = "Success!", cookies = Seq(cask.Cookie("sessionId", sessionId)))
63+
} else {
64+
cask.Response(data = "Authentication failed", statusCode = 401)
65+
}
66+
}
67+
68+
@cask.get("/check")
69+
def checkLogin(request: cask.Request): String = {
70+
val sessionId = request.cookies.get("sessionId")
71+
if (sessionId.exists(cookie => sessionIds.contains(cookie.value))) {
72+
"You are logged in"
73+
} else {
74+
"You are not logged in"
75+
}
76+
}
77+
78+
@cask.get("/logout")
79+
def logout(sessionId: cask.Cookie) = {
80+
sessionIds.remove(sessionId.value)
81+
cask.Response(data = "Successfully logged out!", cookies = Seq(cask.Cookie("sessionId", "", expires = Instant.EPOCH)))
82+
}
83+
84+
initialize()
85+
}
86+
```
87+
{% endtab %}
88+
{% tab 'Scala 3' %}
89+
```scala
90+
import java.util.UUID
91+
import java.util.concurrent.ConcurrentHashMap
92+
93+
object Example extends cask.MainRoutes:
94+
95+
val sessionIds = ConcurrentHashMap.newKeySet[String]()
96+
97+
@cask.get("/login")
98+
def getLogin(): cask.Response[String] =
99+
val html =
100+
"""<!doctype html>
101+
|<html>
102+
|<body>
103+
|<form action="/login" method="post">
104+
| <label for="name">Username:</label><br>
105+
| <input type="text" name="name" value=""><br>
106+
| <label for="password">Password:</label><br>
107+
| <input type="text" name="password" value=""><br><br>
108+
| <input type="submit" value="Submit">
109+
|</form>
110+
|</body>
111+
|</html>""".stripMargin
112+
113+
cask.Response(data = html, headers = Seq("Content-Type" -> "text/html"))
114+
115+
@cask.postForm("/login")
116+
def postLogin(name: String, password: String): cask.Response[String] =
117+
if name == "user" && password == "password" then
118+
val sessionId = UUID.randomUUID().toString
119+
sessionIds.add(sessionId)
120+
cask.Response(data = "Success!", cookies = Seq(cask.Cookie("sessionId", sessionId)))
121+
else
122+
cask.Response(data = "Authentication failed", statusCode = 401)
123+
124+
@cask.get("/check")
125+
def checkLogin(request: cask.Request): String =
126+
val sessionId = request.cookies.get("sessionId")
127+
if sessionId.exists(cookie => sessionIds.contains(cookie.value)) then
128+
"You are logged in"
129+
else
130+
"You are not logged in"
131+
132+
@cask.get("/logout")
133+
def logout(sessionId: cask.Cookie): cask.Response[String] =
134+
sessionIds.remove(sessionId.value)
135+
cask.Response(data = "Successfully logged out!", cookies = Seq(cask.Cookie("sessionId", "", expires = Instant.EPOCH)))
136+
137+
initialize()
138+
```
139+
{% endtab %}
140+
{% endtabs %}
141+
142+
## Using decorators
143+
144+
Decorators can be used for extending endpoints functionality with validation or new parameters. They are defined by extending
145+
`cask.RawDecorator` class. They are used as annotations.
146+
147+
In this example, the `loggedIn` decorator is used to check if the user is logged in before accessing the `/decorated`
148+
endpoint.
149+
150+
The decorator class can pass additional arguments to the decorated endpoint using a map. The passed arguments are available
151+
through the last argument group. Here we are passing the session identifier to an argument named `sessionId`.
152+
153+
{% tabs web-server-cookies-2 class=tabs-scala-version %}
154+
{% tab 'Scala 2' %}
155+
```scala
156+
class loggedIn extends cask.RawDecorator {
157+
override def wrapFunction(ctx: cask.Request, delegate: Delegate): Result[Raw] = {
158+
ctx.cookies.get("sessionId") match {
159+
case Some(cookie) if sessionIds.contains(cookie.value) => delegate(Map("sessionId" -> cookie.value))
160+
case _ => cask.router.Result.Success(cask.model.Response("You aren't logged in", 403))
161+
}
162+
}
163+
}
164+
165+
@loggedIn()
166+
@cask.get("/decorated")
167+
def decorated()(sessionId: String): String = {
168+
s"You are logged in with id: $sessionId"
169+
}
170+
```
171+
{% endtab %}
172+
{% tab 'Scala 3' %}
173+
```scala
174+
class loggedIn extends cask.RawDecorator:
175+
override def wrapFunction(ctx: cask.Request, delegate: Delegate): Result[Raw] =
176+
ctx.cookies.get("sessionId") match
177+
case Some(cookie) if sessionIds.contains(cookie.value) =>
178+
delegate(Map("sessionId" -> cookie.value))
179+
case _ =>
180+
cask.router.Result.Success(cask.model.Response("You aren't logged in", 403))
181+
182+
183+
@loggedIn()
184+
@cask.get("/decorated")
185+
def decorated()(sessionId: String): String = s"You are logged in with id: $sessionId"
186+
```
187+
{% endtab %}
188+
{% endtabs %}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
---
2+
title: How to serve a dynamic page?
3+
type: section
4+
description: Serving a dynamic page with Cask
5+
num: 32
6+
previous-page: web-server-static
7+
next-page: web-server-query-parameters
8+
---
9+
10+
{% include markdown.html path="_markdown/install-cask.md" %}
11+
12+
## Serving dynamically generated content
13+
14+
You can create an endpoint returning dynamically generated content with the `@cask.get` annotation.
15+
16+
{% tabs web-server-dynamic-1 class=tabs-scala-version %}
17+
{% tab 'Scala 2' %}
18+
```scala
19+
import java.time.ZonedDateTime
20+
21+
object Example extends cask.MainRoutes {
22+
@cask.get("/time")
23+
def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}"
24+
25+
initialize()
26+
}
27+
```
28+
{% endtab %}
29+
{% tab 'Scala 3' %}
30+
```scala
31+
import java.time.ZonedDateTime
32+
33+
object Example extends cask.MainRoutes:
34+
@cask.get("/time")
35+
def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}"
36+
37+
initialize()
38+
```
39+
{% endtab %}
40+
{% endtabs %}
41+
42+
The example above creates an endpoint that returns the current date and time available at `/time`. The exact response will be
43+
recreated every time you refresh the webpage.
44+
45+
Since the endpoint method has the `String` output type, the result will be sent with the `text/plain` [content type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type).
46+
If you want an HTML output to be interpreted by the browser, you will need to set the `Content-Type` header manually
47+
or [use the Scalatags templating library](/toolkit/web-server-dynamic.html#using-html-templates), supported by Cask.
48+
49+
### Running the example
50+
51+
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).
52+
53+
{% tabs web-server-dynamic-2 class=tabs-build-tool %}
54+
{% tab 'Scala CLI' %}
55+
In the terminal, the following command will start the server:
56+
```
57+
scala-cli run Example.scala
58+
```
59+
{% endtab %}
60+
{% tab 'sbt' %}
61+
In the terminal, the following command will start the server:
62+
```
63+
sbt example/run
64+
```
65+
{% endtab %}
66+
{% tab 'Mill' %}
67+
In the terminal, the following command will start the server:
68+
```
69+
./mill run
70+
```
71+
{% endtab %}
72+
{% endtabs %}
73+
74+
Access the endpoint at [http://localhost:8080/time](http://localhost:8080/time). You should see a result similar to the one below.
75+
76+
```
77+
Current date is: 2024-07-22T09:07:05.752534+02:00[Europe/Warsaw]
78+
```
79+
80+
## Using path segments
81+
82+
Cask gives you the ability to access segments of the URL path within the endpoint function.
83+
Building on the example above, you can add a segment to specify that the endpoint should return the date and time
84+
in a given city.
85+
86+
{% tabs web-server-dynamic-3 class=tabs-scala-version %}
87+
{% tab 'Scala 2' %}
88+
```scala
89+
import java.time.{ZoneId, ZonedDateTime}
90+
91+
object Example extends cask.MainRoutes {
92+
93+
private def getZoneIdForCity(city: String): Option[ZoneId] = {
94+
import scala.jdk.CollectionConverters._
95+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
96+
}
97+
98+
@cask.get("/time/:city")
99+
def dynamicWithCity(city: String): String = {
100+
getZoneIdForCity(city) match {
101+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
102+
case None => s"Couldn't find time zone for city $city"
103+
}
104+
}
105+
106+
initialize()
107+
}
108+
```
109+
{% endtab %}
110+
{% tab 'Scala 3' %}
111+
```scala
112+
import java.time.{ZoneId, ZonedDateTime}
113+
114+
object Example extends cask.MainRoutes:
115+
116+
private def getZoneIdForCity(city: String): Option[ZoneId] =
117+
import scala.jdk.CollectionConverters.*
118+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
119+
120+
@cask.get("/time/:city")
121+
def dynamicWithCity(city: String): String =
122+
getZoneIdForCity(city) match
123+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
124+
case None => s"Couldn't find time zone for city $city"
125+
126+
initialize()
127+
```
128+
{% endtab %}
129+
{% endtabs %}
130+
131+
In the example above, the `:city` segment in `/time/:city` is available through the `city` argument of the endpoint method.
132+
The name of the argument must be identical to the segment name. The `getZoneIdForCity` helper method finds the timezone for
133+
a given city, and then the current date and time are translated to that timezone.
134+
135+
Accessing the endpoint at [http://localhost:8080/time/Paris](http://localhost:8080/time/Paris) will result in:
136+
```
137+
Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris]
138+
```
139+
140+
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
141+
with an unspecified number of segments (for example `/path/foo/bar/baz/`) by giving the endpoint method an argument with `cask.RemainingPathSegments` type.
142+
Consult the [documentation](https://com-lihaoyi.github.io/cask/index.html#variable-routes) for more details.
143+
144+
## Using HTML templates
145+
146+
To create an HTML response, you can combine Cask with the [Scalatags](https://com-lihaoyi.github.io/scalatags/) templating library.
147+
148+
Import the Scalatags library:
149+
150+
{% tabs web-server-dynamic-4 class=tabs-build-tool %}
151+
{% tab 'Scala CLI' %}
152+
Add the Scalatags dependency in `Example.sc` file:
153+
```scala
154+
//> using dep "com.lihaoyi::scalatags::0.13.1"
155+
```
156+
{% endtab %}
157+
{% tab 'sbt' %}
158+
Add the Scalatags dependency in `build.sbt` file:
159+
```scala
160+
libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.13.1"
161+
```
162+
{% endtab %}
163+
{% tab 'Mill' %}
164+
Add the Scalatags dependency in `build.cs` file:
165+
```scala
166+
ivy"com.lihaoyi::scalatags::0.13.1"
167+
```
168+
{% endtab %}
169+
{% endtabs %}
170+
171+
Now the example above can be rewritten to use a template. Cask will build a response out of the `doctype` automatically,
172+
setting the `Content-Type` header to `text/html`.
173+
174+
{% tabs web-server-dynamic-5 class=tabs-scala-version %}
175+
{% tab 'Scala 2' %}
176+
```scala
177+
import java.time.{ZoneId, ZonedDateTime}
178+
import scalatags.Text.all._
179+
180+
object Example extends cask.MainRoutes {
181+
182+
private def getZoneIdForCity(city: String): Option[ZoneId] = {
183+
import scala.jdk.CollectionConverters._
184+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
185+
}
186+
187+
@cask.get("/time/:city")
188+
def dynamicWithCity(city: String): doctype = {
189+
val text = getZoneIdForCity(city) match {
190+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
191+
case None => s"Couldn't find time zone for city $city"
192+
}
193+
194+
doctype("html")(
195+
html(
196+
body(
197+
p(text)
198+
)
199+
)
200+
)
201+
}
202+
203+
initialize()
204+
}
205+
```
206+
{% endtab %}
207+
{% tab 'Scala 3' %}
208+
```scala
209+
import java.time.{ZoneId, ZonedDateTime}
210+
import scalatags.Text.all.*
211+
212+
object Example extends cask.MainRoutes:
213+
214+
private def getZoneIdForCity(city: String): Option[ZoneId] =
215+
import scala.jdk.CollectionConverters.*
216+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
217+
218+
@cask.get("/time/:city")
219+
def dynamicWithCity(city: String): doctype =
220+
val text = getZoneIdForCity(city) match
221+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
222+
case None => s"Couldn't find time zone for city $city"
223+
doctype("html")(
224+
html(
225+
body(
226+
p(text)
227+
)
228+
)
229+
)
230+
231+
initialize()
232+
```
233+
{% endtab %}
234+
{% endtabs %}
235+
236+
Here we get the text of the response and wrap it in a Scalatags template. Notice that the return type changed from `String`
237+
to `doctype`.
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
---
2+
title: How to handle user input?
3+
type: section
4+
description: Handling user input with Cask
5+
num: 34
6+
previous-page: web-server-query-parameters
7+
next-page: web-server-websockets
8+
---
9+
10+
{% include markdown.html path="_markdown/install-cask.md" %}
11+
12+
## Handling form-encoded input
13+
14+
To create an endpoint that handles the data provided in an HTML form, use the `@cask.postForm` annotation. Add arguments to the endpoint method
15+
with names corresponding to names of fields in the form and set the form method to `post`.
16+
17+
{% tabs web-server-input-1 class=tabs-scala-version %}
18+
{% tab 'Scala 2' %}
19+
```scala
20+
object Example extends cask.MainRoutes {
21+
22+
@cask.get("/form")
23+
def getForm(): cask.Response[String] = {
24+
val html =
25+
"""<!doctype html>
26+
|<html>
27+
|<body>
28+
|<form action="/form" method="post">
29+
| <label for="name">First name:</label><br>
30+
| <input type="text" name="name" value=""><br>
31+
| <label for="surname">Last name:</label><br>
32+
| <input type="text" name="surname" value=""><br><br>
33+
| <input type="submit" value="Submit">
34+
|</form>
35+
|</body>
36+
|</html>""".stripMargin
37+
38+
cask.Response(data = html, headers = Seq("Content-Type" -> "text/html"))
39+
}
40+
41+
@cask.postForm("/form")
42+
def formEndpoint(name: String, surname: String): String =
43+
"Hello " + name + " " + surname
44+
45+
initialize()
46+
}
47+
```
48+
{% endtab %}
49+
{% tab 'Scala 3' %}
50+
```scala
51+
object Example extends cask.MainRoutes:
52+
53+
@cask.get("/form")
54+
def getForm(): cask.Response[String] =
55+
val html =
56+
"""<!doctype html>
57+
|<html>
58+
|<body>
59+
|<form action="/form" method="post">
60+
| <label for="name">First name:</label><br>
61+
| <input type="text" name="name" value=""><br>
62+
| <label for="surname">Last name:</label><br>
63+
| <input type="text" name="surname" value=""><br><br>
64+
| <input type="submit" value="Submit">
65+
|</form>
66+
|</body>
67+
|</html>""".stripMargin
68+
69+
cask.Response(data = html, headers = Seq("Content-Type" -> "text/html"))
70+
71+
@cask.postForm("/form")
72+
def formEndpoint(name: String, surname: String): String =
73+
"Hello " + name + " " + surname
74+
75+
initialize()
76+
```
77+
{% endtab %}
78+
{% endtabs %}
79+
80+
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
81+
use of `cask.Response`. The `cask.Response` type allows the user to set the status code, headers and cookies. The default
82+
content type for an endpoint method returning a `String` is `text/plain`. Set it to `text/html` in order for the browser to display the form correctly.
83+
84+
The `formEndpoint` endpoint reads the form data using the `name` and `surname` parameters. The names of parameters must
85+
be identical to the field names of the form.
86+
87+
## Handling JSON-encoded input
88+
89+
JSON fields are handled in the same way as form fields, except that we use the `@cask.PostJson` annotation. The fields
90+
will be read into the endpoint method arguments.
91+
92+
{% tabs web-server-input-2 class=tabs-scala-version %}
93+
{% tab 'Scala 2' %}
94+
```scala
95+
object Example extends cask.MainRoutes {
96+
97+
@cask.postJson("/json")
98+
def jsonEndpoint(name: String, surname: String): String =
99+
"Hello " + name + " " + surname
100+
101+
initialize()
102+
}
103+
```
104+
{% endtab %}
105+
{% tab 'Scala 3' %}
106+
```scala
107+
object Example extends cask.MainRoutes:
108+
109+
@cask.postJson("/json")
110+
def jsonEndpoint(name: String, surname: String): String =
111+
"Hello " + name + " " + surname
112+
113+
initialize()
114+
```
115+
{% endtab %}
116+
{% endtabs %}
117+
118+
Send the POST request using `curl`:
119+
120+
```shell
121+
curl --header "Content-Type: application/json" \
122+
--data '{"name":"John","surname":"Smith"}' \
123+
http://localhost:8080/json
124+
```
125+
126+
The response will be:
127+
```
128+
Hello John Smith
129+
```
130+
131+
The endpoint will accept JSONs that have only the fields with names specified as the endpoint method arguments. If there
132+
are more fields than expected, some fields are missing or have an incorrect data type, an error message
133+
will be returned with the response code 400.
134+
135+
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,
136+
from uPickle library.
137+
138+
{% tabs web-server-input-3 class=tabs-scala-version %}
139+
{% tab 'Scala 2' %}
140+
```scala
141+
object Example extends cask.MainRoutes {
142+
143+
@cask.postJson("/json")
144+
def jsonEndpoint(value: ujson.Value): String =
145+
value.toString
146+
147+
initialize()
148+
}
149+
150+
```
151+
{% endtab %}
152+
{% tab 'Scala 3' %}
153+
```scala
154+
object Example extends cask.MainRoutes:
155+
156+
@cask.postJson("/json")
157+
def jsonEndpoint(value: ujson.Value): String =
158+
value.toString
159+
160+
initialize()
161+
162+
```
163+
{% endtab %}
164+
{% endtabs %}
165+
166+
In this example the JSON is merely converted to `String`. Check the [*uPickle tutorial*](/toolkit/json-intro.html) for more information
167+
on what can be done with the `ujson.Value` type.
168+
169+
Send a POST request.
170+
```shell
171+
curl --header "Content-Type: application/json" \
172+
--data '{"value":{"name":"John","surname":"Smith"}}' \
173+
http://localhost:8080/json2
174+
```
175+
176+
The server will respond with:
177+
```
178+
"{\"name\":\"John\",\"surname\":\"Smith\"}"
179+
```
180+
181+
## Handling JSON-encoded output
182+
183+
Cask endpoints can return JSON objects returned by uPickle library functions. Cask will automatically handle the `ujson.Value`
184+
type and set the `Content-Type` header to `application/json`.
185+
186+
In this example, the `TimeData` case class stores the information about the time zone and current time in a chosen
187+
location. To serialize a case class into JSON, use type class derivation or define the serializer in its companion object in the case of Scala 2.
188+
189+
{% tabs web-server-input-4 class=tabs-scala-version %}
190+
{% tab 'Scala 2' %}
191+
```scala
192+
import java.time.{ZoneId, ZonedDateTime}
193+
194+
object Example extends cask.MainRoutes {
195+
import upickle.default.{ReadWriter, macroRW, writeJs}
196+
case class TimeData(timezone: Option[String], time: String)
197+
object TimeData {
198+
implicit val rw: ReadWriter[TimeData] = macroRW
199+
}
200+
201+
private def getZoneIdForCity(city: String): Option[ZoneId] = {
202+
import scala.jdk.CollectionConverters._
203+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
204+
}
205+
206+
@cask.get("/time_json/:city")
207+
def timeJSON(city: String): ujson.Value = {
208+
val timezone = getZoneIdForCity(city)
209+
val time = timezone match {
210+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
211+
case None => s"Couldn't find time zone for city $city"
212+
}
213+
writeJs(TimeData(timezone.map(_.toString), time))
214+
}
215+
216+
initialize()
217+
}
218+
```
219+
{% endtab %}
220+
{% tab 'Scala 3' %}
221+
```scala
222+
import java.time.{ZoneId, ZonedDateTime}
223+
224+
object Example extends cask.MainRoutes:
225+
import upickle.default.{ReadWriter, writeJs}
226+
case class TimeData(timezone: Option[String], time: String) derives ReadWriter
227+
228+
private def getZoneIdForCity(city: String): Option[ZoneId] =
229+
import scala.jdk.CollectionConverters.*
230+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
231+
232+
@cask.get("/time_json/:city")
233+
def timeJSON(city: String): ujson.Value =
234+
val timezone = getZoneIdForCity(city)
235+
val time = timezone match
236+
case Some(zoneId)=> s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
237+
case None => s"Couldn't find time zone for city $city"
238+
writeJs(TimeData(timezone.map(_.toString), time))
239+
240+
initialize()
241+
```
242+
{% endtab %}
243+
{% endtabs %}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
title: Building web servers with Cask
3+
type: chapter
4+
description: The introduction of the Cask library
5+
num: 30
6+
previous-page: http-client-what-else
7+
next-page: web-server-static
8+
---
9+
10+
Cask is an HTTP micro-framework, providing a simple and flexible way to build web applications.
11+
12+
Its main focus is on the ease of use, which makes it ideal for newcomers, at the cost of eschewing some features other
13+
frameworks provide, like asynchronicity.
14+
15+
To define an endpoint it's enough to annotate a function with an annotation specifying the request path.
16+
Cask allows for building the response manually using tools that the library provides, specifying the content, headers,
17+
status code, etc. An endpoint function can also return a string, a [uPickle](https://com-lihaoyi.github.io/upickle/) JSON type, or a [Scalatags](https://com-lihaoyi.github.io/scalatags/)
18+
template. In that case, Cask will automatically create a response with the appropriate headers.
19+
20+
Cask comes bundled with the uPickle library for handling JSONs, supports WebSockets and allows for extending endpoints with
21+
decorators, which can be used to handle authentication or rate limiting.
22+
23+
{% include markdown.html path="_markdown/install-cask.md" %}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
title: How to handle query parameters?
3+
type: section
4+
description: Handling query parameters with Cask
5+
num: 33
6+
previous-page: web-server-dynamic
7+
next-page: web-server-input
8+
---
9+
10+
{% include markdown.html path="_markdown/install-cask.md" %}
11+
12+
Query parameters are the key-value pairs coming after the question mark in a URL. They can be used for filtering,
13+
sorting or limiting the results provided by the server. For example, in the `<host>/time?city=Paris` URL, the `city` part
14+
is the name of a parameter, and `Paris` is its value. Cask allows for reading the query parameters by defining an endpoint
15+
method with arguments matching the names of the expected parameters and not matching any of the URL segments.
16+
17+
In this example, we give an `Option` type and the default value `None` to the `city` parameter. This tells Cask that it is optional.
18+
If not provided, the time for the current timezone will be returned.
19+
20+
{% tabs web-server-query-1 class=tabs-scala-version %}
21+
{% tab 'Scala 2' %}
22+
```scala
23+
import java.time.{ZoneId, ZonedDateTime}
24+
25+
object Example extends cask.MainRoutes {
26+
27+
private def getZoneIdForCity(city: String): Option[ZoneId] = {
28+
import scala.jdk.CollectionConverters._
29+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
30+
}
31+
32+
@cask.get("/time")
33+
def dynamicWithParam(city: Option[String] = None): String = {
34+
city match {
35+
case Some(value) => getZoneIdForCity(value) match {
36+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
37+
case None => s"Couldn't find time zone for city $value"
38+
}
39+
case None => s"Current date is: ${ZonedDateTime.now()}"
40+
}
41+
}
42+
43+
initialize()
44+
}
45+
```
46+
{% endtab %}
47+
{% tab 'Scala 3' %}
48+
```scala
49+
import java.time.{ZoneId, ZonedDateTime}
50+
51+
object Example extends cask.MainRoutes:
52+
53+
private def getZoneIdForCity(city: String): Option[ZoneId] =
54+
import scala.jdk.CollectionConverters.*
55+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
56+
57+
@cask.get("/time")
58+
def dynamicWithParam(city: Option[String] = None): String =
59+
city match
60+
case Some(value) => getZoneIdForCity(value) match
61+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
62+
case None => s"Couldn't find time zone for city $value"
63+
case None => s"Current date is: ${ZonedDateTime.now()}"
64+
65+
initialize()
66+
```
67+
{% endtab %}
68+
{% endtabs %}
69+
70+
Run the example as before and access the endpoint at [http://localhost:8080/time?city=Paris](http://localhost:8080/time?city=Paris).
71+
You should get a result similar to the following one.
72+
```
73+
Current date is: 2024-07-22T10:08:18.218736+02:00[Europe/Paris]
74+
```
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
title: How to serve a static file?
3+
type: section
4+
description: Serving a static file with Cask
5+
num: 31
6+
previous-page: web-server-intro
7+
next-page: web-server-dynamic
8+
---
9+
10+
{% include markdown.html path="_markdown/install-cask.md" %}
11+
12+
## Serving a static file
13+
14+
An endpoint is a specific URL where a particular webpage can be accessed. In Cask, an endpoint is a function returning the
15+
webpage data, together with an annotation describing its URL.
16+
17+
To create an endpoint serving static files, we need two things: an HTML file with the page content and a function that
18+
points to that file.
19+
20+
Create a minimal HTML file named `hello.html` with the following contents.
21+
22+
```html
23+
<!doctype html>
24+
<html>
25+
<head>
26+
<title>Hello World</title>
27+
</head>
28+
<body>
29+
<h1>Hello world</h1>
30+
</body>
31+
</html>
32+
```
33+
34+
Place it in the `resources` directory.
35+
36+
{% tabs web-server-static-1 class=tabs-build-tool %}
37+
{% tab 'Scala CLI' %}
38+
```
39+
example
40+
├── Example.scala
41+
└── resources
42+
└── hello.html
43+
```
44+
{% endtab %}
45+
{% tab 'sbt' %}
46+
```
47+
example
48+
└──src
49+
└── main
50+
├── resources
51+
│ └── hello.html
52+
└── scala
53+
└── Example.scala
54+
```
55+
{% endtab %}
56+
{% tab 'Mill' %}
57+
```
58+
example
59+
├── src
60+
│ └── Example.scala
61+
└── resources
62+
└── hello.html
63+
```
64+
{% endtab %}
65+
{% endtabs %}
66+
67+
The `@cask.staticFiles` annotation specifies at which path the webpage will be available. The endpoint function returns
68+
the location of the file.
69+
70+
{% tabs web-server-static-2 class=tabs-scala-version %}
71+
{% tab 'Scala 2' %}
72+
```scala
73+
object Example extends cask.MainRoutes {
74+
@cask.staticFiles("/static")
75+
def staticEndpoint(): String = "src/main/resources" // or "resources" if not using SBT
76+
77+
initialize()
78+
}
79+
```
80+
{% endtab %}
81+
{% tab 'Scala 3' %}
82+
```scala
83+
object Example extends cask.MainRoutes:
84+
@cask.staticFiles("/static")
85+
def staticEndpoint(): String = "src/main/resources" // or "resources" if not using SBT
86+
87+
initialize()
88+
```
89+
{% endtab %}
90+
{% endtabs %}
91+
92+
In the example above, `@cask.staticFiles` instructs the server to look for files accessed at the `/static` path in the
93+
`src/main/resources` directory. Cask will match any subpath coming after `/static` and append it to the directory path.
94+
If you access the `/static/hello.html` file, it will serve the file available at `src/main/resources/hello.html`.
95+
The directory path can be any path available to the server, relative or not. If the requested file cannot be found in the
96+
specified location, the server will return a 404 response with an error message.
97+
98+
The `Example` object inherits from the `cask.MainRoutes` class. It provides the main function that starts the server. The `initialize()`
99+
method call initializes the server routes, i.e., the association between URL paths and the code that handles them.
100+
101+
### Using the resources directory
102+
103+
The `@cask.staticResources` annotation works in the same way as the `@cask.staticFiles` used above, with the difference that
104+
the path returned by the endpoint method describes the location of files _inside_ the resources directory. Since the
105+
previous example conveniently used the resources directory, it can be simplified with `@cask.staticResources`.
106+
107+
{% tabs web-server-static-3 class=tabs-scala-version %}
108+
{% tab 'Scala 2' %}
109+
```scala
110+
object Example extends cask.MainRoutes {
111+
@cask.staticResources("/static")
112+
def staticEndpoint(): String = "."
113+
114+
initialize()
115+
}
116+
```
117+
{% endtab %}
118+
{% tab 'Scala 3' %}
119+
```scala
120+
object Example extends cask.MainRoutes:
121+
@cask.staticResources("/static")
122+
def staticEndpoint(): String = "."
123+
124+
initialize()
125+
```
126+
{% endtab %}
127+
{% endtabs %}
128+
129+
In the endpoint method, the location is set to `"."`, telling the server that the files are available directly in the
130+
resources directory. In general, you can use any nested location within the resources directory. For instance, you could opt
131+
for placing your HTML files in the `static` directory inside the resources directory or using different directories to sort out
132+
files used by different endpoints.
133+
134+
## Running the example
135+
136+
Run the example with the build tool of your choice.
137+
138+
{% tabs munit-unit-test-4 class=tabs-build-tool %}
139+
{% tab 'Scala CLI' %}
140+
In the terminal, the following command will start the server:
141+
```
142+
scala-cli run Example.scala
143+
```
144+
{% endtab %}
145+
{% tab 'sbt' %}
146+
In the terminal, the following command will start the server:
147+
```
148+
sbt example/run
149+
```
150+
{% endtab %}
151+
{% tab 'Mill' %}
152+
In the terminal, the following command will start the server:
153+
```
154+
./mill run
155+
```
156+
{% endtab %}
157+
{% endtabs %}
158+
159+
The example page will be available at [http://localhost:8080/static/hello.html](http://localhost:8080/static/hello.html).
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
title: How to use websockets?
3+
type: section
4+
description: Using websockets with Cask
5+
num: 35
6+
previous-page: web-server-input
7+
next-page: web-server-cookies-and-decorators
8+
---
9+
10+
{% include markdown.html path="_markdown/install-cask.md" %}
11+
12+
You can create a WebSocket endpoint with the `@cask.websocket` annotation. The endpoint method should return a
13+
`cask.WsHandler` instance defining how the communication should take place. It can also return a `cask.Response`, which rejects the
14+
attempt at forming a WebSocket connection.
15+
16+
The connection can also be closed by sending a `cask.Ws.close()` message through the WebSocket channel.
17+
18+
Create an HTML file named `websockets.html` with the following content and place it in the `resources ` directory.
19+
20+
```html
21+
<!DOCTYPE html>
22+
<html>
23+
<body>
24+
<div>
25+
<input type="text" id="input" placeholder="Provide city name">
26+
<button onclick="sendMessage()">Send</button>
27+
</div>
28+
<div id="time"></div>
29+
<script>
30+
const ws = new WebSocket('ws://localhost:8080/websocket');
31+
ws.onmessage = function(event) {
32+
receiveMessage(event.data);
33+
};
34+
35+
ws.onclose = function(event) {
36+
receiveMessage('The connection has been closed');
37+
};
38+
39+
function sendMessage() {
40+
const inputElement = document.getElementById('input');
41+
const message = inputElement.value;
42+
ws.send(message);
43+
}
44+
45+
function receiveMessage(message) {
46+
const timeElement = document.getElementById('time');
47+
timeElement.textContent = message;
48+
}
49+
</script>
50+
</body>
51+
</html>
52+
```
53+
54+
The JavaScript code opens a WebSocket connection using the `ws://localhost:8080/websocket` endpoint. The `ws.onmessage`
55+
event handler is executed when the server pushes a message to the browser and `ws.onclose` when the connection is closed.
56+
57+
Create an endpoint for serving static files using the `@cask.staticResources` annotation and an endpoint for handling
58+
the WebSocket connection.
59+
60+
{% tabs web-server-websocket-1 class=tabs-scala-version %}
61+
{% tab 'Scala 2' %}
62+
```scala
63+
@cask.staticResources("/static")
64+
def static() = "."
65+
66+
private def getZoneIdForCity(city: String): Option[ZoneId] = {
67+
import scala.jdk.CollectionConverters._
68+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
69+
}
70+
71+
@cask.websocket("/websocket")
72+
def websocket(): cask.WsHandler = {
73+
cask.WsHandler { channel =>
74+
cask.WsActor {
75+
case cask.Ws.Text("") => channel.send(cask.Ws.Close())
76+
case cask.Ws.Text(city) =>
77+
val text = getZoneIdForCity(city) match {
78+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
79+
case None => s"Couldn't find time zone for city $city"
80+
}
81+
channel.send(cask.Ws.Text(text))
82+
}
83+
}
84+
}
85+
86+
initialize()
87+
```
88+
{% endtab %}
89+
{% tab 'Scala 3' %}
90+
```scala
91+
@cask.staticResources("/static")
92+
def static() = "."
93+
94+
private def getZoneIdForCity(city: String): Option[ZoneId] =
95+
import scala.jdk.CollectionConverters.*
96+
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
97+
98+
@cask.websocket("/websocket")
99+
def websocket(): cask.WsHandler =
100+
cask.WsHandler { channel =>
101+
cask.WsActor {
102+
case cask.Ws.Text("") => channel.send(cask.Ws.Close())
103+
case cask.Ws.Text(city) =>
104+
val text = getZoneIdForCity(city) match
105+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
106+
case None => s"Couldn't find time zone for city $city"
107+
channel.send(cask.Ws.Text(text))
108+
}
109+
}
110+
111+
initialize()
112+
```
113+
{% endtab %}
114+
{% endtabs %}
115+
116+
In the `cask.WsHandler` we define a `cask.WsActor`. It reacts to events (of type `cask.util.Ws.Event`) and uses the
117+
WebSocket channel to send messages. In this example, we receive the name of a city and return the current time there. If the server
118+
receives an empty message, the connection is closed.

0 commit comments

Comments
 (0)
Please sign in to comment.