Skip to content

Commit 9760f28

Browse files
committed
Add Cask tutorials
1 parent 178372b commit 9760f28

11 files changed

+1055
-1
lines changed

_includes/_markdown/install-cask.md

+37
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 dependency on Cask with `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

+7
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

+1-1
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

+4
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?
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-query-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 `cask.Response` constructor.
15+
16+
In this example we are building a rudimentary authentication service. The `getLogin` method provides a form where
17+
username and password can be inputted. The `postLogin` reads the credentials and if they match the expected ones, a session
18+
identifier is generated, saved in the application state and sends back a cookie with the identifier.
19+
20+
Cookies can be read either with a method parameter with `cask.Cookie` type or by accessing `cask.Request` directly.
21+
If using the former method, names of parameters have to match the names of cookies. If a cookie with 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 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 MyApp extends cask.MainRoutes {
35+
36+
val sessionIds = ConcurrentHashMap.newKeySet[String]()
37+
38+
@cask.get("/login")
39+
def getLogin() = {
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) = {
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) = {
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 MyApp extends cask.MainRoutes:
94+
95+
val sessionIds = ConcurrentHashMap.newKeySet[String]()
96+
97+
@cask.get("/login")
98+
def getLogin() =
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) =
117+
if name == "user" && password == "password":
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) =
126+
val sessionId = request.cookies.get("sessionId")
127+
if sessionId.exists(cookie => sessionIds.contains(cookie.value)):
128+
"You are logged in"
129+
else
130+
"You are not logged in"
131+
132+
@cask.get("/logout")
133+
def logout(sessionId: cask.Cookie) =
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 and then used as annotations.
146+
147+
In this example, the `loggedIn` decorator is used for checking if 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) = {
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) = {
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) =
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) = s"You are logged in with id: $sessionId"
186+
```
187+
{% endtab %}
188+
{% endtabs %}

0 commit comments

Comments
 (0)