Skip to content

Commit a837327

Browse files
committed
Update
1 parent 9760f28 commit a837327

7 files changed

+148
-123
lines changed

_overviews/toolkit/OrderedListOfMdFiles

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ web-server-dynamic.md
3333
web-server-query-parameters.md
3434
web-server-input.md
3535
web-server-websockets.md
36-
web-server-cookies-and-decorators.md
36+
web-server-cookies-and-decorators.md

_overviews/toolkit/web-server-dynamic.md

+42-33
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@ next-page: web-server-query-parameters
99

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

12-
## Basic example
12+
## Serving dynamically generated content
1313

14-
To create an endpoint returning dynamically generated content, use `@cask.get` annotation.
15-
16-
For example, create an endpoint that returns the current date and time.
14+
You can create an endpoint returning dynamically generated content with `@cask.get` annotation.
1715

1816
{% tabs web-server-dynamic-1 class=tabs-scala-version %}
1917
{% tab 'Scala 2' %}
@@ -22,7 +20,7 @@ import java.time.ZonedDateTime
2220

2321
object MyApp extends cask.MainRoutes {
2422
@cask.get("/time")
25-
def dynamic() = s"Current date is: ${ZonedDateTime.now()}"
23+
def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}"
2624

2725
initialize()
2826
}
@@ -34,21 +32,29 @@ import java.time.ZonedDateTime
3432

3533
object MyApp extends cask.MainRoutes:
3634
@cask.get("/time")
37-
def dynamic() = s"Current date is: ${ZonedDateTime.now()}"
35+
def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}"
3836

3937
initialize()
4038
```
4139
{% endtab %}
4240
{% endtabs %}
4341

42+
The example above creates an endpoint returning 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 `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).
46+
If you want an HTML output being interpreted by the browser, you else 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
4450

45-
Run the example the same way as before (assuming you use the same project structure).
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).
4652

4753
{% tabs web-server-dynamic-2 class=tabs-build-tool %}
4854
{% tab 'Scala CLI' %}
4955
In the terminal, the following command will start the server:
5056
```
51-
scala-cli run Example.sc
57+
scala-cli run Example.scala
5258
```
5359
{% endtab %}
5460
{% tab 'sbt' %}
@@ -65,17 +71,17 @@ In the terminal, the following command will start the server:
6571
{% endtab %}
6672
{% endtabs %}
6773

68-
Access the endpoint at [http://localhost:8080/time](http://localhost:8080/time). You should see a result similar to the
69-
one below.
74+
Access [the endpoint](http://localhost:8080/time). You should see a result similar to the one below.
7075

7176
```
7277
Current date is: 2024-07-22T09:07:05.752534+02:00[Europe/Warsaw]
7378
```
7479

7580
## Using path segments
7681

77-
You can use path segments to specify the returned data more precisely. Building on the example above, add the `:city`
78-
segment to get the current time in a city of choice.
82+
Cask gives you the ability to access segments of the URL path withing 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.
7985

8086
{% tabs web-server-dynamic-3 class=tabs-scala-version %}
8187
{% tab 'Scala 2' %}
@@ -90,7 +96,7 @@ object MyApp extends cask.MainRoutes {
9096
}
9197

9298
@cask.get("/time/:city")
93-
def dynamicWithCity(city: String) = {
99+
def dynamicWithCity(city: String): String = {
94100
getZoneIdForCity(city) match {
95101
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
96102
case None => s"Couldn't find time zone for city $city"
@@ -112,7 +118,7 @@ object MyApp extends cask.MainRoutes:
112118
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
113119

114120
@cask.get("/time/:city")
115-
def dynamicWithCity(city: String) =
121+
def dynamicWithCity(city: String): String =
116122
getZoneIdForCity(city) match
117123
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
118124
case None => s"Couldn't find time zone for city $city"
@@ -122,23 +128,22 @@ object MyApp extends cask.MainRoutes:
122128
{% endtab %}
123129
{% endtabs %}
124130

125-
Accessing the endpoint at [http://localhost:8080/time/Paris](http://localhost:8080/time/Paris) will result with:
126-
```
127-
Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris]
128-
```
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 find the timezone for
133+
a given city and then the current date and time is translated to that timezone.
129134

130-
and at [http://localhost:8080/time/Tokyo](http://localhost:8080/time/Tokyo) you will see:
135+
Accessing [the endpoint](http://localhost:8080/time/Paris) (notice the `Paris` segment in the URL) will result with:
131136
```
132-
Current date is: 2024-07-22T16:08:41.137563+09:00[Asia/Tokyo]
137+
Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris]
133138
```
134139

135-
Cask endpoints can handle either fixed or arbitrary number of path segments. Please consult the
136-
[documentation](https://com-lihaoyi.github.io/cask/index.html#variable-routes) for more details.
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.
137143

138144
## Using HTML templates
139145

140-
You can combine Cask code with a templating library like [Scalatags](https://com-lihaoyi.github.io/scalatags/) to
141-
build an HTML response.
146+
To create an HTML response, you can combine Cask code with the [Scalatags](https://com-lihaoyi.github.io/scalatags/) templating library.
142147

143148
Import the Scalatags library:
144149

@@ -163,9 +168,10 @@ ivy"com.lihaoyi::scalatags::0.13.1"
163168
{% endtab %}
164169
{% endtabs %}
165170

166-
Now the example above can be rewritten to use a template. Cask will build a response out of the `doctype` automatically.
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`.
167173

168-
{% tabs web-server-dynamic-3 class=tabs-scala-version %}
174+
{% tabs web-server-dynamic-5 class=tabs-scala-version %}
169175
{% tab 'Scala 2' %}
170176
```scala
171177
import java.time.{ZoneId, ZonedDateTime}
@@ -179,7 +185,7 @@ object MyApp extends cask.MainRoutes {
179185
}
180186

181187
@cask.get("/time/:city")
182-
def dynamicWithCity(city: String) = {
188+
def dynamicWithCity(city: String): doctype = {
183189
val text = getZoneIdForCity(city) match {
184190
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
185191
case None => s"Couldn't find time zone for city $city"
@@ -188,7 +194,7 @@ object MyApp extends cask.MainRoutes {
188194
doctype("html")(
189195
html(
190196
body(
191-
h1(text)
197+
p(text)
192198
)
193199
)
194200
)
@@ -210,19 +216,22 @@ object MyApp extends cask.MainRoutes:
210216
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
211217

212218
@cask.get("/time/:city")
213-
def dynamicWithCity(city: String) =
219+
def dynamicWithCity(city: String): doctype =
214220
val text = getZoneIdForCity(city) match
215-
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
216-
case None => s"Couldn't find time zone for city $city"
221+
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
222+
case None => s"Couldn't find time zone for city $city"
217223
doctype("html")(
218224
html(
219225
body(
220-
h1(text)
226+
p(text)
221227
)
222228
)
223229
)
224230

225231
initialize()
226232
```
227233
{% endtab %}
228-
{% endtabs %}
234+
{% endtabs %}
235+
236+
Here we get the text of response and wrap it in a Scalatags template. Notice that the return type changed from `String`
237+
to `doctype`.

_overviews/toolkit/web-server-input.md

+39-34
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,16 @@ next-page: web-server-websockets
1111

1212
## Handling form-encoded input
1313

14-
Similarly to path segments and query parameters, form fields are read by using endpoint method arguments. Use `cask.postForm`
15-
annotation and set the HTML form method to `post`.
16-
17-
In this example we create a form asking for name and surname of a user and then redirect to a greeting page. Notice the
18-
use of `cask.Response`. The default returned content type is `text/plain`, set it to `text/html` in order for browser to display
19-
the form correctly.
20-
21-
The `formMethod` endpoint reads the form data using `name` and `surname` parameters. The names of parameters must
22-
be identical to the field names of the form.
14+
To create an endpoint that handles the data provided in an HTML form, use `cask.postForm` annotation, give the endpoint method arguments
15+
with names corresponding to names of fields in the form and set the form method to `post`.
2316

2417
{% tabs web-server-input-1 class=tabs-scala-version %}
2518
{% tab 'Scala 2' %}
2619
```scala
2720
object MyApp extends cask.MainRoutes {
2821

2922
@cask.get("/form")
30-
def getForm() = {
23+
def getForm(): String = {
3124
val html =
3225
"""<!doctype html>
3326
|<html>
@@ -46,7 +39,7 @@ object MyApp extends cask.MainRoutes {
4639
}
4740

4841
@cask.postForm("/form")
49-
def formEndpoint(name: String, surname: String) =
42+
def formEndpoint(name: String, surname: String): String =
5043
"Hello " + name + " " + surname
5144

5245
initialize()
@@ -58,7 +51,7 @@ object MyApp extends cask.MainRoutes {
5851
object MyApp extends cask.MainRoutes:
5952

6053
@cask.get("/form")
61-
def getForm() =
54+
def getForm(): String =
6255
val html =
6356
"""<!doctype html>
6457
|<html>
@@ -76,27 +69,33 @@ object MyApp extends cask.MainRoutes:
7669
cask.Response(data = html, headers = Seq("Content-Type" -> "text/html"))
7770

7871
@cask.postForm("/form")
79-
def formEndpoint(name: String, surname: String) =
72+
def formEndpoint(name: String, surname: String): String =
8073
"Hello " + name + " " + surname
8174

8275
initialize()
8376
```
8477
{% endtab %}
8578
{% endtabs %}
8679

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 default returned content type in case of `String` returning endpoint method is `text/plain`,
82+
set it to `text/html` in order for browser to display the form correctly.
83+
84+
The `formEndpoint` endpoint reads the form data using `name` and `surname` parameters. The names of parameters must
85+
be identical to the field names of the form.
86+
8787
## Handling JSON-encoded input
8888

89-
JSON fields are handled in the same way as form fields, except that `cask.PostJson` annotation is used. The topmost fields
90-
will be read into the endpoint method arguments and if any of them is missing or has an incorrect type, an error message
91-
will be returned with 400 response code.
89+
JSON fields are handled in the same way as form fields, except that `cask.PostJson` annotation is used. The fields
90+
will be read into the endpoint method arguments.
9291

9392
{% tabs web-server-input-2 class=tabs-scala-version %}
9493
{% tab 'Scala 2' %}
9594
```scala
9695
object MyApp extends cask.MainRoutes {
9796

9897
@cask.postJson("/json")
99-
def jsonEndpoint(name: String, surname: String) =
98+
def jsonEndpoint(name: String, surname: String): String =
10099
"Hello " + name + " " + surname
101100

102101
initialize()
@@ -107,8 +106,8 @@ object MyApp extends cask.MainRoutes {
107106
```scala
108107
object MyApp extends cask.MainRoutes:
109108

110-
@cask.postJson("/json")
111-
def jsonEndpoint(name: String, surname: String) =
109+
@cask.postJson("/json")
110+
def jsonEndpoint(name: String, surname: String): String =
112111
"Hello " + name + " " + surname
113112

114113
initialize()
@@ -129,15 +128,20 @@ The response will be:
129128
Hello John Smith
130129
```
131130

132-
Deserialization is handled by uPickle JSON library. To deserialize an object, use `ujson.Value` type.
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 400 response code.
134+
135+
To handle the case when the fields of the JSON are not known in advance, you can use argument with the `ujson.Value` type
136+
from uPickle library.
133137

134138
{% tabs web-server-input-3 class=tabs-scala-version %}
135139
{% tab 'Scala 2' %}
136140
```scala
137141
object MyApp extends cask.MainRoutes {
138142

139143
@cask.postJson("/json")
140-
def jsonEndpoint(value: ujson.Value) =
144+
def jsonEndpoint(value: ujson.Value): String =
141145
value.toString
142146

143147
initialize()
@@ -150,7 +154,7 @@ object MyApp extends cask.MainRoutes {
150154
object MyApp extends cask.MainRoutes:
151155

152156
@cask.postJson("/json")
153-
def jsonEndpoint(value: ujson.Value) =
157+
def jsonEndpoint(value: ujson.Value): String =
154158
value.toString
155159

156160
initialize()
@@ -159,27 +163,30 @@ object MyApp extends cask.MainRoutes:
159163
{% endtab %}
160164
{% endtabs %}
161165

166+
In this example the JSON is merely converted to `String`, check the [*uPickle tutorial*](/toolkit/json-introduction.html) for more information
167+
on what can be done with `ujson.Value` type.
168+
162169
Send a POST request.
163170
```shell
164171
curl --header "Content-Type: application/json" \
165172
--data '{"value":{"name":"John","surname":"Smith"}}' \
166173
http://localhost:8080/json2
167174
```
168175

169-
Server will respond with:
176+
The server will respond with:
170177
```
171178
"{\"name\":\"John\",\"surname\":\"Smith\"}"
172179
```
173180

174181
## Handling JSON-encoded output
175182

176183
Cask endpoint can return JSON objects returned by uPickle library functions. Cask will automatically handle the `ujson.Value`
177-
type and set the `Content-Type application/json` header.
184+
type and set the `Content-Type` header to `application/json`.
178185

179-
In this example we use a simple `TimeData` case class to send information about the time zone and current time in a chosen
180-
location. To serialize a case class into JSON you need to define a serializer in its companion object.
186+
In this example `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 case of Scala 2.
181188

182-
{% tabs web-server-input-3 class=tabs-scala-version %}
189+
{% tabs web-server-input-4 class=tabs-scala-version %}
183190
{% tab 'Scala 2' %}
184191
```scala
185192
object MyApp extends cask.MainRoutes {
@@ -195,7 +202,7 @@ object MyApp extends cask.MainRoutes {
195202
}
196203

197204
@cask.get("/time_json/:city")
198-
def timeJSON(city: String) = {
205+
def timeJSON(city: String): ujson.Value = {
199206
val timezone = getZoneIdForCity(city)
200207
val time = timezone match {
201208
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
@@ -210,20 +217,18 @@ object MyApp extends cask.MainRoutes {
210217
```scala
211218
object MyApp extends cask.MainRoutes {
212219
import upickle.default.{ReadWriter, macroRW, writeJs}
213-
case class TimeData(timezone: Option[String], time: String)
214-
object TimeData:
215-
given rw: ReadWriter[TimeData]= macroRW
220+
case class TimeData(timezone: Option[String], time: String) derives ReadWriter
216221

217222
private def getZoneIdForCity(city: String): Option[ZoneId] =
218223
import scala.jdk.CollectionConverters.*
219224
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
220225

221226
@cask.get("/time_json/:city")
222-
def timeJSON(city: String) = {
227+
def timeJSON(city: String): ujson.Value = {
223228
val timezone = getZoneIdForCity(city)
224229
val time = timezone match
225-
case Some(zoneId)=> s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
226-
case None => s"Couldn't find time zone for city $city"
230+
case Some(zoneId)=> s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
231+
case None => s"Couldn't find time zone for city $city"
227232
writeJs(TimeData(timezone.map(_.toString), time))
228233
}
229234
}

0 commit comments

Comments
 (0)