diff --git a/_config.yml b/_config.yml index 3439515b5d..02ccc1703c 100644 --- a/_config.yml +++ b/_config.yml @@ -192,6 +192,14 @@ defaults: overview-name: "Scaladoc" layout: multipage-overview permalink: "/scala3/guides/scaladoc/:title.html" + - + scope: + path: "_overviews/toolkit" + values: + partof: toolkit + overview-name: "The Scala Toolkit" + layout: multipage-overview + permalink: "/toolkit/:title.html" - scope: path: "scala3" @@ -203,6 +211,6 @@ highlighter: rouge permalink: /:categories/:title.html:output_ext baseurl: scala3ref: "https://docs.scala-lang.org/scala3/reference" -exclude: ["vendor"] +exclude: ["vendor", ".metals"] plugins: - jekyll-redirect-from diff --git a/_data/doc-nav-header.yml b/_data/doc-nav-header.yml index 392012e3b5..6eddea4ed7 100644 --- a/_data/doc-nav-header.yml +++ b/_data/doc-nav-header.yml @@ -37,6 +37,8 @@ url: "/tutorials/scala-on-android.html" - title: Scala with Maven url: "/tutorials/scala-with-maven.html" + - title: Using the Scala Toolkit + url: "/toolkit/introduction.html" - title: Reference url: "#" submenu: diff --git a/_includes/_markdown/install-munit.md b/_includes/_markdown/install-munit.md new file mode 100644 index 0000000000..e79aca2e44 --- /dev/null +++ b/_includes/_markdown/install-munit.md @@ -0,0 +1,51 @@ +{% altDetails install-info-box 'Getting MUnit' %} + +{% tabs munit-unit-test-1 class=tabs-build-tool %} +{% tab 'Scala CLI' %} +You can require the entire toolkit in a single line: +```scala +//> using dep "org.scala-lang::toolkit-test:0.1.7" +``` + +Alternatively, you can require just a specific version of MUnit: +```scala +//> using dep "org.scalameta::munit:1.0.0-M7" +``` +{% endtab %} +{% tab 'sbt' %} +In your build.sbt file, you can add the dependency on toolkit-test: +```scala +lazy val example = project.in(file("example")) + .settings( + scalaVersion := "3.2.2", + libraryDependencies += "org.scala-lang" %% "toolkit-test" % "0.1.7" % Test + ) +``` +Here the `Test` configuration means that the dependency is only used by the source files in `example/src/test`. + +Alternatively, you can require just a specific version of MUnit: +```scala +libraryDependencies += "org.scalameta" %% "munit" % "1.0.0-M7" % Test +``` +{% endtab %} +{% tab 'Mill' %} +In your build.sc file, you can add a `test` object extending `Tests` and `TestModule.Munit`: +```scala +object example extends ScalaModule { + def scalaVersion = "3.2.2" + object test extends Tests with TestModule.Munit { + def ivyDeps = + Agg( + ivy"org.scala-lang::toolkit-test:0.1.7" + ) + } +} +``` + +Alternatively, you can require just a specific version of MUnit: +```scala +ivy"org.scalameta::munit:1.0.0-M7" +``` +{% endtab %} +{% endtabs %} +{% endaltDetails %} diff --git a/_includes/_markdown/install-os-lib.md b/_includes/_markdown/install-os-lib.md new file mode 100644 index 0000000000..0d3057c0e9 --- /dev/null +++ b/_includes/_markdown/install-os-lib.md @@ -0,0 +1,46 @@ +{% altDetails require-info-box 'Getting OS-Lib' %} + +{% tabs oslib-install class=tabs-build-tool %} +{% tab 'Scala CLI' %} +You can require the entire toolkit in a single line: +```scala +//> using toolkit "latest" +``` + +Alternatively, you can require just a specific version of OS-Lib: +```scala +//> using dep "com.lihaoyi::os-lib:0.9.1" +``` +{% endtab %} +{% tab 'sbt' %} +In your `build.sbt`, you can add a dependency on the toolkit: +```scala +lazy val example = project.in(file("example")) + .settings( + scalaVersion := "3.2.2", + libraryDependencies += "org.scala-lang" %% "toolkit" % "0.1.7" + ) +``` +Alternatively, you can require just a specific version of OS-Lib: +```scala +libraryDependencies += "com.lihaoyi" %% "os-lib" % "0.9.1" +``` +{% endtab %} +{% tab 'Mill' %} +In your `build.sc` file, you can add a dependency on the Toolkit: +```scala +object example extends ScalaModule { + def scalaVersion = "3.2.2" + def ivyDeps = + Agg( + ivy"org.scala-lang::toolkit:0.1.7" + ) +} +``` +Alternatively, you can require just a specific version of OS-Lib: +```scala +ivy"com.lihaoyi::os-lib:0.9.1" +``` +{% endtab %} +{% endtabs %} +{% endaltDetails %} diff --git a/_includes/_markdown/install-sttp.md b/_includes/_markdown/install-sttp.md new file mode 100644 index 0000000000..16cd90f485 --- /dev/null +++ b/_includes/_markdown/install-sttp.md @@ -0,0 +1,47 @@ +{% altDetails install-info-box 'Getting sttp' %} + +{% tabs sttp-install-methods class=tabs-build-tool%} +{% tab 'Scala CLI' %} +You can require the entire toolkit in a single line: +```scala +//> using toolkit "latest" +``` + +Alternatively, you can require just a specific version of sttp: +```scala +//> using dep "com.softwaremill.sttp.client4::core:4.0.0-M1" +``` +{% endtab %} +{% tab 'sbt' %} +In your build.sbt file, you can add a dependency on the Toolkit: +```scala +lazy val example = project.in(file("example")) + .settings( + scalaVersion := "3.2.2", + libraryDependencies += "org.scala-lang" %% "toolkit" % "0.1.7" + ) +``` + +Alternatively, you can require just a specific version of sttp: +```scala +libraryDependencies += "com.softwaremill.sttp.client4" %% "core" % "4.0.0-M1" +``` +{% endtab %} +{% tab 'Mill' %} +In your build.sc file, you can add a dependency on the Toolkit: +```scala +object example extends ScalaModule { + def scalaVersion = "3.2.2" + def ivyDeps = + Agg( + ivy"org.scala-lang::toolkit:0.1.7" + ) +} +``` +Alternatively, you can require just a specific version of sttp: +```scala +ivy"com.softwaremill.sttp.client4::core:4.0.0-M1" +``` +{% endtab %} +{% endtabs %} +{% endaltDetails %} diff --git a/_includes/_markdown/install-upickle.md b/_includes/_markdown/install-upickle.md new file mode 100644 index 0000000000..2160f4dc91 --- /dev/null +++ b/_includes/_markdown/install-upickle.md @@ -0,0 +1,46 @@ +{% altDetails install-info-box 'Getting upickle' %} + +{% tabs upickle-install-methods class=tabs-build-tool %} +{% tab 'Scala CLI' %} +Using Scala CLI, you can require the entire toolkit in a single line: +```scala +//> using toolkit "latest" +``` + +Alternatively, you can require just a specific version of UPickle: +```scala +//> using dep "com.lihaoyi::upickle:3.1.0 +``` +{% endtab %} +{% tab 'sbt' %} +In your build.sbt file, you can add the dependency on the Toolkit: +```scala +lazy val example = project.in(file("example")) + .settings( + scalaVersion := "3.2.2", + libraryDependencies += "org.scala-lang" %% "toolkit" % "0.1.7" + ) +``` +Alternatively, you can require just a specific version of UPickle: +```scala +libraryDependencies += "com.lihaoyi" %% "upickle" % "3.1.0" +``` +{% endtab %} +{% tab 'Mill' %} +In your build.sc file, you can add the dependency to the upickle library: +```scala +object example extends ScalaModule { + def scalaVersion = "3.2.2" + def ivyDeps = + Agg( + ivy"org.scala-lang::toolkit:0.1.7" + ) +} +``` +Alternatively, you can require just a specific version of UPickle: +```scala +ivy"com.lihaoyi::upickle:3.1.0" +``` +{% endtab %} +{% endtabs %} +{% endaltDetails %} diff --git a/_includes/markdown.html b/_includes/markdown.html new file mode 100644 index 0000000000..cd79243a41 --- /dev/null +++ b/_includes/markdown.html @@ -0,0 +1,3 @@ +{%if include.selector%}<{{include.selector}} {%if include.classes%}class="{{include.classes}}"{%endif%} {%if include.id%}id="{{include.id}}{%endif%}">{%endif%} + {% capture markdown %}{% include {{include.path}} %}{% endcapture %}{{ markdown | markdownify }} +{%if include.selector%}{%endif%} diff --git a/_overviews/toolkit/OrderedListOfMdFiles b/_overviews/toolkit/OrderedListOfMdFiles new file mode 100644 index 0000000000..ea248772fe --- /dev/null +++ b/_overviews/toolkit/OrderedListOfMdFiles @@ -0,0 +1,29 @@ +introduction.md +testing-intro.md +testing-suite.md +testing-run.md +testing-run-only.md +testing-exceptions.md +testing-asynchronous.md +testing-resources.md +testing-what-else.md +os-intro.md +os-read-directory.md +os-read-file.md +os-write-file.md +os-run-process.md +os-what-else.md +json-intro.md +json-parse.md +json-modify.md +json-deserialize.md +json-serialize.md +json-files.md +json-what-else.md +http-client-intro.md +http-client-request.md +http-client-uris.md +http-client-request-body.md +http-client-json.md +http-client-upload-file.md +http-client-what-else.md diff --git a/_overviews/toolkit/http-client-intro.md b/_overviews/toolkit/http-client-intro.md new file mode 100644 index 0000000000..fd2e132c54 --- /dev/null +++ b/_overviews/toolkit/http-client-intro.md @@ -0,0 +1,20 @@ +--- +title: Sending HTTP requests with sttp +type: chapter +description: The introduction of the sttp library +num: 23 +previous-page: json-what-else +next-page: http-client-request +--- + +sttp is a popular and feature-rich library for making HTTP requests to web servers. + +It provides both a synchronous API and an asynchronous `Future`-based API. It also supports WebSockets. + +Extensions are available that add capabilities such as streaming, logging, telemetry, and serialization. + +sttp offers the same APIs on all platforms (JVM, Scala.js, and Scala Native). + +sttp is a good choice for small synchronous scripts as well as large-scale, highly concurrent, asynchronous applications. + +{% include markdown.html path="_markdown/install-sttp.md" %} diff --git a/_overviews/toolkit/http-client-json.md b/_overviews/toolkit/http-client-json.md new file mode 100644 index 0000000000..4fca18f91f --- /dev/null +++ b/_overviews/toolkit/http-client-json.md @@ -0,0 +1,128 @@ +--- +title: How to send and receive JSON? +type: section +description: How to send JSON in a request and to parse JSON from the response. +num: 27 +previous-page: http-client-request-body +next-page: http-client-upload-file +--- + +{% include markdown.html path="_markdown/install-sttp.md" %} + +## HTTP and JSON + +JSON is a common format for HTTP request and response bodies. + +In the examples below, we use the [GitHub REST API](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28). +You will need a secret [GitHub authentication token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) to run the programs. +Do not share your token with anyone. + +## Sending and receiving JSON + +To send a JSON request and parse a JSON response we use sttp in combination with uJson. + +### Sending JSON + +To send JSON, you can construct a `uJson.Value` and write it as a string in the body of the request. + +In the following example we use [GitHub users endpoint](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28) to update the profile of the authenticated user. +We provide the new location and bio of the profile in a JSON object. + +{% tabs 'json' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc:compile-only +import sttp.client4.quick._ + +val json = ujson.Obj( + "location" -> "hometown", + "bio" -> "Scala programmer" +) + +val response = quickRequest + .patch(uri"https://api.github.com/user") + .auth.bearer(sys.env("GITHUB_TOKEN")) + .header("Content-Type", "application/json") + .body(ujson.write(json)) + .send() + +println(response.code) +// prints: 200 + +println(response.body) +// prints the full updated profile in JSON +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import sttp.client4.quick.* + +val json = ujson.Obj( + "location" -> "hometown", + "bio" -> "Scala programmer" +) + +val response = quickRequest + .patch(uri"https://api.github.com/user") + .auth.bearer(sys.env("GITHUB_TOKEN")) + .header("Content-Type", "application/json") + .body(ujson.write(json)) + .send() + +println(response.code) +// prints: 200 + +println(response.body) +// prints the full updated profile in JSON +``` +{% endtab %} +{% endtabs %} + +Before running the program, set the `GITHUB_TOKEN` environment variable. +After running it, you should see your new bio and location on your GitHub profile. + +### Parsing JSON from the response + +To parse JSON from the response of a request, you can use `ujson.read`. + +Again we use the GitHub user endpoint, this time to get the authenticated user. + +{% tabs 'json-2' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc:compile-only +import sttp.client4.quick._ + +val response = quickRequest + .get(uri"https://api.github.com/user") + .auth.bearer(sys.env("GITHUB_TOKEN")) + .send() + +val json = ujson.read(response.body) + +println(json("login").str) +// prints your login +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import sttp.client4.quick.* + +val response = quickRequest + .get(uri"https://api.github.com/user") + .auth.bearer(sys.env("GITHUB_TOKEN")) + .send() + +val json = ujson.read(response.body) + +println(json("login").str) +// prints your login +``` +{% endtab %} +{% endtabs %} + +Before running the program, set the `GITHUB_TOKEN` environment variable. +Running the program should print your own login. + +## Sending and receiving Scala objects using JSON + +Alternatively, you can use uPickle to send or receive Scala objects using JSON. +Read the following to learn [*How to serialize an object to JSON*](/toolkit/json-serialize) and [*How to deserialize JSON to an object*](/toolkit/json-deserialize). diff --git a/_overviews/toolkit/http-client-request-body.md b/_overviews/toolkit/http-client-request-body.md new file mode 100644 index 0000000000..b54357e1cb --- /dev/null +++ b/_overviews/toolkit/http-client-request-body.md @@ -0,0 +1,61 @@ +--- +title: How to send a request with a body? +type: section +description: Sending a string body with sttp +num: 26 +previous-page: http-client-uris +next-page: http-client-json +--- + +{% include markdown.html path="_markdown/install-sttp.md" %} + +## Sending a request with a string body + +To send a POST request with a string body, you can chain `post` and `body` on a `quickRequest`: +{% tabs 'body' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +import sttp.client4.quick._ + +val response = quickRequest + .post(uri"https://example.com/") + .body("Lorem ipsum") + .send() + +println(response.code) +// prints: 200 +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import sttp.client4.quick.* + +val response = quickRequest + .post(uri"https://example.com/") + .body("Lorem ipsum") + .send() + +println(response.code) +// prints: 200 +``` +{% endtab %} +{% endtabs %} + +In a request with string body, sttp adds the `Content-Type: text/plain; charset=utf-8` header and computes the `Content-Length` header. + +## Binary data + +The `body` method can also take a `Array[Byte]`, a `ByteBuffer` or an `InputStream`. + +{% tabs 'binarydata' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val bytes: Array[Byte] = "john".getBytes +val request = quickRequest.post(uri"https://example.com/").body(bytes) +``` +{% endtab %} +{% endtabs %} + +The binary body of a request is sent with `Content-Type: application/octet-stream`. + +Learn more in the [sttp documentation chapter about request bodies](https://sttp.softwaremill.com/en/latest/requests/body.html). diff --git a/_overviews/toolkit/http-client-request.md b/_overviews/toolkit/http-client-request.md new file mode 100644 index 0000000000..3a63e29c45 --- /dev/null +++ b/_overviews/toolkit/http-client-request.md @@ -0,0 +1,123 @@ +--- +title: How to send a request? +type: section +description: Sending a simple HTTP request with sttp. +num: 24 +previous-page: http-client-intro +next-page: http-client-uris +--- + +{% include markdown.html path="_markdown/install-sttp.md" %} + +## Sending an HTTP request + +The simplest way to send a request with sttp is `quickRequest`. + +You can define a GET request with `.get` and send it with `.send`. + +{% tabs 'request' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +import sttp.client4.quick._ +import sttp.client4.Response + +val response: Response[String] = quickRequest + .get(uri"https://httpbin.org/get") + .send() + +println(response.code) +// prints: 200 + +println(response.body) +// prints some JSON string +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import sttp.client4.quick.* +import sttp.client4.Response + +val response: Response[String] = quickRequest + .get(uri"https://httpbin.org/get") + .send() + +println(response.code) +// prints: 200 + +println(response.body) +// prints some JSON string +``` +{% endtab %} +{% endtabs %} + +A `Response[String]` contains a status code and a string body. + +## The request definition + +### The HTTP method and URI + +To specify the HTTP method and URI of a `quickRequest`, you can use `get`, `post`, `put`, or `delete`. + +To construct a URI you can use the `uri` interpolator, for e.g. `uri"https://example.com"`. +To learn more about that, see [*How to construct URIs and query parameters?*](/toolkit/http-client-uris). + +### The headers + +By default, the `quickRequest` contains the "Accept-Encoding" and the "deflate" headers. +To add more headers, you can call one of the `header` or `headers` overloads: + +{% tabs 'headers' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc:reset +import sttp.client4.quick._ + +val request = quickRequest + .get(uri"https://example.com") + .header("Origin", "https://scala-lang.org") + +println(request.headers) +// prints: Vector(Accept-Encoding: gzip, deflate, Origin: https://scala-lang.org) +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import sttp.client4.quick.* + +val request = quickRequest + .get(uri"https://example.com") + .header("Origin", "https://scala-lang.org") + +println(request.headers) +// prints: Vector(Accept-Encoding: gzip, deflate, Origin: https://scala-lang.org) +``` +{% endtab %} +{% endtabs %} + +sttp can also add "Content-Type" and "Content-Length" automatically if the request contains a body. + +## Authentication + +If you need authentication to access a resource, you can use one of the `auth.basic`, `auth.basicToken`, `auth.bearer` or `auth.digest` methods. + +{% tabs 'auth' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc:reset +import sttp.client4.quick._ + +// a request with a authentication +val request = quickRequest + .get(uri"https://example.com") + .auth.basic(user = "user", password = "***") +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import sttp.client4.quick.* + +// a request with a authentication +val request = quickRequest + .get(uri"https://example.com") + .auth.basic(user = "user", password = "***") +``` +{% endtab %} +{% endtabs %} diff --git a/_overviews/toolkit/http-client-upload-file.md b/_overviews/toolkit/http-client-upload-file.md new file mode 100644 index 0000000000..a67af28fbd --- /dev/null +++ b/_overviews/toolkit/http-client-upload-file.md @@ -0,0 +1,80 @@ +--- +title: How to upload a file over HTTP? +type: section +description: Uploading a file over HTTP with sttp. +num: 28 +previous-page: http-client-json +next-page: http-client-what-else +--- + +{% include markdown.html path="_markdown/install-sttp.md" %} + +## Uploading a file + +To upload a file, you can put a Java `Path` in the body of a request. + +You can get a `Path` directly using `Paths.get("path/to/file")` or by converting an OS-Lib path to a Java path with `toNIO`. + +{% tabs 'file' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc:compile-only +import sttp.client4.quick._ + +val file: java.nio.file.Path = (os.pwd / "image.png").toNIO +val response = quickRequest.post(uri"https://example.com/").body(file).send() + +println(response.code) +// prints: 200 +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import sttp.client4.quick.* + +val file: java.nio.file.Path = (os.pwd / "image.png").toNIO +val response = quickRequest.post(uri"https://example.com/").body(file).send() + +println(response.code) +// prints: 200 +``` +{% endtab %} +{% endtabs %} + +## Multi-part requests + +If the web server can receive multiple files at once, you can use a multipart body, as follows: + +{% tabs 'multipart' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +import sttp.client4.quick._ + +val file1 = (os.pwd / "avatar1.png").toNIO +val file2 = (os.pwd / "avatar2.png").toNIO +val response = quickRequest + .post(uri"https://example.com/") + .multipartBody( + multipartFile("avatar1.png", file1), + multipartFile("avatar2.png", file2) + ) + .send() +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import sttp.client4.quick.* + +val file1 = (os.pwd / "avatar1.png").toNIO +val file2 = (os.pwd / "avatar2.png").toNIO +val response = quickRequest + .post(uri"https://example.com/") + .multipartBody( + multipartFile("avatar1.png", file1), + multipartFile("avatar2.png", file2) + ) + .send() +``` +{% endtab %} +{% endtabs %} + +Learn more about multipart requests in the [sttp documention](https://sttp.softwaremill.com/en/latest/requests/multipart.html). diff --git a/_overviews/toolkit/http-client-uris.md b/_overviews/toolkit/http-client-uris.md new file mode 100644 index 0000000000..bfb9beb332 --- /dev/null +++ b/_overviews/toolkit/http-client-uris.md @@ -0,0 +1,128 @@ +--- +title: How to construct URIs and query parameters? +type: section +description: Using interpolation to construct URIs +num: 25 +previous-page: http-client-request +next-page: http-client-request-body +--- + +{% include markdown.html path="_markdown/install-sttp.md" %} + +## The `uri` interpolator + +`uri` is a custom [string interpolator](/overviews/core/string-interpolation.html) that allows you to create valid web addresses, also called URIs. For example, you can write `uri"https://example.com/"`. + +You can insert any variable or expression in your URI with the usual `$` or `${}` syntax. +For instance `uri"https://example.com/$name"`, interpolates the value of the variable `name` into an URI. +If `name` contains `"peter"`, the result is `https://example.com/peter`. + +`uri` escapes special characters automatically, as seen in this example: + +{% tabs 'uri' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +import sttp.client4.quick._ +import sttp.model.Uri + +val book = "programming in scala" +val bookUri: Uri = uri"https://example.com/books/$book" + +println(bookUri) +// prints: https://example.com/books/programming%20in%20scala +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import sttp.client4.quick.* +import sttp.model.Uri + +val book = "programming in scala" +val bookUri: Uri = uri"https://example.com/books/$book" + +println(bookUri) +// prints: https://example.com/books/programming%20in%20scala +``` +{% endtab %} +{% endtabs %} + +## Query parameters + +A query parameter is a key-value pair that is appended to the end of a URI in an HTTP request to specify additional details about the request. +The web server can use those parameters to compute the appropriate response. + +For example, consider the following URL: + +``` +https://example.com/search?q=scala&limit=10&page=1 +``` + +It contains three query parameters: `q=scala`, `limit=10` and `page=1`. + +### Using a map of query parameters + +The `uri` interpolator can interpolate a `Map[String, String]` as query parameters: + +{% tabs 'queryparams' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val queryParams = Map( + "q" -> "scala", + "limit" -> "10", + "page" -> "1" +) +val uriWithQueryParams = uri"https://example.com/search?$queryParams" +println(uriWithQueryParams) +// prints: https://example.com/search?q=scala&limit=10&page=1 +``` +{% endtab %} +{% endtabs %} + +For safety, special characters in the parameters are automatically escaped by the interpolator. + +## Using an optional query parameter + +A query parameter might be optional. +The `uri` interpolator can interpolate `Option`s: + +{% tabs 'optional' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +def getUri(limit: Option[Int]): Uri = + uri"https://example.com/all?limit=$limit" + +println(getUri(Some(10))) +// prints: https://example.com/all?limit=100 + +println(getUri(None)) +// prints: https://example.com/all +``` +{% endtab %} +{% endtabs %} + +Notice that the query parameter disappears entirely when `limit` is `None`. + +## Using a sequence as values of a single query parameter + +A query parameter can be repeated in a URI to represent a list of values. +For example, the `version` parameter in `?version=1.0.0&version=1.0.1&version=1.1.0` contains 3 values: `1.0.0`, `1.0.1` and `1.1.0`. + +To build such query parameter in a URI, you can interpolate a `Seq` (or `List`, `Array`, etc) in a `uri"..."`. + +{% tabs 'seq' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:nest +def getUri(versions: Seq[String]): Uri = + uri"https://example.com/scala?version=$versions" + +println(getUri(Seq("3.2.2"))) +// prints: https://example.com/scala?version=3.2.2 + +println(getUri(Seq("2.13.8", "2.13.9", "2.13.10"))) +// prints: https://example.com/scala?version=2.13.8&version=2.13.9&version=2.13.10 + +println(getUri(Seq.empty)) +// prints: https://example.com/scala +``` +{% endtab %} +{% endtabs %} diff --git a/_overviews/toolkit/http-client-what-else.md b/_overviews/toolkit/http-client-what-else.md new file mode 100644 index 0000000000..11b577449d --- /dev/null +++ b/_overviews/toolkit/http-client-what-else.md @@ -0,0 +1,112 @@ +--- +title: What else can sttp do? +type: section +description: An incomplete list of features of sttp +num: 29 +previous-page: http-client-upload-file +next-page: +--- + +{% include markdown.html path="_markdown/install-upickle.md" %} + +## Asynchronous requests + +To send a request asynchronously you can use a `DefaultFutureBackend`: + +{% tabs 'async' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +import scala.concurrent.Future +import sttp.client4._ + +val asyncBackend = DefaultFutureBackend() +val response: Future[Response[String]] = quickRequest + .get(uri"https://example.com") + .send(asyncBackend) +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import scala.concurrent.Future +import sttp.client4.* + +val asyncBackend = DefaultFutureBackend() +val response: Future[Response[String]] = quickRequest + .get(uri"https://example.com") + .send(asyncBackend) +``` +{% endtab %} +{% endtabs %} + +You can learn more about `Future`-based backends in the [sttp documentation](https://sttp.softwaremill.com/en/latest/backends/future.html). + +sttp supports other asynchronous wrappers such as Monix `Task`s, cats-effect `Effect`s, ZIO's `ZIO` type, and more. +You can see the full list of supported backends [here](https://sttp.softwaremill.com/en/latest/backends/summary.html). + +## Websockets + +You can use a `DefaultFutureBackend` to open a websocket, as follows. + +{% tabs 'ws' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc:reset +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future} +import scala.concurrent.ExecutionContext.Implicits.global + +import sttp.client4._ +import sttp.ws.WebSocket + +val asyncBackend = DefaultFutureBackend() + +def useWebSocket(ws: WebSocket[Future]): Future[Unit] = + for { + _ <- ws.sendText("Hello") + text <- ws.receiveText() + } yield { + println(text) + } + +val response = quickRequest + .get(uri"wss://ws.postman-echo.com/raw") + .response(asWebSocketAlways(useWebSocket)) + .send(asyncBackend) + +Await.result(response, Duration.Inf) +// prints: Hello +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future} +import scala.concurrent.ExecutionContext.Implicits.global + +import sttp.client4.* +import sttp.ws.WebSocket + +val asyncBackend = DefaultFutureBackend() + +def useWebSocket(ws: WebSocket[Future]): Future[Unit] = + for + _ <- ws.sendText("Hello") + text <- ws.receiveText() + yield + println(text) + +val response = quickRequest + .get(uri"wss://ws.postman-echo.com/raw") + .response(asWebSocketAlways(useWebSocket)) + .send(asyncBackend) + +Await.result(response, Duration.Inf) +// prints: Hello +``` +{% endtab %} +{% endtabs %} + +Learn more about WebSockets in [sttp documentation](https://sttp.softwaremill.com/en/latest/websockets.html). + +## More features + +You can discover many more features such as streaming, logging, timeouts, and more in [sttp documentation](https://sttp.softwaremill.com/en/latest/quickstart.html#). diff --git a/_overviews/toolkit/introduction.md b/_overviews/toolkit/introduction.md new file mode 100644 index 0000000000..1656ed9662 --- /dev/null +++ b/_overviews/toolkit/introduction.md @@ -0,0 +1,77 @@ +--- +title: Introduction +type: chapter +description: Introducing the Scala Toolkit tutorials +num: 1 +previous-page: +next-page: testing-intro +toolkit-index: + - title: Tests + description: Testing code with MUnit. + icon: "fa fa-vial-circle-check" + link: /toolkit/testing-intro.html + - title: Files and Processes + description: Writing files and running processes with OS-Lib. + icon: "fa fa-folder-open" + link: /toolkit/os-intro.html + - title: JSON + description: Parsing JSON and serializing objects to JSON with uPickle. + icon: "fa fa-file-code" + link: /toolkit/json-intro.html + - title: HTTP Requests + description: Sending HTTP requests and uploading files with sttp. + icon: "fa fa-globe" + link: /toolkit/http-client-intro.html +--- + +## What is the Scala Toolkit? + +The Scala Toolkit is a set of libraries designed to effectively perform common programming tasks. It includes tools for working with files and processes, parsing JSON, sending HTTP requests, and unit testing. + +The Toolkit supports: +* Scala 3 and Scala 2 +* JVM, Scala.js, and Scala Native + +Use cases for the Toolkit include: + +- short-lived programs running on the JVM, to scrape a website, to collect and transform data, or to fetch and process some files, +- frontend scripts that run on the browser and power your websites, +- command-line tools packaged as native binaries for instant startup + +{% include inner-documentation-sections.html links=page.toolkit-index %} + +## What are these tutorials? + +This series of tutorials focuses on short code examples, to help you get started quickly. + +If you need more in-depth information, the tutorials include links to further documentation for all of the libraries in the toolkit. + +## How to run the code? + +You can follow the tutorials regardless of how you choose to run your +Scala code. The tutorials focus on the code itself, not on the process +of running it. + +Ways to run Scala code include: +* in your **browser** with [Scastie](https://scastie.scala-lang.org) + * pros: zero installation, online sharing + * cons: single-file only, online-only +* interactively in the Scala **REPL** (Read/Eval/Print Loop) + * pros: interactive exploration in the terminal + * cons: doesn't save your code anywhere +* interactively in a **worksheet** in your IDE such as [IntelliJ](https://www.jetbrains.com/help/idea/discover-intellij-idea-for-scala.html) or [Metals](http://scalameta.org/metals/) + * pros: interactive exploration in a GUI + * cons: requires worksheet environment to run +* in **scripts**, using [Scala CLI](https://scala-cli.virtuslab.com) + * pros: terminal-based workflow with little setup + * cons: may not be suitable for large projects +* using a **build tool** (such as [sbt](https://www.scala-sbt.org) or [mill](https://com-lihaoyi.github.io/mill/)) + * pros: terminal-based workflow for projects of any size + * cons: requires some additional setup and learning +* using an **IDE** such as [IntelliJ](https://www.jetbrains.com/help/idea/discover-intellij-idea-for-scala.html) or [Metals](http://scalameta.org/metals/) + * pros: GUI based workflow for projects of any size + * cons: requires some additional setup and learning + +These choices, with their pros and cons, are common to most programing +languages. +Feel free to use whichever option you're most comfortable with. diff --git a/_overviews/toolkit/json-deserialize.md b/_overviews/toolkit/json-deserialize.md new file mode 100644 index 0000000000..23fd6391d1 --- /dev/null +++ b/_overviews/toolkit/json-deserialize.md @@ -0,0 +1,121 @@ +--- +title: How to deserialize JSON to an object? +type: section +description: Parsing JSON to a custom data type +num: 19 +previous-page: json-modify +next-page: json-serialize +--- + +{% include markdown.html path="_markdown/install-upickle.md" %} + +## Parsing vs. deserialization + +Parsing with uJson only accepts valid JSON, but it does not validate that the names and types of fields are as expected. + +Deserialization, on the other hand, transforms a JSON string to some user-specified Scala data type, if required fields are present and have the correct types. + +In this tutorial, we show how to deserialize to a `Map` and also to a custom `case class`. + +## Deserializing JSON to a `Map` + +For a type `T`, uPickle can deserialize JSON to a `Map[String, T]`, checking that all fields conform to `T`. + +We can for instance, deserialize to a `Map[String, List[Int]]`: + +{% tabs 'parsemap' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val json = """{"primes": [2, 3, 5], "evens": [2, 4, 6]} """ +val map: Map[String, List[Int]] = + upickle.default.read[Map[String, List[Int]]](json) + +println(map("primes")) +// prints: List(2, 3, 5) +``` +{% endtab %} +{% endtabs %} + +If a value is the wrong type, uPickle throws a `upickle.core.AbortException`. + +{% tabs 'parsemap-error' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:reset:crash +val json = """{"name": "Peter"} """ +upickle.default.read[Map[String, List[Int]]](json) +// throws: upickle.core.AbortException: expected sequence got string at index 9 +``` +{% endtab %} +{% endtabs %} + +### Deserializing JSON to a custom data type + +In Scala, you can use a `case class` to define your own data type. +For example, to represent a pet owner, you might: +```scala mdoc:reset +case class PetOwner(name: String, pets: List[String]) +``` + +To read a `PetOwner` from JSON, we must provide a `ReadWriter[PetOwner]`. +uPickle can do that automatically: + +{% tabs 'given' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +import upickle.default._ + +implicit val ownerRw: ReadWriter[PetOwner] = macroRW[PetOwner] +``` +Some explanations: +- An `implicit val` is a value that can be automatically provided as an argument to a method or function call, without having to explicitly pass it. +- `macroRW` is a method provided by uPickle that can generate a instances of `ReadWriter` for case classes, using the information about its fields. +{% endtab %} +{% tab 'Scala 3' %} +```scala +import upickle.default.* + +case class PetOwner(name: String, pets: List[String]) + derives ReadWriter +``` +The `derives` keyword is used to automatically generate given instances. +Using the compiler's knowledge of the fields in `PetOwner`, it generates a `ReadWriter[PetOwner]`. +{% endtab %} +{% endtabs %} + +This means that you can now read (and write) `PetOwner` objects from JSON with `upickle.default.read(petOwner)`. + +Notice that you do not need to pass the instance of `ReadWriter[PetOwner]` explicitly to the `read` method. But it does, nevertheless, get it from the context, as "given" value. You may find more information about contextual abstractions in the [Scala 3 Book](https://docs.scala-lang.org/scala3/book/ca-contextual-abstractions-intro.html). + +Putting everything together you should get: + +{% tabs 'full' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc:reset +import upickle.default._ + +case class PetOwner(name: String, pets: List[String]) +implicit val ownerRw: ReadWriter[PetOwner] = macroRW + +val json = """{"name": "Peter", "pets": ["Toolkitty", "Scaniel"]}""" +val petOwner: PetOwner = read[PetOwner](json) + +val firstPet = petOwner.pets.head +println(s"${petOwner.name} has a pet called $firstPet") +// prints: Peter has a pet called Toolkitty +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import upickle.default.* + +case class PetOwner(name: String, pets: List[String]) derives ReadWriter + +val json = """{"name": "Peter", "pets": ["Toolkitty", "Scaniel"]}""" +val petOwner: PetOwner = read[PetOwner](json) + +val firstPet = petOwner.pets.head +println(s"${petOwner.name} has a pet called $firstPet") +// prints: Peter has a pet called Toolkitty +``` +{% endtab %} +{% endtabs %} diff --git a/_overviews/toolkit/json-files.md b/_overviews/toolkit/json-files.md new file mode 100644 index 0000000000..63fd2759ec --- /dev/null +++ b/_overviews/toolkit/json-files.md @@ -0,0 +1,72 @@ +--- +title: How to read and write JSON files? +type: section +description: Reading and writing JSON files using UPickle and OSLib +num: 21 +previous-page: json-serialize +next-page: json-what-else +--- + +{% include markdown.html path="_markdown/install-upickle.md" %} + +## Read and write raw JSON + +To read and write JSON files, you can use uJson and OS-Lib as follows: + +{% tabs 'raw' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:compile-only +// read a JSON file +val json = ujson.read(os.read(os.pwd / "raw.json")) + +// modify the JSON content +json("updated") = "now" + +//write to a new file +os.write(os.pwd / "raw-updated.json", ujson.write(json)) +``` +{% endtab %} +{% endtabs %} + +## Read and write Scala objects using JSON + +To read and write Scala objects to and from JSON files, you can use uPickle and OS-Lib as follows: + +{% tabs 'object' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc:compile-only +import upickle.default._ + +case class PetOwner(name: String, pets: List[String]) +implicit val ownerRw: ReadWriter[PetOwner] = macroRW + +// read a PetOwner from a JSON file +val petOwner: PetOwner = read[PetOwner](os.read(os.pwd / "pet-owner.json")) + +// create a new PetOwner +val petOwnerUpdated = petOwner.copy(pets = "Toolkitty" :: petOwner.pets) + +// write to a new file +os.write(os.pwd / "pet-owner-updated.json", write(petOwnerUpdated)) +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import upickle.default.* + +case class PetOwner(name: String, pets: List[String]) derives ReadWriter + +// read a PetOwner from a JSON file +val petOwner: PetOwner = read[PetOwner](os.read(os.pwd / "pet-owner.json")) + +// create a new PetOwner +val petOwnerUpdated = petOwner.copy(pets = "Toolkitty" :: petOwner.pets) + +// write to a new file +os.write(os.pwd / "pet-owner-updated.json", write(petOwnerUpdated)) +``` +{% endtab %} +{% endtabs %} + +To serialize and deserialize Scala case classes (or enums) to JSON we need an instance of `ReadWriter`. +To understand how uPickle generates it for you, you can read the [*How to deserialize JSON to an object?*](/toolkit/json-deserialize) or the [*How to serialize an object to JSON?*](/toolkit/json-serialize) tutorials. diff --git a/_overviews/toolkit/json-intro.md b/_overviews/toolkit/json-intro.md new file mode 100644 index 0000000000..7fb3e890de --- /dev/null +++ b/_overviews/toolkit/json-intro.md @@ -0,0 +1,16 @@ +--- +title: Handling JSON with uPickle +type: chapter +description: Description of the uPickle library. +num: 16 +previous-page: os-what-else +next-page: json-parse +--- + +uPickle is a lightweight serialization library for Scala. + +It includes uJson, a JSON manipulation library that can parse JSON strings, access or mutate their values in memory, and write them back out again. + +uPickle can serialize and deserialize Scala objects directly to and from JSON. It knows how to handle the Scala collections such as `Map` and `Seq`, as well as your own data types, such as `case class`s and Scala 3 `enum`s. + +{% include markdown.html path="_markdown/install-upickle.md" %} diff --git a/_overviews/toolkit/json-modify.md b/_overviews/toolkit/json-modify.md new file mode 100644 index 0000000000..8f1cd63e6d --- /dev/null +++ b/_overviews/toolkit/json-modify.md @@ -0,0 +1,33 @@ +--- +title: How to modify JSON? +type: section +description: Modifying JSON with uPickle. +num: 18 +previous-page: json-parse +next-page: json-deserialize +--- + +{% include markdown.html path="_markdown/install-upickle.md" %} + +`ujson.read` returns a mutable representation of JSON that you can update. Fields and elemnts can be added, modified, or removed. + +First you read the JSON string, then you update it in memory, and finally you write it back out again. + +{% tabs 'modify' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +// Parse the JSON string +val json: ujson.Value = ujson.read("""{"name":"John","pets":["Toolkitty","Scaniel"]}""") + +// Update it +json("name") = "Peter" +json("nickname") = "Pete" +json("pets").arr.remove(1) + +// Write it back to a String +val result: String = ujson.write(json) +println(result) +// prints: {"name":"Peter","pets":["Toolkitty"],"nickname":"Pete"} +``` +{% endtab %} +{% endtabs %} diff --git a/_overviews/toolkit/json-parse.md b/_overviews/toolkit/json-parse.md new file mode 100644 index 0000000000..71b1fab391 --- /dev/null +++ b/_overviews/toolkit/json-parse.md @@ -0,0 +1,82 @@ +--- +title: How to access values inside JSON? +type: section +description: Accessing JSON values using ujson. +num: 17 +previous-page: json-intro +next-page: json-modify +--- + +{% include markdown.html path="_markdown/install-upickle.md" %} + +## Accessing values inside JSON + +To parse a JSON string and access fields inside it, you can use uJson, which is part of uPickle. + +The method `ujson.read` parses a JSON string into memory: +{% tabs 'read' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val jsonString = """{"name": "Peter", "age": 13, "pets": ["Toolkitty", "Scaniel"]}""" +val json: ujson.Value = ujson.read(jsonString) +println(json("name").str) +// prints: Peter +``` +{% endtab %} +{% endtabs %} + +To access the `"name"` field, we do `json("name")` and then call `str` to type it as a string. + +To access the elements by index in a JSON array, + +{% tabs 'array' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val pets: ujson.Value = json("pets") + +val firstPet: String = pets(0).str +val secondPet: String = pets(1).str + +println(s"The pets are $firstPet and $secondPet") +// prints: The pets are Toolkitty and Scaniel +``` +{% endtab %} +{% endtabs %} + +You can traverse the JSON structure as deeply as you want, to extract any nested value. +For instance, `json("field1")(0)("field2").str` is the string value of `field2` in the first element of the array in `field1`. + +## JSON types + +In the previous examples we used `str` to type a JSON value as a string. +Similar methods exist for other types of values, namely: + - `num` for numeric values, returning `Double` + - `bool` for boolean values, returning `Boolean` + - `arr` for arrays, returning a mutable `Buffer[ujson.Value]` + - `obj` for objects, returning a mutable `Map[String, ujson.Value]` + +{% tabs 'typing' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:reset +import scala.collection.mutable + +val jsonString = """{"name": "Peter", "age": 13, "pets": ["Toolkitty", "Scaniel"]}""" +val json = ujson.read(jsonString) + +val person: mutable.Map[String, ujson.Value] = json.obj +val age: Double = person("age").num +val pets: mutable.Buffer[ujson.Value] = person("pets").arr +``` +{% endtab %} +{% endtabs %} + +If a JSON value does not conform to the expected type, uJson throws a `ujson.Value.InvalidData` exception. + +{% tabs 'exception' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:crash +val name: Boolean = person("name").bool +// throws a ujson.Value.InvalidData: Expected ujson.Bool (data: "Peter") +``` +{% endtab %} +{% endtabs %} diff --git a/_overviews/toolkit/json-serialize.md b/_overviews/toolkit/json-serialize.md new file mode 100644 index 0000000000..bd3049def0 --- /dev/null +++ b/_overviews/toolkit/json-serialize.md @@ -0,0 +1,95 @@ +--- +title: How to serialize an object to JSON? +type: section +description: How to write JSON with Scala Toolkit. +num: 20 +previous-page: json-deserialize +next-page: json-files +--- + +{% include markdown.html path="_markdown/install-upickle.md" %} + +## Serializing a Map to JSON + +uPickle can serialize your Scala objects to JSON, so that you can save them in files or send them over the network. + +By default it can serialize primitive types such as `Int` or `String`, as well as standard collections such as `Map` and `List`. + +{% tabs 'array' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val map: Map[String, Int] = + Map("Toolkitty" -> 3, "Scaniel" -> 5) +val jsonString: String = upickle.default.write(map) +println(jsonString) +// prints: {"Toolkitty":3,"Scaniel":5} +``` +{% endtab %} +{% endtabs %} + +## Serializing a custom object to JSON + +In Scala, you can use a `case class` to define your own data type. +For example, to represent a pet owner with the name of its pets, you can +```scala mdoc:reset +case class PetOwner(name: String, pets: List[String]) +``` + +To be able to write a `PetOwner` to JSON we need to provide a `ReadWriter` instance for the case class `PetOwner`. +Luckily, `upickle` is able to fully automate that: + +{% tabs 'given' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +import upickle.default._ + +implicit val ownerRw: ReadWriter[PetOwner] = macroRW[PetOwner] +``` +Some explanations: +- An `implicit val` is a value that can be automatically provided as an argument to a method or function call, without having to explicitly pass it. +- `macroRW` is a method provided by uPickle that can generate a instances of `ReadWriter` for case classes, using the information about its fields. +{% endtab %} +{% tab 'Scala 3' %} +```scala +import upickle.default.* + +case class PetOwner(name: String, pets: List[String]) derives ReadWriter +``` +The `derives` keyword is used to automatically generate given instances. +Using the compiler's knowledge of the fields in `PetOwner`, it generates a `ReadWriter[PetOwner]`. +{% endtab %} +{% endtabs %} + +This means that you can now write (and read) `PetOwner` objects to JSON with `upickle.default.write(petOwner)`. + +Notice that you do not need to pass the instance of `ReadWriter[PetOwner]` explicitly to the `write` method. But it does, nevertheless, get it from the context, as "given" value. You may find more information about contextual abstractions in the [Scala 3 Book](https://docs.scala-lang.org/scala3/book/ca-contextual-abstractions-intro.html). + +Putting everything together you should get: + +{% tabs 'full' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +import upickle.default._ + +case class PetOwner(name: String, pets: List[String]) +implicit val ownerRw: ReadWriter[PetOwner] = macroRW + +val petOwner = PetOwner("Peter", List("Toolkitty", "Scaniel")) +val json: String = write(petOwner) +println(json) +// prints: {"name":"Peter","pets":["Toolkitty","Scaniel"]}" +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import upickle.default._ + +case class PetOwner(name: String, pets: List[String]) derives ReadWriter + +val petOwner = PetOwner("Peter", List("Toolkitty", "Scaniel")) +val json: String = write(petOwner) +println(json) +// prints: {"name":"Peter","pets":["Toolkitty","Scaniel"]}" +``` +{% endtab %} +{% endtabs %} diff --git a/_overviews/toolkit/json-what-else.md b/_overviews/toolkit/json-what-else.md new file mode 100644 index 0000000000..1f34daffe4 --- /dev/null +++ b/_overviews/toolkit/json-what-else.md @@ -0,0 +1,74 @@ +--- +title: What else can uPickle do? +type: section +description: An incomplete list of features of uPickle +num: 22 +previous-page: json-files +next-page: http-client-intro +--- + +{% include markdown.html path="_markdown/install-upickle.md" %} +## Construct a new JSON structure with uJson + +{% tabs construct%} +{% tab 'Scala 2 and Scala 3' %} +```scala mdoc +val obj: ujson.Value = ujson.Obj( + "library" -> "upickle", + "versions" -> ujson.Arr("1.6.0", "2.0.0", "3.1.0"), + "documentation" -> "https://com-lihaoyi.github.io/upickle/", +) +``` +{% endtab %} +{% endtabs %} + +Learn more about constructing JSON in the [uJson documentation](https://com-lihaoyi.github.io/upickle/#Construction). + +## Defining custom JSON serialization + +You can customize the `ReadWriter` of your data type by mapping the `ujson.Value`, like this: + +{% tabs custom-serializer class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +import upickle.default._ + +case class Bar(i: Int, s: String) + +object Bar { + implicit val barReadWriter: ReadWriter[Bar] = readwriter[ujson.Value] + .bimap[Bar]( + x => ujson.Arr(x.s, x.i), + json => new Bar(json(1).num.toInt, json(0).str) + ) +} + +val bar = Bar(5, "bar") +val json = upickle.default.write(bar) +println(json) +// prints: [5, "bar"] +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import upickle.default.* + +case class Bar(i: Int, s: String) + +object Bar: + given ReadWriter[Bar] = readwriter[ujson.Value] + .bimap[Bar]( + x => ujson.Arr(x.s, x.i), + json => new Bar(json(1).num.toInt, json(0).str) + ) + +val bar = Bar(5, "bar") +val json = upickle.default.write(bar) +println(json) +// prints: [5, "bar"] +``` +{% endtab %} +{% endtabs %} + +Learn more about custom JSON serialization in the [uPickle documentation](https://com-lihaoyi.github.io/upickle/#Customization). + diff --git a/_overviews/toolkit/os-intro.md b/_overviews/toolkit/os-intro.md new file mode 100644 index 0000000000..71d30f0c49 --- /dev/null +++ b/_overviews/toolkit/os-intro.md @@ -0,0 +1,20 @@ +--- +title: Working with files and processes with OS-Lib +type: chapter +description: The introduction of the OS-lib library +num: 10 +previous-page: testing-what-else +next-page: os-read-directory +--- + +OS-Lib is a library for manipulating files and processes. It is part of the Scala Toolkit. + +OS-Lib aims to replace the `java.nio.file` and `java.lang.ProcessBuilder` APIs. You should not need to use any underlying Java APIs directly. + +OS-lib also aims to supplant the older `scala.io` and `scala.sys` APIs in the Scala standard library. + +OS-Lib has no dependencies. + +All of OS-Lib is in the `os.*` namespace. + +{% include markdown.html path="_markdown/install-os-lib.md" %} diff --git a/_overviews/toolkit/os-read-directory.md b/_overviews/toolkit/os-read-directory.md new file mode 100644 index 0000000000..b24b664a49 --- /dev/null +++ b/_overviews/toolkit/os-read-directory.md @@ -0,0 +1,59 @@ +--- +title: How to read a directory? +type: section +description: Reading a directory's contents with OS-Lib +num: 11 +previous-page: os-intro +next-page: os-read-file +--- + +{% include markdown.html path="_markdown/install-os-lib.md" %} + +## Paths + +A fundamental data type in OS-Lib is `os.Path`, representing a path +on the filesystem. An `os.Path` is always an absolute path. + +OS-Lib also provides `os.RelPath` (relative paths) and `os.SubPath` (a +relative path which cannot ascend to parent directories). + +Typical starting points for making paths are `os.pwd` (the +current working directory), `os.home` (the current user's home +directory), `os.root` (the root of the filesystem), or +`os.temp.dir()` (a new temporary directory). + +Paths have a `/` method for adding path segments. For example: + +{% tabs 'etc' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val etc: os.Path = os.root / "etc" +``` +{% endtab %} +{% endtabs %} + +## Reading a directory + +`os.list` returns the contents of a directory: + +{% tabs 'list-etc' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val entries: Seq[os.Path] = os.list(os.root / "etc") +``` +{% endtab %} +{% endtabs %} + +Or if we only want subdirectories: + +{% tabs 'subdirs' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val dirs: Seq[os.Path] = os.list(os.root / "etc").filter(os.isDir) +``` +{% endtab %} +{% endtabs %} + +To recursively descend an entire subtree, change `os.list` to +`os.walk`. To process results on the fly rather than reading them all +into memory first, substitute `os.walk.stream`. diff --git a/_overviews/toolkit/os-read-file.md b/_overviews/toolkit/os-read-file.md new file mode 100644 index 0000000000..bc23ca2588 --- /dev/null +++ b/_overviews/toolkit/os-read-file.md @@ -0,0 +1,64 @@ +--- +title: How to read a file? +type: section +description: Reading files from disk with OS-Lib +num: 12 +previous-page: os-read-directory +next-page: os-write-file +--- + +{% include markdown.html path="_markdown/install-os-lib.md" %} + +## Reading a file + +Supposing we have the path to a file: + +{% tabs 'path' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val path: os.Path = os.root / "usr" / "share" / "dict" / "words" +``` +{% endtab %} +{% endtabs %} + +Then we can slurp the entire file into a string with `os.read`: + +{% tabs slurp %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:compile-only +val content: String = os.read(path) +``` +{% endtab %} +{% endtabs %} + +To read the file as line at a time, substitute `os.read.lines`. + +We can find the longest word in the dictionary: + +{% tabs lines %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:compile-only +val lines: Seq[String] = os.read.lines(path) +println(lines.maxBy(_.size)) +// prints: antidisestablishmentarianism +``` +{% endtab %} +{% endtabs %} + +There's also `os.read.lines.stream` if you want to process the lines +on the fly rather than read them all into memory at once. For example, +if we just want to read the first line, the most efficient way is: + +{% tabs lines-stream %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:compile-only +val lineStream: geny.Generator[String] = os.read.lines.stream(path) +val firstLine: String = lineStream.head +println(firstLine) +// prints: A +``` +{% endtab %} +{% endtabs %} + +OS-Lib takes care of closing the file once the generator returned +by `stream` is exhausted. diff --git a/_overviews/toolkit/os-run-process.md b/_overviews/toolkit/os-run-process.md new file mode 100644 index 0000000000..7bbe4ace58 --- /dev/null +++ b/_overviews/toolkit/os-run-process.md @@ -0,0 +1,79 @@ +--- +title: How to run a process? +type: section +description: Starting external subprocesses with OS-Lib +num: 14 +previous-page: os-write-file +next-page: os-what-else +--- + +{% include markdown.html path="_markdown/install-os-lib.md" %} + +## Starting an external process + +To set up a process, use `os.proc`, then to actually start it, +`call()`: + +{% tabs 'touch' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:compile-only +val path: os.Path = os.pwd / "output.txt" +println(os.exists(path)) +// prints: false +val result: os.CommandResult = os.proc("touch", path).call() +println(result.exitCode) +// prints: 0 +println(os.exists(path)) +// prints: true +``` +{% endtab %} +{% endtabs %} + +Note that `proc` accepts both strings and `os.Path`s. + +## Reading the output of a process + +(The particular commands in the following examples might not exist on all +machines.) + +Above we saw that `call()` returned an `os.CommandResult`. We can +access the result's entire output with `out.text()`, or as lines +with `out.lines()`. + +For example, we could use `bc` to do some math for us: + +{% tabs 'bc' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:compile-only +val res: os.CommandResult = os.proc("bc", "-e", "2 + 2").call() +val text: String = res.out.text() +println(text.trim.toInt) +// prints: 4 +``` +{% endtab %} +{% endtabs %} + +Or have `cal` show us a calendar: + +{% tabs 'cal' %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:compile-only +val res: os.CommandResult = os.proc("cal", "-h", "2", "2023").call() +res.out.lines().foreach(println) +// prints: +// February 2023 +// Su Mo Tu We Th Fr Sa +// 1 2 3 4 +// ... +``` +{% endtab %} +{% endtabs %} + +## Customizing the process + +`call()` takes various optional arguments, too many to explain +individually here. For example, you can set the working directory +(`cwd = ...`), set environment variables (`env = ...`), or redirect +input and output (`stdin = ...`, `stdout = ...`, `stderr = ...`). +Find more information about the `call` method on [the README of OS-Lib](https://github.com/com-lihaoyi/os-lib#osproccall). + diff --git a/_overviews/toolkit/os-what-else.md b/_overviews/toolkit/os-what-else.md new file mode 100644 index 0000000000..1671540845 --- /dev/null +++ b/_overviews/toolkit/os-what-else.md @@ -0,0 +1,18 @@ +--- +title: What else can OS-Lib do? +type: section +description: An incomplete list of features of OS-Lib +num: 15 +previous-page: os-run-process +next-page: json-intro +--- + +{% include markdown.html path="_markdown/install-os-lib.md" %} + +[OS-Lib on GitHub](https://github.com/com-lihaoyi/os-lib) has many additional examples of how to perform common tasks: +- creating, moving, copying, removing files and folders, +- reading filesystem metadata and permissions, +- spawning subprocesses, +- watching changes in folders. + +See also Chapter 7 of Li Haoyi's book [_Hands-On Scala Programming_](https://www.handsonscala.com). (Li Haoyi is the author of OS-Lib.) diff --git a/_overviews/toolkit/os-write-file.md b/_overviews/toolkit/os-write-file.md new file mode 100644 index 0000000000..2bef6fff0c --- /dev/null +++ b/_overviews/toolkit/os-write-file.md @@ -0,0 +1,54 @@ +--- +title: How to write a file? +type: section +description: Writing files to disk with OS-Lib +num: 13 +previous-page: os-read-file +next-page: os-run-process +--- + +{% include markdown.html path="_markdown/install-os-lib.md" %} + +## Writing a file all at once + +`os.write` writes the supplied string to a new file: + +{% tabs write %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +val path: os.Path = os.temp.dir() / "output.txt" +os.write(path, "hello\nthere\n") +println(os.read.lines(path).size) +// prints: 2 +``` +{% endtab %} +{% endtabs %} + +## Overwriting or appending + +`os.write` throws an exception if the file already exists: + +{% tabs already-exists %} +{% tab 'Scala 2 and 3' %} +```scala mdoc:crash +os.write(path, "this will fail") +// this exception is thrown: +// java.nio.file.FileAlreadyExistsException +``` +{% endtab %} +{% endtabs %} + +To avoid this, use `os.write.over` to replace the existing +contents. + +You can also use `os.write.append` to add more to the end: + +{% tabs append %} +{% tab 'Scala 2 and 3' %} +```scala mdoc +os.write.append(path, "two more\nlines\n") +println(os.read.lines(path).size) +// prints: 4 +``` +{% endtab %} +{% endtabs %} diff --git a/_overviews/toolkit/testing-asynchronous.md b/_overviews/toolkit/testing-asynchronous.md new file mode 100644 index 0000000000..68862cc1cc --- /dev/null +++ b/_overviews/toolkit/testing-asynchronous.md @@ -0,0 +1,87 @@ +--- +title: How to write asynchronous tests? +type: section +description: Writing asynchronous tests using MUnit +num: 7 +previous-page: testing-exceptions +next-page: testing-resources +--- + +{% include markdown.html path="_markdown/install-munit.md" %} + +## Asynchronous tests + +In Scala, it's common for an *asynchronous* method to return a `Future`. +MUnit offers special support for `Future`s. + +For example, consider an asynchronous variant of a `square` method: + +{% tabs 'async-1' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +import scala.concurrent.{ExecutionContext, Future} + +object AsyncMathLib { + def square(x: Int)(implicit ec: ExecutionContext): Future[Int] = + Future(x * x) +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import scala.concurrent.{ExecutionContext, Future} + +object AsyncMathLib: + def square(x: Int)(using ExecutionContext): Future[Int] = + Future(x * x) +``` +{% endtab %} +{% endtabs %} + +A test itself can return a `Future[Unit]`. +MUnit will wait behind the scenes for the resulting `Future` to complete, failing the test if any assertion fails. + +You can therefore write the test as follows: + +{% tabs 'async-3' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +// Import the global execution context, required to call async methods +import scala.concurrent.ExecutionContext.Implicits.global + +class AsyncMathLibTests extends munit.FunSuite { + test("square") { + for { + squareOf3 <- AsyncMathLib.square(3) + squareOfMinus4 <- AsyncMathLib.square(-4) + } yield { + assertEquals(squareOf3, 9) + assertEquals(squareOfMinus4, 16) + } + } +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +// Import the global execution context, required to call async methods +import scala.concurrent.ExecutionContext.Implicits.global + +class AsyncMathLibTests extends munit.FunSuite: + test("square"): + for + squareOf3 <- AsyncMathLib.square(3) + squareOfMinus4 <- AsyncMathLib.square(-4) + yield + assertEquals(squareOf3, 9) + assertEquals(squareOfMinus4, 16) +``` +{% endtab %} +{% endtabs %} + +The test first asynchronously computes `square(3)` and `square(-4)`. +Once both computations are completed, and if they are both successful, it proceeds with the calls to `assertEquals`. +If any of the assertion fails, the resulting `Future[Unit]` fails, and MUnit will cause the test to fail. + +You may read more about asynchronous tests [in the MUnit documentation](https://scalameta.org/munit/docs/tests.html#declare-async-test). +It shows how to use other asynchronous types besides `Future`. diff --git a/_overviews/toolkit/testing-exceptions.md b/_overviews/toolkit/testing-exceptions.md new file mode 100644 index 0000000000..0e26e51bd5 --- /dev/null +++ b/_overviews/toolkit/testing-exceptions.md @@ -0,0 +1,65 @@ +--- +title: How to test exceptions? +type: section +description: Describe the intercept assertion +num: 6 +previous-page: testing-run-only +next-page: testing-asynchronous +--- + +{% include markdown.html path="_markdown/install-munit.md" %} + +## Intercepting an exception + +In a test, you can use `intercept` to check that your code throws an exception. + +{% tabs 'intercept-1' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +import java.nio.file.NoSuchFileException + +class FileTests extends munit.FunSuite { + test("read missing file") { + val missingFile = os.pwd / "missing.txt" + + intercept[NoSuchFileException] { + os.read(missingFile) + } + } +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import java.nio.file.NoSuchFileException + +class FileTests extends munit.FunSuite: + test("read missing file"): + val missingFile = os.pwd / "missing.txt" + intercept[NoSuchFileException]: + // the code that should throw an exception + os.read(missingFile) +``` +{% endtab %} +{% endtabs %} + +The type parameter of the `intercept` assertion is the expected exception. +Here it is `NoSuchFileException`. +The body of the `intercept` assertion contains the code that should throw the exception. + +The test passes if the code throws the expected exception and it fails otherwise. + +The `intercept` method returns the exception that is thrown. +You can check more assertions on it. + +{% tabs 'intercept-2' %} +{% tab 'Scala 2 and 3' %} +```scala +val exception = intercept[NoSuchFileException](os.read(missingFile)) +assert(clue(exception.getMessage).contains("missing.txt")) +``` +{% endtab %} +{% endtabs %} + +You can also use the more concise `interceptMessage` method to test the exception and its message in a single assertion. +Learn more about it in the [MUnit documentation](https://scalameta.org/munit/docs/assertions.html#interceptmessage). diff --git a/_overviews/toolkit/testing-intro.md b/_overviews/toolkit/testing-intro.md new file mode 100644 index 0000000000..8aa5021b76 --- /dev/null +++ b/_overviews/toolkit/testing-intro.md @@ -0,0 +1,21 @@ +--- +title: Testing with MUnit +type: chapter +description: The introduction of the MUnit library +num: 2 +previous-page: introduction +next-page: testing-suite +--- + +MUnit is a lightweight testing library. It provides a single style for writing tests, a style that can be learned quickly. + +Despite its simplicity, MUnit has useful features such as: +- assertions to verify the behavior of the program +- fixtures to ensure that the tests have access to all the necessary resources +- asynchronous support, for testing concurrent and distributed applications. + +MUnit produces actionable error reports, with diff and source location, to help you quickly understand failures. + +Testing is essential for any software development process because it helps catch bugs early, improves code quality and facilitates collaboration. + +{% include markdown.html path="_markdown/install-munit.md" %} diff --git a/_overviews/toolkit/testing-resources.md b/_overviews/toolkit/testing-resources.md new file mode 100644 index 0000000000..9e0d45f51f --- /dev/null +++ b/_overviews/toolkit/testing-resources.md @@ -0,0 +1,101 @@ +--- +title: How to manage the resources of a test? +type: section +description: Describe the functional fixtures +num: 8 +previous-page: testing-asynchronous +next-page: testing-what-else +--- + +{% include markdown.html path="_markdown/install-munit.md" %} + +## `FunFixture` + +In MUnit, we use functional fixtures to manage resources in a concise and safe way. +A `FunFixture` creates one resource for each test, ensuring that each test runs in isolation from the others. + +In a test suite, you can define and use a `FunFixture` as follows: + +{% tabs 'resources-1' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +class FileTests extends munit.FunSuite { + val usingTempFile: FunFixture[os.Path] = FunFixture( + setup = _ => os.temp(prefix = "file-tests"), + teardown = tempFile => os.remove(tempFile) + ) + usingTempFile.test("overwrite on file") { tempFile => + os.write.over(tempFile, "Hello, World!") + val obtained = os.read(tempFile) + assertEquals(obtained, "Hello, World!") + } +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +class FileTests extends munit.FunSuite: + val usingTempFile: FunFixture[os.Path] = FunFixture( + setup = _ => os.temp(prefix = "file-tests"), + teardown = tempFile => os.remove(tempFile) + ) + usingTempFile.test("overwrite on file") { tempFile => + os.write.over(tempFile, "Hello, World!") + val obtained = os.read(tempFile) + assertEquals(obtained, "Hello, World!") + } +``` +{% endtab %} +{% endtabs %} + +`usingTempFile` is a fixture of type `FunFixture[os.Path]`. +It contains two functions: + - The `setup` function, of type `TestOptions => os.Path`, creates a new temporary file. + - The `teardown` function, of type `os.Path => Unit`, deletes this temporary file. + +We use the `usingTempFile` fixture to define a test that needs a temporary file. +Notice that the body of the test takes a `tempFile`, of type `os.Path`, as parameter. +The fixture automatically creates this temporary file, calls its `setup` function, and cleans it up after the test by calling `teardown`. + +In the example, we used a fixture to manage a temporary file. +In general, fixtures can manage other kinds of resources, such as a temporary folder, a temporary table in a database, a connection to a local server, and so on. + +## Composing `FunFixture`s + +In some tests, you may need more than one resource. +You can use `FunFixture.map2` to compose two functional fixtures into one. + +{% tabs 'resources-2' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +val using2TempFiles: FunFixture[(os.Path, os.Path)] = + FunFixture.map2(usingTempFile, usingTempFile) + +using2TempFiles.test("merge two files") { + (file1, file2) => + // body of the test +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +val using2TempFiles: FunFixture[(os.Path, os.Path)] = + FunFixture.map2(usingTempFile, usingTempFile) + +using2TempFiles.test("merge two files"): + (file1, file2) => + // body of the test +``` +{% endtab %} +{% endtabs %} + +Using `FunFixture.map2` on a `FunFixture[A]` and a `FunFixture[B]` returns a `FunFixture[(A, B)]`. + +## Other fixtures + +`FunFixture` is the recommended type of fixture because: +- it is explicit: each test declares the resource they need, +- it is safe to use: each test uses its own resource in isolation. + +For more flexibility, `MUnit` contains other types of fixtures: the reusable fixture, the ad-hoc fixture and the asynchronous fixture. +Learn more about them in the [MUnit documentation](https://scalameta.org/munit/docs/fixtures.html). diff --git a/_overviews/toolkit/testing-run-only.md b/_overviews/toolkit/testing-run-only.md new file mode 100644 index 0000000000..af6ba02cd7 --- /dev/null +++ b/_overviews/toolkit/testing-run-only.md @@ -0,0 +1,106 @@ +--- +title: How to run a single test? +type: section +description: About testOnly in the build tool and .only in MUnit +num: 5 +previous-page: testing-run +next-page: testing-exceptions +--- + +{% include markdown.html path="_markdown/install-munit.md" %} + +## Running a single test suite + +{% tabs munit-unit-test-only class=tabs-build-tool %} +{% tab 'Scala CLI' %} +To run a single `example.MyTests` suite with Scala CLI, use the `--test-only` option of the `test` command. +``` +scala-cli test example --test-only example.MyTests +``` + +{% endtab %} +{% tab 'sbt' %} +To run a single `example.MyTests` suite in sbt, use the `testOnly` task: +``` +sbt:example> example/testOnly example.MyTests +``` +{% endtab %} +{% tab 'Mill' %} +To run a single `example.MyTests` suite in Mill, use the `testOnly` task: +``` +./mill example.test.testOnly example.MyTests +``` +{% endtab %} +{% endtabs %} + +## Running a single test in a test suite + +Within a test suite file, you can select individual tests to run by temporarily appending `.only`, e.g. + +{% tabs 'only-demo' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc +class MathSuite extends munit.FunSuite { + test("addition") { + assert(1 + 1 == 2) + } + test("multiplication".only) { + assert(3 * 7 == 21) + } +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +class MathSuite extends munit.FunSuite: + test("addition"): + assert(1 + 1 == 2) + test("multiplication".only): + assert(3 * 7 == 21) +``` +{% endtab %} +{% endtabs %} + +In the above example, only the `"multiplication"` tests will run (i.e. `"addition"` is ignored). +This is useful to quickly debug a specific test in a suite. + +## Alternative: excluding specific tests + +You can exclude specific tests from running by appending `.ignore` to the test name. +For example the following ignores the `"addition"` test, and run all the others: + +{% tabs 'ignore-demo' class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala mdoc:reset +class MathSuite extends munit.FunSuite { + test("addition".ignore) { + assert(1 + 1 == 2) + } + test("multiplication") { + assert(3 * 7 == 21) + } + test("remainder") { + assert(13 % 5 == 3) + } +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +class MathSuite extends munit.FunSuite: + test("addition".ignore): + assert(1 + 1 == 2) + test("multiplication"): + assert(3 * 7 == 21) + test("remainder"): + assert(13 % 5 == 3) +``` +{% endtab %} +{% endtabs %} + +## Use tags to group tests, and run specific tags + +MUnit lets you group and run tests across suites by tags, which are textual labels. +[The MUnit docs][munit-tags] have instructions on how to do this. + +[munit-tags]: https://scalameta.org/munit/docs/filtering.html#include-and-exclude-tests-based-on-tags diff --git a/_overviews/toolkit/testing-run.md b/_overviews/toolkit/testing-run.md new file mode 100644 index 0000000000..cb3fde0ade --- /dev/null +++ b/_overviews/toolkit/testing-run.md @@ -0,0 +1,91 @@ +--- +title: How to run tests? +type: section +description: Running the MUnit tests +num: 4 +previous-page: testing-suite +next-page: testing-run-only +--- + +{% include markdown.html path="_markdown/install-munit.md" %} + +## Running the tests + +You can run all of your test suites with a single command. + +{% tabs munit-unit-test-4 class=tabs-build-tool %} +{% tab 'Scala CLI' %} +Using Scala CLI, the following command runs all the tests in the folder `example`: +``` +scala-cli test example +# Compiling project (test, Scala 3.2.1, JVM) +# Compiled project (test, Scala 3.2.1, JVM) +# MyTests: +# + sum of two integers 0.009s +``` +{% endtab %} +{% tab 'sbt' %} +In the sbt shell, the following command runs all the tests of the project `example`: +``` +sbt:example> example/test +# MyTests: +# + sum of two integers 0.006s +# [info] Passed: Total 1, Failed 0, Errors 0, Passed 1 +# [success] Total time: 0 s, completed Nov 11, 2022 12:54:08 PM +``` +{% endtab %} +{% tab 'Mill' %} +In Mill, the following command runs all the tests of the module `example`: +``` +./mill example.test.test +# [71/71] example.test.test +# MyTests: +# + sum of two integers 0.008s +``` +{% endtab %} +{% endtabs %} + +The test report, printed in the console, shows the status of each test. +The `+` symbol before a test name shows that the test passed successfully. + +Add and run a failing test to see how a failure looks: + +{% tabs assertions-1 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +test("failing test") { + val obtained = 2 + 3 + val expected = 4 + assertEquals(obtained, expected) +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +test("failing test"): + val obtained = 2 + 3 + val expected = 4 + assertEquals(obtained, expected) +``` +{% endtab %} +{% endtabs %} + +``` +# MyTests: +# + sum of two integers 0.008s +# ==> X MyTests.failing test 0.015s munit.ComparisonFailException: ./example/MyTests.test.scala:13 +# 12: val expected = 4 +# 13: assertEquals(obtained, expected) +# 14: } +# values are not the same +# => Obtained +# 5 +# => Diff (- obtained, + expected) +# -5 +# +4 +# at munit.Assertions.failComparison(Assertions.scala:274) +``` + +The line starting with `==> X` indicates that the test named `failing test` fails. +The following lines show where and how it failed. +Here it shows that the obtained value is 5, where 4 was expected. diff --git a/_overviews/toolkit/testing-suite.md b/_overviews/toolkit/testing-suite.md new file mode 100644 index 0000000000..8b17de43a6 --- /dev/null +++ b/_overviews/toolkit/testing-suite.md @@ -0,0 +1,128 @@ +--- +title: How to write tests? +type: section +description: The basics of writing a test suite with MUnit +num: 3 +previous-page: testing-intro +next-page: testing-run +--- + +{% include markdown.html path="_markdown/install-munit.md" %} + +## Writing a test suite + +A group of tests in a single class is called a test class or test suite. + +Each test suite validates a particular component or feature of the software. +Typically we define one test suite for each source file or class that we want to test. + +{% tabs munit-unit-test-2 class=tabs-build-tool %} +{% tab 'Scala CLI' %} +In Scala CLI, the test file can live in the same folder as the actual code, but the name of the file must end with `.test.scala`. +In the following, `MyTests.test.scala` is a test file. +``` +example/ +├── MyApp.scala +└── MyTests.test.scala +``` +Other valid structures and conventions are described in the [Scala CLI documentation](https://scala-cli.virtuslab.org/docs/commands/test/#test-sources). +{% endtab %} +{% tab 'sbt' %} +In sbt, test sources go in the `src/test/scala` folder. + +For instance, the following is the file structure of a project `example`: +``` +example +└── src + ├── main + │ └── scala + │ └── MyApp.scala + └── test + └── scala + └── MyTests.scala +``` +{% endtab %} +{% tab 'Mill' %} +In Mill, test sources go in the `test/src` folder. + +For instance, the following is the file structure of a module `example`: +``` +example +└── src +| └── MyApp.scala +└── test + └── src + └── MyTests.scala +``` +{% endtab %} +{% endtabs %} + +In the test source file, define a suite containing a single test: + +{% tabs munit-unit-test-3 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +package example + +class MyTests extends munit.FunSuite { + test("sum of two integers") { + val obtained = 2 + 2 + val expected = 4 + assertEquals(obtained, expected) + } +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +package example + +class MyTests extends munit.FunSuite: + test("sum of two integers"): + val obtained = 2 + 2 + val expected = 4 + assertEquals(obtained, expected) +``` +{% endtab %} +{% endtabs %} + +A test suite is a Scala class that extends `munit.FunSuite`. +It contains one or more tests, each defined by a call to the `test` method. + +In the previous example, we have a single test `"sum of integers"` that checks that `2 + 2` equals `4`. +We use the assertion method `assertEquals` to check that two values are equal. +The test passes if all the assertions are correct and fails otherwise. + +## Assertions + +It is important to use assertions in each and every test to describe what to check. +The main assertion methods in MUnit are: +- `assertEquals` to check that what you obtain is equal to what you expected +- `assert` to check a boolean condition + +The following is an example of a test that use `assert` to check a boolean condition on a list. + +{% tabs assertions-1 class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +test("all even numbers") { + val input: List[Int] = List(1, 2, 3, 4) + val obtainedResults: List[Int] = input.map(_ * 2_) + // check that obtained values are all even numbers + assert(obtainedResults.forall(x => x % 2 == 0)) +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +test("all even numbers"): + val input: List[Int] = List(1, 2, 3, 4) + val obtainedResults: List[Int] = input.map(_ * 2_) + // check that obtained values are all even numbers + assert(obtainedResults.forall(x => x % 2 == 0)) +``` +{% endtab %} +{% endtabs %} + +MUnit contains more assertion methods that you can discover in its [documentation](https://scalameta.org/munit/docs/assertions.html): +`assertNotEquals`, `assertNoDiff`, `fail`, and `compileErrors`. diff --git a/_overviews/toolkit/testing-what-else.md b/_overviews/toolkit/testing-what-else.md new file mode 100644 index 0000000000..305765ec30 --- /dev/null +++ b/_overviews/toolkit/testing-what-else.md @@ -0,0 +1,83 @@ +--- +title: What else can MUnit do? +type: section +description: A incomplete list of features of MUnit +num: 9 +previous-page: testing-resources +next-page: os-intro +--- + +{% include markdown.html path="_markdown/install-munit.md" %} + +## Adding clues to get better error report + +Use `clue` inside an `assert` to a get a better error report when the assertion fails. + +{% tabs clues %} +{% tab 'Scala 2 and 3' %} +```scala +assert(clue(List(a).head) > clue(b)) +// munit.FailException: assertion failed +// Clues { +// List(a).head: Int = 1 +// b: Int = 2 +// } +``` +{% endtab %} +{% endtabs %} + +Learn more about clues in the [MUnit documentation](https://scalameta.org/munit/docs/assertions.html#assert). + +## Writing environment-specific tests + +Use `assume` to write environment-specific tests. +`assume` can contain a boolean condition. You can check the operating system, the Java version, a Java property, an environment variable, or anything else. +A test is skipped if one of its assumptions isn't met. + +{% tabs assumption class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +import scala.util.Properties + +test("home directory") { + assume(Properties.isLinux, "this test runs only on Linux") + assert(os.home.toString.startsWith("/home/")) +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +import scala.util.Properties + +test("home directory"): + assume(Properties.isLinux, "this test runs only on Linux") + assert(os.home.toString.startsWith("/home/")) +``` +{% endtab %} +{% endtabs %} + +Learn more about filtering tests in the [MUnit documentation](https://scalameta.org/munit/docs/filtering.html). + +## Tagging flaky tests + +You can tag a test with `flaky` to mark it as being flaky. +Flaky tests can be skipped by setting the `MUNIT_FLAKY_OK` environment variable to `true`. + +{% tabs flaky class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +test("requests".flaky) { + // I/O heavy tests that sometimes fail +} +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +test("requests".flaky): + // I/O heavy tests that sometimes fail +``` +{% endtab %} +{% endtabs %} + +Learn more about flaky tests in the [MUnit documentation](https://scalameta.org/munit/docs/tests.html#tag-flaky-tests) + diff --git a/_plugins/jekyll-tabs-lib/jekyll-tabs-4scala.rb b/_plugins/jekyll-tabs-lib/jekyll-tabs-4scala.rb index eee013d138..b592fcc7f9 100644 --- a/_plugins/jekyll-tabs-lib/jekyll-tabs-4scala.rb +++ b/_plugins/jekyll-tabs-lib/jekyll-tabs-4scala.rb @@ -4,6 +4,7 @@ module Jekyll module Tabs ScalaVersions = ['Scala 2', 'Scala 3'] + BuildTools = ['Scala CLI', 'sbt', 'Mill'] def self.unquote(string) string.gsub(/^['"]|['"]$/, '') @@ -25,13 +26,11 @@ def initialize(block_name, markup, tokens) if markup =~ SYNTAX @name = Tabs::unquote($1) @css_classes = "" - @is_scala_tabs = false + @tab_class = "" if $2 css_class = Tabs::unquote($2) css_class.strip! - if css_class == "tabs-scala-version" - @is_scala_tabs = true - end + @tab_class = css_class # append $2 to @css_classes @css_classes = " #{css_class}" end @@ -56,36 +55,36 @@ def render(context) seenTabs = [] - def joinScalaVersions() - Tabs::ScalaVersions.to_a.map{|item| "'#{item}'"}.join(", ") + def joinTabs(tabs) + tabs.to_a.map{|item| "'#{item}'"}.join(", ") end - def errorNonScalaVersion(tab) + def errorInvalidTab(tab, expectedTabs) SyntaxError.new( - "Scala version tab label '#{tab.label}' is not valid for tabs '#{@name}' with " + - "class=tabs-scala-version. Valid tab labels are: #{joinScalaVersions()}") + "Tab label '#{tab.label}' is not valid for tabs '#{@name}' with " + + "class=#{@tab_class}. Valid tab labels are: #{joinTabs(expectedTabs)}") end - def errorScalaVersionWithoutClass(tab) + def errorTabWithoutClass(tab, tabClass) SyntaxError.new( - "Scala version tab label '#{tab.label}' is not valid for tabs '#{@name}' without " + - "class=tabs-scala-version") + "Tab label '#{tab.label}' is not valid for tabs '#{@name}' without " + + "class=#{tabClass}") end - def errorMissingScalaVersion() + def errorMissingTab(expectedTabs) SyntaxError.new( - "Tabs '#{@name}' with class=tabs-scala-version must have exactly the following " + - "tab labels: #{joinScalaVersions()}") + "Tabs '#{@name}' with class=#{@tab_class} must have exactly the following " + + "tab labels: #{joinTabs(expectedTabs)}") end def errorDuplicateTab(tab) SyntaxError.new("Duplicate tab label '#{tab.label}' in tabs '#{@name}'") end - def errorScalaVersionDefault(tab) + def errorTabDefault(tab) SyntaxError.new( - "Scala version tab label '#{tab.label}' should not be default for tabs '#{@name}' " + - "with class=tabs-scala-version") + "Tab label '#{tab.label}' should not be default for tabs '#{@name}' " + + "with class=#{@tab_class}") end allTabs.each do | tab | @@ -97,25 +96,34 @@ def errorScalaVersionDefault(tab) foundDefault = true end - isScalaTab = Tabs::ScalaVersions.include? tab.label - - if @is_scala_tabs - if !isScalaTab - raise errorNonScalaVersion(tab) - elsif tab.defaultTab - raise errorScalaVersionDefault(tab) + def checkTab(tab, tabClass, expectedTabs, raiseIfMissingClass) + isValid = expectedTabs.include? tab.label + if @tab_class == tabClass + if !isValid + raise errorInvalidTab(tab, expectedTabs) + elsif tab.defaultTab + raise errorTabDefault(tab) + end + elsif raiseIfMissingClass and isValid + raise errorTabWithoutClass(tab, tabClass) end - elsif !@is_scala_tabs and isScalaTab - raise errorScalaVersionWithoutClass(tab) end + + checkTab(tab, "tabs-scala-version", Tabs::ScalaVersions, true) + checkTab(tab, "tabs-build-tool", Tabs::BuildTools, false) end - if @is_scala_tabs and seenTabs != Tabs::ScalaVersions - raise errorMissingScalaVersion() + def checkExhaustivity(seenTabs, tabClass, expectedTabs) + if @tab_class == tabClass and seenTabs != expectedTabs + raise errorMissingTab(expectedTabs) + end end + checkExhaustivity(seenTabs, "tabs-scala-version", Tabs::ScalaVersions) + checkExhaustivity(seenTabs, "tabs-build-tool", Tabs::BuildTools) + if !foundDefault and allTabs.length > 0 - if @is_scala_tabs + if @tab_class == "tabs-scala-version" # set last tab to default ('Scala 3') allTabs[-1].defaultTab = true else diff --git a/_plugins/mdoc_replace.rb b/_plugins/mdoc_replace.rb index 0b62828227..a4d8bf6e11 100644 --- a/_plugins/mdoc_replace.rb +++ b/_plugins/mdoc_replace.rb @@ -12,6 +12,7 @@ def output_ext(ext) end def convert(content) + content = content.gsub("```scala mdoc:compile-only\n", "```scala\n") content = content.gsub("```scala mdoc:fail\n", "```scala\n") content = content.gsub("```scala mdoc:crash\n", "```scala\n") content = content.gsub("```scala mdoc:nest\n", "```scala\n") diff --git a/index.md b/index.md index 4f97d4d051..2ec3031c68 100644 --- a/index.md +++ b/index.md @@ -26,6 +26,10 @@ sections: description: "Learn Scala by reading a series of short lessons." icon: "fa fa-book-open" link: /scala3/book/introduction.html + - title: "Scala Toolkit" + description: "Sending HTTP requests, writing files, running processes, parsing JSON..." + icon: "fa fa-toolbox" + link: /toolkit/introduction.html - title: Online Courses description: "MOOCs to learn Scala, for beginners and experienced programmers." icon: "fa fa-cloud" diff --git a/resources/js/functions.js b/resources/js/functions.js index c31e52b22e..8dd9e9ec76 100644 --- a/resources/js/functions.js +++ b/resources/js/functions.js @@ -415,37 +415,36 @@ $(document).ready(function() { }); }; - /** Links all tabs created in Liquid templates with class ".tabs-scala-version" + function activateTab(tabs, value) { + // check the code tab corresponding to the preferred value + tabs.find('input[data-target=' + value + ']').prop("checked", true); + } + + /** Links all tabs created in Liquid templates with class ".tabs-$namespace" * on the page together, such that - * changing a tab to `Scala 2` will activate all other tab sections to - * also change to "Scala 2". - * Also records a preference for the Scala version in localStorage, so + * changing a tab to some value will activate all other tab sections to + * also change to that value. + * Also records a preference for the tab in localStorage, so * that when the page is refreshed, the same tab will be selected. - * On page load, selects the tab corresponding to stored Scala version. + * On page load, selects the tab corresponding to stored value. */ - function setupScalaVersionTabs(scalaVersionTabs) { - const DocsPreferences = Storage('org.scala-lang.docs.preferences'); - const Scala3 = 'scala-3'; - const scalaVersion = DocsPreferences.getPreference('scalaVersion', Scala3); - - function activateTab(tabs, scalaVersion) { - // click the code tab corresponding to the preferred Scala version. - tabs.find('input[data-target=' + scalaVersion + ']').prop("checked", true); - } - - activateTab(scalaVersionTabs, scalaVersion); + function setupTabs(tabs, namespace, defaultValue) { + const PreferenceStorage = Storage('org.scala-lang.docs.preferences'); + const preferredValue = PreferenceStorage.getPreference(namespace, defaultValue); + + activateTab(tabs, preferredValue) // setup listeners to record new preferred Scala version. - scalaVersionTabs.find('input').on('change', function() { + tabs.find('input').on('change', function() { // if checked then set the preferred version, and activate the other tabs on page. if ($(this).is(':checked')) { const parent = $(this).parent(); - const scalaVersion = $(this).data('target'); + const newValue = $(this).data('target'); - DocsPreferences.setPreference('scalaVersion', scalaVersion, oldValue => { + PreferenceStorage.setPreference(namespace, newValue, oldValue => { // when we set a new scalaVersion, find scalaVersionTabs except current one // and activate those tabs. - activateTab(scalaVersionTabs.not(parent), scalaVersion); + activateTab(tabs.not(parent), newValue); }); } @@ -456,7 +455,11 @@ $(document).ready(function() { if (storageAvailable('localStorage')) { var scalaVersionTabs = $(".tabsection.tabs-scala-version"); if (scalaVersionTabs.length) { - setupScalaVersionTabs(scalaVersionTabs); + setupTabs(scalaVersionTabs, "scalaVersion", "scala-3"); + } + var buildToolTabs = $(".tabsection.tabs-build-tool"); + if (buildToolTabs.length) { + setupTabs(buildToolTabs, "buildTool", "scala-cli"); } } diff --git a/scripts/run-mdoc.sh b/scripts/run-mdoc.sh index 32e7d23d2c..a4f4acbe2a 100755 --- a/scripts/run-mdoc.sh +++ b/scripts/run-mdoc.sh @@ -1,10 +1,15 @@ #!/bin/bash set -eux -cs launch org.scalameta:mdoc_2.13:2.3.3 -- \ +cs launch --scala-version 2.13.10 org.scalameta::mdoc:2.3.3 -- \ --in . \ --out /tmp/mdoc-out/ \ - --classpath $(cs fetch -p com.chuusai:shapeless_2.13:2.3.10) \ + --classpath \ + $(cs fetch --scala-version 2.13.10 -p \ + com.chuusai::shapeless:2.3.10 \ + org.scala-lang::toolkit:0.1.7 \ + org.scala-lang::toolkit-test:0.1.7 \ + ) \ --scalac-options "-Xfatal-warnings -feature" \ --no-link-hygiene \ --include '**.md'