Skip to content

Commit

Permalink
Make all examples runnable using scala-cli (#2376)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw authored Dec 19, 2024
1 parent 15d15b1 commit 5e3939c
Show file tree
Hide file tree
Showing 97 changed files with 919 additions and 1,147 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ jobs:
sudo apt-get update
sudo apt-get install libidn2-dev libcurl3-dev
echo "STTP_NATIVE=1" >> $GITHUB_ENV
- name: Install scala-cli
if: matrix.target-platform == 'JVM'
uses: VirtusLab/scala-cli-setup@main
with:
jvm: '' # needed because scala-cli-setup otherwise forces the installation of their default JVM (17)
- name: Enable Loom-specific modules
if: matrix.java == '21'
run: echo "ONLY_LOOM=1" >> $GITHUB_ENV
Expand All @@ -57,6 +62,12 @@ jobs:
- name: Compile documentation
if: matrix.target-platform == 'JVM' && matrix.java == '11'
run: sbt -v compileDocs
- name: Verify that examples compile using Scala CLI
if: matrix.target-platform == 'JVM' && matrix.java == '21' && matrix.scala-version == '3'
run: sbt $SBT_JAVA_OPTS -v "project examples3" verifyExamplesCompileUsingScalaCli
- name: Verify that examples-ce2 compile using Scala CLI
if: matrix.target-platform == 'JVM' && matrix.java == '11' && matrix.scala-version == '2.13'
run: sbt $SBT_JAVA_OPTS -v "project examplesCe2" verifyExamplesCompileUsingScalaCli
- name: Test
run: sbt -v "testScoped ${{ matrix.scala-version }} ${{ matrix.target-platform }}"
- name: Prepare release notes
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ core/native/local.sbt
.bsp/
.java-version
metals.sbt
.scala-build

.vscode

Expand Down
33 changes: 18 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,29 @@ requests and how to handle responses. Requests are sent using one of the backend

Backend implementations include the HTTP client that is shipped with Java, as well as ones based on [akka-http](https://doc.akka.io/docs/akka-http/current/scala/http/), [http4s](https://http4s.org), [OkHttp](http://square.github.io/okhttp/). They integrate with [Akka](https://akka.io), [Monix](https://monix.io), [fs2](https://github.com/functional-streams-for-scala/fs2), [cats-effect](https://github.com/typelevel/cats-effect), [scalaz](https://github.com/scalaz/scalaz) and [ZIO](https://github.com/zio/zio). Supported Scala versions include 2.12, 2.13 and 3, Scala.JS and Scala Native; supported Java versions include 11+.

Here's a quick example of sttp client in action:
Here's a quick example of sttp client in action, runnable using [scala-cli](https://scala-cli.virtuslab.org):

```scala
import sttp.client4._
//> using dep com.softwaremill.sttp.client4::core:4.0.0-M20

import sttp.client4.*

@main def sttpDemo(): Unit =
val sort: Option[String] = None
val query = "http language:scala"

val sort: Option[String] = None
val query = "http language:scala"
// the `query` parameter is automatically url-encoded
// `sort` is removed, as the value is not defined
val request = basicRequest.get(uri"https://api.github.com/search/repositories?q=$query&sort=$sort")

// the `query` parameter is automatically url-encoded
// `sort` is removed, as the value is not defined
val request = basicRequest.get(uri"https://api.github.com/search/repositories?q=$query&sort=$sort")

val backend = DefaultSyncBackend()
val response = request.send(backend)
val backend = DefaultSyncBackend()
val response = request.send(backend)

// response.header(...): Option[String]
println(response.header("Content-Length"))
// response.header(...): Option[String]
println(response.header("Content-Length"))

// response.body: by default read into an Either[String, String] to indicate failure or success
println(response.body)
// response.body: by default read into an Either[String, String] to indicate failure or success
println(response.body)
```

## Documentation
Expand Down Expand Up @@ -135,7 +138,7 @@ The documentation is typechecked using [mdoc](https://scalameta.org/mdoc/). The

When generating documentation, it's best to set the version to the current one, so that the generated doc files don't include modifications with the current snapshot version.

That is, in sbt run: `set version := "4.0.0-M20"`, before running `mdoc` in `docs`.
That is, in sbt run: `set ThisBuild/version := "4.0.0-M20"`, before running `mdoc` in `docs`.

### Testing the Scala.JS backend

Expand Down
10 changes: 8 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ val ideScalaVersion = scala3

lazy val testServerPort = settingKey[Int]("Port to run the http test server on")
lazy val startTestServer = taskKey[Unit]("Start a http server used by tests")
lazy val verifyExamplesCompileUsingScalaCli = taskKey[Unit]("Verify that each example compiles using Scala CLI")

// slow down for CI
parallelExecution in Global := false
Expand All @@ -41,6 +42,9 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq(
val files1 = UpdateVersionInDocs(sLog.value, organization.value, version.value, List(file("README.md")))
Def.task {
(docs.jvm(scala3) / mdoc).toTask("").value
// Generating the list only after mdoc is done (as it overrides what's in generated_doc)
// For the root project the sourceDirectory points to src, so ../ will point to the root directory of the project
GenerateListOfExamples(sLog.value, sourceDirectory.value.getParentFile)
files1 ++ Seq(file("generated-docs/out"))
}
}.value,
Expand Down Expand Up @@ -952,7 +956,8 @@ lazy val examplesCe2 = (projectMatrix in file("examples-ce2"))
publish / skip := true,
libraryDependencies ++= Seq(
"io.circe" %% "circe-generic" % circeVersion
)
),
verifyExamplesCompileUsingScalaCli := VerifyExamplesCompileUsingScalaCli(sLog.value, sourceDirectory.value)
)
.jvmPlatform(scalaVersions = List(scala2_13))
.dependsOn(circe, monix)
Expand All @@ -967,7 +972,8 @@ lazy val examples = (projectMatrix in file("examples"))
"org.json4s" %% "json4s-native" % json4sVersion,
pekkoStreams,
logback
)
),
verifyExamplesCompileUsingScalaCli := VerifyExamplesCompileUsingScalaCli(sLog.value, sourceDirectory.value)
)
.jvmPlatform(scalaVersions = List(examplesScalaVersion))
.dependsOn(
Expand Down
10 changes: 7 additions & 3 deletions core/src/main/scala/sttp/client4/ResponseException.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import scala.annotation.tailrec
* @tparam DE
* A deserialization-library-specific error type, describing the deserialization error in more detail.
*/
sealed abstract class ResponseException[+HE, +DE](error: String) extends Exception(error)
sealed abstract class ResponseException[+HE, +DE](error: String, cause: Option[Throwable])
extends Exception(error, cause.orNull)

/** Represents an http error, where the response was received successfully, but the status code is other than the
* expected one (typically other than 2xx).
Expand All @@ -28,15 +29,18 @@ sealed abstract class ResponseException[+HE, +DE](error: String) extends Excepti
* The type of the body to which the error response is deserialized.
*/
case class HttpError[+HE](body: HE, statusCode: StatusCode)
extends ResponseException[HE, Nothing](s"statusCode: $statusCode, response: $body")
extends ResponseException[HE, Nothing](s"statusCode: $statusCode, response: $body", None)

/** Represents an error that occured during deserialization of `body`.
*
* @tparam DE
* A deserialization-library-specific error type, describing the deserialization error in more detail.
*/
case class DeserializationException[+DE: ShowError](body: String, error: DE)
extends ResponseException[Nothing, DE](implicitly[ShowError[DE]].show(error))
extends ResponseException[Nothing, DE](
implicitly[ShowError[DE]].show(error),
if (error.isInstanceOf[Throwable]) Some(error.asInstanceOf[Throwable]) else None
)

object HttpError {
@tailrec def find(exception: Throwable): Option[HttpError[_]] =
Expand Down
2 changes: 1 addition & 1 deletion docs/.python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.7.2
3.12
4 changes: 2 additions & 2 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python -msphinx
SPHINXPROJ = sttp
SPHINXPROJ = tapir
SOURCEDIR = .
BUILDDIR = _build

Expand All @@ -17,4 +17,4 @@ help:
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
42 changes: 42 additions & 0 deletions docs/_static/css/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* general style for all example tags */
.example-tag {
border-width: 1px;
border-radius: 9999px;
border-style: solid;
padding-left: 0.5rem;
padding-right: 0.5rem;
margin-right: 0.25rem;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}

/* different colors for specific tags */
.example-effects {
color: rgb(193 21 116);
background-color: rgb(253 242 250);
border-color: rgb(252 206 238);
}

.example-json {
color: rgb(185 56 21);
background-color: rgb(254 246 238);
border-color: rgb(249 219 175);
}

.example-backend {
color: rgb(6 118 71);
background-color: rgb(236 253 243);
border-color: rgb(169 239 197);
}

.example-docs {
color: rgb(52 64 84);
background-color: rgb(249 250 251);
border-color: rgb(234 236 240);
}

.example-client {
color: rgb(6 89 134);
background-color: rgb(240 249 255);
border-color: rgb(185 230 254);
}
2 changes: 1 addition & 1 deletion docs/backends/catseffect.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ val client = WebClient.builder("https://my-service.com")
val backend = ArmeriaCatsBackend.usingClient[IO](client)
```

```eval_rst
```{eval-rst}
.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI.
```

Expand Down
2 changes: 1 addition & 1 deletion docs/backends/fs2.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ val client = WebClient.builder("https://my-service.com")
val backend = ArmeriaFs2Backend.usingClient[IO](client, dispatcher)
```

```eval_rst
```{eval-rst}
.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI.
```

Expand Down
4 changes: 2 additions & 2 deletions docs/backends/future.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ There are several backend implementations which are `scala.concurrent.Future`-ba

Apart from the ones described below, also the [Pekko](pekko.md) & [Akka](akka.md) backends are `Future`-based.

```eval_rst
```{eval-rst}
===================================== ================================================= ==========================
Class Supported stream type Websocket support
===================================== ================================================= ==========================
Expand Down Expand Up @@ -125,7 +125,7 @@ val client = WebClient.builder("https://my-service.com")
val backend = ArmeriaFutureBackend.usingClient(client)
```

```eval_rst
```{eval-rst}
.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI.
```

Expand Down
2 changes: 1 addition & 1 deletion docs/backends/javascript/fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ val response: Task[Response[Observable[ByteBuffer]]] =
.send(backend)
```

```eval_rst
```{eval-rst}
.. note:: Currently no browsers support passing a stream as the request body. As such, using the ``Fetch`` backend with a streaming request will result in it being converted into an in-memory array before being sent. Response bodies are returned as a "proper" stream.
```

Expand Down
2 changes: 1 addition & 1 deletion docs/backends/monix.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ val client = WebClient.builder("https://my-service.com")
val backend = ArmeriaMonixBackend.usingClient(client)
```

```eval_rst
```{eval-rst}
.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI.
```

Expand Down
2 changes: 1 addition & 1 deletion docs/backends/scalaz.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ val client = WebClient.builder("https://my-service.com")
val backend = ArmeriaScalazBackend.usingClient(client)
```

```eval_rst
```{eval-rst}
.. note:: A WebClient could fail to follow redirects if the WebClient is created with a base URI and a redirect location is a different URI.
```

Expand Down
Loading

0 comments on commit 5e3939c

Please sign in to comment.