You can declare a dependency on Cask with the following using
directive:
//> using dep com.lihaoyi::cask::0.9.2
In your build.sbt
, you can add a dependency on Cask:
lazy val example = project.in(file("example"))
.settings(
scalaVersion := "3.4.2",
libraryDependencies += "com.lihaoyi" %% "cask" % "0.9.2",
fork := true
)
In your build.sc
, you can add a dependency on Cask:
object example extends RootModule with ScalaModule {
def scalaVersion = "3.3.4"
def ivyDeps = Agg(
ivy"com.lihaoyi::cask::0.9.2"
)
}
Handling form-encoded input
To create an endpoint that handles the data provided in an HTML form, use the @cask.postForm
annotation. Add arguments to the endpoint method
with names corresponding to names of fields in the form and set the form method to post
.
object Example extends cask.MainRoutes {
@cask.get("/form")
def getForm(): cask.Response[String] = {
val html =
"""<!doctype html>
|<html>
|<body>
|<form action="/form" method="post">
| <label for="name">First name:</label><br>
| <input type="text" name="name" value=""><br>
| <label for="surname">Last name:</label><br>
| <input type="text" name="surname" value=""><br><br>
| <input type="submit" value="Submit">
|</form>
|</body>
|</html>""".stripMargin
cask.Response(data = html, headers = Seq("Content-Type" -> "text/html"))
}
@cask.postForm("/form")
def formEndpoint(name: String, surname: String): String =
"Hello " + name + " " + surname
initialize()
}
object Example extends cask.MainRoutes:
@cask.get("/form")
def getForm(): cask.Response[String] =
val html =
"""<!doctype html>
|<html>
|<body>
|<form action="/form" method="post">
| <label for="name">First name:</label><br>
| <input type="text" name="name" value=""><br>
| <label for="surname">Last name:</label><br>
| <input type="text" name="surname" value=""><br><br>
| <input type="submit" value="Submit">
|</form>
|</body>
|</html>""".stripMargin
cask.Response(data = html, headers = Seq("Content-Type" -> "text/html"))
@cask.postForm("/form")
def formEndpoint(name: String, surname: String): String =
"Hello " + name + " " + surname
initialize()
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
use of cask.Response
. The cask.Response
type allows the user to set the status code, headers and cookies. The default
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.
The formEndpoint
endpoint reads the form data using the name
and surname
parameters. The names of parameters must
be identical to the field names of the form.
Handling JSON-encoded input
JSON fields are handled in the same way as form fields, except that we use the @cask.PostJson
annotation. The fields
will be read into the endpoint method arguments.
object Example extends cask.MainRoutes {
@cask.postJson("/json")
def jsonEndpoint(name: String, surname: String): String =
"Hello " + name + " " + surname
initialize()
}
object Example extends cask.MainRoutes:
@cask.postJson("/json")
def jsonEndpoint(name: String, surname: String): String =
"Hello " + name + " " + surname
initialize()
Send the POST request using curl
:
curl --header "Content-Type: application/json" \
--data '{"name":"John","surname":"Smith"}' \
http://localhost:8080/json
The response will be:
Hello John Smith
The endpoint will accept JSONs that have only the fields with names specified as the endpoint method arguments. If there are more fields than expected, some fields are missing or have an incorrect data type, an error message will be returned with the response code 400.
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,
from uPickle library.
object Example extends cask.MainRoutes {
@cask.postJson("/json")
def jsonEndpoint(value: ujson.Value): String =
value.toString
initialize()
}
object Example extends cask.MainRoutes:
@cask.postJson("/json")
def jsonEndpoint(value: ujson.Value): String =
value.toString
initialize()
In this example the JSON is merely converted to String
. Check the uPickle tutorial for more information
on what can be done with the ujson.Value
type.
Send a POST request.
curl --header "Content-Type: application/json" \
--data '{"value":{"name":"John","surname":"Smith"}}' \
http://localhost:8080/json2
The server will respond with:
"{\"name\":\"John\",\"surname\":\"Smith\"}"
Handling JSON-encoded output
Cask endpoints can return JSON objects returned by uPickle library functions. Cask will automatically handle the ujson.Value
type and set the Content-Type
header to application/json
.
In this example, the TimeData
case class stores the information about the time zone and current time in a chosen
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.
import java.time.{ZoneId, ZonedDateTime}
object Example extends cask.MainRoutes {
import upickle.default.{ReadWriter, macroRW, writeJs}
case class TimeData(timezone: Option[String], time: String)
object TimeData {
implicit val rw: ReadWriter[TimeData] = macroRW
}
private def getZoneIdForCity(city: String): Option[ZoneId] = {
import scala.jdk.CollectionConverters._
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
}
@cask.get("/time_json/:city")
def timeJSON(city: String): ujson.Value = {
val timezone = getZoneIdForCity(city)
val time = timezone match {
case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
case None => s"Couldn't find time zone for city $city"
}
writeJs(TimeData(timezone.map(_.toString), time))
}
initialize()
}
import java.time.{ZoneId, ZonedDateTime}
object Example extends cask.MainRoutes:
import upickle.default.{ReadWriter, writeJs}
case class TimeData(timezone: Option[String], time: String) derives ReadWriter
private def getZoneIdForCity(city: String): Option[ZoneId] =
import scala.jdk.CollectionConverters.*
ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of)
@cask.get("/time_json/:city")
def timeJSON(city: String): ujson.Value =
val timezone = getZoneIdForCity(city)
val time = timezone match
case Some(zoneId)=> s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}"
case None => s"Couldn't find time zone for city $city"
writeJs(TimeData(timezone.map(_.toString), time))
initialize()