From 9760f287f816b65f4498d43f9e860185a1e88db7 Mon Sep 17 00:00:00 2001 From: gkepka Date: Mon, 12 Aug 2024 08:23:09 +0200 Subject: [PATCH 1/6] Add Cask tutorials --- _includes/_markdown/install-cask.md | 37 +++ _overviews/toolkit/OrderedListOfMdFiles | 7 + _overviews/toolkit/http-client-what-else.md | 2 +- _overviews/toolkit/introduction.md | 4 + .../web-server-cookies-and-decorators.md | 188 ++++++++++++++ _overviews/toolkit/web-server-dynamic.md | 228 +++++++++++++++++ _overviews/toolkit/web-server-input.md | 232 ++++++++++++++++++ _overviews/toolkit/web-server-intro.md | 22 ++ .../toolkit/web-server-query-parameters.md | 77 ++++++ _overviews/toolkit/web-server-static.md | 146 +++++++++++ _overviews/toolkit/web-server-websockets.md | 113 +++++++++ 11 files changed, 1055 insertions(+), 1 deletion(-) create mode 100644 _includes/_markdown/install-cask.md create mode 100644 _overviews/toolkit/web-server-cookies-and-decorators.md create mode 100644 _overviews/toolkit/web-server-dynamic.md create mode 100644 _overviews/toolkit/web-server-input.md create mode 100644 _overviews/toolkit/web-server-intro.md create mode 100644 _overviews/toolkit/web-server-query-parameters.md create mode 100644 _overviews/toolkit/web-server-static.md create mode 100644 _overviews/toolkit/web-server-websockets.md diff --git a/_includes/_markdown/install-cask.md b/_includes/_markdown/install-cask.md new file mode 100644 index 0000000000..d0b050408c --- /dev/null +++ b/_includes/_markdown/install-cask.md @@ -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: +```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 %} diff --git a/_overviews/toolkit/OrderedListOfMdFiles b/_overviews/toolkit/OrderedListOfMdFiles index ea248772fe..db4caf242f 100644 --- a/_overviews/toolkit/OrderedListOfMdFiles +++ b/_overviews/toolkit/OrderedListOfMdFiles @@ -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 \ No newline at end of file diff --git a/_overviews/toolkit/http-client-what-else.md b/_overviews/toolkit/http-client-what-else.md index 11b577449d..865a031557 100644 --- a/_overviews/toolkit/http-client-what-else.md +++ b/_overviews/toolkit/http-client-what-else.md @@ -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" %} diff --git a/_overviews/toolkit/introduction.md b/_overviews/toolkit/introduction.md index 1656ed9662..9bc97cb2d1 100644 --- a/_overviews/toolkit/introduction.md +++ b/_overviews/toolkit/introduction.md @@ -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? diff --git a/_overviews/toolkit/web-server-cookies-and-decorators.md b/_overviews/toolkit/web-server-cookies-and-decorators.md new file mode 100644 index 0000000000..dbb68eff81 --- /dev/null +++ b/_overviews/toolkit/web-server-cookies-and-decorators.md @@ -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 = + """ + | + | + |
+ |
+ |
+ |
+ |

+ | + |
+ | + |""".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 = + """ + | + | + |
+ |
+ |
+ |
+ |

+ | + |
+ | + |""".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. + +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 %} \ No newline at end of file diff --git a/_overviews/toolkit/web-server-dynamic.md b/_overviews/toolkit/web-server-dynamic.md new file mode 100644 index 0000000000..57f837eb02 --- /dev/null +++ b/_overviews/toolkit/web-server-dynamic.md @@ -0,0 +1,228 @@ +--- +title: How to serve a dynamic page? +type: section +description: Serving a dynamic page with Cask +num: 32 +previous-page: web-server-static +next-page: web-server-query-parameters +--- + +{% include markdown.html path="_markdown/install-cask.md" %} + +## Basic example + +To create an endpoint returning dynamically generated content, use `@cask.get` annotation. + +For example, create an endpoint that returns the current date and time. + +{% tabs web-server-dynamic-1 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +import java.time.ZonedDateTime + +object MyApp extends cask.MainRoutes { + @cask.get("/time") + def dynamic() = s"Current date is: ${ZonedDateTime.now()}" + + initialize() +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import java.time.ZonedDateTime + +object MyApp extends cask.MainRoutes: + @cask.get("/time") + def dynamic() = s"Current date is: ${ZonedDateTime.now()}" + + initialize() +``` +{% endtab %} +{% endtabs %} + + +Run the example the same way as before (assuming you use the same project structure). + +{% tabs web-server-dynamic-2 class=tabs-build-tool %} +{% tab 'Scala CLI' %} +In the terminal, the following command will start the server: +``` +scala-cli run Example.sc +``` +{% endtab %} +{% tab 'sbt' %} +In the sbt shell, the following command will start the server: +``` +sbt:example> example/run +``` +{% endtab %} +{% tab 'Mill' %} +In the terminal, the following command will start the server: +``` +./mill run +``` +{% endtab %} +{% endtabs %} + +Access the endpoint at [http://localhost:8080/time](http://localhost:8080/time). You should see a result similar to the +one below. + +``` +Current date is: 2024-07-22T09:07:05.752534+02:00[Europe/Warsaw] +``` + +## Using path segments + +You can use path segments to specify the returned data more precisely. Building on the example above, add the `:city` +segment to get the current time in a city of choice. + +{% tabs web-server-dynamic-3 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +import java.time.{ZoneId, ZonedDateTime} + +object MyApp extends cask.MainRoutes { + + private def getZoneIdForCity(city: String): Option[ZoneId] = { + import scala.jdk.CollectionConverters._ + ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) + } + + @cask.get("/time/:city") + def dynamicWithCity(city: String) = { + getZoneIdForCity(city) match { + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $city" + } + } + + initialize() +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import java.time.{ZoneId, ZonedDateTime} + +object MyApp extends cask.MainRoutes: + + private def getZoneIdForCity(city: String): Option[ZoneId] = + import scala.jdk.CollectionConverters.* + ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) + + @cask.get("/time/:city") + def dynamicWithCity(city: String) = + getZoneIdForCity(city) match + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $city" + + initialize() +``` +{% endtab %} +{% endtabs %} + +Accessing the endpoint at [http://localhost:8080/time/Paris](http://localhost:8080/time/Paris) will result with: +``` +Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris] +``` + +and at [http://localhost:8080/time/Tokyo](http://localhost:8080/time/Tokyo) you will see: +``` +Current date is: 2024-07-22T16:08:41.137563+09:00[Asia/Tokyo] +``` + +Cask endpoints can handle either fixed or arbitrary number of path segments. Please consult the +[documentation](https://com-lihaoyi.github.io/cask/index.html#variable-routes) for more details. + +## Using HTML templates + +You can combine Cask code with a templating library like [Scalatags](https://com-lihaoyi.github.io/scalatags/) to +build an HTML response. + +Import the Scalatags library: + +{% tabs web-server-dynamic-4 class=tabs-build-tool %} +{% tab 'Scala CLI' %} +Add the Scalatags dependency in `Example.sc` file: +```scala +//> using dep "com.lihaoyi::scalatags::0.13.1" +``` +{% endtab %} +{% tab 'sbt' %} +Add the Scalatags dependency in `build.sbt` file: +```scala +libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.13.1" +``` +{% endtab %} +{% tab 'Mill' %} +Add the Scalatags dependency in `build.cs` file: +```scala +ivy"com.lihaoyi::scalatags::0.13.1" +``` +{% endtab %} +{% endtabs %} + +Now the example above can be rewritten to use a template. Cask will build a response out of the `doctype` automatically. + +{% tabs web-server-dynamic-3 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +import java.time.{ZoneId, ZonedDateTime} +import scalatags.Text.all._ + +object MyApp extends cask.MainRoutes { + + private def getZoneIdForCity(city: String): Option[ZoneId] = { + import scala.jdk.CollectionConverters._ + ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) + } + + @cask.get("/time/:city") + def dynamicWithCity(city: String) = { + val text = getZoneIdForCity(city) match { + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $city" + } + + doctype("html")( + html( + body( + h1(text) + ) + ) + ) + } + + initialize() +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import java.time.{ZoneId, ZonedDateTime} +import scalatags.Text.all.* + +object MyApp extends cask.MainRoutes: + + private def getZoneIdForCity(city: String): Option[ZoneId] = + import scala.jdk.CollectionConverters.* + ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) + + @cask.get("/time/:city") + def dynamicWithCity(city: String) = + val text = getZoneIdForCity(city) match + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $city" + doctype("html")( + html( + body( + h1(text) + ) + ) + ) + + initialize() +``` +{% endtab %} +{% endtabs %} \ No newline at end of file diff --git a/_overviews/toolkit/web-server-input.md b/_overviews/toolkit/web-server-input.md new file mode 100644 index 0000000000..3446ad35fa --- /dev/null +++ b/_overviews/toolkit/web-server-input.md @@ -0,0 +1,232 @@ +--- +title: How to handle user input? +type: section +description: Handling user input with Cask +num: 34 +previous-page: web-server-query-parameters +next-page: web-server-websockets +--- + +{% include markdown.html path="_markdown/install-cask.md" %} + +## Handling form-encoded input + +Similarly to path segments and query parameters, form fields are read by using endpoint method arguments. Use `cask.postForm` +annotation and set the HTML form method to `post`. + +In this example we create a form asking for name and surname of a user and then redirect to a greeting page. Notice the +use of `cask.Response`. The default returned content type is `text/plain`, set it to `text/html` in order for browser to display +the form correctly. + +The `formMethod` endpoint reads the form data using `name` and `surname` parameters. The names of parameters must +be identical to the field names of the form. + +{% tabs web-server-input-1 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +object MyApp extends cask.MainRoutes { + + @cask.get("/form") + def getForm() = { + val html = + """ + | + | + |
+ |
+ |
+ |
+ |

+ | + |
+ | + |""".stripMargin + + cask.Response(data = html, headers = Seq("Content-Type" -> "text/html")) + } + + @cask.postForm("/form") + def formEndpoint(name: String, surname: String) = + "Hello " + name + " " + surname + + initialize() +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +object MyApp extends cask.MainRoutes: + + @cask.get("/form") + def getForm() = + val html = + """ + | + | + |
+ |
+ |
+ |
+ |

+ | + |
+ | + |""".stripMargin + + cask.Response(data = html, headers = Seq("Content-Type" -> "text/html")) + + @cask.postForm("/form") + def formEndpoint(name: String, surname: String) = + "Hello " + name + " " + surname + + initialize() +``` +{% endtab %} +{% endtabs %} + +## Handling JSON-encoded input + +JSON fields are handled in the same way as form fields, except that `cask.PostJson` annotation is used. The topmost fields +will be read into the endpoint method arguments and if any of them is missing or has an incorrect type, an error message +will be returned with 400 response code. + +{% tabs web-server-input-2 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +object MyApp extends cask.MainRoutes { + + @cask.postJson("/json") + def jsonEndpoint(name: String, surname: String) = + "Hello " + name + " " + surname + + initialize() +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +object MyApp extends cask.MainRoutes: + + @cask.postJson("/json") + def jsonEndpoint(name: String, surname: String) = + "Hello " + name + " " + surname + + initialize() +``` +{% endtab %} +{% endtabs %} + +Send the POST request using `curl`: + +```shell +curl --header "Content-Type: application/json" \ + --data '{"name":"John","surname":"Smith"}' \ + http://localhost:8080/json +``` + +The response will be: +``` +Hello John Smith +``` + +Deserialization is handled by uPickle JSON library. To deserialize an object, use `ujson.Value` type. + +{% tabs web-server-input-3 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +object MyApp extends cask.MainRoutes { + + @cask.postJson("/json") + def jsonEndpoint(value: ujson.Value) = + value.toString + + initialize() +} + +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +object MyApp extends cask.MainRoutes: + + @cask.postJson("/json") + def jsonEndpoint(value: ujson.Value) = + value.toString + + initialize() + +``` +{% endtab %} +{% endtabs %} + +Send a POST request. +```shell +curl --header "Content-Type: application/json" \ + --data '{"value":{"name":"John","surname":"Smith"}}' \ + http://localhost:8080/json2 +``` + +Server will respond with: +``` +"{\"name\":\"John\",\"surname\":\"Smith\"}" +``` + +## Handling JSON-encoded output + +Cask endpoint can return JSON objects returned by uPickle library functions. Cask will automatically handle the `ujson.Value` +type and set the `Content-Type application/json` header. + +In this example we use a simple `TimeData` case class to send information about the time zone and current time in a chosen +location. To serialize a case class into JSON you need to define a serializer in its companion object. + +{% tabs web-server-input-3 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +object MyApp 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) = { + 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)) + } +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +object MyApp extends cask.MainRoutes { + import upickle.default.{ReadWriter, macroRW, writeJs} + case class TimeData(timezone: Option[String], time: String) + object TimeData: + given 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) = { + 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)) + } +} +``` +{% endtab %} +{% endtabs %} \ No newline at end of file diff --git a/_overviews/toolkit/web-server-intro.md b/_overviews/toolkit/web-server-intro.md new file mode 100644 index 0000000000..1b18ff9b7a --- /dev/null +++ b/_overviews/toolkit/web-server-intro.md @@ -0,0 +1,22 @@ +--- +title: Building web servers with Cask +type: chapter +description: The introduction of the Cask library +num: 30 +previous-page: http-client-what-else +next-page: web-server-static +--- + +Cask is an HTTP micro-framework, providing a simple and flexible way to build web applications. + +Its main focus is on the ease of use, which makes it ideal for newcomers, at cost of eschewing some features other +frameworks provide, like asynchronicity. + +To define an endpoint it's enough to annotate a function with an appropriate annotation, specifying the request path. +The function can either return a `cask.Response`, specifying the content, headers, status code etc., `String`, in which case +the result will be sent as `text/plain`, uJson JSON type or a Scalatags template. + +Cask comes bundled with uPickle library for handling JSONs, supports WebSockets and allows for extending endpoints with +decorators, which can be used to handle authentication or rate limiting. + +{% include markdown.html path="_markdown/install-cask.md" %} diff --git a/_overviews/toolkit/web-server-query-parameters.md b/_overviews/toolkit/web-server-query-parameters.md new file mode 100644 index 0000000000..ae192c63b7 --- /dev/null +++ b/_overviews/toolkit/web-server-query-parameters.md @@ -0,0 +1,77 @@ +--- +title: How to handle query parameters? +type: section +description: Handling query parameters with Cask +num: 33 +previous-page: web-server-dynamic +next-page: web-server-input +--- + +{% include markdown.html path="_markdown/install-cask.md" %} + +You can read the query parameters by adding to an endpoint method arguments that don't have corresponding path segments +defined in the annotation. + +In this example `city` is an optional parameter (note the `Option` type and `None` default value, they're required). +If not provided, time for the current timezone will be returned. + +{% tabs web-server-query-1 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +import java.time.{ZoneId, ZonedDateTime} + +object MyApp extends cask.MainRoutes { + + private def getZoneIdForCity(city: String): Option[ZoneId] = { + import scala.jdk.CollectionConverters._ + ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) + } + + @cask.get("/time") + def dynamicWithParam(city: Option[String] = None) = { + city match { + case Some(value) => getZoneIdForCity(value) match { + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $value" + } + case None => s"Current date is: ${ZonedDateTime.now()}" + } + } + + initialize() +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import java.time.{ZoneId, ZonedDateTime} + +object MyApp extends cask.MainRoutes: + + private def getZoneIdForCity(city: String): Option[ZoneId] = + import scala.jdk.CollectionConverters.* + ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) + + @cask.get("/time") + def dynamicWithParam(city: Option[String] = None) = + city match + case Some(value) => getZoneIdForCity(value) match + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $value" + case None => s"Current date is: ${ZonedDateTime.now()}" + + initialize() +``` +{% endtab %} +{% endtabs %} + +Run the example as before and access the endpoint at [http://localhost:8080/time?city=Paris](http://localhost:8080/time?city=Paris). +You should get a similar result to following. +``` +Current date is: 2024-07-22T10:08:18.218736+02:00[Europe/Paris] +``` + +If you omit the `city` param, you will get time for your current timezone. +``` +Current date is: 2024-07-22T10:11:45.004285+02:00[Europe/Warsaw] +``` diff --git a/_overviews/toolkit/web-server-static.md b/_overviews/toolkit/web-server-static.md new file mode 100644 index 0000000000..c0471c508e --- /dev/null +++ b/_overviews/toolkit/web-server-static.md @@ -0,0 +1,146 @@ +--- +title: How to serve a static file? +type: section +description: Serving a static file with Cask +num: 31 +previous-page: web-server-intro +next-page: web-server-dynamic +--- + +{% include markdown.html path="_markdown/install-cask.md" %} + +## Writing an endpoint + +To create a static file serving endpoint first we need to prepare the project structure. + +Create an example HTML file named `hello.html` with following contents. + +```html + + + + + Hello World + + +

Hello world

+ + +``` + +Place it in the `resources` directory. + +{% tabs web-server-static-1 class=tabs-build-tool %} +{% tab 'Scala CLI' %} +``` +example +├── Example.sc +└── resources + └── hello.html +``` +{% endtab %} +{% tab 'sbt' %} + +For instance, the following is the file structure of a project `example`: +``` +example +└──src + └── main + ├── resources + │ └── hello.html + └── scala + └── MyApp.scala +``` +{% endtab %} +{% tab 'Mill' %} +For instance, the following is the file structure of an example project: +``` +example +├── src +│ └── MyApp.scala +└── resources + └── hello.html +``` +{% endtab %} +{% endtabs %} + +The `@cask.staticFiles` annotation tells the server to match the part of path coming after what is specified in the +annotation itself and find a matching file in the directory defined by the endpoint function. + +In this example the directory with static files is `src/main/resources` and the URL path under which files are available is +`/static`. Thus, the `hello.html` page is available under `/static/hello.html`. + +{% tabs web-server-static-2 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +object MyApp extends cask.MainRoutes { + @cask.staticFiles("/static") + def staticEndpoint() = "src/main/resources" + + initialize() +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +object MyApp extends cask.MainRoutes: + @cask.staticFiles("/static") + def staticEndpoint() = "src/main/resources" + + initialize() +``` +{% endtab %} +{% endtabs %} + +As the file is placed in the resources directory, you can achieve the same effect using `@cask.staticResources`. In +this case the path is set to `"."`, as the `hello.html` file is available directly in the resources directory, as opposed +to being present in a nested directory. + +{% tabs web-server-static-3 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +object MyApp extends cask.MainRoutes { + @cask.staticResources("/static") + def staticEndpoint() = "." + + initialize() +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +object MyApp extends cask.MainRoutes: + @cask.staticResources("/static") + def staticEndpoint() = "." + + initialize() +``` +{% endtab %} +{% endtabs %} + +## Running the example + +Run the example with the build tool of your choice. + +{% tabs munit-unit-test-4 class=tabs-build-tool %} +{% tab 'Scala CLI' %} +In the terminal, the following command will start the server: +``` +scala-cli run Example.sc +``` +{% endtab %} +{% tab 'sbt' %} +In the sbt shell, the following command will start the server: +``` +sbt:example> example/run +``` +{% endtab %} +{% tab 'Mill' %} +In the terminal, the following command will start the server: +``` +./mill run +``` +{% endtab %} +{% endtabs %} + +The example page will be available at [http://localhost:8080/static/hello.html](http://localhost:8080/static/hello.html). \ No newline at end of file diff --git a/_overviews/toolkit/web-server-websockets.md b/_overviews/toolkit/web-server-websockets.md new file mode 100644 index 0000000000..589b64e14e --- /dev/null +++ b/_overviews/toolkit/web-server-websockets.md @@ -0,0 +1,113 @@ +--- +title: How to use websockets? +type: section +description: Using websockets with Cask +num: 35 +previous-page: web-server-input +next-page: web-server-cookies-and-decorators +--- + +{% include markdown.html path="_markdown/install-cask.md" %} + +You can create a WebSockets endpoint by using the `@cask.websocket` annotation. The endpoint method can return either a +`cask.WsHandler` instance defining how the communication should take place, or a `cask.Response`, which rejects the +attempt of forming a WebSocket connection. + +The connection can also be closed by sending `cask.Ws.close()` message through the WebSocket channel. + +Create an HTML file named `websockets.html` with the following content and place it in `resources ` directory. + +```html + + + +
+ + +
+
+ + + +``` + +The JavaScript code opens a WebSocket connection using the `ws://localhost:8080/websocket` endpoint. The `ws.onmessage` +event handler is executed when server pushes a message to the browser and `ws.onclose` when the connection is closed. + +Create an endpoint for serving static files using `@cask.staticResources` annotation and the endpoint for handling +the WebSocket connection. + +{% tabs web-server-query-1 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +@cask.staticResources("/static") +def static() = "." + +private def getZoneIdForCity(city: String): Option[ZoneId] = { + import scala.jdk.CollectionConverters._ + ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) +} + +@cask.websocket("/websocket") +def websocket(): cask.WsHandler = + cask.WsHandler { channel => + cask.WsActor { + case cask.Ws.Text("") => channel.send(cask.Ws.Close()) + case cask.Ws.Text(city) => + val text = getZoneIdForCity(city) match { + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $city" + } + channel.send(cask.Ws.Text(text)) + } + } +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +@cask.staticResources("/static") +def static() = "." + +private def getZoneIdForCity(city: String): Option[ZoneId] = + import scala.jdk.CollectionConverters.* + ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) + +@cask.websocket("/websocket") +def websocket(): cask.WsHandler = + cask.WsHandler { channel => + cask.WsActor { + case cask.Ws.Text("") => channel.send(cask.Ws.Close()) + case cask.Ws.Text(city) => + val text = getZoneIdForCity(city) match + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $city" + channel.send(cask.Ws.Text(text)) + } + } +``` +{% endtab %} +{% endtabs %} + +In the `cask.WsHandler` we define a `cask.WsActor` which reacts to events (of `cask.util.Ws.Event` type) and uses +WebSocket channel to send messages. In this example we receive a name of city and return the current time there. If server +receives an empty message, the connection is closed. \ No newline at end of file From a837327b27b3f79772394901fb3b4539fb9ef3cb Mon Sep 17 00:00:00 2001 From: gkepka Date: Mon, 26 Aug 2024 10:33:00 +0200 Subject: [PATCH 2/6] Update --- _overviews/toolkit/OrderedListOfMdFiles | 2 +- _overviews/toolkit/web-server-dynamic.md | 75 +++++++++-------- _overviews/toolkit/web-server-input.md | 73 ++++++++-------- _overviews/toolkit/web-server-intro.md | 7 +- .../toolkit/web-server-query-parameters.md | 27 +++--- _overviews/toolkit/web-server-static.md | 83 +++++++++++-------- _overviews/toolkit/web-server-websockets.md | 4 +- 7 files changed, 148 insertions(+), 123 deletions(-) diff --git a/_overviews/toolkit/OrderedListOfMdFiles b/_overviews/toolkit/OrderedListOfMdFiles index db4caf242f..b2790bd58a 100644 --- a/_overviews/toolkit/OrderedListOfMdFiles +++ b/_overviews/toolkit/OrderedListOfMdFiles @@ -33,4 +33,4 @@ web-server-dynamic.md web-server-query-parameters.md web-server-input.md web-server-websockets.md -web-server-cookies-and-decorators.md \ No newline at end of file +web-server-cookies-and-decorators.md diff --git a/_overviews/toolkit/web-server-dynamic.md b/_overviews/toolkit/web-server-dynamic.md index 57f837eb02..d271ff37c1 100644 --- a/_overviews/toolkit/web-server-dynamic.md +++ b/_overviews/toolkit/web-server-dynamic.md @@ -9,11 +9,9 @@ next-page: web-server-query-parameters {% include markdown.html path="_markdown/install-cask.md" %} -## Basic example +## Serving dynamically generated content -To create an endpoint returning dynamically generated content, use `@cask.get` annotation. - -For example, create an endpoint that returns the current date and time. +You can create an endpoint returning dynamically generated content with `@cask.get` annotation. {% tabs web-server-dynamic-1 class=tabs-scala-version %} {% tab 'Scala 2' %} @@ -22,7 +20,7 @@ import java.time.ZonedDateTime object MyApp extends cask.MainRoutes { @cask.get("/time") - def dynamic() = s"Current date is: ${ZonedDateTime.now()}" + def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}" initialize() } @@ -34,21 +32,29 @@ import java.time.ZonedDateTime object MyApp extends cask.MainRoutes: @cask.get("/time") - def dynamic() = s"Current date is: ${ZonedDateTime.now()}" + def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}" initialize() ``` {% endtab %} {% endtabs %} +The example above creates an endpoint returning the current date and time available at `/time`. The exact response will be +recreated every time you refresh the webpage. + +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). +If you want an HTML output being interpreted by the browser, you else need to set the `Content-Type` header manually +or [use the Scalatags templating library](/toolkit/web-server-dynamic.html#using-html-templates), supported by Cask. + +### Running the example -Run the example the same way as before (assuming you use the same project structure). +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). {% tabs web-server-dynamic-2 class=tabs-build-tool %} {% tab 'Scala CLI' %} In the terminal, the following command will start the server: ``` -scala-cli run Example.sc +scala-cli run Example.scala ``` {% endtab %} {% tab 'sbt' %} @@ -65,8 +71,7 @@ In the terminal, the following command will start the server: {% endtab %} {% endtabs %} -Access the endpoint at [http://localhost:8080/time](http://localhost:8080/time). You should see a result similar to the -one below. +Access [the endpoint](http://localhost:8080/time). You should see a result similar to the one below. ``` Current date is: 2024-07-22T09:07:05.752534+02:00[Europe/Warsaw] @@ -74,8 +79,9 @@ Current date is: 2024-07-22T09:07:05.752534+02:00[Europe/Warsaw] ## Using path segments -You can use path segments to specify the returned data more precisely. Building on the example above, add the `:city` -segment to get the current time in a city of choice. +Cask gives you the ability to access segments of the URL path withing the endpoint function. +Building on the example above, you can add a segment to specify that the endpoint should return the date and time +in a given city. {% tabs web-server-dynamic-3 class=tabs-scala-version %} {% tab 'Scala 2' %} @@ -90,7 +96,7 @@ object MyApp extends cask.MainRoutes { } @cask.get("/time/:city") - def dynamicWithCity(city: String) = { + def dynamicWithCity(city: String): String = { getZoneIdForCity(city) match { case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" case None => s"Couldn't find time zone for city $city" @@ -112,7 +118,7 @@ object MyApp extends cask.MainRoutes: ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) @cask.get("/time/:city") - def dynamicWithCity(city: String) = + def dynamicWithCity(city: String): String = getZoneIdForCity(city) match case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" case None => s"Couldn't find time zone for city $city" @@ -122,23 +128,22 @@ object MyApp extends cask.MainRoutes: {% endtab %} {% endtabs %} -Accessing the endpoint at [http://localhost:8080/time/Paris](http://localhost:8080/time/Paris) will result with: -``` -Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris] -``` +In the example above, the `:city` segment in `/time/:city` is available through the `city` argument of the endpoint method. +The name of the argument must be identical to the segment name. The `getZoneIdForCity` helper method find the timezone for +a given city and then the current date and time is translated to that timezone. -and at [http://localhost:8080/time/Tokyo](http://localhost:8080/time/Tokyo) you will see: +Accessing [the endpoint](http://localhost:8080/time/Paris) (notice the `Paris` segment in the URL) will result with: ``` -Current date is: 2024-07-22T16:08:41.137563+09:00[Asia/Tokyo] +Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris] ``` -Cask endpoints can handle either fixed or arbitrary number of path segments. Please consult the -[documentation](https://com-lihaoyi.github.io/cask/index.html#variable-routes) for more details. +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 +with an unspecified number of segments (for example `/path/foo/bar/baz/`) by giving the endpoint method an argument with `cask.RemainingPathSegments` type. +Consult the [documentation](https://com-lihaoyi.github.io/cask/index.html#variable-routes) for more details. ## Using HTML templates -You can combine Cask code with a templating library like [Scalatags](https://com-lihaoyi.github.io/scalatags/) to -build an HTML response. +To create an HTML response, you can combine Cask code with the [Scalatags](https://com-lihaoyi.github.io/scalatags/) templating library. Import the Scalatags library: @@ -163,9 +168,10 @@ ivy"com.lihaoyi::scalatags::0.13.1" {% endtab %} {% endtabs %} -Now the example above can be rewritten to use a template. Cask will build a response out of the `doctype` automatically. +Now the example above can be rewritten to use a template. Cask will build a response out of the `doctype` automatically, +setting the `Content-Type` header to `text/html`. -{% tabs web-server-dynamic-3 class=tabs-scala-version %} +{% tabs web-server-dynamic-5 class=tabs-scala-version %} {% tab 'Scala 2' %} ```scala import java.time.{ZoneId, ZonedDateTime} @@ -179,7 +185,7 @@ object MyApp extends cask.MainRoutes { } @cask.get("/time/:city") - def dynamicWithCity(city: String) = { + def dynamicWithCity(city: String): doctype = { val text = getZoneIdForCity(city) match { case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" case None => s"Couldn't find time zone for city $city" @@ -188,7 +194,7 @@ object MyApp extends cask.MainRoutes { doctype("html")( html( body( - h1(text) + p(text) ) ) ) @@ -210,14 +216,14 @@ object MyApp extends cask.MainRoutes: ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) @cask.get("/time/:city") - def dynamicWithCity(city: String) = + def dynamicWithCity(city: String): doctype = val text = getZoneIdForCity(city) match - case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" - case None => s"Couldn't find time zone for city $city" + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $city" doctype("html")( html( body( - h1(text) + p(text) ) ) ) @@ -225,4 +231,7 @@ object MyApp extends cask.MainRoutes: initialize() ``` {% endtab %} -{% endtabs %} \ No newline at end of file +{% endtabs %} + +Here we get the text of response and wrap it in a Scalatags template. Notice that the return type changed from `String` +to `doctype`. \ No newline at end of file diff --git a/_overviews/toolkit/web-server-input.md b/_overviews/toolkit/web-server-input.md index 3446ad35fa..5d08f928c1 100644 --- a/_overviews/toolkit/web-server-input.md +++ b/_overviews/toolkit/web-server-input.md @@ -11,15 +11,8 @@ next-page: web-server-websockets ## Handling form-encoded input -Similarly to path segments and query parameters, form fields are read by using endpoint method arguments. Use `cask.postForm` -annotation and set the HTML form method to `post`. - -In this example we create a form asking for name and surname of a user and then redirect to a greeting page. Notice the -use of `cask.Response`. The default returned content type is `text/plain`, set it to `text/html` in order for browser to display -the form correctly. - -The `formMethod` endpoint reads the form data using `name` and `surname` parameters. The names of parameters must -be identical to the field names of the form. +To create an endpoint that handles the data provided in an HTML form, use `cask.postForm` annotation, give the endpoint method arguments +with names corresponding to names of fields in the form and set the form method to `post`. {% tabs web-server-input-1 class=tabs-scala-version %} {% tab 'Scala 2' %} @@ -27,7 +20,7 @@ be identical to the field names of the form. object MyApp extends cask.MainRoutes { @cask.get("/form") - def getForm() = { + def getForm(): String = { val html = """ | @@ -46,7 +39,7 @@ object MyApp extends cask.MainRoutes { } @cask.postForm("/form") - def formEndpoint(name: String, surname: String) = + def formEndpoint(name: String, surname: String): String = "Hello " + name + " " + surname initialize() @@ -58,7 +51,7 @@ object MyApp extends cask.MainRoutes { object MyApp extends cask.MainRoutes: @cask.get("/form") - def getForm() = + def getForm(): String = val html = """ | @@ -76,7 +69,7 @@ object MyApp extends cask.MainRoutes: cask.Response(data = html, headers = Seq("Content-Type" -> "text/html")) @cask.postForm("/form") - def formEndpoint(name: String, surname: String) = + def formEndpoint(name: String, surname: String): String = "Hello " + name + " " + surname initialize() @@ -84,11 +77,17 @@ object MyApp extends cask.MainRoutes: {% endtab %} {% endtabs %} +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 default returned content type in case of `String` returning endpoint method is `text/plain`, +set it to `text/html` in order for browser to display the form correctly. + +The `formEndpoint` endpoint reads the form data using `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 `cask.PostJson` annotation is used. The topmost fields -will be read into the endpoint method arguments and if any of them is missing or has an incorrect type, an error message -will be returned with 400 response code. +JSON fields are handled in the same way as form fields, except that `cask.PostJson` annotation is used. The fields +will be read into the endpoint method arguments. {% tabs web-server-input-2 class=tabs-scala-version %} {% tab 'Scala 2' %} @@ -96,7 +95,7 @@ will be returned with 400 response code. object MyApp extends cask.MainRoutes { @cask.postJson("/json") - def jsonEndpoint(name: String, surname: String) = + def jsonEndpoint(name: String, surname: String): String = "Hello " + name + " " + surname initialize() @@ -107,8 +106,8 @@ object MyApp extends cask.MainRoutes { ```scala object MyApp extends cask.MainRoutes: - @cask.postJson("/json") - def jsonEndpoint(name: String, surname: String) = +@cask.postJson("/json") + def jsonEndpoint(name: String, surname: String): String = "Hello " + name + " " + surname initialize() @@ -129,7 +128,12 @@ The response will be: Hello John Smith ``` -Deserialization is handled by uPickle JSON library. To deserialize an object, use `ujson.Value` type. +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 400 response code. + +To handle the case when the fields of the JSON are not known in advance, you can use argument with the `ujson.Value` type +from uPickle library. {% tabs web-server-input-3 class=tabs-scala-version %} {% tab 'Scala 2' %} @@ -137,7 +141,7 @@ Deserialization is handled by uPickle JSON library. To deserialize an object, us object MyApp extends cask.MainRoutes { @cask.postJson("/json") - def jsonEndpoint(value: ujson.Value) = + def jsonEndpoint(value: ujson.Value): String = value.toString initialize() @@ -150,7 +154,7 @@ object MyApp extends cask.MainRoutes { object MyApp extends cask.MainRoutes: @cask.postJson("/json") - def jsonEndpoint(value: ujson.Value) = + def jsonEndpoint(value: ujson.Value): String = value.toString initialize() @@ -159,6 +163,9 @@ object MyApp extends cask.MainRoutes: {% endtab %} {% endtabs %} +In this example the JSON is merely converted to `String`, check the [*uPickle tutorial*](/toolkit/json-introduction.html) for more information +on what can be done with `ujson.Value` type. + Send a POST request. ```shell curl --header "Content-Type: application/json" \ @@ -166,7 +173,7 @@ curl --header "Content-Type: application/json" \ http://localhost:8080/json2 ``` -Server will respond with: +The server will respond with: ``` "{\"name\":\"John\",\"surname\":\"Smith\"}" ``` @@ -174,12 +181,12 @@ Server will respond with: ## Handling JSON-encoded output Cask endpoint can return JSON objects returned by uPickle library functions. Cask will automatically handle the `ujson.Value` -type and set the `Content-Type application/json` header. +type and set the `Content-Type` header to `application/json`. -In this example we use a simple `TimeData` case class to send information about the time zone and current time in a chosen -location. To serialize a case class into JSON you need to define a serializer in its companion object. +In this example `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 case of Scala 2. -{% tabs web-server-input-3 class=tabs-scala-version %} +{% tabs web-server-input-4 class=tabs-scala-version %} {% tab 'Scala 2' %} ```scala object MyApp extends cask.MainRoutes { @@ -195,7 +202,7 @@ object MyApp extends cask.MainRoutes { } @cask.get("/time_json/:city") - def timeJSON(city: String) = { + 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)}" @@ -210,20 +217,18 @@ object MyApp extends cask.MainRoutes { ```scala object MyApp extends cask.MainRoutes { import upickle.default.{ReadWriter, macroRW, writeJs} - case class TimeData(timezone: Option[String], time: String) - object TimeData: - given rw: ReadWriter[TimeData]= macroRW + 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) = { + 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" + 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)) } } diff --git a/_overviews/toolkit/web-server-intro.md b/_overviews/toolkit/web-server-intro.md index 1b18ff9b7a..653ac6c71a 100644 --- a/_overviews/toolkit/web-server-intro.md +++ b/_overviews/toolkit/web-server-intro.md @@ -12,9 +12,10 @@ Cask is an HTTP micro-framework, providing a simple and flexible way to build we Its main focus is on the ease of use, which makes it ideal for newcomers, at cost of eschewing some features other frameworks provide, like asynchronicity. -To define an endpoint it's enough to annotate a function with an appropriate annotation, specifying the request path. -The function can either return a `cask.Response`, specifying the content, headers, status code etc., `String`, in which case -the result will be sent as `text/plain`, uJson JSON type or a Scalatags template. +To define an endpoint it's enough to annotate a function with an appropriate annotation, specifying the request path. +Cask allows for building the response manually using tools the Cask library provides, specifying the content, headers, +status code etc. An endpoint function can also just return a string, [uPickle](https://com-lihaoyi.github.io/upickle/) JSON type, or a [Scalatags](https://com-lihaoyi.github.io/scalatags/) +template and Cask will automatically create a response, setting all the necessary headers. Cask comes bundled with uPickle library for handling JSONs, supports WebSockets and allows for extending endpoints with decorators, which can be used to handle authentication or rate limiting. diff --git a/_overviews/toolkit/web-server-query-parameters.md b/_overviews/toolkit/web-server-query-parameters.md index ae192c63b7..d53d34eaf0 100644 --- a/_overviews/toolkit/web-server-query-parameters.md +++ b/_overviews/toolkit/web-server-query-parameters.md @@ -9,11 +9,13 @@ next-page: web-server-input {% include markdown.html path="_markdown/install-cask.md" %} -You can read the query parameters by adding to an endpoint method arguments that don't have corresponding path segments -defined in the annotation. +Query parameters are the key-value pairs coming after the question mark in a URL. They can be used for filtering, +sorting or limiting the results provided by the server. For example, in `/time?city=Paris` URL, the `city` part +is the name of a parameter and `Paris` is its value. Cask allows for reading the query parameters by defining an endpoint +method with arguments matching the names of the expected parameters and not matching any of the URL segments. -In this example `city` is an optional parameter (note the `Option` type and `None` default value, they're required). -If not provided, time for the current timezone will be returned. +In this example the `city` parameter will be optional, which you specify in Cask by giving the argument `Option` type and +`None` default value. If not provided, the time for the current timezone will be returned. {% tabs web-server-query-1 class=tabs-scala-version %} {% tab 'Scala 2' %} @@ -28,7 +30,7 @@ object MyApp extends cask.MainRoutes { } @cask.get("/time") - def dynamicWithParam(city: Option[String] = None) = { + def dynamicWithParam(city: Option[String] = None): String = { city match { case Some(value) => getZoneIdForCity(value) match { case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" @@ -53,11 +55,11 @@ object MyApp extends cask.MainRoutes: ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) @cask.get("/time") - def dynamicWithParam(city: Option[String] = None) = + def dynamicWithParam(city: Option[String] = None): String = city match case Some(value) => getZoneIdForCity(value) match - case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" - case None => s"Couldn't find time zone for city $value" + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $value" case None => s"Current date is: ${ZonedDateTime.now()}" initialize() @@ -65,13 +67,8 @@ object MyApp extends cask.MainRoutes: {% endtab %} {% endtabs %} -Run the example as before and access the endpoint at [http://localhost:8080/time?city=Paris](http://localhost:8080/time?city=Paris). -You should get a similar result to following. +Run the example as before and access the [endpoint](http://localhost:8080/time?city=Paris) (notice the `?city=Paris` part of the URL). +You should get a result similar to the following one. ``` Current date is: 2024-07-22T10:08:18.218736+02:00[Europe/Paris] ``` - -If you omit the `city` param, you will get time for your current timezone. -``` -Current date is: 2024-07-22T10:11:45.004285+02:00[Europe/Warsaw] -``` diff --git a/_overviews/toolkit/web-server-static.md b/_overviews/toolkit/web-server-static.md index c0471c508e..1d5cb1e1cd 100644 --- a/_overviews/toolkit/web-server-static.md +++ b/_overviews/toolkit/web-server-static.md @@ -9,22 +9,25 @@ next-page: web-server-dynamic {% include markdown.html path="_markdown/install-cask.md" %} -## Writing an endpoint +## Serving a static file -To create a static file serving endpoint first we need to prepare the project structure. +An endpoint is a specific URL where a particular webpage can be accessed. In Cask an endpoint is a function returning the +webpage data together with an annotation describing the URL it's available at. -Create an example HTML file named `hello.html` with following contents. +To create a static file serving endpoint we need two things: an HTML file with the page content and a function that +points out to the location of the file. + +Create a minimal HTML file named `hello.html` with following contents. ```html - - - - - Hello World - - -

Hello world

- + + + + Hello World + + +

Hello world

+ ``` @@ -34,14 +37,12 @@ Place it in the `resources` directory. {% tab 'Scala CLI' %} ``` example -├── Example.sc +├── Example.scala └── resources └── hello.html ``` {% endtab %} {% tab 'sbt' %} - -For instance, the following is the file structure of a project `example`: ``` example └──src @@ -49,33 +50,29 @@ example ├── resources │ └── hello.html └── scala - └── MyApp.scala + └── Example.scala ``` {% endtab %} {% tab 'Mill' %} -For instance, the following is the file structure of an example project: ``` example ├── src -│ └── MyApp.scala +│ └── Example.scala └── resources └── hello.html ``` {% endtab %} {% endtabs %} -The `@cask.staticFiles` annotation tells the server to match the part of path coming after what is specified in the -annotation itself and find a matching file in the directory defined by the endpoint function. - -In this example the directory with static files is `src/main/resources` and the URL path under which files are available is -`/static`. Thus, the `hello.html` page is available under `/static/hello.html`. +The `@cask.staticFiles` annotation specifies at which path the webpage will be available. The endpoint function returns +the location in which the file can be found. {% tabs web-server-static-2 class=tabs-scala-version %} {% tab 'Scala 2' %} ```scala -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { @cask.staticFiles("/static") - def staticEndpoint() = "src/main/resources" + def staticEndpoint(): String = "src/main/resources" // or "resources" if not using SBT initialize() } @@ -83,25 +80,36 @@ object MyApp extends cask.MainRoutes { {% endtab %} {% tab 'Scala 3' %} ```scala -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: @cask.staticFiles("/static") - def staticEndpoint() = "src/main/resources" + def staticEndpoint(): String = "src/main/resources" // or "resources" if not using SBT initialize() ``` {% endtab %} {% endtabs %} -As the file is placed in the resources directory, you can achieve the same effect using `@cask.staticResources`. In -this case the path is set to `"."`, as the `hello.html` file is available directly in the resources directory, as opposed -to being present in a nested directory. +In the example above, `@cask.staticFiles` instructs the server to look for files accessed at `/static` path in the +`src/main/resources` directory. Cask will match any subpath coming after `/static` and append it to the directory path. +If you access the `/static/hello.html` file, it will serve the file available at `src/main/resources/hello.html`. +The directory path can be any path available to the server, relative or not. If the requested file cannot be found in the +specified location, a 404 response with an error message will be returned instead. + +The `Example` object inherits from `cask.MainRoutes` class, providing the main function starting the server. The `initialize()` +method call initializes the server routes, i.e. the association between URL paths and the code that handles them. + +### Using the resources directory + +The `@cask.staticResources` annotation works in the same way as `@cask.staticFiles` used above with the difference that +the path returned by the endpoint method describes the location of files _inside_ the resources directory. Since the +previous example conveniently used the resources directory, it can be simplified with `@cask.staticResources`. {% tabs web-server-static-3 class=tabs-scala-version %} {% tab 'Scala 2' %} ```scala -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { @cask.staticResources("/static") - def staticEndpoint() = "." + def staticEndpoint(): String = "." initialize() } @@ -109,15 +117,20 @@ object MyApp extends cask.MainRoutes { {% endtab %} {% tab 'Scala 3' %} ```scala -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: @cask.staticResources("/static") - def staticEndpoint() = "." + def staticEndpoint(): String = "." initialize() ``` {% endtab %} {% endtabs %} +In the endpoint method the location is set to `"."`, telling the server that the files are available directly in the +resources directory. In general, you can use any nested location within the resources directory, for instance you could opt +for placing your HTML files in `static` directory inside the resources directory or use different directories to sort out +files used by different endpoints. + ## Running the example Run the example with the build tool of your choice. @@ -126,7 +139,7 @@ Run the example with the build tool of your choice. {% tab 'Scala CLI' %} In the terminal, the following command will start the server: ``` -scala-cli run Example.sc +scala-cli run Example.scala ``` {% endtab %} {% tab 'sbt' %} diff --git a/_overviews/toolkit/web-server-websockets.md b/_overviews/toolkit/web-server-websockets.md index 589b64e14e..b5d06bd239 100644 --- a/_overviews/toolkit/web-server-websockets.md +++ b/_overviews/toolkit/web-server-websockets.md @@ -9,7 +9,7 @@ next-page: web-server-cookies-and-decorators {% include markdown.html path="_markdown/install-cask.md" %} -You can create a WebSockets endpoint by using the `@cask.websocket` annotation. The endpoint method can return either a +You can create a WebSocket endpoint by using the `@cask.websocket` annotation. The endpoint method can return either a `cask.WsHandler` instance defining how the communication should take place, or a `cask.Response`, which rejects the attempt of forming a WebSocket connection. @@ -57,7 +57,7 @@ event handler is executed when server pushes a message to the browser and `ws.on Create an endpoint for serving static files using `@cask.staticResources` annotation and the endpoint for handling the WebSocket connection. -{% tabs web-server-query-1 class=tabs-scala-version %} +{% tabs web-server-websocket-1 class=tabs-scala-version %} {% tab 'Scala 2' %} ```scala @cask.staticResources("/static") From 7d388d544aa7b95975f896b543a3d86087824129 Mon Sep 17 00:00:00 2001 From: gkepka Date: Mon, 26 Aug 2024 10:43:05 +0200 Subject: [PATCH 3/6] Fixes --- _overviews/toolkit/web-server-cookies-and-decorators.md | 2 +- _overviews/toolkit/web-server-input.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/_overviews/toolkit/web-server-cookies-and-decorators.md b/_overviews/toolkit/web-server-cookies-and-decorators.md index dbb68eff81..a4826095f2 100644 --- a/_overviews/toolkit/web-server-cookies-and-decorators.md +++ b/_overviews/toolkit/web-server-cookies-and-decorators.md @@ -3,7 +3,7 @@ title: How to use cookies and decorators? type: section description: Using cookies and decorators with Cask num: 36 -previous-page: web-server-query-websockets +previous-page: web-server-websockets next-page: --- diff --git a/_overviews/toolkit/web-server-input.md b/_overviews/toolkit/web-server-input.md index 5d08f928c1..61dd9d0f15 100644 --- a/_overviews/toolkit/web-server-input.md +++ b/_overviews/toolkit/web-server-input.md @@ -20,7 +20,7 @@ with names corresponding to names of fields in the form and set the form method object MyApp extends cask.MainRoutes { @cask.get("/form") - def getForm(): String = { + def getForm(): cask.Response = { val html = """ | @@ -51,7 +51,7 @@ object MyApp extends cask.MainRoutes { object MyApp extends cask.MainRoutes: @cask.get("/form") - def getForm(): String = + def getForm(): cask.Response = val html = """ | @@ -163,7 +163,7 @@ object MyApp extends cask.MainRoutes: {% endtab %} {% endtabs %} -In this example the JSON is merely converted to `String`, check the [*uPickle tutorial*](/toolkit/json-introduction.html) for more information +In this example the JSON is merely converted to `String`, check the [*uPickle tutorial*](/toolkit/json-intro.html) for more information on what can be done with `ujson.Value` type. Send a POST request. From 48431b68f1487320c1d76fff796e4219651a015b Mon Sep 17 00:00:00 2001 From: gkepka Date: Thu, 29 Aug 2024 11:04:18 +0200 Subject: [PATCH 4/6] Grammar corrections --- .../web-server-cookies-and-decorators.md | 20 +++++++++---------- _overviews/toolkit/web-server-dynamic.md | 18 ++++++++--------- _overviews/toolkit/web-server-input.md | 12 +++++------ _overviews/toolkit/web-server-intro.md | 10 +++++----- .../toolkit/web-server-query-parameters.md | 8 ++++---- _overviews/toolkit/web-server-static.md | 20 +++++++++---------- _overviews/toolkit/web-server-websockets.md | 12 +++++------ 7 files changed, 50 insertions(+), 50 deletions(-) diff --git a/_overviews/toolkit/web-server-cookies-and-decorators.md b/_overviews/toolkit/web-server-cookies-and-decorators.md index a4826095f2..ae4171cf05 100644 --- a/_overviews/toolkit/web-server-cookies-and-decorators.md +++ b/_overviews/toolkit/web-server-cookies-and-decorators.md @@ -11,18 +11,18 @@ next-page: ## Using cookies -Cookies are saved by adding them to the `cookies` parameter of `cask.Response` constructor. +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 -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. +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. +Cookies can be read either with a method parameter of `cask.Cookie` type or by accessing `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`. +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' %} @@ -144,7 +144,7 @@ object MyApp extends cask.MainRoutes: 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. -In this example, the `loggedIn` decorator is used for checking if user is logged in before accessing the `/decorated` +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 diff --git a/_overviews/toolkit/web-server-dynamic.md b/_overviews/toolkit/web-server-dynamic.md index d271ff37c1..649cfed8ca 100644 --- a/_overviews/toolkit/web-server-dynamic.md +++ b/_overviews/toolkit/web-server-dynamic.md @@ -39,11 +39,11 @@ object MyApp extends cask.MainRoutes: {% endtab %} {% endtabs %} -The example above creates an endpoint returning the current date and time available at `/time`. The exact response will be +The example above creates an endpoint, returning the current date and time available at `/time`. The exact response will be recreated every time you refresh the webpage. -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). -If you want an HTML output being interpreted by the browser, you else need to set the `Content-Type` header manually +Since the endpoint method has the `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). +If you want an HTML output to be interpreted by the browser, you will need to set the `Content-Type` header manually or [use the Scalatags templating library](/toolkit/web-server-dynamic.html#using-html-templates), supported by Cask. ### Running the example @@ -71,7 +71,7 @@ In the terminal, the following command will start the server: {% endtab %} {% endtabs %} -Access [the endpoint](http://localhost:8080/time). You should see a result similar to the one below. +Access the endpoint at [http://localhost:8080/time](http://localhost:8080/time). You should see a result similar to the one below. ``` Current date is: 2024-07-22T09:07:05.752534+02:00[Europe/Warsaw] @@ -79,7 +79,7 @@ Current date is: 2024-07-22T09:07:05.752534+02:00[Europe/Warsaw] ## Using path segments -Cask gives you the ability to access segments of the URL path withing the endpoint function. +Cask gives you the ability to access segments of the URL path within the endpoint function. Building on the example above, you can add a segment to specify that the endpoint should return the date and time in a given city. @@ -129,10 +129,10 @@ object MyApp extends cask.MainRoutes: {% endtabs %} In the example above, the `:city` segment in `/time/:city` is available through the `city` argument of the endpoint method. -The name of the argument must be identical to the segment name. The `getZoneIdForCity` helper method find the timezone for -a given city and then the current date and time is translated to that timezone. +The name of the argument must be identical to the segment name. The `getZoneIdForCity` helper method finds the timezone for +a given city, and then the current date and time are translated to that timezone. -Accessing [the endpoint](http://localhost:8080/time/Paris) (notice the `Paris` segment in the URL) will result with: +Accessing the endpoint at[http://localhost:8080/time/Paris](http://localhost:8080/time/Paris) will result in: ``` Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris] ``` @@ -233,5 +233,5 @@ object MyApp extends cask.MainRoutes: {% endtab %} {% endtabs %} -Here we get the text of response and wrap it in a Scalatags template. Notice that the return type changed from `String` +Here we get the text of the response and wrap it in a Scalatags template. Notice that the return type changed from `String` to `doctype`. \ No newline at end of file diff --git a/_overviews/toolkit/web-server-input.md b/_overviews/toolkit/web-server-input.md index 61dd9d0f15..168fa66580 100644 --- a/_overviews/toolkit/web-server-input.md +++ b/_overviews/toolkit/web-server-input.md @@ -163,8 +163,8 @@ object MyApp extends cask.MainRoutes: {% endtab %} {% endtabs %} -In this example the JSON is merely converted to `String`, check the [*uPickle tutorial*](/toolkit/json-intro.html) for more information -on what can be done with `ujson.Value` type. +In this example the JSON is merely converted to `String`. Check the [*uPickle tutorial*](/toolkit/json-intro.html) for more information +on what can be done with the `ujson.Value` type. Send a POST request. ```shell @@ -180,11 +180,11 @@ The server will respond with: ## Handling JSON-encoded output -Cask endpoint can return JSON objects returned by uPickle library functions. Cask will automatically handle the `ujson.Value` +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 `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 case of Scala 2. +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. {% tabs web-server-input-4 class=tabs-scala-version %} {% tab 'Scala 2' %} @@ -216,7 +216,7 @@ object MyApp extends cask.MainRoutes { {% tab 'Scala 3' %} ```scala object MyApp extends cask.MainRoutes { - import upickle.default.{ReadWriter, macroRW, writeJs} + import upickle.default.{ReadWriter, writeJs} case class TimeData(timezone: Option[String], time: String) derives ReadWriter private def getZoneIdForCity(city: String): Option[ZoneId] = diff --git a/_overviews/toolkit/web-server-intro.md b/_overviews/toolkit/web-server-intro.md index 653ac6c71a..4154247ac2 100644 --- a/_overviews/toolkit/web-server-intro.md +++ b/_overviews/toolkit/web-server-intro.md @@ -9,15 +9,15 @@ next-page: web-server-static Cask is an HTTP micro-framework, providing a simple and flexible way to build web applications. -Its main focus is on the ease of use, which makes it ideal for newcomers, at cost of eschewing some features other +Its main focus is on the ease of use, which makes it ideal for newcomers, at the cost of eschewing some features other frameworks provide, like asynchronicity. -To define an endpoint it's enough to annotate a function with an appropriate annotation, specifying the request path. -Cask allows for building the response manually using tools the Cask library provides, specifying the content, headers, -status code etc. An endpoint function can also just return a string, [uPickle](https://com-lihaoyi.github.io/upickle/) JSON type, or a [Scalatags](https://com-lihaoyi.github.io/scalatags/) +To define an endpoint it's enough to annotate a function with an annotation specifying the request path. +Cask allows for building the response manually using tools Cask library provides, specifying the content, headers, +status code, etc. An endpoint function can also just return a string, a [uPickle](https://com-lihaoyi.github.io/upickle/) JSON type, or a [Scalatags](https://com-lihaoyi.github.io/scalatags/) template and Cask will automatically create a response, setting all the necessary headers. -Cask comes bundled with uPickle library for handling JSONs, supports WebSockets and allows for extending endpoints with +Cask comes bundled with the uPickle library for handling JSONs, supports WebSockets and allows for extending endpoints with decorators, which can be used to handle authentication or rate limiting. {% include markdown.html path="_markdown/install-cask.md" %} diff --git a/_overviews/toolkit/web-server-query-parameters.md b/_overviews/toolkit/web-server-query-parameters.md index d53d34eaf0..e2576f935f 100644 --- a/_overviews/toolkit/web-server-query-parameters.md +++ b/_overviews/toolkit/web-server-query-parameters.md @@ -10,11 +10,11 @@ next-page: web-server-input {% include markdown.html path="_markdown/install-cask.md" %} Query parameters are the key-value pairs coming after the question mark in a URL. They can be used for filtering, -sorting or limiting the results provided by the server. For example, in `/time?city=Paris` URL, the `city` part -is the name of a parameter and `Paris` is its value. Cask allows for reading the query parameters by defining an endpoint +sorting or limiting the results provided by the server. For example, in the `/time?city=Paris` URL, the `city` part +is the name of a parameter, and `Paris` is its value. Cask allows for reading the query parameters by defining an endpoint method with arguments matching the names of the expected parameters and not matching any of the URL segments. -In this example the `city` parameter will be optional, which you specify in Cask by giving the argument `Option` type and +In this example, the `city` parameter will be optional, which you specify in Cask by giving the argument `Option` type and `None` default value. If not provided, the time for the current timezone will be returned. {% tabs web-server-query-1 class=tabs-scala-version %} @@ -67,7 +67,7 @@ object MyApp extends cask.MainRoutes: {% endtab %} {% endtabs %} -Run the example as before and access the [endpoint](http://localhost:8080/time?city=Paris) (notice the `?city=Paris` part of the URL). +Run the example as before and access the endpoint at [http://localhost:8080/time?city=Paris](http://localhost:8080/time?city=Paris). You should get a result similar to the following one. ``` Current date is: 2024-07-22T10:08:18.218736+02:00[Europe/Paris] diff --git a/_overviews/toolkit/web-server-static.md b/_overviews/toolkit/web-server-static.md index 1d5cb1e1cd..12c8e22866 100644 --- a/_overviews/toolkit/web-server-static.md +++ b/_overviews/toolkit/web-server-static.md @@ -11,13 +11,13 @@ next-page: web-server-dynamic ## Serving a static file -An endpoint is a specific URL where a particular webpage can be accessed. In Cask an endpoint is a function returning the +An endpoint is a specific URL where a particular webpage can be accessed. In Cask, an endpoint is a function returning the webpage data together with an annotation describing the URL it's available at. -To create a static file serving endpoint we need two things: an HTML file with the page content and a function that -points out to the location of the file. +To create a static file serving endpoint, we need two things: an HTML file with the page content and a function that +points out the location of the file. -Create a minimal HTML file named `hello.html` with following contents. +Create a minimal HTML file named `hello.html` with the following contents. ```html @@ -89,18 +89,18 @@ object Example extends cask.MainRoutes: {% endtab %} {% endtabs %} -In the example above, `@cask.staticFiles` instructs the server to look for files accessed at `/static` path in the +In the example above, `@cask.staticFiles` instructs the server to look for files accessed at the `/static` path in the `src/main/resources` directory. Cask will match any subpath coming after `/static` and append it to the directory path. If you access the `/static/hello.html` file, it will serve the file available at `src/main/resources/hello.html`. The directory path can be any path available to the server, relative or not. If the requested file cannot be found in the specified location, a 404 response with an error message will be returned instead. -The `Example` object inherits from `cask.MainRoutes` class, providing the main function starting the server. The `initialize()` -method call initializes the server routes, i.e. the association between URL paths and the code that handles them. +The `Example` object inherits from the `cask.MainRoutes` class, providing the main function that starts the server. The `initialize()` +method call initializes the server routes, i.e., the association between URL paths and the code that handles them. ### Using the resources directory -The `@cask.staticResources` annotation works in the same way as `@cask.staticFiles` used above with the difference that +The `@cask.staticResources` annotation works in the same way as the `@cask.staticFiles` used above, with the difference that the path returned by the endpoint method describes the location of files _inside_ the resources directory. Since the previous example conveniently used the resources directory, it can be simplified with `@cask.staticResources`. @@ -126,9 +126,9 @@ object Example extends cask.MainRoutes: {% endtab %} {% endtabs %} -In the endpoint method the location is set to `"."`, telling the server that the files are available directly in the +In the endpoint method, the location is set to `"."`, telling the server that the files are available directly in the resources directory. In general, you can use any nested location within the resources directory, for instance you could opt -for placing your HTML files in `static` directory inside the resources directory or use different directories to sort out +for placing your HTML files in the `static` directory inside the resources directory or using different directories to sort out files used by different endpoints. ## Running the example diff --git a/_overviews/toolkit/web-server-websockets.md b/_overviews/toolkit/web-server-websockets.md index b5d06bd239..d0343ad6a2 100644 --- a/_overviews/toolkit/web-server-websockets.md +++ b/_overviews/toolkit/web-server-websockets.md @@ -11,11 +11,11 @@ next-page: web-server-cookies-and-decorators You can create a WebSocket endpoint by using the `@cask.websocket` annotation. The endpoint method can return either a `cask.WsHandler` instance defining how the communication should take place, or a `cask.Response`, which rejects the -attempt of forming a WebSocket connection. +attempt at forming a WebSocket connection. -The connection can also be closed by sending `cask.Ws.close()` message through the WebSocket channel. +The connection can also be closed by sending a `cask.Ws.close()` message through the WebSocket channel. -Create an HTML file named `websockets.html` with the following content and place it in `resources ` directory. +Create an HTML file named `websockets.html` with the following content and place it in the `resources ` directory. ```html @@ -52,9 +52,9 @@ Create an HTML file named `websockets.html` with the following content and place ``` The JavaScript code opens a WebSocket connection using the `ws://localhost:8080/websocket` endpoint. The `ws.onmessage` -event handler is executed when server pushes a message to the browser and `ws.onclose` when the connection is closed. +event handler is executed when the server pushes a message to the browser and `ws.onclose` when the connection is closed. -Create an endpoint for serving static files using `@cask.staticResources` annotation and the endpoint for handling +Create an endpoint for serving static files using the `@cask.staticResources` annotation and an endpoint for handling the WebSocket connection. {% tabs web-server-websocket-1 class=tabs-scala-version %} @@ -109,5 +109,5 @@ def websocket(): cask.WsHandler = {% endtabs %} In the `cask.WsHandler` we define a `cask.WsActor` which reacts to events (of `cask.util.Ws.Event` type) and uses -WebSocket channel to send messages. In this example we receive a name of city and return the current time there. If server +WebSocket channel to send messages. In this example, we receive the name of a city and return the current time there. If server receives an empty message, the connection is closed. \ No newline at end of file From 569d5df7a4135aa998d6e2a890488296f65c6247 Mon Sep 17 00:00:00 2001 From: gkepka Date: Sun, 8 Sep 2024 16:54:41 +0200 Subject: [PATCH 5/6] Fixes --- .../web-server-cookies-and-decorators.md | 40 +++++++++---------- _overviews/toolkit/web-server-dynamic.md | 16 ++++---- _overviews/toolkit/web-server-input.md | 38 ++++++++++-------- .../toolkit/web-server-query-parameters.md | 18 ++++----- _overviews/toolkit/web-server-static.md | 4 +- _overviews/toolkit/web-server-websockets.md | 7 +++- 6 files changed, 67 insertions(+), 56 deletions(-) diff --git a/_overviews/toolkit/web-server-cookies-and-decorators.md b/_overviews/toolkit/web-server-cookies-and-decorators.md index ae4171cf05..3cab7aaea9 100644 --- a/_overviews/toolkit/web-server-cookies-and-decorators.md +++ b/_overviews/toolkit/web-server-cookies-and-decorators.md @@ -31,12 +31,12 @@ To delete a cookie, set its `expires` parameter to an instant in the past, for e import java.util.UUID import java.util.concurrent.ConcurrentHashMap -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { val sessionIds = ConcurrentHashMap.newKeySet[String]() @cask.get("/login") - def getLogin() = { + def getLogin(): cask.Response[String] = { val html = """ | @@ -55,7 +55,7 @@ object MyApp extends cask.MainRoutes { } @cask.postForm("/login") - def postLogin(name: String, password: String) = { + def postLogin(name: String, password: String): cask.Response[String] = { if (name == "user" && password == "password") { val sessionId = UUID.randomUUID().toString sessionIds.add(sessionId) @@ -66,7 +66,7 @@ object MyApp extends cask.MainRoutes { } @cask.get("/check") - def checkLogin(request: cask.Request) = { + def checkLogin(request: cask.Request): String = { val sessionId = request.cookies.get("sessionId") if (sessionId.exists(cookie => sessionIds.contains(cookie.value))) { "You are logged in" @@ -90,12 +90,12 @@ object MyApp extends cask.MainRoutes { import java.util.UUID import java.util.concurrent.ConcurrentHashMap -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: val sessionIds = ConcurrentHashMap.newKeySet[String]() @cask.get("/login") - def getLogin() = + def getLogin(): cask.Response[String] = val html = """ | @@ -113,24 +113,24 @@ object MyApp extends cask.MainRoutes: cask.Response(data = html, headers = Seq("Content-Type" -> "text/html")) @cask.postForm("/login") - def postLogin(name: String, password: String) = - if name == "user" && password == "password": + 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) = - 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("/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) = + 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))) @@ -154,7 +154,7 @@ through the last argument group. Here we are passing the session identifier to a {% tab 'Scala 2' %} ```scala class loggedIn extends cask.RawDecorator { - override def wrapFunction(ctx: cask.Request, delegate: Delegate) = { + 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)) @@ -164,7 +164,7 @@ class loggedIn extends cask.RawDecorator { @loggedIn() @cask.get("/decorated") -def decorated()(sessionId: String) = { +def decorated()(sessionId: String): String = { s"You are logged in with id: $sessionId" } ``` @@ -172,7 +172,7 @@ def decorated()(sessionId: String) = { {% tab 'Scala 3' %} ```scala class loggedIn extends cask.RawDecorator: - override def wrapFunction(ctx: cask.Request, delegate: Delegate) = + 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)) @@ -182,7 +182,7 @@ class loggedIn extends cask.RawDecorator: @loggedIn() @cask.get("/decorated") -def decorated()(sessionId: String) = s"You are logged in with id: $sessionId" +def decorated()(sessionId: String): String = s"You are logged in with id: $sessionId" ``` {% endtab %} {% endtabs %} \ No newline at end of file diff --git a/_overviews/toolkit/web-server-dynamic.md b/_overviews/toolkit/web-server-dynamic.md index 649cfed8ca..e3c24810a4 100644 --- a/_overviews/toolkit/web-server-dynamic.md +++ b/_overviews/toolkit/web-server-dynamic.md @@ -18,7 +18,7 @@ You can create an endpoint returning dynamically generated content with `@cask.g ```scala import java.time.ZonedDateTime -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { @cask.get("/time") def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}" @@ -30,7 +30,7 @@ object MyApp extends cask.MainRoutes { ```scala import java.time.ZonedDateTime -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: @cask.get("/time") def dynamic(): String = s"Current date is: ${ZonedDateTime.now()}" @@ -58,9 +58,9 @@ scala-cli run Example.scala ``` {% endtab %} {% tab 'sbt' %} -In the sbt shell, the following command will start the server: +In the terminal, the following command will start the server: ``` -sbt:example> example/run +sbt example/run ``` {% endtab %} {% tab 'Mill' %} @@ -88,7 +88,7 @@ in a given city. ```scala import java.time.{ZoneId, ZonedDateTime} -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { private def getZoneIdForCity(city: String): Option[ZoneId] = { import scala.jdk.CollectionConverters._ @@ -111,7 +111,7 @@ object MyApp extends cask.MainRoutes { ```scala import java.time.{ZoneId, ZonedDateTime} -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: private def getZoneIdForCity(city: String): Option[ZoneId] = import scala.jdk.CollectionConverters.* @@ -177,7 +177,7 @@ setting the `Content-Type` header to `text/html`. import java.time.{ZoneId, ZonedDateTime} import scalatags.Text.all._ -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { private def getZoneIdForCity(city: String): Option[ZoneId] = { import scala.jdk.CollectionConverters._ @@ -209,7 +209,7 @@ object MyApp extends cask.MainRoutes { import java.time.{ZoneId, ZonedDateTime} import scalatags.Text.all.* -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: private def getZoneIdForCity(city: String): Option[ZoneId] = import scala.jdk.CollectionConverters.* diff --git a/_overviews/toolkit/web-server-input.md b/_overviews/toolkit/web-server-input.md index 168fa66580..720662c33f 100644 --- a/_overviews/toolkit/web-server-input.md +++ b/_overviews/toolkit/web-server-input.md @@ -17,10 +17,10 @@ with names corresponding to names of fields in the form and set the form method {% tabs web-server-input-1 class=tabs-scala-version %} {% tab 'Scala 2' %} ```scala -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { @cask.get("/form") - def getForm(): cask.Response = { + def getForm(): cask.Response[String] = { val html = """ | @@ -48,10 +48,10 @@ object MyApp extends cask.MainRoutes { {% endtab %} {% tab 'Scala 3' %} ```scala -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: @cask.get("/form") - def getForm(): cask.Response = + def getForm(): cask.Response[String] = val html = """ | @@ -78,8 +78,8 @@ object MyApp extends cask.MainRoutes: {% endtabs %} 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 default returned content type in case of `String` returning endpoint method is `text/plain`, -set it to `text/html` in order for browser to display the form correctly. +use of `cask.Response`. The `cask.Response` type allows user to set the status code, headers and cookies. The default +content type in case of `String` returning endpoint method is `text/plain`, set it to `text/html` in order for browser to display the form correctly. The `formEndpoint` endpoint reads the form data using `name` and `surname` parameters. The names of parameters must be identical to the field names of the form. @@ -92,7 +92,7 @@ will be read into the endpoint method arguments. {% tabs web-server-input-2 class=tabs-scala-version %} {% tab 'Scala 2' %} ```scala -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { @cask.postJson("/json") def jsonEndpoint(name: String, surname: String): String = @@ -104,9 +104,9 @@ object MyApp extends cask.MainRoutes { {% endtab %} {% tab 'Scala 3' %} ```scala -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: -@cask.postJson("/json") + @cask.postJson("/json") def jsonEndpoint(name: String, surname: String): String = "Hello " + name + " " + surname @@ -138,7 +138,7 @@ from uPickle library. {% tabs web-server-input-3 class=tabs-scala-version %} {% tab 'Scala 2' %} ```scala -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { @cask.postJson("/json") def jsonEndpoint(value: ujson.Value): String = @@ -151,7 +151,7 @@ object MyApp extends cask.MainRoutes { {% endtab %} {% tab 'Scala 3' %} ```scala -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: @cask.postJson("/json") def jsonEndpoint(value: ujson.Value): String = @@ -189,7 +189,9 @@ location. To serialize a case class into JSON, use type class derivation or defi {% tabs web-server-input-4 class=tabs-scala-version %} {% tab 'Scala 2' %} ```scala -object MyApp extends cask.MainRoutes { +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 { @@ -210,12 +212,16 @@ object MyApp extends cask.MainRoutes { } writeJs(TimeData(timezone.map(_.toString), time)) } + + initialize() } ``` {% endtab %} {% tab 'Scala 3' %} ```scala -object MyApp extends cask.MainRoutes { +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 @@ -224,14 +230,14 @@ object MyApp extends cask.MainRoutes { ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) @cask.get("/time_json/:city") - def timeJSON(city: String): ujson.Value = { + 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() ``` {% endtab %} {% endtabs %} \ No newline at end of file diff --git a/_overviews/toolkit/web-server-query-parameters.md b/_overviews/toolkit/web-server-query-parameters.md index e2576f935f..9435fdbbca 100644 --- a/_overviews/toolkit/web-server-query-parameters.md +++ b/_overviews/toolkit/web-server-query-parameters.md @@ -22,7 +22,7 @@ In this example, the `city` parameter will be optional, which you specify in Cas ```scala import java.time.{ZoneId, ZonedDateTime} -object MyApp extends cask.MainRoutes { +object Example extends cask.MainRoutes { private def getZoneIdForCity(city: String): Option[ZoneId] = { import scala.jdk.CollectionConverters._ @@ -48,19 +48,19 @@ object MyApp extends cask.MainRoutes { ```scala import java.time.{ZoneId, ZonedDateTime} -object MyApp extends cask.MainRoutes: +object Example extends cask.MainRoutes: private def getZoneIdForCity(city: String): Option[ZoneId] = import scala.jdk.CollectionConverters.* ZoneId.getAvailableZoneIds.asScala.find(_.endsWith("/" + city)).map(ZoneId.of) - @cask.get("/time") - def dynamicWithParam(city: Option[String] = None): String = - city match - case Some(value) => getZoneIdForCity(value) match - case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" - case None => s"Couldn't find time zone for city $value" - case None => s"Current date is: ${ZonedDateTime.now()}" + @cask.get("/time") + def dynamicWithParam(city: Option[String] = None): String = + city match + case Some(value) => getZoneIdForCity(value) match + case Some(zoneId) => s"Current date is: ${ZonedDateTime.now().withZoneSameInstant(zoneId)}" + case None => s"Couldn't find time zone for city $value" + case None => s"Current date is: ${ZonedDateTime.now()}" initialize() ``` diff --git a/_overviews/toolkit/web-server-static.md b/_overviews/toolkit/web-server-static.md index 12c8e22866..07a2c4a5e3 100644 --- a/_overviews/toolkit/web-server-static.md +++ b/_overviews/toolkit/web-server-static.md @@ -143,9 +143,9 @@ scala-cli run Example.scala ``` {% endtab %} {% tab 'sbt' %} -In the sbt shell, the following command will start the server: +In the terminal, the following command will start the server: ``` -sbt:example> example/run +sbt example/run ``` {% endtab %} {% tab 'Mill' %} diff --git a/_overviews/toolkit/web-server-websockets.md b/_overviews/toolkit/web-server-websockets.md index d0343ad6a2..bb63fc33fe 100644 --- a/_overviews/toolkit/web-server-websockets.md +++ b/_overviews/toolkit/web-server-websockets.md @@ -69,7 +69,7 @@ private def getZoneIdForCity(city: String): Option[ZoneId] = { } @cask.websocket("/websocket") -def websocket(): cask.WsHandler = +def websocket(): cask.WsHandler = { cask.WsHandler { channel => cask.WsActor { case cask.Ws.Text("") => channel.send(cask.Ws.Close()) @@ -81,6 +81,9 @@ def websocket(): cask.WsHandler = channel.send(cask.Ws.Text(text)) } } +} + +initialize() ``` {% endtab %} {% tab 'Scala 3' %} @@ -104,6 +107,8 @@ def websocket(): cask.WsHandler = channel.send(cask.Ws.Text(text)) } } + +initialize() ``` {% endtab %} {% endtabs %} From 4d4264f47e2113a2290135f9560dd9721c9d81d1 Mon Sep 17 00:00:00 2001 From: gkepka Date: Tue, 1 Oct 2024 13:46:01 +0200 Subject: [PATCH 6/6] Grammar fixes --- _includes/_markdown/install-cask.md | 2 +- .../toolkit/web-server-cookies-and-decorators.md | 8 ++++---- _overviews/toolkit/web-server-dynamic.md | 10 +++++----- _overviews/toolkit/web-server-input.md | 14 +++++++------- _overviews/toolkit/web-server-intro.md | 6 +++--- _overviews/toolkit/web-server-query-parameters.md | 4 ++-- _overviews/toolkit/web-server-static.md | 14 +++++++------- _overviews/toolkit/web-server-websockets.md | 8 ++++---- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/_includes/_markdown/install-cask.md b/_includes/_markdown/install-cask.md index d0b050408c..afb2754321 100644 --- a/_includes/_markdown/install-cask.md +++ b/_includes/_markdown/install-cask.md @@ -3,7 +3,7 @@ {% tabs cask-install class=tabs-build-tool %} {% tab 'Scala CLI' %} -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" ``` diff --git a/_overviews/toolkit/web-server-cookies-and-decorators.md b/_overviews/toolkit/web-server-cookies-and-decorators.md index 3cab7aaea9..36caeac4de 100644 --- a/_overviews/toolkit/web-server-cookies-and-decorators.md +++ b/_overviews/toolkit/web-server-cookies-and-decorators.md @@ -14,10 +14,10 @@ next-page: 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 -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. +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 `cask.Request` directly. +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. @@ -142,7 +142,7 @@ object Example extends cask.MainRoutes: ## 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. +`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. diff --git a/_overviews/toolkit/web-server-dynamic.md b/_overviews/toolkit/web-server-dynamic.md index e3c24810a4..49101505c7 100644 --- a/_overviews/toolkit/web-server-dynamic.md +++ b/_overviews/toolkit/web-server-dynamic.md @@ -11,7 +11,7 @@ next-page: web-server-query-parameters ## Serving dynamically generated content -You can create an endpoint returning dynamically generated content with `@cask.get` annotation. +You can create an endpoint returning dynamically generated content with the `@cask.get` annotation. {% tabs web-server-dynamic-1 class=tabs-scala-version %} {% tab 'Scala 2' %} @@ -39,10 +39,10 @@ object Example extends cask.MainRoutes: {% endtab %} {% endtabs %} -The example above creates an endpoint, returning the current date and time available at `/time`. The exact response will be +The example above creates an endpoint that returns the current date and time available at `/time`. The exact response will be recreated every time you refresh the webpage. -Since the endpoint method has the `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). +Since the endpoint method has the `String` output type, the result will be sent with the `text/plain` [content type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type). If you want an HTML output to be interpreted by the browser, you will need to set the `Content-Type` header manually or [use the Scalatags templating library](/toolkit/web-server-dynamic.html#using-html-templates), supported by Cask. @@ -132,7 +132,7 @@ In the example above, the `:city` segment in `/time/:city` is available through The name of the argument must be identical to the segment name. The `getZoneIdForCity` helper method finds the timezone for a given city, and then the current date and time are translated to that timezone. -Accessing the endpoint at[http://localhost:8080/time/Paris](http://localhost:8080/time/Paris) will result in: +Accessing the endpoint at [http://localhost:8080/time/Paris](http://localhost:8080/time/Paris) will result in: ``` Current date is: 2024-07-22T09:08:33.806259+02:00[Europe/Paris] ``` @@ -143,7 +143,7 @@ Consult the [documentation](https://com-lihaoyi.github.io/cask/index.html#variab ## Using HTML templates -To create an HTML response, you can combine Cask code with the [Scalatags](https://com-lihaoyi.github.io/scalatags/) templating library. +To create an HTML response, you can combine Cask with the [Scalatags](https://com-lihaoyi.github.io/scalatags/) templating library. Import the Scalatags library: diff --git a/_overviews/toolkit/web-server-input.md b/_overviews/toolkit/web-server-input.md index 720662c33f..8be4c659d3 100644 --- a/_overviews/toolkit/web-server-input.md +++ b/_overviews/toolkit/web-server-input.md @@ -11,7 +11,7 @@ next-page: web-server-websockets ## Handling form-encoded input -To create an endpoint that handles the data provided in an HTML form, use `cask.postForm` annotation, give the endpoint method arguments +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`. {% tabs web-server-input-1 class=tabs-scala-version %} @@ -78,15 +78,15 @@ object Example extends cask.MainRoutes: {% endtabs %} 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 user to set the status code, headers and cookies. The default -content type in case of `String` returning endpoint method is `text/plain`, set it to `text/html` in order for browser to display the form correctly. +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 `name` and `surname` parameters. The names of parameters must +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 `cask.PostJson` annotation is used. The fields +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. {% tabs web-server-input-2 class=tabs-scala-version %} @@ -130,9 +130,9 @@ 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 400 response code. +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 argument with the `ujson.Value` type +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. {% tabs web-server-input-3 class=tabs-scala-version %} diff --git a/_overviews/toolkit/web-server-intro.md b/_overviews/toolkit/web-server-intro.md index 4154247ac2..4a1efcdc6a 100644 --- a/_overviews/toolkit/web-server-intro.md +++ b/_overviews/toolkit/web-server-intro.md @@ -13,9 +13,9 @@ Its main focus is on the ease of use, which makes it ideal for newcomers, at the frameworks provide, like asynchronicity. To define an endpoint it's enough to annotate a function with an annotation specifying the request path. -Cask allows for building the response manually using tools Cask library provides, specifying the content, headers, -status code, etc. An endpoint function can also just return a string, a [uPickle](https://com-lihaoyi.github.io/upickle/) JSON type, or a [Scalatags](https://com-lihaoyi.github.io/scalatags/) -template and Cask will automatically create a response, setting all the necessary headers. +Cask allows for building the response manually using tools that the library provides, specifying the content, headers, +status code, etc. An endpoint function can also return a string, a [uPickle](https://com-lihaoyi.github.io/upickle/) JSON type, or a [Scalatags](https://com-lihaoyi.github.io/scalatags/) +template. In that case, Cask will automatically create a response with the appropriate headers. Cask comes bundled with the uPickle library for handling JSONs, supports WebSockets and allows for extending endpoints with decorators, which can be used to handle authentication or rate limiting. diff --git a/_overviews/toolkit/web-server-query-parameters.md b/_overviews/toolkit/web-server-query-parameters.md index 9435fdbbca..8da1dc9ccc 100644 --- a/_overviews/toolkit/web-server-query-parameters.md +++ b/_overviews/toolkit/web-server-query-parameters.md @@ -14,8 +14,8 @@ sorting or limiting the results provided by the server. For example, in the `