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
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 dependency on Cask with `using` directive:
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 declare dependency on Cask with `using` directive:
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-query-websockets
next-page:
---

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

## Using cookies

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

In this example we are building a rudimentary authentication service. The `getLogin` method provides a form where
username and password can be inputted. The `postLogin` reads the credentials and if they match the expected ones, a session
identifier is generated, saved in the application state and sends back a cookie with the identifier.

Cookies can be read either with a method parameter with `cask.Cookie` type or by accessing `cask.Request` directly.
If using the former method, names of parameters have to match the names of cookies. If a cookie with 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 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 MyApp extends cask.MainRoutes {

val sessionIds = ConcurrentHashMap.newKeySet[String]()

@cask.get("/login")
def getLogin() = {
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) = {
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) = {
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 MyApp extends cask.MainRoutes:

val sessionIds = ConcurrentHashMap.newKeySet[String]()

@cask.get("/login")
def getLogin() =
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) =
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) =
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 %}
{% endtabs %}

## Using decorators

Decorators can be used for extending endpoints functionality with validation or new parameters. They are defined by extending
`cask.RawDecorator` class and then used as annotations.
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
`cask.RawDecorator` class and then used as annotations.
the `cask.RawDecorator` class. They are then used as annotations.


In this example, the `loggedIn` decorator is used for checking if 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) = {
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) = {
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) =
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) = s"You are logged in with id: $sessionId"
```
{% endtab %}
{% endtabs %}
Loading
Loading