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 all commits
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
37 changes: 37 additions & 0 deletions _includes/_markdown/install-cask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% altDetails require-info-box 'Getting Cask' %}

{% tabs cask-install class=tabs-build-tool %}

{% tab 'Scala CLI' %}
You can declare a dependency on Cask with the following `using` directive:
```scala
//> using dep "com.lihaoyi::cask::0.9.2"
```
{% endtab %}

{% tab 'sbt' %}
In your `build.sbt`, you can add a dependency on Cask:
```scala
lazy val example = project.in(file("example"))
.settings(
scalaVersion := "3.4.2",
libraryDependencies += "com.lihaoyi" %% "cask" % "0.9.2",
fork := true
)
```
{% endtab %}

{% tab 'Mill' %}
In your `build.sc`, you can add a dependency on Cask:
```scala
object example extends RootModule with ScalaModule {
def scalaVersion = "3.3.3"
def ivyDeps = Agg(
ivy"com.lihaoyi::cask::0.9.2"
)
}
```
{% endtab %}

{% endtabs %}
{% endaltDetails %}
7 changes: 7 additions & 0 deletions _overviews/toolkit/OrderedListOfMdFiles
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ http-client-request-body.md
http-client-json.md
http-client-upload-file.md
http-client-what-else.md
web-server-intro.md
web-server-static.md
web-server-dynamic.md
web-server-query-parameters.md
web-server-input.md
web-server-websockets.md
web-server-cookies-and-decorators.md
2 changes: 1 addition & 1 deletion _overviews/toolkit/http-client-what-else.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ type: section
description: An incomplete list of features of sttp
num: 29
previous-page: http-client-upload-file
next-page:
next-page: web-server-intro
---

{% include markdown.html path="_markdown/install-upickle.md" %}
Expand Down
4 changes: 4 additions & 0 deletions _overviews/toolkit/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ toolkit-index:
description: Sending HTTP requests and uploading files with sttp.
icon: "fa fa-globe"
link: /toolkit/http-client-intro.html
- title: Web servers
description: Building web servers with Cask.
icon: "fa fa-server"
link: /toolkit/web-server-intro.html
---

## What is the Scala Toolkit?
Expand Down
188 changes: 188 additions & 0 deletions _overviews/toolkit/web-server-cookies-and-decorators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
---
title: How to use cookies and decorators?
type: section
description: Using cookies and decorators with Cask
num: 36
previous-page: web-server-websockets
next-page:
---

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

## Using cookies

Cookies are saved by adding them to the `cookies` parameter of the `cask.Response` constructor.

In this example, we are building a rudimentary authentication service. The `getLogin` method provides a form where
the user can enter their username and password. The `postLogin` method reads the credentials. If they match the expected ones, it generates a session
identifier is generated, saves it in the application state, and sends back a cookie with the identifier.

Cookies can be read either with a method parameter of `cask.Cookie` type or by accessing the `cask.Request` directly.
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
found, an error response will be returned. In the `checkLogin` function, the former method is used, as the cookie is not
present before the user logs in.

To delete a cookie, set its `expires` parameter to an instant in the past, for example `Instant.EPOCH`.

{% tabs web-server-cookies-1 class=tabs-scala-version %}
{% tab 'Scala 2' %}

```scala
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap

object Example extends cask.MainRoutes {

val sessionIds = ConcurrentHashMap.newKeySet[String]()

@cask.get("/login")
def getLogin(): cask.Response[String] = {
val html =
"""<!doctype html>
|<html>
|<body>
|<form action="/login" method="post">
| <label for="name">Username:</label><br>
| <input type="text" name="name" value=""><br>
| <label for="password">Password:</label><br>
| <input type="text" name="password" value=""><br><br>
| <input type="submit" value="Submit">
|</form>
|</body>
|</html>""".stripMargin

cask.Response(data = html, headers = Seq("Content-Type" -> "text/html"))
}

@cask.postForm("/login")
def postLogin(name: String, password: String): cask.Response[String] = {
if (name == "user" && password == "password") {
val sessionId = UUID.randomUUID().toString
sessionIds.add(sessionId)
cask.Response(data = "Success!", cookies = Seq(cask.Cookie("sessionId", sessionId)))
} else {
cask.Response(data = "Authentication failed", statusCode = 401)
}
}

@cask.get("/check")
def checkLogin(request: cask.Request): String = {
val sessionId = request.cookies.get("sessionId")
if (sessionId.exists(cookie => sessionIds.contains(cookie.value))) {
"You are logged in"
} else {
"You are not logged in"
}
}

@cask.get("/logout")
def logout(sessionId: cask.Cookie) = {
sessionIds.remove(sessionId.value)
cask.Response(data = "Successfully logged out!", cookies = Seq(cask.Cookie("sessionId", "", expires = Instant.EPOCH)))
}

initialize()
}
```
{% endtab %}
{% tab 'Scala 3' %}
```scala
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap

object Example extends cask.MainRoutes:

val sessionIds = ConcurrentHashMap.newKeySet[String]()

@cask.get("/login")
def getLogin(): cask.Response[String] =
val html =
"""<!doctype html>
|<html>
|<body>
|<form action="/login" method="post">
| <label for="name">Username:</label><br>
| <input type="text" name="name" value=""><br>
| <label for="password">Password:</label><br>
| <input type="text" name="password" value=""><br><br>
| <input type="submit" value="Submit">
|</form>
|</body>
|</html>""".stripMargin

cask.Response(data = html, headers = Seq("Content-Type" -> "text/html"))

@cask.postForm("/login")
def postLogin(name: String, password: String): cask.Response[String] =
if name == "user" && password == "password" then
val sessionId = UUID.randomUUID().toString
sessionIds.add(sessionId)
cask.Response(data = "Success!", cookies = Seq(cask.Cookie("sessionId", sessionId)))
else
cask.Response(data = "Authentication failed", statusCode = 401)

@cask.get("/check")
def checkLogin(request: cask.Request): String =
val sessionId = request.cookies.get("sessionId")
if sessionId.exists(cookie => sessionIds.contains(cookie.value)) then
"You are logged in"
else
"You are not logged in"

@cask.get("/logout")
def logout(sessionId: cask.Cookie): cask.Response[String] =
sessionIds.remove(sessionId.value)
cask.Response(data = "Successfully logged out!", cookies = Seq(cask.Cookie("sessionId", "", expires = Instant.EPOCH)))

initialize()
```
{% endtab %}
{% endtabs %}

## Using decorators

Decorators can be used for extending endpoints functionality with validation or new parameters. They are defined by extending
`cask.RawDecorator` class. They are used as annotations.

In this example, the `loggedIn` decorator is used to check if the user is logged in before accessing the `/decorated`
endpoint.

The decorator class can pass additional arguments to the decorated endpoint using a map. The passed arguments are available
through the last argument group. Here we are passing the session identifier to an argument named `sessionId`.

{% tabs web-server-cookies-2 class=tabs-scala-version %}
{% tab 'Scala 2' %}
```scala
class loggedIn extends cask.RawDecorator {
override def wrapFunction(ctx: cask.Request, delegate: Delegate): Result[Raw] = {
ctx.cookies.get("sessionId") match {
case Some(cookie) if sessionIds.contains(cookie.value) => delegate(Map("sessionId" -> cookie.value))
case _ => cask.router.Result.Success(cask.model.Response("You aren't logged in", 403))
}
}
}

@loggedIn()
@cask.get("/decorated")
def decorated()(sessionId: String): String = {
s"You are logged in with id: $sessionId"
}
```
{% endtab %}
{% tab 'Scala 3' %}
```scala
class loggedIn extends cask.RawDecorator:
override def wrapFunction(ctx: cask.Request, delegate: Delegate): Result[Raw] =
ctx.cookies.get("sessionId") match
case Some(cookie) if sessionIds.contains(cookie.value) =>
delegate(Map("sessionId" -> cookie.value))
case _ =>
cask.router.Result.Success(cask.model.Response("You aren't logged in", 403))


@loggedIn()
@cask.get("/decorated")
def decorated()(sessionId: String): String = s"You are logged in with id: $sessionId"
```
{% endtab %}
{% endtabs %}
Loading