diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f5a0f00b0..971a8fe100 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/.gitignore b/.gitignore index 9d9dd33ac9..25725b5ba8 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ core/native/local.sbt .bsp/ .java-version metals.sbt +.scala-build .vscode diff --git a/README.md b/README.md index 8d5eeab293..ccf6de0b0d 100755 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/build.sbt b/build.sbt index ca49566528..92aa4fd80f 100644 --- a/build.sbt +++ b/build.sbt @@ -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 @@ -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, @@ -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) @@ -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( diff --git a/core/src/main/scala/sttp/client4/ResponseException.scala b/core/src/main/scala/sttp/client4/ResponseException.scala index b8153ae052..91d90a4de9 100644 --- a/core/src/main/scala/sttp/client4/ResponseException.scala +++ b/core/src/main/scala/sttp/client4/ResponseException.scala @@ -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). @@ -28,7 +29,7 @@ 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`. * @@ -36,7 +37,10 @@ case class HttpError[+HE](body: HE, statusCode: StatusCode) * 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[_]] = diff --git a/docs/.python-version b/docs/.python-version index 0b2eb36f50..e4fba21835 100644 --- a/docs/.python-version +++ b/docs/.python-version @@ -1 +1 @@ -3.7.2 +3.12 diff --git a/docs/Makefile b/docs/Makefile index 7f0a64e11d..3eda9474b0 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx -SPHINXPROJ = sttp +SPHINXPROJ = tapir SOURCEDIR = . BUILDDIR = _build @@ -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) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000000..c35ad22aa0 --- /dev/null +++ b/docs/_static/css/custom.css @@ -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); +} \ No newline at end of file diff --git a/docs/backends/catseffect.md b/docs/backends/catseffect.md index a1b94f775f..fb4e813964 100644 --- a/docs/backends/catseffect.md +++ b/docs/backends/catseffect.md @@ -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. ``` diff --git a/docs/backends/fs2.md b/docs/backends/fs2.md index 3758488094..c60235d86b 100644 --- a/docs/backends/fs2.md +++ b/docs/backends/fs2.md @@ -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. ``` diff --git a/docs/backends/future.md b/docs/backends/future.md index e065a90531..6482efc9e6 100644 --- a/docs/backends/future.md +++ b/docs/backends/future.md @@ -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 ===================================== ================================================= ========================== @@ -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. ``` diff --git a/docs/backends/javascript/fetch.md b/docs/backends/javascript/fetch.md index 9417c1a5c9..c44a5e8a5f 100644 --- a/docs/backends/javascript/fetch.md +++ b/docs/backends/javascript/fetch.md @@ -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. ``` diff --git a/docs/backends/monix.md b/docs/backends/monix.md index bc28222b71..c5c2eee9b1 100644 --- a/docs/backends/monix.md +++ b/docs/backends/monix.md @@ -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. ``` diff --git a/docs/backends/scalaz.md b/docs/backends/scalaz.md index 36dfaf7f66..092faaacc2 100644 --- a/docs/backends/scalaz.md +++ b/docs/backends/scalaz.md @@ -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. ``` diff --git a/docs/backends/summary.md b/docs/backends/summary.md index c681179233..93d1fcdba5 100644 --- a/docs/backends/summary.md +++ b/docs/backends/summary.md @@ -26,33 +26,33 @@ Which one to choose? Below is a summary of all the JVM backends; see the sections on individual backend implementations for more information: -```eval_rst -==================================== ================================ ================================================= ========================== =================== -Class Effect type Supported stream type Supports websockets Fully non-blocking -==================================== ================================ ================================================= ========================== =================== -``DefaultSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no -``HttpClientSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no -``DefaultFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no -``HttpClientFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no -``HttpClientMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[ByteBuffer]`` yes (regular & streaming) yes -``HttpClientFs2Backend`` ``F[_]: cats.effect.Concurrent`` ``fs2.Stream[F, Byte]`` yes (regular & streaming) yes -``HttpClientZioBackend`` ``zio.Task`` ``zio.stream.Stream[Throwable, Byte]`` yes (regular & streaming) yes -``HttpURLConnectionBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) no no -``TryHttpURLConnectionBackend`` ``scala.util.Try`` ``java.io.InputStream`` (blocking) no no -``AkkaHttpBackend`` ``scala.concurrent.Future`` ``akka.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) yes -``PekkoHttpBackend`` ``scala.concurrent.Future`` ``org.apache.pekko.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) yes -``ArmeriaFutureBackend`` ``scala.concurrent.Future`` n/a no yes -``ArmeriaScalazBackend`` ``scalaz.concurrent.Task`` n/a no yes -``ArmeriaZioBackend`` ``zio.Task`` ``zio.stream.Stream[Throwable, Byte]`` no yes -``ArmeriaMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[HttpData]`` no yes -``ArmeriaCatsBackend`` ``F[_]: cats.effect.Concurrent`` n/a no yes -``ArmeriaFs2Backend`` ``F[_]: cats.effect.Concurrent`` ``fs2.Stream[F, Byte]`` no yes -``OkHttpSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no -``OkHttpFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no -``OkHttpMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[ByteBuffer]`` yes (regular & streaming) no -``Http4sBackend`` ``F[_]: cats.effect.Effect`` ``fs2.Stream[F, Byte]`` no no -``FinagleBackend`` ``com.twitter.util.Future`` n/a no no -==================================== ================================ ================================================= ========================== =================== +```{eval-rst} +==================================== ================================ ============================================================ ========================== =================== +Class Effect type Supported stream type Supports websockets Fully non-blocking +==================================== ================================ ============================================================ ========================== =================== +``DefaultSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no +``HttpClientSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no +``DefaultFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no +``HttpClientFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no +``HttpClientMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[ByteBuffer]`` yes (regular & streaming) yes +``HttpClientFs2Backend`` ``F[_]: cats.effect.Concurrent`` ``fs2.Stream[F, Byte]`` yes (regular & streaming) yes +``HttpClientZioBackend`` ``zio.Task`` ``zio.stream.Stream[Throwable, Byte]`` yes (regular & streaming) yes +``HttpURLConnectionBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) no no +``TryHttpURLConnectionBackend`` ``scala.util.Try`` ``java.io.InputStream`` (blocking) no no +``AkkaHttpBackend`` ``scala.concurrent.Future`` ``akka.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) yes +``PekkoHttpBackend`` ``scala.concurrent.Future`` ``org.apache.pekko.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) yes +``ArmeriaFutureBackend`` ``scala.concurrent.Future`` n/a no yes +``ArmeriaScalazBackend`` ``scalaz.concurrent.Task`` n/a no yes +``ArmeriaZioBackend`` ``zio.Task`` ``zio.stream.Stream[Throwable, Byte]`` no yes +``ArmeriaMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[HttpData]`` no yes +``ArmeriaCatsBackend`` ``F[_]: cats.effect.Concurrent`` n/a no yes +``ArmeriaFs2Backend`` ``F[_]: cats.effect.Concurrent`` ``fs2.Stream[F, Byte]`` no yes +``OkHttpSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no +``OkHttpFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no +``OkHttpMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[ByteBuffer]`` yes (regular & streaming) no +``Http4sBackend`` ``F[_]: cats.effect.Effect`` ``fs2.Stream[F, Byte]`` no no +``FinagleBackend`` ``com.twitter.util.Future`` n/a no no +==================================== ================================ ============================================================ ========================== =================== ``` The backends work with Scala 2.12, 2.13 and 3. @@ -75,7 +75,7 @@ There are also backends which wrap other backends to provide additional function In addition, there are also backends for Scala.JS: -```eval_rst +```{eval-rst} ================================ ================================ ========================================= =================== Class Effect type Supported stream type Supports websockets ================================ ================================ ========================================= =================== @@ -89,7 +89,7 @@ Class Effect type Supported stre And a backend for scala-native: -```eval_rst +```{eval-rst} ================================ ============================ ========================================= =================== Class Effect type Supported stream type Supports websockets ================================ ============================ ========================================= =================== diff --git a/docs/backends/wrappers/opentelemetry.md b/docs/backends/wrappers/opentelemetry.md index cfaf5e679c..34e679f737 100644 --- a/docs/backends/wrappers/opentelemetry.md +++ b/docs/backends/wrappers/opentelemetry.md @@ -6,7 +6,7 @@ Currently, the following OpenTelemetry features are supported: - tracing using `OpenTelemetryTracingZioBackend`, wrapping any ZIO2 backend - tracing using [trace4cats](https://github.com/trace4cats/trace4cats), wrapping a cats-effect backend -### Metrics +## Metrics The backend depends only on [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-java). To use add the following dependency to your project: @@ -50,7 +50,7 @@ OpenTelemetryMetricsBackend( ) ``` -### Tracing (ZIO) +## Tracing (ZIO) To use, add the following dependency to your project: @@ -81,6 +81,6 @@ By default, the span is named after the HTTP method (e.g "HTTP POST") as [recomm and the http method, url and response status codes are set as span attributes. You can override these defaults by supplying a custom `OpenTelemetryZioTracer`. -### Tracing (cats-effect) +## Tracing (cats-effect) The [trace4cats](https://github.com/trace4cats/trace4cats) project includes sttp-client integration. diff --git a/docs/backends/zio.md b/docs/backends/zio.md index e50bef5eda..9ca38b2a33 100644 --- a/docs/backends/zio.md +++ b/docs/backends/zio.md @@ -67,7 +67,7 @@ ArmeriaZioBackend.scoped().flatMap { backend => ??? } ArmeriaZioBackend.usingDefaultClient().flatMap { backend => ??? } ``` -```eval_rst +```{eval-rst} .. note:: The default client factory is reused to create `ArmeriaZioBackend` if a `SttpBackendOptions` is unspecified. So you only need to manage a resource when `SttpBackendOptions` is used. ``` @@ -87,7 +87,7 @@ val client = WebClient.builder("https://my-service.com") ArmeriaZioBackend.usingClient(client).flatMap { backend => ??? } ``` -```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. ``` diff --git a/docs/conf.py b/docs/conf.py index f0ba51073f..d59d989412 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# sttp documentation build configuration file, created by +# sttp client documentation build configuration file, created by # sphinx-quickstart on Thu Oct 12 15:51:09 2017. # # This file is execfile()d with the current directory set to its @@ -43,6 +43,8 @@ # ones. extensions = ['myst_parser', 'sphinx_rtd_theme'] +myst_enable_extensions = ['attrs_block'] + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -50,9 +52,6 @@ # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -from recommonmark.parser import CommonMarkParser -from recommonmark.transform import AutoStructify - source_suffix = { '.rst': 'restructuredtext', '.txt': 'markdown', @@ -64,7 +63,7 @@ # General information about the project. project = u'sttp' -copyright = u'2021, SoftwareMill' +copyright = u'2024, SoftwareMill' author = u'SoftwareMill' # The version info for the project you're documenting, acts as replacement for @@ -72,16 +71,16 @@ # built documents. # # The short X.Y version. -version = u'3' +version = u'4' # The full version, including alpha/beta/rc tags. -release = u'3' +release = u'4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -89,7 +88,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'default' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -113,6 +112,12 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +# These paths are either relative to html_static_path +# or fully qualified paths (eg. https://...) +html_css_files = [ + 'css/custom.css', +] + # Custom sidebar templates, must be a dictionary that maps document names # to template names. # @@ -159,7 +164,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'sttp.tex', u'sttp Documentation', + (master_doc, 'sttp.tex', u'sttp client Documentation', u'Adam Warski', 'manual'), ] @@ -169,7 +174,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'sttp', u'sttp Documentation', + (master_doc, 'sttp', u'sttp client Documentation', [author], 1) ] @@ -180,7 +185,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'sttp', u'sttp documentation', + (master_doc, 'sttp', u'sttp client documentation', author, 'sttp', 'The Scala HTTP client you always wanted!', 'Scala'), ] @@ -195,12 +200,3 @@ 'github_version': 'master', # Version 'conf_py_path': '/docs/', # Path in the checkout to the docs root } - -# app setup hook -def setup(app): - app.add_config_value('recommonmark_config', { - 'auto_toc_tree_section': 'Contents', - 'enable_auto_doc_ref': False - }, True) - app.add_transform(AutoStructify) - diff --git a/docs/examples.md b/docs/examples.md index 6602d7899e..bede5439bc 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -1,292 +1,17 @@ -# Usage examples +# Examples by category -All of the examples are available [in the sources](https://github.com/softwaremill/sttp/blob/master/examples/src/main/scala/sttp/client4/examples) in runnable form. +The sttp client repository contains a number of how-to guides. If you're missing an example for your use-case, please let us +know by [reporting an issue](https://github.com/softwaremill/sttp)! -## Use the simple synchronous client +Each example is fully self-contained and can be run using [scala-cli](https://scala-cli.virtuslab.org) (you just need +to copy the content of the file, apart from scala-cli, no additional setup is required!). Hopefully this will make +experimenting with sttp client as frictionless as possible! -Required dependencies: +Examples are tagged with the stack being used (Direct-style, cats-effect, ZIO, Future) and backend implementation -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "core" % "@VERSION@") +```{eval-rst} +.. include:: includes/examples_list.md + :parser: markdown ``` -Example code: -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/SimpleClientGetAndPost.scala - :language: scala -``` - -## POST a form using the synchronous backend - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "core" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala - :language: scala -``` - -## GET and parse JSON using the akka-http backend and json4s - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "akka-http-backend" % "@VERSION@", - "com.softwaremill.sttp.client4" %% "json4s" % "@VERSION@", - "org.json4s" %% "json4s-native" % "3.6.0" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/GetAndParseJsonAkkaHttpJson4s.scala - :language: scala -``` - -## GET and parse JSON using the ZIO http-client backend and circe - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "zio" % "@VERSION@", - "com.softwaremill.sttp.client4" %% "circe" % "@VERSION@", - "io.circe" %% "circe-generic" % "@CIRCE_VERSION@" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/GetAndParseJsonZioCirce.scala - :language: scala -``` - -## GET and parse JSON using the http-client Monix backend and circe, treating deserialization errors as failed effects - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "monix" % "@VERSION@", - "com.softwaremill.sttp.client4" %% "circe" % "@VERSION@", - "io.circe" %% "circe-generic" % "@CIRCE_VERSION@" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples-ce2/src/main/scala/sttp/client4/examples/GetAndParseJsonGetRightMonixCirce.scala - :language: scala -``` - -## Log requests & responses using slf4j - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "slf4j-backend" % "@VERSION@", - "com.softwaremill.sttp.client4" %% "circe" % "@VERSION@", - "io.circe" %% "circe-generic" % "@CIRCE_VERSION@" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/LogRequestsSlf4j.scala - :language: scala -``` - -## POST and serialize JSON using the Monix http-client backend and circe - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "monix" % "@VERSION@", - "com.softwaremill.sttp.client4" %% "circe" % "@VERSION@", - "io.circe" %% "circe-generic" % "@CIRCE_VERSION@" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala - :language: scala -``` - -## Test an endpoint which requires multiple query parameters - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "core" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/TestEndpointMultipleQueryParameters.scala - :language: scala -``` -## Open a websocket using ZIO - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "zio" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/WebSocketZio.scala - :language: scala -``` - -## Open a websocket using FS2 streams - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "fs2" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/WebSocketStreamFs2.scala - :language: scala -``` - -## Test Monix websockets - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "monix" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples-ce2/src/main/scala/sttp/client4/examples/WebSocketTesting.scala - :language: scala -``` - -## Open a websocket using Akka - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "akka-http-backend" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/WebSocketAkka.scala - :language: scala -``` - -## Open a websocket using Pekko - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "pekko-http-backend" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/WebSocketPekko.scala - :language: scala -``` - -## Open a websocket using Monix - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "monix" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples-ce2/src/main/scala/sttp/client4/examples/WebSocketMonix.scala - :language: scala -``` - -## Stream request and response bodies using fs2 - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "fs2" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/StreamFs2.scala - :language: scala -``` - -## Stream request and response bodies using zio-stream - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "zio" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/StreamZio.scala - :language: scala -``` - -## Retry a request using ZIO - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "zio" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/RetryZio.scala - :language: scala -``` - -## GET parsed and raw response bodies - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "core" % "@VERSION@") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/GetRawResponseBodySynchronous.scala - :language: scala -``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index c1c6c263aa..3857d76ca2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,28 +9,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/), [pekko-http](https://pekko.apache.org/docs/pekko-http/current/), [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 mdoc:compile-only +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M20 + import sttp.client4.* -val query = "http language:scala" -val sort: Option[String] = None +@main def sttpDemo(): Unit = + 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) ``` For more examples, see the [usage examples](examples.md) section. To start using sttp client in your project, see the [quickstart](quickstart.md). Or, browse the documentation to find the topics that interest you the most! ScalaDoc is available at [https://www.javadoc.io](https://www.javadoc.io/doc/com.softwaremill.sttp.client4/core_2.12/4.0.0-M9). @@ -66,7 +67,7 @@ We offer commercial support for sttp and related technologies, as well as develo # Table of contents -```eval_rst +```{eval-rst} .. toctree:: :maxdepth: 2 :caption: Getting started @@ -75,6 +76,11 @@ We offer commercial support for sttp and related technologies, as well as develo how goals community + +.. toctree:: + :maxdepth: 2 + :caption: How-to's + examples .. toctree:: diff --git a/docs/requests/authentication.md b/docs/requests/authentication.md index 6566e74d45..b1868ecf20 100644 --- a/docs/requests/authentication.md +++ b/docs/requests/authentication.md @@ -19,7 +19,7 @@ val token = "zMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F" basicRequest.auth.bearer(token) ``` -### Important Note on the `Authorization` Header and Redirects +## Important Note on the `Authorization` Header and Redirects The `Authorization` header is by default removed during redirects. See [redirects](../conf/redirects.md) for more details. diff --git a/docs/requests/basics.md b/docs/requests/basics.md index 2a5be56a88..fb49216584 100644 --- a/docs/requests/basics.md +++ b/docs/requests/basics.md @@ -42,7 +42,7 @@ val response: Response[Either[String, String]] = request.send(backend) The default backend uses the `Identity` effect to return responses, which is equivalent to a synchronous call (no effect at all). Other asynchronous backends use other effect types. See the section on [backends](../backends/summary.md) for more details. -```eval_rst +```{eval-rst} .. note:: Only requests with the request method and uri can be sent. If trying to send a request without these components specified, a compile-time error will be reported. On how this is implemented, see the documentation on the :doc:`type of request definitions `. diff --git a/docs/requests/body.md b/docs/requests/body.md index 96c9f422c1..e0c907713b 100644 --- a/docs/requests/body.md +++ b/docs/requests/body.md @@ -43,7 +43,7 @@ basicRequest.body(inputStream) If not specified before, these methods will set the content type to `application/octet-stream`. When using a byte array, additionally the content length will be set to the length of the array (unless specified explicitly). -```eval_rst +```{eval-rst} .. note:: While the object defining a request is immutable, setting a mutable request body will make the whole request definition mutable as well. With ``InputStream``, the request can be moreover sent only once, as input streams can be consumed once. diff --git a/docs/requests/streaming.md b/docs/requests/streaming.md index 674d318bfe..749fa940bb 100644 --- a/docs/requests/streaming.md +++ b/docs/requests/streaming.md @@ -2,7 +2,7 @@ Some backends (see [backends summary](../backends/summary.md)) support streaming bodies, as described by the `Streams[S]` capability. If that's the case, you can set a stream of the supported type as a request body using the `streamBody` method, instead of the usual `body` method. -```eval_rst +```{eval-rst} .. note:: Here, streaming refers to (usually) non-blocking, asynchronous streams of data. To send data which is available as an ``InputStream``, or a file from local storage (which is available as a ``File`` or ``Path``), no special backend support is needed. See the documenttation on :doc:`setting the request body `. @@ -27,7 +27,7 @@ basicRequest .streamBody(PekkoStreams)(source) ``` -```eval_rst +```{eval-rst} .. note:: A request with the body set as a stream can only be sent using a backend supporting exactly the given type of streams. ``` diff --git a/docs/requirements.txt b/docs/requirements.txt index 654b597d27..e8548070c2 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,4 @@ -sphinx_rtd_theme==1.0.0 -recommonmark==0.7.1 -sphinx==4.2.0 -sphinx-autobuild==2021.3.14 -myst-parser==0.15.2 \ No newline at end of file +sphinx_rtd_theme==2.0.0 +sphinx==7.3.7 +sphinx-autobuild==2024.4.16 +myst-parser==2.0.0 diff --git a/docs/responses/basics.md b/docs/responses/basics.md index 480d3156ad..5254dab1c4 100644 --- a/docs/responses/basics.md +++ b/docs/responses/basics.md @@ -4,7 +4,7 @@ Responses are represented as instances of the case class `Response[T]`, where `T If sending the request fails, either due to client or connection errors, an exception will be thrown (synchronous backends), or a failed effect will be returned (e.g. a failed future). -```eval_rst +```{eval-rst} .. note:: If the request completes, but results in a non-2xx return code, the request is still considered successful, that is, a ``Response[T]`` will be returned. See :doc:`response body specifications ` for details on how such cases are handled. ``` diff --git a/docs/responses/body.md b/docs/responses/body.md index 7be3b8e05c..88aa6d9f57 100644 --- a/docs/responses/body.md +++ b/docs/responses/body.md @@ -71,7 +71,7 @@ val someFile = new File("some/path") basicRequest.response(asFile(someFile)) ``` -```eval_rst +```{eval-rst} .. note:: As the handling of response is specified upfront, there's no need to "consume" the response body. It can be safely discarded if not needed. @@ -89,7 +89,7 @@ basicRequest.response(asString.orFail): PartialRequest[String] The combinator works in all cases where the response body is specified to be deserialized as an `Either`. If the left is already an exception, it will be thrown unchanged. Otherwise, the left-value will be wrapped in an `HttpError`. -```eval_rst +```{eval-rst} .. note:: While both ``asStringAlways`` and ``asString.orFail`` have the type ``ResponseAs[String, Any]``, they are different. The first will return the response body as a string always, regardless of the responses' status code. The second will return a failed effect / throw a ``HttpError`` exception for non-2xx status codes, and the string as body only for 2xx status codes. @@ -101,7 +101,7 @@ There's also a variant of the combinator, `.getEither`, which can be used to ext It's possible to define custom body deserializers by taking any of the built-in response descriptions and mapping over them. Each `ResponseAs` instance has `map` and `mapWithMetadata` methods, which can be used to transform it to a description for another type (optionally using response metadata, such as headers or the status code). Each such value is immutable and can be used multiple times. -```eval_rst +```{eval-rst} .. note:: Alternatively, response descriptions can be modified directly from the request description, by using the ``request.mapResponse(...)`` and ``request.mapResponseRight(...)`` methods (which is available, if the response body is deserialized to an either). That's equivalent to calling ``request.response(request.response.map(...))``, that is setting a new response description, to a modified old response description; but with shorter syntax. ``` diff --git a/docs/testing.md b/docs/testing.md index d7f35f44fe..496d8238d5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -68,7 +68,7 @@ val response2 = basicRequest.post(uri"http://example.org/partialAda").send(testi // response2.body will be Right("Ada") ``` -```eval_rst +```{eval-rst} .. note:: This approach to testing has one caveat: the responses are not type-safe. That is, the stub backend cannot match on or verify that the type of the response body matches the response body type, as it was requested. However, when a "raw" response is provided (a ``String``, ``Array[Byte]``, ``InputStream``, or a non-blocking stream wrapped in ``RawStream``), it will be handled as specified by the response specification - see below for details. diff --git a/docs/websockets.md b/docs/websockets.md index 5aa955f8c2..2686f4c8ee 100644 --- a/docs/websockets.md +++ b/docs/websockets.md @@ -86,7 +86,7 @@ When working with streams of websocket frames keep in mind that a text payload m sttp provides two useful methods (`fromTextPipe`, `fromTextPipeF`) for each backend to aggregate these fragments back into complete messages. These methods can be found in corresponding WebSockets classes for given effect type: -```eval_rst +```{eval-rst} ================ ========================================== effect type class name ================ ========================================== diff --git a/examples-ce2/src/main/scala/sttp/client4/examples/GetAndParseJsonOrFailMonixCirce.scala b/examples-ce2/src/main/scala/sttp/client4/examples/GetAndParseJsonOrFailMonixCirce.scala index fedbe735e4..675d84b58e 100644 --- a/examples-ce2/src/main/scala/sttp/client4/examples/GetAndParseJsonOrFailMonixCirce.scala +++ b/examples-ce2/src/main/scala/sttp/client4/examples/GetAndParseJsonOrFailMonixCirce.scala @@ -1,3 +1,10 @@ +// {cat=JSON; effects=Monix; backend=HttpClient}: Receive & parse JSON using circe + +//> using scala 2.13 +//> using dep com.softwaremill.sttp.client4::monix:4.0.0-M20 +//> using dep com.softwaremill.sttp.client4::circe:4.0.0-M20 +//> using dep io.circe::circe-generic:0.14.10 + package sttp.client4.examples import io.circe.generic.auto._ diff --git a/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala b/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala index 2444118a25..59a72ecebc 100644 --- a/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala +++ b/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala @@ -1,3 +1,10 @@ +// {cat=Hello, World!; effects=Monix; backend=HttpClient}: Post JSON data + +//> using scala 2.13 +//> using dep com.softwaremill.sttp.client4::monix:4.0.0-M20 +//> using dep com.softwaremill.sttp.client4::circe:4.0.0-M20 +//> using dep io.circe::circe-generic:0.14.10 + package sttp.client4.examples object PostSerializeJsonMonixHttpClientCirce extends App { diff --git a/examples-ce2/src/main/scala/sttp/client4/examples/WebSocketMonix.scala b/examples-ce2/src/main/scala/sttp/client4/examples/WebSocketMonix.scala index bf5ed08057..7ecd3a585a 100644 --- a/examples-ce2/src/main/scala/sttp/client4/examples/WebSocketMonix.scala +++ b/examples-ce2/src/main/scala/sttp/client4/examples/WebSocketMonix.scala @@ -1,3 +1,8 @@ +// {cat=WebSocket; effects=Monix; backend=HttpClient}: Connect to & interact with a WebSocket + +//> using scala 2.13 +//> using dep com.softwaremill.sttp.client4::monix:4.0.0-M20 + package sttp.client4.examples import monix.eval.Task diff --git a/examples/src/main/scala/sttp/client4/examples/GetAndParseJsonZioCirce.scala b/examples/src/main/scala/sttp/client4/examples/GetAndParseJsonZioCirce.scala index 021bb09b96..432fccab60 100644 --- a/examples/src/main/scala/sttp/client4/examples/GetAndParseJsonZioCirce.scala +++ b/examples/src/main/scala/sttp/client4/examples/GetAndParseJsonZioCirce.scala @@ -1,3 +1,9 @@ +// {cat=JSON; effects=ZIO; backend=HttpClient}: Receive & parse JSON using circe + +//> using dep com.softwaremill.sttp.client4::circe:4.0.0-M20 +//> using dep com.softwaremill.sttp.client4::zio:4.0.0-M20 +//> using dep io.circe::circe-generic:0.14.10 + package sttp.client4.examples import io.circe.generic.auto.* diff --git a/examples/src/main/scala/sttp/client4/examples/GetRawResponseBodySynchronous.scala b/examples/src/main/scala/sttp/client4/examples/GetRawResponseBodySynchronous.scala index 3b6cc81a6b..ad7df403b1 100644 --- a/examples/src/main/scala/sttp/client4/examples/GetRawResponseBodySynchronous.scala +++ b/examples/src/main/scala/sttp/client4/examples/GetRawResponseBodySynchronous.scala @@ -1,10 +1,14 @@ +// {cat=Other; effects=Direct; backend=HttpClient}: Handle the body by both parsing it to JSON and returning the raw string + +//> using dep com.softwaremill.sttp.client4::circe:4.0.0-M20 +//> using dep io.circe::circe-generic:0.14.10 + package sttp.client4.examples import io.circe import io.circe.generic.auto.* import sttp.client4.* import sttp.client4.circe.* -import sttp.client4.httpclient.HttpClientSyncBackend @main def getRawResponseBodySynchronous(): Unit = case class HttpBinResponse(origin: String, headers: Map[String, String]) @@ -13,7 +17,7 @@ import sttp.client4.httpclient.HttpClientSyncBackend .get(uri"https://httpbin.org/get") .response(asBoth(asJson[HttpBinResponse], asStringAlways)) - val backend: SyncBackend = HttpClientSyncBackend() + val backend: SyncBackend = DefaultSyncBackend() try val response: Response[(Either[ResponseException[String, circe.Error], HttpBinResponse], String)] = diff --git a/examples/src/main/scala/sttp/client4/examples/LogRequestsSlf4j.scala b/examples/src/main/scala/sttp/client4/examples/LogRequestsSlf4j.scala index a14b55d1f4..eb0e7d66c9 100644 --- a/examples/src/main/scala/sttp/client4/examples/LogRequestsSlf4j.scala +++ b/examples/src/main/scala/sttp/client4/examples/LogRequestsSlf4j.scala @@ -1,9 +1,15 @@ +// {cat=Logging; effects=Direct; backend=HttpClient}: Add a logging backend wrapper, which uses slf4j + +//> using dep com.softwaremill.sttp.client4::circe:4.0.0-M20 +//> using dep com.softwaremill.sttp.client4::slf4j-backend:4.0.0-M20 +//> using dep io.circe::circe-generic:0.14.10 +//> using dep ch.qos.logback:logback-classic:1.5.13 + package sttp.client4.examples import io.circe.generic.auto.* import sttp.client4.* import sttp.client4.circe.* -import sttp.client4.httpclient.HttpClientSyncBackend import sttp.client4.logging.LogConfig import sttp.client4.logging.slf4j.Slf4jLoggingBackend @@ -16,7 +22,7 @@ import sttp.client4.logging.slf4j.Slf4jLoggingBackend val backend: SyncBackend = Slf4jLoggingBackend( - HttpClientSyncBackend(), + DefaultSyncBackend(), LogConfig( includeTiming = true, logRequestBody = false, diff --git a/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala b/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala index 5580a21d0c..d8eee71d5f 100644 --- a/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala +++ b/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala @@ -1,7 +1,10 @@ +// {cat=Hello, World!; effects=Direct; backend=HttpClient}: Post form data + +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M20 + package sttp.client4.examples import sttp.client4.* -import sttp.client4.httpclient.HttpClientSyncBackend @main def postFormSynchronous(): Unit = val signup = Some("yes") @@ -12,7 +15,7 @@ import sttp.client4.httpclient.HttpClientSyncBackend // use an optional parameter in the URI .post(uri"https://httpbin.org/post?signup=$signup") - val backend = HttpClientSyncBackend() + val backend = DefaultSyncBackend() val response = request.send(backend) println(response.body) diff --git a/examples/src/main/scala/sttp/client4/examples/RetryZio.scala b/examples/src/main/scala/sttp/client4/examples/RetryZio.scala index 02701fc6c1..3457f49c9c 100644 --- a/examples/src/main/scala/sttp/client4/examples/RetryZio.scala +++ b/examples/src/main/scala/sttp/client4/examples/RetryZio.scala @@ -1,3 +1,7 @@ +// {cat=Resilience; effects=ZIO; backend=HttpClient}: Retry sending a request + +//> using dep com.softwaremill.sttp.client4::zio:4.0.0-M20 + package sttp.client4.examples import sttp.client4.* diff --git a/examples/src/main/scala/sttp/client4/examples/StreamFs2.scala b/examples/src/main/scala/sttp/client4/examples/StreamFs2.scala index 4f8faf69e6..0af4bb4ea6 100644 --- a/examples/src/main/scala/sttp/client4/examples/StreamFs2.scala +++ b/examples/src/main/scala/sttp/client4/examples/StreamFs2.scala @@ -1,3 +1,7 @@ +// {cat=Streaming; effects=cats-effect; backend=HttpClient}: Stream request & response bodies using fs2 + +//> using dep com.softwaremill.sttp.client4::fs2:4.0.0-M20 + package sttp.client4.examples import cats.effect.ExitCode diff --git a/examples/src/main/scala/sttp/client4/examples/StreamZio.scala b/examples/src/main/scala/sttp/client4/examples/StreamZio.scala index 65ba02da0a..684a72b460 100644 --- a/examples/src/main/scala/sttp/client4/examples/StreamZio.scala +++ b/examples/src/main/scala/sttp/client4/examples/StreamZio.scala @@ -1,3 +1,7 @@ +// {cat=Streaming; effects=ZIO; backend=HttpClient}: Stream request & response bodies using ZIO-Streams + +//> using dep com.softwaremill.sttp.client4::zio:4.0.0-M20 + package sttp.client4.examples import sttp.capabilities.zio.ZioStreams @@ -11,7 +15,7 @@ import zio.stream.* object StreamZio extends ZIOAppDefault: def streamRequestBody: RIO[SttpClient, Unit] = - val stream: Stream[Throwable, Byte] = ZStream("Hello, world".getBytes.toIndexedSeq: _*) + val stream: Stream[Throwable, Byte] = ZStream("Hello, world".getBytes.toIndexedSeq*) send( basicRequest .post(uri"https://httpbin.org/post") diff --git a/examples/src/main/scala/sttp/client4/examples/TestEndpointMultipleQueryParameters.scala b/examples/src/main/scala/sttp/client4/examples/TestEndpointMultipleQueryParameters.scala index 5ee1abfd68..ff83bb1725 100644 --- a/examples/src/main/scala/sttp/client4/examples/TestEndpointMultipleQueryParameters.scala +++ b/examples/src/main/scala/sttp/client4/examples/TestEndpointMultipleQueryParameters.scala @@ -1,3 +1,7 @@ +// {cat=Testing}: Create a backend stub which simulates interactions using multiple query parameters + +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M20 + package sttp.client4.examples import sttp.client4.* diff --git a/examples/src/main/scala/sttp/client4/examples/WebSocketPekko.scala b/examples/src/main/scala/sttp/client4/examples/WebSocketPekko.scala index d71891334a..cf43ef811c 100644 --- a/examples/src/main/scala/sttp/client4/examples/WebSocketPekko.scala +++ b/examples/src/main/scala/sttp/client4/examples/WebSocketPekko.scala @@ -1,3 +1,8 @@ +// {cat=WebSocket; effects=Future; backend=Pekko}: Connect to & interact with a WebSocket + +//> using dep com.softwaremill.sttp.client4::pekko-http-backend:4.0.0-M20 +//> using dep org.apache.pekko::pekko-stream:1.1.2 + package sttp.client4.examples import sttp.client4._ diff --git a/examples/src/main/scala/sttp/client4/examples/WebSocketStreamFs2.scala b/examples/src/main/scala/sttp/client4/examples/WebSocketStreamFs2.scala index c2c8b19d4b..f4e41a66f7 100644 --- a/examples/src/main/scala/sttp/client4/examples/WebSocketStreamFs2.scala +++ b/examples/src/main/scala/sttp/client4/examples/WebSocketStreamFs2.scala @@ -1,3 +1,7 @@ +// {cat=WebSocket; effects=cats-effect; backend=HttpClient}: Connect to & interact with a WebSocket, using fs2 streaming + +//> using dep com.softwaremill.sttp.client4::fs2:4.0.0-M20 + package sttp.client4.examples import cats.effect.ExitCode diff --git a/examples/src/main/scala/sttp/client4/examples/WebSocketSynchronous.scala b/examples/src/main/scala/sttp/client4/examples/WebSocketSynchronous.scala index 96e073a184..af68070d45 100644 --- a/examples/src/main/scala/sttp/client4/examples/WebSocketSynchronous.scala +++ b/examples/src/main/scala/sttp/client4/examples/WebSocketSynchronous.scala @@ -1,3 +1,7 @@ +// {cat=WebSocket; effects=Direct; backend=HttpClient}: Connect to & interact with a WebSocket + +//> using dep com.softwaremill.sttp.client4::core:4.0.0-M20 + package sttp.client4.examples import sttp.client4.* diff --git a/examples-ce2/src/main/scala/sttp/client4/examples/WebSocketTesting.scala b/examples/src/main/scala/sttp/client4/examples/WebSocketTesting.scala similarity index 52% rename from examples-ce2/src/main/scala/sttp/client4/examples/WebSocketTesting.scala rename to examples/src/main/scala/sttp/client4/examples/WebSocketTesting.scala index 1e0dda8a5f..87b19f82d7 100644 --- a/examples-ce2/src/main/scala/sttp/client4/examples/WebSocketTesting.scala +++ b/examples/src/main/scala/sttp/client4/examples/WebSocketTesting.scala @@ -1,25 +1,32 @@ +// {cat=Testing; effects=cats-effect; backend=HttpClient}: Create a backend stub which simulates interactions with a WebSocket + +//> using dep com.softwaremill.sttp.client4::fs2:4.0.0-M20 + package sttp.client4.examples -import monix.eval.Task -import sttp.capabilities.monix.MonixStreams -import sttp.client4._ -import sttp.client4.ws.async._ -import sttp.client4.httpclient.monix.HttpClientMonixBackend +import sttp.client4.* +import sttp.client4.ws.async.* import sttp.client4.testing.WebSocketStreamBackendStub import sttp.model.StatusCode import sttp.ws.{WebSocket, WebSocketFrame} import sttp.ws.testing.WebSocketStub +import cats.effect.IOApp +import cats.effect.IO +import sttp.capabilities.fs2.Fs2Streams +import sttp.client4.httpclient.fs2.HttpClientFs2Backend +import cats.effect.ExitCode + +object WebSocketTesting extends IOApp: -object WebSocketTesting extends App { // the web socket-handling logic - def useWebSocket(ws: WebSocket[Task]): Task[Unit] = { + def useWebSocket(ws: WebSocket[IO]): IO[Unit] = { def send(i: Int) = ws.sendText(s"Hello $i!") - val receive = ws.receiveText().flatMap(t => Task(println(s"RECEIVED [$t]"))) + val receive = ws.receiveText().flatMap(t => IO(println(s"RECEIVED [$t]"))) send(1) *> send(2) *> receive *> receive } // the request description - def openWebSocket(backend: WebSocketBackend[Task]): Task[Unit] = + def openWebSocket(backend: WebSocketBackend[IO]): IO[Unit] = basicRequest .get(uri"wss://echo.websocket.org") .response(asWebSocket(useWebSocket)) @@ -27,8 +34,9 @@ object WebSocketTesting extends App { .void // the backend stub which we'll use instead of a "real" backend - val stubBackend: WebSocketStreamBackendStub[Task, MonixStreams] = - HttpClientMonixBackend.stub + val stubBackend: WebSocketStreamBackendStub[IO, Fs2Streams[IO]] = + HttpClientFs2Backend + .stub[IO] .whenRequestMatches(_.uri.toString().contains("echo.websocket.org")) .thenRespond( WebSocketStub.noInitialReceive.thenRespond { @@ -40,6 +48,5 @@ object WebSocketTesting extends App { ) // running the test - import monix.execution.Scheduler.Implicits.global - openWebSocket(stubBackend).runSyncUnsafe() -} + override def run(args: List[String]): IO[ExitCode] = + openWebSocket(stubBackend).map(_ => ExitCode.Success) diff --git a/examples/src/main/scala/sttp/client4/examples/WebSocketZio.scala b/examples/src/main/scala/sttp/client4/examples/WebSocketZio.scala index 7f73036cd2..d21ddf6ec4 100644 --- a/examples/src/main/scala/sttp/client4/examples/WebSocketZio.scala +++ b/examples/src/main/scala/sttp/client4/examples/WebSocketZio.scala @@ -1,3 +1,7 @@ +// {cat=WebSocket; effects=ZIO; backend=HttpClient}: Connect to & interact with a WebSocket + +//> using dep com.softwaremill.sttp.client4::zio:4.0.0-M20 + package sttp.client4.examples import sttp.client4.* diff --git a/examples/src/main/scala/sttp/client4/examples/getAndParseJsonPekkoHttpJson4s.scala b/examples/src/main/scala/sttp/client4/examples/getAndParseJsonPekkoHttpJson4s.scala index 3e6e116746..7633c43a61 100644 --- a/examples/src/main/scala/sttp/client4/examples/getAndParseJsonPekkoHttpJson4s.scala +++ b/examples/src/main/scala/sttp/client4/examples/getAndParseJsonPekkoHttpJson4s.scala @@ -1,3 +1,10 @@ +// {cat=JSON; effects=Future; backend=Pekko}: Receive & parse JSON using json4s + +//> using dep com.softwaremill.sttp.client4::json4s:4.0.0-M20 +//> using dep com.softwaremill.sttp.client4::pekko-http-backend:4.0.0-M20 +//> using dep org.json4s::json4s-native:4.0.7 +//> using dep org.apache.pekko::pekko-stream:1.1.2 + package sttp.client4.examples import org.json4s.Formats @@ -16,7 +23,7 @@ import scala.concurrent.Future given Formats = org.json4s.DefaultFormats val request = basicRequest - .get(uri"https://httpbin.org/get ") + .get(uri"https://httpbin.org/get") .response(asJson[HttpBinResponse]) val backend: Backend[Future] = PekkoHttpBackend() diff --git a/examples/src/main/scala/sttp/client4/examples/wsOxExample.scala b/examples/src/main/scala/sttp/client4/examples/wsOxExample.scala index 120c690b38..f89563555b 100644 --- a/examples/src/main/scala/sttp/client4/examples/wsOxExample.scala +++ b/examples/src/main/scala/sttp/client4/examples/wsOxExample.scala @@ -1,3 +1,7 @@ +// {cat=WebSocket; effects=Direct; backend=HttpClient}: Connect to & interact with a WebSocket, using Ox channels for streaming + +//> using dep com.softwaremill.sttp.client4::ox:4.0.0-M20 + package sttp.client4.examples import ox.* @@ -13,7 +17,7 @@ import sttp.ws.WebSocketFrame supervised: val inputs = Source.fromValues(1, 2, 3).map(i => WebSocketFrame.text(s"Frame no $i")) val (wsSource, wsSink) = asSourceAndSink(ws) - forkDiscard: + fork: inputs.pipeTo(wsSink, propagateDone = true) wsSource.foreach: frame => println(s"RECEIVED: $frame") diff --git a/examples/src/main/scala/sttp/client4/examples/x.scala b/examples/src/main/scala/sttp/client4/examples/x.scala new file mode 100644 index 0000000000..42d8bc22db --- /dev/null +++ b/examples/src/main/scala/sttp/client4/examples/x.scala @@ -0,0 +1,20 @@ +//> 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" + + // 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) + + // 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) diff --git a/generated-docs/out/.python-version b/generated-docs/out/.python-version index 0b2eb36f50..e4fba21835 100644 --- a/generated-docs/out/.python-version +++ b/generated-docs/out/.python-version @@ -1 +1 @@ -3.7.2 +3.12 diff --git a/generated-docs/out/Makefile b/generated-docs/out/Makefile index 7f0a64e11d..3eda9474b0 100644 --- a/generated-docs/out/Makefile +++ b/generated-docs/out/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx -SPHINXPROJ = sttp +SPHINXPROJ = tapir SOURCEDIR = . BUILDDIR = _build @@ -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) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/generated-docs/out/_static/css/custom.css b/generated-docs/out/_static/css/custom.css new file mode 100644 index 0000000000..c35ad22aa0 --- /dev/null +++ b/generated-docs/out/_static/css/custom.css @@ -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); +} \ No newline at end of file diff --git a/generated-docs/out/backends/akka.md b/generated-docs/out/backends/akka.md index fbb4d3e841..05ef6466e7 100644 --- a/generated-docs/out/backends/akka.md +++ b/generated-docs/out/backends/akka.md @@ -66,7 +66,7 @@ val response: Future[Response[Either[String, Source[ByteString, Any]]]] = basicRequest .post(uri"...") .response(asStreamUnsafe(AkkaStreams)) - .send(backend) + .send(backend) ``` The akka-http backend support both regular and streaming [websockets](../websockets.md). @@ -101,7 +101,7 @@ Non-standard behavior: Received data streams can be parsed to a stream of server-sent events (SSE): -```scala +```scala import scala.concurrent.Future import akka.stream.scaladsl.Source diff --git a/generated-docs/out/backends/catseffect.md b/generated-docs/out/backends/catseffect.md index 6c9d40eb08..3b9c83be19 100644 --- a/generated-docs/out/backends/catseffect.md +++ b/generated-docs/out/backends/catseffect.md @@ -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. ``` diff --git a/generated-docs/out/backends/fs2.md b/generated-docs/out/backends/fs2.md index 9f272c1eeb..e4b78b5c9f 100644 --- a/generated-docs/out/backends/fs2.md +++ b/generated-docs/out/backends/fs2.md @@ -72,7 +72,6 @@ Host header override is supported in environments running Java 12 onwards, but i -Djdk.httpclient.allowRestrictedHeaders=host ``` - ## Using Armeria To use, add the following dependency to your project: @@ -103,7 +102,7 @@ or, if you'd like to instantiate the [WebClient](https://armeria.dev/docs/client import cats.effect.IO import cats.effect.std.Dispatcher import com.linecorp.armeria.client.WebClient -import com.linecorp.armeria.client.circuitbreaker._ +import com.linecorp.armeria.client.circuitbreaker.* import sttp.client4.armeria.fs2.ArmeriaFs2Backend val dispatcher: Dispatcher[IO] = ??? @@ -118,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. ``` @@ -136,7 +135,7 @@ Requests can be sent with a streaming body like this: import cats.effect.IO import fs2.Stream import sttp.capabilities.fs2.Fs2Streams -import sttp.client4._ +import sttp.client4.* import sttp.client4.httpclient.fs2.HttpClientFs2Backend val effect = HttpClientFs2Backend.resource[IO]().use { backend => @@ -156,7 +155,7 @@ Responses can also be streamed: import cats.effect.IO import fs2.Stream import sttp.capabilities.fs2.Fs2Streams -import sttp.client4._ +import sttp.client4.* import sttp.client4.httpclient.fs2.HttpClientFs2Backend import scala.concurrent.duration.Duration @@ -182,9 +181,9 @@ The fs2 backends support both regular and streaming [websockets](../websockets.m Received data streams can be parsed to a stream of server-sent events (SSE): ```scala -import cats.effect._ +import cats.effect.* import fs2.Stream -import sttp.client4._ +import sttp.client4.* import sttp.capabilities.fs2.Fs2Streams import sttp.client4.impl.fs2.Fs2ServerSentEvents import sttp.model.sse.ServerSentEvent diff --git a/generated-docs/out/backends/future.md b/generated-docs/out/backends/future.md index b65ecd3be9..86c7c44dfc 100644 --- a/generated-docs/out/backends/future.md +++ b/generated-docs/out/backends/future.md @@ -2,17 +2,18 @@ There are several backend implementations which are `scala.concurrent.Future`-based. These backends are **asynchronous**, sending a request is a non-blocking operation and results in a response wrapped in a `Future`. -Apart from the ones described below, also the [Akka](akka.md) backend is `Future`-based. +Apart from the ones described below, also the [Pekko](pekko.md) & [Akka](akka.md) backends are `Future`-based. -```eval_rst -===================================== ================================================ ========================== -Class Supported stream type Websocket support -===================================== ================================================ ========================== -``HttpClientFutureBackend`` n/a yes (regular) -``AkkaHttpBackend`` ``akka.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) -``OkHttpFutureBackend`` n/a yes (regular) -``ArmeriaFutureBackend`` n/a n/a -===================================== ================================================ ========================== +```{eval-rst} +===================================== ================================================= ========================== +Class Supported stream type Websocket support +===================================== ================================================= ========================== +``HttpClientFutureBackend`` n/a yes (regular) +``PekkoHttpBackend`` ``pekko.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) +``AkkaHttpBackend`` ``akka.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) +``OkHttpFutureBackend`` n/a yes (regular) +``ArmeriaFutureBackend`` n/a n/a +===================================== ================================================= ========================== ``` ## Using HttpClient @@ -53,7 +54,6 @@ Host header override is supported in environments running Java 12 onwards, but i -Djdk.httpclient.allowRestrictedHeaders=host ``` - ## Using OkHttp To use, add the following dependency to your project: @@ -112,7 +112,7 @@ ArmeriaFutureBackend.usingDefaultClient() or, if you'd like to instantiate the [WebClient](https://armeria.dev/docs/client-http) yourself:: ```scala -import com.linecorp.armeria.client.circuitbreaker._ +import com.linecorp.armeria.client.circuitbreaker.* import com.linecorp.armeria.client.WebClient // Fluently build Armeria WebClient with built-in decorators @@ -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. ``` diff --git a/generated-docs/out/backends/http4s.md b/generated-docs/out/backends/http4s.md index df05f9b676..da8c7c4889 100644 --- a/generated-docs/out/backends/http4s.md +++ b/generated-docs/out/backends/http4s.md @@ -11,10 +11,10 @@ This backend is based on [http4s](https://http4s.org) (client) and is **asynchro The backend can be created in a couple of ways, e.g.: ```scala -import cats.effect._ +import cats.effect.* import sttp.capabilities.fs2.Fs2Streams -import sttp.client4._ -import sttp.client4.http4s._ +import sttp.client4.* +import sttp.client4.http4s.* // the "org.http4s" %% "http4s-ember-client" % http4sVersion dependency needs to be explicitly added Http4sBackend.usingDefaultEmberClientBuilder[IO](): Resource[IO, StreamBackend[IO, Fs2Streams[IO]]] diff --git a/generated-docs/out/backends/javascript/fetch.md b/generated-docs/out/backends/javascript/fetch.md index 1faf56f902..da2ad1f521 100644 --- a/generated-docs/out/backends/javascript/fetch.md +++ b/generated-docs/out/backends/javascript/fetch.md @@ -135,8 +135,8 @@ To use, add the following dependency to your project: An example of streaming a response: ```scala -import sttp.client4._ -import sttp.client4.impl.monix._ +import sttp.client4.* +import sttp.client4.impl.monix.* import java.nio.ByteBuffer import monix.eval.Task @@ -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. ``` @@ -170,7 +170,7 @@ import monix.eval.Task import sttp.capabilities.monix.MonixStreams import sttp.client4.impl.monix.MonixServerSentEvents import sttp.model.sse.ServerSentEvent -import sttp.client4._ +import sttp.client4.* def processEvents(source: Observable[ServerSentEvent]): Task[Unit] = ??? diff --git a/generated-docs/out/backends/monix.md b/generated-docs/out/backends/monix.md index 080fedaf78..ef7efa3600 100644 --- a/generated-docs/out/backends/monix.md +++ b/generated-docs/out/backends/monix.md @@ -44,7 +44,6 @@ Host header override is supported in environments running Java 12 onwards, but i -Djdk.httpclient.allowRestrictedHeaders=host ``` - ## Using OkHttp To use, add the following dependency to your project: @@ -98,7 +97,7 @@ ArmeriaMonixBackend.usingDefaultClient() or, if you'd like to instantiate the [WebClient](https://armeria.dev/docs/client-http) yourself: ```scala -import com.linecorp.armeria.client.circuitbreaker._ +import com.linecorp.armeria.client.circuitbreaker.* import com.linecorp.armeria.client.WebClient // Fluently build Armeria WebClient with built-in decorators @@ -111,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. ``` @@ -125,7 +124,7 @@ The Monix backends support streaming. The streams capability is represented as ` ```scala import sttp.capabilities.monix.MonixStreams -import sttp.client4._ +import sttp.client4.* import sttp.client4.httpclient.monix.HttpClientMonixBackend import monix.reactive.Observable @@ -144,7 +143,7 @@ And receive responses as an observable stream: ```scala import sttp.capabilities.monix.MonixStreams -import sttp.client4._ +import sttp.client4.* import sttp.client4.httpclient.monix.HttpClientMonixBackend import monix.eval.Task @@ -177,7 +176,7 @@ import monix.eval.Task import sttp.capabilities.monix.MonixStreams import sttp.client4.impl.monix.MonixServerSentEvents import sttp.model.sse.ServerSentEvent -import sttp.client4._ +import sttp.client4.* def processEvents(source: Observable[ServerSentEvent]): Task[Unit] = ??? diff --git a/generated-docs/out/backends/pekko.md b/generated-docs/out/backends/pekko.md index f75c8e0a8f..4441387d89 100644 --- a/generated-docs/out/backends/pekko.md +++ b/generated-docs/out/backends/pekko.md @@ -17,14 +17,14 @@ Note that you'll also need an explicit dependency on pekko-streams, as pekko-htt Next you'll need to add create the backend instance: ```scala -import sttp.client4.pekkohttp._ +import sttp.client4.pekkohttp.* val backend = PekkoHttpBackend() ``` or, if you'd like to use an existing actor system: ```scala -import sttp.client4.pekkohttp._ +import sttp.client4.pekkohttp.* import org.apache.pekko.actor.ActorSystem val actorSystem: ActorSystem = ??? @@ -37,7 +37,7 @@ To set the request body as a stream: ```scala import sttp.capabilities.pekko.PekkoStreams -import sttp.client4._ +import sttp.client4.* import org.apache.pekko import pekko.stream.scaladsl.Source @@ -55,7 +55,7 @@ To receive the response body as a stream: ```scala import scala.concurrent.Future import sttp.capabilities.pekko.PekkoStreams -import sttp.client4._ +import sttp.client4.* import sttp.client4.pekkohttp.PekkoHttpBackend import org.apache.pekko @@ -82,7 +82,7 @@ That way, you can "mock" a server that the backend will talk to, without startin If your application provides a client library for its dependants to use, this is a great way to ensure that the client actually matches the routes exposed by your application: ```scala -import sttp.client4.pekkohttp._ +import sttp.client4.pekkohttp.* import org.apache.pekko import pekko.http.scaladsl.server.Route import pekko.actor.ActorSystem diff --git a/generated-docs/out/backends/scalaz.md b/generated-docs/out/backends/scalaz.md index 4167765d84..3962d7fefc 100644 --- a/generated-docs/out/backends/scalaz.md +++ b/generated-docs/out/backends/scalaz.md @@ -13,13 +13,13 @@ To use, add the following dependency to your project: add imports: -```scala +```scala import sttp.client4.armeria.scalaz.ArmeriaScalazBackend ``` create client: -```scala +```scala val backend = ArmeriaScalazBackend() // You can use the default client which reuses the connection pool of ClientFactory.ofDefault() @@ -28,7 +28,7 @@ ArmeriaScalazBackend.usingDefaultClient() or, if you'd like to instantiate the [WebClient](https://armeria.dev/docs/client-http) yourself: -```scala +```scala import com.linecorp.armeria.client.circuitbreaker._ import com.linecorp.armeria.client.WebClient @@ -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. ``` diff --git a/generated-docs/out/backends/summary.md b/generated-docs/out/backends/summary.md index 247e59c977..93d1fcdba5 100644 --- a/generated-docs/out/backends/summary.md +++ b/generated-docs/out/backends/summary.md @@ -26,39 +26,33 @@ Which one to choose? Below is a summary of all the JVM backends; see the sections on individual backend implementations for more information: -```eval_rst -==================================== ================================ ================================================= ========================== =================== -Class Effect type Supported stream type Supports websockets Fully non-blocking -==================================== ================================ ================================================= ========================== =================== -``DefaultSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no -``HttpClientSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no -``DefaultFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no -``HttpClientFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no -``HttpClientMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[ByteBuffer]`` yes (regular & streaming) yes -``HttpClientFs2Backend`` ``F[_]: cats.effect.Concurrent`` ``fs2.Stream[F, Byte]`` yes (regular & streaming) yes -``HttpClientZioBackend`` ``zio.Task`` ``zio.stream.Stream[Throwable, Byte]`` yes (regular & streaming) yes -``HttpURLConnectionBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) no no -``TryHttpURLConnectionBackend`` ``scala.util.Try`` ``java.io.InputStream`` (blocking) no no -``AkkaHttpBackend`` ``scala.concurrent.Future`` ``akka.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) yes -``PekkoHttpBackend`` ``scala.concurrent.Future`` ``org.apache.pekko.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) yes -``ArmeriaFutureBackend`` ``scala.concurrent.Future`` n/a no yes -``ArmeriaScalazBackend`` ``scalaz.concurrent.Task`` n/a no yes -``ArmeriaZioBackend`` ``zio.Task`` ``zio.stream.Stream[Throwable, Byte]`` no yes -``ArmeriaMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[HttpData]`` no yes -``ArmeriaCatsBackend`` ``F[_]: cats.effect.Concurrent`` n/a no yes -``ArmeriaFs2Backend`` ``F[_]: cats.effect.Concurrent`` ``fs2.Stream[F, Byte]`` no yes -``OkHttpSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no -``OkHttpFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no -``OkHttpMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[ByteBuffer]`` yes (regular & streaming) no -``Http4sBackend`` ``F[_]: cats.effect.Effect`` ``fs2.Stream[F, Byte]`` no no -``FinagleBackend`` ``com.twitter.util.Future`` n/a no no -``AsyncHttpClientFutureBackend`` ``scala.concurrent.Future`` n/a yes (regular) no -``AsyncHttpClientScalazBackend`` ``scalaz.concurrent.Task`` n/a yes (regular) no -``AsyncHttpClientZioBackend`` ``zio.Task`` ``zio.stream.Stream[Throwable, Byte]`` yes (regular & streaming) no -``AsyncHttpClientMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[ByteBuffer]`` yes (regular & streaming) no -``AsyncHttpClientCatsBackend`` ``F[_]: cats.effect.Concurrent`` n/a no no -``AsyncHttpClientFs2Backend`` ``F[_]: cats.effect.Concurrent`` ``fs2.Stream[F, Byte]`` yes (regular & streaming) no -==================================== ================================ ================================================= ========================== =================== +```{eval-rst} +==================================== ================================ ============================================================ ========================== =================== +Class Effect type Supported stream type Supports websockets Fully non-blocking +==================================== ================================ ============================================================ ========================== =================== +``DefaultSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no +``HttpClientSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no +``DefaultFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no +``HttpClientFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no +``HttpClientMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[ByteBuffer]`` yes (regular & streaming) yes +``HttpClientFs2Backend`` ``F[_]: cats.effect.Concurrent`` ``fs2.Stream[F, Byte]`` yes (regular & streaming) yes +``HttpClientZioBackend`` ``zio.Task`` ``zio.stream.Stream[Throwable, Byte]`` yes (regular & streaming) yes +``HttpURLConnectionBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) no no +``TryHttpURLConnectionBackend`` ``scala.util.Try`` ``java.io.InputStream`` (blocking) no no +``AkkaHttpBackend`` ``scala.concurrent.Future`` ``akka.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) yes +``PekkoHttpBackend`` ``scala.concurrent.Future`` ``org.apache.pekko.stream.scaladsl.Source[ByteString, Any]`` yes (regular & streaming) yes +``ArmeriaFutureBackend`` ``scala.concurrent.Future`` n/a no yes +``ArmeriaScalazBackend`` ``scalaz.concurrent.Task`` n/a no yes +``ArmeriaZioBackend`` ``zio.Task`` ``zio.stream.Stream[Throwable, Byte]`` no yes +``ArmeriaMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[HttpData]`` no yes +``ArmeriaCatsBackend`` ``F[_]: cats.effect.Concurrent`` n/a no yes +``ArmeriaFs2Backend`` ``F[_]: cats.effect.Concurrent`` ``fs2.Stream[F, Byte]`` no yes +``OkHttpSyncBackend`` None (``Identity``) ``java.io.InputStream`` (blocking) yes (regular) no +``OkHttpFutureBackend`` ``scala.concurrent.Future`` ``java.io.InputStream`` (blocking) yes (regular) no +``OkHttpMonixBackend`` ``monix.eval.Task`` ``monix.reactive.Observable[ByteBuffer]`` yes (regular & streaming) no +``Http4sBackend`` ``F[_]: cats.effect.Effect`` ``fs2.Stream[F, Byte]`` no no +``FinagleBackend`` ``com.twitter.util.Future`` n/a no no +==================================== ================================ ============================================================ ========================== =================== ``` The backends work with Scala 2.12, 2.13 and 3. @@ -81,7 +75,7 @@ There are also backends which wrap other backends to provide additional function In addition, there are also backends for Scala.JS: -```eval_rst +```{eval-rst} ================================ ================================ ========================================= =================== Class Effect type Supported stream type Supports websockets ================================ ================================ ========================================= =================== @@ -95,7 +89,7 @@ Class Effect type Supported stre And a backend for scala-native: -```eval_rst +```{eval-rst} ================================ ============================ ========================================= =================== Class Effect type Supported stream type Supports websockets ================================ ============================ ========================================= =================== diff --git a/generated-docs/out/backends/synchronous.md b/generated-docs/out/backends/synchronous.md index d1d6dc7b80..6919912dee 100644 --- a/generated-docs/out/backends/synchronous.md +++ b/generated-docs/out/backends/synchronous.md @@ -77,7 +77,7 @@ or, if you'd like to instantiate the OkHttpClient yourself: ```scala import sttp.client4.okhttp.OkHttpSyncBackend -import okhttp3._ +import okhttp3.* val okHttpClient: OkHttpClient = ??? val backend = OkHttpSyncBackend.usingClient(okHttpClient) diff --git a/generated-docs/out/backends/wrappers/custom.md b/generated-docs/out/backends/wrappers/custom.md index 1e77f2d675..6addf5364f 100644 --- a/generated-docs/out/backends/wrappers/custom.md +++ b/generated-docs/out/backends/wrappers/custom.md @@ -44,21 +44,19 @@ metrics for completed requests and wraps any `Future`-based backend: ```scala import sttp.attributes.AttributeKey import sttp.capabilities.Effect -import sttp.client4._ -import sttp.client4.akkahttp._ +import sttp.client4.* +import sttp.client4.pekkohttp.* import sttp.client4.wrappers.DelegateBackend import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global -import scala.util._ +import scala.util.* // the metrics infrastructure -trait MetricsServer { +trait MetricsServer: def reportDuration(name: String, duration: Long): Unit -} -class CloudMetricsServer extends MetricsServer { +class CloudMetricsServer extends MetricsServer: override def reportDuration(name: String, duration: Long): Unit = ??? -} case class MetricPrefix(prefix: String) val MetricPrefixAttributeKey = AttributeKey[MetricPrefix] @@ -66,35 +64,30 @@ val MetricPrefixAttributeKey = AttributeKey[MetricPrefix] // the backend wrapper abstract class MetricWrapper[P](delegate: GenericBackend[Future, P], metrics: MetricsServer) - extends DelegateBackend(delegate) { + extends DelegateBackend(delegate): - override def send[T](request: GenericRequest[T, P with Effect[Future]]): Future[Response[T]] = { + override def send[T](request: GenericRequest[T, P with Effect[Future]]): Future[Response[T]] = val start = System.currentTimeMillis() - def report(metricSuffix: String): Unit = { + def report(metricSuffix: String): Unit = val metricPrefix = request.attribute(MetricPrefixAttributeKey).getOrElse(MetricPrefix("?")) val end = System.currentTimeMillis() - metrics.reportDuration(metricPrefix + "-" + metricSuffix, end - start) - } + metrics.reportDuration(metricPrefix.prefix + "-" + metricSuffix, end - start) - delegate.send(request).andThen { + delegate.send(request).andThen: case Success(response) if response.is200 => report("ok") case Success(response) => report("notok") case Failure(t) => report("exception") - } - } -} -object MetricWrapper { +object MetricWrapper: def apply[S]( backend: WebSocketStreamBackend[Future, S], metrics: MetricsServer ): WebSocketStreamBackend[Future, S] = new MetricWrapper(backend, metrics) with WebSocketStreamBackend[Future, S] {} -} // example usage -val backend = MetricWrapper(AkkaHttpBackend(), new CloudMetricsServer()) +val backend = MetricWrapper(PekkoHttpBackend(), new CloudMetricsServer()) basicRequest .get(uri"http://company.com/api/service1") @@ -116,41 +109,33 @@ In some cases it's possible to implement a generic retry mechanism; such a mecha ```scala import sttp.capabilities.Effect -import sttp.client4._ +import sttp.client4.* import sttp.client4.wrappers.DelegateBackend class RetryingBackend[F[_], P]( delegate: GenericBackend[F, P], shouldRetry: RetryWhen, maxRetries: Int) - extends DelegateBackend(delegate) { - - override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = { + extends DelegateBackend(delegate): + override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = sendWithRetryCounter(request, 0) - } private def sendWithRetryCounter[T]( - request: GenericRequest[T, P with Effect[F]], retries: Int): F[Response[T]] = { + request: GenericRequest[T, P with Effect[F]], retries: Int): F[Response[T]] = - val r = monad.handleError(delegate.send(request)) { + val r = monad.handleError(delegate.send(request)): case t if shouldRetry(request, Left(t)) && retries < maxRetries => sendWithRetryCounter(request, retries + 1) - } - monad.flatMap(r) { resp => - if (shouldRetry(request, Right(resp)) && retries < maxRetries) { + monad.flatMap(r): resp => + if shouldRetry(request, Right(resp)) && retries < maxRetries then sendWithRetryCounter(request, retries + 1) - } else { + else monad.unit(resp) - } - } - } -} -object RetryingBackend { +object RetryingBackend: def apply[F[_]](backend: WebSocketBackend[F], shouldRetry: RetryWhen, maxRetries: Int): WebSocketBackend[F] = new RetryingBackend(backend, shouldRetry, maxRetries) with WebSocketBackend[F] {} -} ``` ## Example backend with circuit breaker @@ -171,14 +156,12 @@ import java.util.concurrent.TimeUnit class CircuitSttpBackend[F[_], P]( circuitBreaker: CircuitBreaker, - delegate: GenericBackend[F, P]) extends DelegateBackend(delegate) { + delegate: GenericBackend[F, P]) extends DelegateBackend(delegate): - override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = { + override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = CircuitSttpBackend.decorateF(circuitBreaker, delegate.send(request)) - } -} -object CircuitSttpBackend { +object CircuitSttpBackend: def apply[F[_]](circuitBreaker: CircuitBreaker, backend: Backend[F]): Backend[F] = new CircuitSttpBackend(circuitBreaker, backend) with Backend[F] {} @@ -186,31 +169,26 @@ object CircuitSttpBackend { def decorateF[F[_], T]( circuitBreaker: CircuitBreaker, service: => F[T] - )(implicit monadError: MonadError[F]): F[T] = { - monadError.suspend { - if (!circuitBreaker.tryAcquirePermission()) { + )(implicit monadError: MonadError[F]): F[T] = + monadError.suspend: + if !circuitBreaker.tryAcquirePermission() then monadError.error(CallNotPermittedException .createCallNotPermittedException(circuitBreaker)) - } else { + else val start = System.nanoTime() - try { - monadError.handleError(monadError.map(service) { r => + try + monadError.handleError(monadError.map(service): r => circuitBreaker.onSuccess(System.nanoTime() - start, TimeUnit.NANOSECONDS) r - }) { + ) { case t => circuitBreaker.onError(System.nanoTime() - start, TimeUnit.NANOSECONDS, t) monadError.error(t) } - } catch { + catch case t: Throwable => circuitBreaker.onError(System.nanoTime() - start, TimeUnit.NANOSECONDS, t) monadError.error(t) - } - } - } - } -} ``` ## Example backend with rate limiter @@ -229,14 +207,12 @@ import sttp.client4.wrappers.DelegateBackend class RateLimitingSttpBackend[F[_], P]( rateLimiter: RateLimiter, delegate: GenericBackend[F, P] - )(implicit monadError: MonadError[F]) extends DelegateBackend(delegate) { + )(implicit monadError: MonadError[F]) extends DelegateBackend(delegate): - override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = { + override def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = RateLimitingSttpBackend.decorateF(rateLimiter, delegate.send(request)) - } -} -object RateLimitingSttpBackend { +object RateLimitingSttpBackend: def apply[F[_], S]( rateLimiter: RateLimiter, backend: StreamBackend[F, S] @@ -246,18 +222,14 @@ object RateLimitingSttpBackend { def decorateF[F[_], T]( rateLimiter: RateLimiter, service: => F[T] - )(implicit monadError: MonadError[F]): F[T] = { - monadError.suspend { - try { + )(implicit monadError: MonadError[F]): F[T] = + monadError.suspend: + try RateLimiter.waitForPermission(rateLimiter) service - } catch { + catch case t: Throwable => monadError.error(t) - } - } - } -} ``` ## Example new backend @@ -271,15 +243,14 @@ Implementing a new backend is made easy as the tests are published in the `core` Implement your backend and extend the `HttpTest` class: ```scala -import sttp.client4._ +import sttp.client4.* import sttp.client4.testing.{ConvertToFuture, HttpTest} import scala.concurrent.Future -class MyCustomBackendHttpTest extends HttpTest[Future] { +class MyCustomBackendHttpTest extends HttpTest[Future]: override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future - override lazy val backend: Backend[Future] = ??? //new MyCustomBackend() + override val backend: Backend[Future] = ??? //new MyCustomBackend() override def timeoutToNone[T](t: Future[T], timeoutMillis: Int): Future[Option[T]] = ??? -} ``` ## Custom backend wrapper using cats diff --git a/generated-docs/out/backends/wrappers/logging.md b/generated-docs/out/backends/wrappers/logging.md index 80890923c6..b8b3748fab 100644 --- a/generated-docs/out/backends/wrappers/logging.md +++ b/generated-docs/out/backends/wrappers/logging.md @@ -36,7 +36,7 @@ There are three backend wrappers available, which log request & response informa Example usage: ```scala -import sttp.client4._ +import sttp.client4.* import sttp.client4.logging.slf4j.Slf4jLoggingBackend val backend = Slf4jLoggingBackend(DefaultSyncBackend()) diff --git a/generated-docs/out/backends/wrappers/opentelemetry.md b/generated-docs/out/backends/wrappers/opentelemetry.md index 9e1560fa6c..7f2b5c0561 100644 --- a/generated-docs/out/backends/wrappers/opentelemetry.md +++ b/generated-docs/out/backends/wrappers/opentelemetry.md @@ -6,7 +6,7 @@ Currently, the following OpenTelemetry features are supported: - tracing using `OpenTelemetryTracingZioBackend`, wrapping any ZIO2 backend - tracing using [trace4cats](https://github.com/trace4cats/trace4cats), wrapping a cats-effect backend -### Metrics +## Metrics The backend depends only on [opentelemetry-api](https://github.com/open-telemetry/opentelemetry-java). To use add the following dependency to your project: @@ -19,8 +19,8 @@ Then an instance can be obtained as follows: ```scala import scala.concurrent.Future -import sttp.client4._ -import sttp.client4.opentelemetry._ +import sttp.client4.* +import sttp.client4.opentelemetry.* import io.opentelemetry.api.OpenTelemetry // any effect and capabilities are supported @@ -34,8 +34,8 @@ All counters have provided default names, but the names can be customized by set ```scala import scala.concurrent.Future -import sttp.client4._ -import sttp.client4.opentelemetry._ +import sttp.client4.* +import sttp.client4.opentelemetry.* import io.opentelemetry.api.OpenTelemetry val sttpBackend: Backend[Future] = ??? @@ -50,7 +50,7 @@ OpenTelemetryMetricsBackend( ) ``` -### Tracing (ZIO) +## Tracing (ZIO) To use, add the following dependency to your project: @@ -66,10 +66,10 @@ In order to do that, you need to provide the wrapper with a `Tracing` from zio-t Here's how you construct `ZioTelemetryOpenTelemetryBackend`. I would recommend wrapping this is in `ZLayer` ```scala -import sttp.client4._ -import zio._ -import zio.telemetry.opentelemetry.tracing._ -import sttp.client4.opentelemetry.zio._ +import sttp.client4.* +import zio.* +import zio.telemetry.opentelemetry.tracing.* +import sttp.client4.opentelemetry.zio.* val zioBackend: Backend[Task] = ??? val tracing: Tracing = ??? @@ -81,6 +81,6 @@ By default, the span is named after the HTTP method (e.g "HTTP POST") as [recomm and the http method, url and response status codes are set as span attributes. You can override these defaults by supplying a custom `OpenTelemetryZioTracer`. -### Tracing (cats-effect) +## Tracing (cats-effect) The [trace4cats](https://github.com/trace4cats/trace4cats) project includes sttp-client integration. diff --git a/generated-docs/out/backends/wrappers/prometheus.md b/generated-docs/out/backends/wrappers/prometheus.md index 1f4ea53b98..939fa07d76 100644 --- a/generated-docs/out/backends/wrappers/prometheus.md +++ b/generated-docs/out/backends/wrappers/prometheus.md @@ -9,7 +9,7 @@ To use, add the following dependency to your project: and some imports: ```scala -import sttp.client4.prometheus._ +import sttp.client4.prometheus.* ``` This backend depends on [Prometheus JVM Client](https://github.com/prometheus/client_java). Keep in mind this backend registers histograms and gathers request times, but you have to expose those metrics to [Prometheus](https://prometheus.io/). @@ -17,16 +17,16 @@ This backend depends on [Prometheus JVM Client](https://github.com/prometheus/cl The Prometheus backend wraps any other backend, for example: ```scala -import sttp.client4.akkahttp._ -val backend = PrometheusBackend(AkkaHttpBackend()) +import sttp.client4.pekkohttp.* +val backend = PrometheusBackend(PekkoHttpBackend()) ``` It gathers request execution times in `Histogram`. It uses by default `http_client_request_duration_seconds` name, defined in `PrometheusBackend.DefaultHistogramName`. It is possible to define custom histograms name by passing function mapping request to histogram name: ```scala -import sttp.client4.akkahttp._ +import sttp.client4.pekkohttp.* val backend = PrometheusBackend( - AkkaHttpBackend(), + PekkoHttpBackend(), PrometheusConfig( requestToHistogramNameMapper = request => Some(HistogramCollectorConfig(request.uri.host.getOrElse("example.com"))) ) @@ -36,16 +36,16 @@ val backend = PrometheusBackend( You can disable request histograms by passing `None` returning function: ```scala -import sttp.client4.akkahttp._ -val backend = PrometheusBackend(AkkaHttpBackend(), PrometheusConfig(requestToHistogramNameMapper = _ => None)) +import sttp.client4.pekkohttp.* +val backend = PrometheusBackend(PekkoHttpBackend(), PrometheusConfig(requestToHistogramNameMapper = _ => None)) ``` This backend also offers `Gauge` with currently in-progress requests number. It uses by default `http_client_requests_active` name, defined in `PrometheusBackend.DefaultRequestsActiveCounterName`. It is possible to define custom gauge name by passing function mapping request to gauge name: ```scala -import sttp.client4.akkahttp._ +import sttp.client4.pekkohttp.* val backend = PrometheusBackend( - AkkaHttpBackend(), + PekkoHttpBackend(), PrometheusConfig( requestToInProgressGaugeNameMapper = request => Some(CollectorConfig(request.uri.host.getOrElse("example.com"))) ) @@ -55,6 +55,6 @@ val backend = PrometheusBackend( You can disable request in-progress gauges by passing `None` returning function: ```scala -import sttp.client4.akkahttp._ -val backend = PrometheusBackend(AkkaHttpBackend(), PrometheusConfig(requestToInProgressGaugeNameMapper = _ => None)) +import sttp.client4.pekkohttp.* +val backend = PrometheusBackend(PekkoHttpBackend(), PrometheusConfig(requestToInProgressGaugeNameMapper = _ => None)) ``` diff --git a/generated-docs/out/backends/zio.md b/generated-docs/out/backends/zio.md index 0d0e3a323f..bbfea81408 100644 --- a/generated-docs/out/backends/zio.md +++ b/generated-docs/out/backends/zio.md @@ -67,14 +67,14 @@ ArmeriaZioBackend.scoped().flatMap { backend => ??? } ArmeriaZioBackend.usingDefaultClient().flatMap { backend => ??? } ``` -```eval_rst +```{eval-rst} .. note:: The default client factory is reused to create `ArmeriaZioBackend` if a `SttpBackendOptions` is unspecified. So you only need to manage a resource when `SttpBackendOptions` is used. ``` or, if you'd like to instantiate the [WebClient](https://armeria.dev/docs/client-http) yourself: ```scala -import com.linecorp.armeria.client.circuitbreaker._ +import com.linecorp.armeria.client.circuitbreaker.* import com.linecorp.armeria.client.WebClient // Fluently build Armeria WebClient with built-in decorators @@ -87,7 +87,7 @@ val client = WebClient.builder("https://my-service.com") ArmeriaZioBackend.usingClient(client).flatMap { backend => ??? } ``` -```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. ``` @@ -107,9 +107,9 @@ When using constructors to express service dependencies, ZIO layers can be used The layers can be used to provide an implementation of the `SttpBackend` dependency when creating services. For example: ```scala -import sttp.client4._ -import sttp.client4.httpclient.zio._ -import zio._ +import sttp.client4.* +import sttp.client4.httpclient.zio.* +import zio.* class MyService(sttpBackend: Backend[Task]) { def runLogic(): Task[Response[String]] = { @@ -143,9 +143,9 @@ The lifecycle of the `SttpClient` service is described by `ZLayer`s, which can b The `SttpClient` companion object contains effect descriptions which use the `SttpClient` service from the environment to send requests or open websockets. This is different from sttp usage with other effect libraries (which require invoking `.send(backend)` on the request), but is more in line with one of the styles of using ZIO. For example: ```scala mdoc:compile-only - import sttp.client4._ - import sttp.client4.httpclient.zio._ - import zio._ + import sttp.client4.* + import sttp.client4.httpclient.zio.* + import zio.* val request = basicRequest.get(uri"https://httpbin.org/get") val sent: ZIO[SttpClient, Throwable, Response[Either[String, String]]] = @@ -162,8 +162,8 @@ Requests can be sent with a streaming body: ```scala import sttp.capabilities.zio.ZioStreams -import sttp.client4._ -import zio.stream._ +import sttp.client4.* +import zio.stream.* import zio.Task val sttpBackend: StreamBackend[Task, ZioStreams] = ??? @@ -180,10 +180,10 @@ And receive response bodies as a stream: ```scala import sttp.capabilities.zio.ZioStreams -import sttp.client4._ +import sttp.client4.* -import zio._ -import zio.stream._ +import zio.* +import zio.stream.* import scala.concurrent.duration.Duration @@ -214,13 +214,13 @@ A layer with the stub `SttpBackend` can be then created by simply calling `ZLaye Received data streams can be parsed to a stream of server-sent events (SSE): ```scala -import zio._ -import zio.stream._ +import zio.* +import zio.stream.* import sttp.capabilities.zio.ZioStreams import sttp.client4.impl.zio.ZioServerSentEvents import sttp.model.sse.ServerSentEvent -import sttp.client4._ +import sttp.client4.* def processEvents(source: Stream[Throwable, ServerSentEvent]): Task[Unit] = ??? diff --git a/generated-docs/out/conf.py b/generated-docs/out/conf.py index f0ba51073f..d59d989412 100644 --- a/generated-docs/out/conf.py +++ b/generated-docs/out/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# sttp documentation build configuration file, created by +# sttp client documentation build configuration file, created by # sphinx-quickstart on Thu Oct 12 15:51:09 2017. # # This file is execfile()d with the current directory set to its @@ -43,6 +43,8 @@ # ones. extensions = ['myst_parser', 'sphinx_rtd_theme'] +myst_enable_extensions = ['attrs_block'] + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -50,9 +52,6 @@ # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -from recommonmark.parser import CommonMarkParser -from recommonmark.transform import AutoStructify - source_suffix = { '.rst': 'restructuredtext', '.txt': 'markdown', @@ -64,7 +63,7 @@ # General information about the project. project = u'sttp' -copyright = u'2021, SoftwareMill' +copyright = u'2024, SoftwareMill' author = u'SoftwareMill' # The version info for the project you're documenting, acts as replacement for @@ -72,16 +71,16 @@ # built documents. # # The short X.Y version. -version = u'3' +version = u'4' # The full version, including alpha/beta/rc tags. -release = u'3' +release = u'4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -89,7 +88,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'default' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -113,6 +112,12 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +# These paths are either relative to html_static_path +# or fully qualified paths (eg. https://...) +html_css_files = [ + 'css/custom.css', +] + # Custom sidebar templates, must be a dictionary that maps document names # to template names. # @@ -159,7 +164,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'sttp.tex', u'sttp Documentation', + (master_doc, 'sttp.tex', u'sttp client Documentation', u'Adam Warski', 'manual'), ] @@ -169,7 +174,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'sttp', u'sttp Documentation', + (master_doc, 'sttp', u'sttp client Documentation', [author], 1) ] @@ -180,7 +185,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'sttp', u'sttp documentation', + (master_doc, 'sttp', u'sttp client documentation', author, 'sttp', 'The Scala HTTP client you always wanted!', 'Scala'), ] @@ -195,12 +200,3 @@ 'github_version': 'master', # Version 'conf_py_path': '/docs/', # Path in the checkout to the docs root } - -# app setup hook -def setup(app): - app.add_config_value('recommonmark_config', { - 'auto_toc_tree_section': 'Contents', - 'enable_auto_doc_ref': False - }, True) - app.add_transform(AutoStructify) - diff --git a/generated-docs/out/conf/proxy.md b/generated-docs/out/conf/proxy.md index 961c2d5a72..f083128228 100644 --- a/generated-docs/out/conf/proxy.md +++ b/generated-docs/out/conf/proxy.md @@ -13,7 +13,7 @@ Settings are loaded **in given order** and the **first existing value** is being Otherwise, proxy values can be specified manually when creating a backend: ```scala -import sttp.client4._ +import sttp.client4.* val backend = DefaultSyncBackend( options = BackendOptions.httpProxy("some.host", 8080)) @@ -26,7 +26,7 @@ basicRequest Or in case your proxy requires authentication (supported by the JVM backends): ```scala -import sttp.client4._ +import sttp.client4.* BackendOptions.httpProxy("some.host", 8080, "username", "password") ``` diff --git a/generated-docs/out/conf/redirects.md b/generated-docs/out/conf/redirects.md index 334ea01c08..dd8fb7cd85 100644 --- a/generated-docs/out/conf/redirects.md +++ b/generated-docs/out/conf/redirects.md @@ -5,7 +5,7 @@ By default, sttp follows redirects. If you'd like to disable following redirects, use the `followRedirects` method: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.followRedirects(false) ``` @@ -19,7 +19,7 @@ If a `POST` or `PUT` request is redirected, by default it will be sent unchanged To enable this behavior, use the `redirectToGet` method: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.redirectToGet(true) ``` @@ -33,7 +33,7 @@ Most modern http clients will, by default, strip the `Authorization` header when You can disable the stripping of all sensitive headers using the following code: ```scala -import sttp.client4._ +import sttp.client4.* import sttp.client4.wrappers.{FollowRedirectsBackend, FollowRedirectsConfig} val myBackend: SyncBackend = DefaultSyncBackend() @@ -48,8 +48,8 @@ val backend: SyncBackend = FollowRedirectsBackend( If you just want to disable stripping of the `Authorization` header, you can do the following: ```scala -import sttp.client4._ -import sttp.model._ +import sttp.client4.* +import sttp.model.* import sttp.client4.wrappers.{FollowRedirectsBackend, FollowRedirectsConfig} val myBackend: SyncBackend = DefaultSyncBackend() @@ -71,26 +71,23 @@ For example: ```scala import sttp.capabilities.Effect -import sttp.client4._ +import sttp.client4.* import sttp.client4.wrappers.FollowRedirectsBackend import sttp.monad.MonadError abstract class MyWrapper[F[_], P] private (delegate: GenericBackend[F, P]) - extends GenericBackend[F, P] { + extends GenericBackend[F, P]: def send[T](request: GenericRequest[T, P with Effect[F]]): F[Response[T]] = ??? def close(): F[Unit] = ??? def monad: MonadError[F] = ??? -} -object MyWrapper { - def apply[F[_]](delegate: Backend[F]): Backend[F] = { +object MyWrapper: + def apply[F[_]](delegate: Backend[F]): Backend[F] = // disables any other FollowRedirectsBackend-s further down the delegate chain FollowRedirectsBackend(new MyWrapper(delegate) with Backend[F] {}) - } -} ``` ### Custom URI encoding @@ -100,7 +97,7 @@ Whenever a redirect request is about to be created, the `FollowRedirectsBackend` For example: ```scala -import sttp.client4._ +import sttp.client4.* import sttp.client4.wrappers.{FollowRedirectsBackend, FollowRedirectsConfig} import sttp.model.Uri.QuerySegmentEncoding diff --git a/generated-docs/out/conf/ssl.md b/generated-docs/out/conf/ssl.md index fd801a026f..a5fcc1c5f7 100644 --- a/generated-docs/out/conf/ssl.md +++ b/generated-docs/out/conf/ssl.md @@ -19,13 +19,12 @@ Sample code might look like this: import java.io.FileInputStream import java.security.{KeyStore, SecureRandom} import java.security.cert.X509Certificate -import javax.net.ssl._ +import javax.net.ssl.* -val TrustAllCerts: X509TrustManager = new X509TrustManager() { +val TrustAllCerts: X509TrustManager = new X509TrustManager(): def getAcceptedIssuers: Array[X509Certificate] = Array[X509Certificate]() override def checkServerTrusted(x509Certificates: Array[X509Certificate], s: String): Unit = () override def checkClientTrusted(x509Certificates: Array[X509Certificate], s: String): Unit = () -} val ks: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType) ks.load(new FileInputStream("/path/to/your_cert.p12"), "password".toCharArray) @@ -57,43 +56,62 @@ val ssl: SSLContext = SSLContext.getInstance("TLS") ssl.init(kmf.getKeyManagers, tmf.getTrustManagers, new SecureRandom) ``` +## Using HttpClient + +Backends using `HttpClient` provides factory methods accepting `HttpClient`. +In this example we are using `IO` and `HttpClientFs2Backend`. + +Using `SSLContext` from [first section](#ssl-context): + +```scala +import cats.effect.IO +import cats.effect.kernel.Resource +import cats.effect.std.Dispatcher +import java.net.http.HttpClient +import sttp.capabilities.fs2.Fs2Streams +import sttp.client4.WebSocketStreamBackend +import sttp.client4.httpclient.fs2.HttpClientFs2Backend + +val httpClient: HttpClient = HttpClient.newBuilder().sslContext(ssl).build() +val backend: Resource[IO, WebSocketStreamBackend[IO, Fs2Streams[IO]]] = HttpClientFs2Backend.resourceUsingClient[IO](httpClient) +``` + ## Using HttpUrlConnection Using `SSLContext` from [first section](#ssl-context) define a function to customize connection. ```scala -import sttp.client4._ +import sttp.client4.* import sttp.client4.httpurlconnection.HttpURLConnectionBackend import java.net.HttpURLConnection import javax.net.ssl.HttpsURLConnection def useSSL(conn: HttpURLConnection): Unit = - conn match { + conn match case https: HttpsURLConnection => https.setSSLSocketFactory(ssl.getSocketFactory) case _ => () - } val backend = HttpURLConnectionBackend(customizeConnection = useSSL) ``` It is also possible to set default `SSLContext` using `SSLContext.setDefault(ssl)`. -## Using Akka-http +## Using Pekko-http Using `SSLContext` from [first section](#ssl-context) create a `HttpsConnectionContext`. ```scala -import akka.actor.ActorSystem -import akka.http.scaladsl.{ConnectionContext, HttpsConnectionContext} -import sttp.client4.akkahttp._ +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.{ConnectionContext, HttpsConnectionContext} +import sttp.client4.pekkohttp.* val actorSystem: ActorSystem = ActorSystem() val https: HttpsConnectionContext = ConnectionContext.httpsClient(ssl) -val backend = AkkaHttpBackend.usingActorSystem(actorSystem, customHttpsContext = Some(https)) +val backend = PekkoHttpBackend.usingActorSystem(actorSystem, customHttpsContext = Some(https)) ``` -For more information refer to [akka docs](https://doc.akka.io/docs/akka-http/current/client-side/client-https-support.html). +For more information refer to [pekko docs](https://pekko.apache.org/docs/pekko-http/current/client-side/client-https-support.html). ## Using OkHttp @@ -116,46 +134,4 @@ val client: OkHttpClient = new OkHttpClient.Builder() val backend = OkHttpFutureBackend.usingClient(client) ``` -For more information refer to [okhttp docs](https://square.github.io/okhttp/https/). - -## Using HttpClient - -Backends using `HttpClient` provides factory methods accepting `HttpClient`. -In this example we are using `IO` and `HttpClientFs2Backend`. - -Using `SSLContext` from [first section](#ssl-context): - -```scala -import cats.effect.IO -import cats.effect.kernel.Resource -import cats.effect.std.Dispatcher -import java.net.http.HttpClient -import sttp.capabilities.fs2.Fs2Streams -import sttp.client4.WebSocketStreamBackend -import sttp.client4.httpclient.fs2.HttpClientFs2Backend - -val httpClient: HttpClient = HttpClient.newBuilder().sslContext(ssl).build() -val backend: Resource[IO, WebSocketStreamBackend[IO, Fs2Streams[IO]]] = HttpClientFs2Backend.resourceUsingClient[IO](httpClient) -``` - -## Using Async-http-client - -Using `kmf: KeyManagerFactory` and `tmf: TrustManagerFactory` from [first section](#ssl-context) create a `AsyncHttpClientConfig`. - -Backends using `AsyncHttpClient` provides factory methods accepting custom config. - - -```scala -import io.netty.handler.ssl.SslContextBuilder -import org.asynchttpclient.{AsyncHttpClientConfig, DefaultAsyncHttpClientConfig} -import sttp.client4.asynchttpclient.future._ - -val sslContext = SslContextBuilder.forClient() - .keyManager(kmf) - .trustManager(tmf) - .build() - -val config: AsyncHttpClientConfig = new DefaultAsyncHttpClientConfig.Builder().setSslContext(sslContext).build() - -val backend = AsyncHttpClientFutureBackend.usingConfig(config) -``` +For more information refer to [okhttp docs](https://square.github.io/okhttp/https/). \ No newline at end of file diff --git a/generated-docs/out/conf/timeouts.md b/generated-docs/out/conf/timeouts.md index a654ded994..3e6224f55a 100644 --- a/generated-docs/out/conf/timeouts.md +++ b/generated-docs/out/conf/timeouts.md @@ -8,8 +8,8 @@ sttp supports read and connection timeouts: How to use: ```scala -import sttp.client4._ -import scala.concurrent.duration._ +import sttp.client4.* +import scala.concurrent.duration.* // all backends provide a constructor that allows to specify backend options val backend = DefaultSyncBackend( diff --git a/generated-docs/out/examples.md b/generated-docs/out/examples.md index 8a0c6b632d..bede5439bc 100644 --- a/generated-docs/out/examples.md +++ b/generated-docs/out/examples.md @@ -1,292 +1,17 @@ -# Usage examples +# Examples by category -All of the examples are available [in the sources](https://github.com/softwaremill/sttp/blob/master/examples/src/main/scala/sttp/client4/examples) in runnable form. +The sttp client repository contains a number of how-to guides. If you're missing an example for your use-case, please let us +know by [reporting an issue](https://github.com/softwaremill/sttp)! -## Use the simple synchronous client +Each example is fully self-contained and can be run using [scala-cli](https://scala-cli.virtuslab.org) (you just need +to copy the content of the file, apart from scala-cli, no additional setup is required!). Hopefully this will make +experimenting with sttp client as frictionless as possible! -Required dependencies: +Examples are tagged with the stack being used (Direct-style, cats-effect, ZIO, Future) and backend implementation -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "core" % "4.0.0-M20") +```{eval-rst} +.. include:: includes/examples_list.md + :parser: markdown ``` -Example code: -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/SimpleClientGetAndPost.scala - :language: scala -``` - -## POST a form using the synchronous backend - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "core" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala - :language: scala -``` - -## GET and parse JSON using the akka-http backend and json4s - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "akka-http-backend" % "4.0.0-M20", - "com.softwaremill.sttp.client4" %% "json4s" % "4.0.0-M20", - "org.json4s" %% "json4s-native" % "3.6.0" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/GetAndParseJsonAkkaHttpJson4s.scala - :language: scala -``` - -## GET and parse JSON using the ZIO http-client backend and circe - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "zio" % "4.0.0-M20", - "com.softwaremill.sttp.client4" %% "circe" % "4.0.0-M20", - "io.circe" %% "circe-generic" % "0.14.10" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/GetAndParseJsonZioCirce.scala - :language: scala -``` - -## GET and parse JSON using the http-client Monix backend and circe, treating deserialization errors as failed effects - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "monix" % "4.0.0-M20", - "com.softwaremill.sttp.client4" %% "circe" % "4.0.0-M20", - "io.circe" %% "circe-generic" % "0.14.10" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples-ce2/src/main/scala/sttp/client4/examples/GetAndParseJsonGetRightMonixCirce.scala - :language: scala -``` - -## Log requests & responses using slf4j - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "slf4j-backend" % "4.0.0-M20", - "com.softwaremill.sttp.client4" %% "circe" % "4.0.0-M20", - "io.circe" %% "circe-generic" % "0.14.10" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/LogRequestsSlf4j.scala - :language: scala -``` - -## POST and serialize JSON using the Monix http-client backend and circe - -Required dependencies: - -```scala -libraryDependencies ++= List( - "com.softwaremill.sttp.client4" %% "monix" % "4.0.0-M20", - "com.softwaremill.sttp.client4" %% "circe" % "4.0.0-M20", - "io.circe" %% "circe-generic" % "0.14.10" -) -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala - :language: scala -``` - -## Test an endpoint which requires multiple query parameters - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "core" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/TestEndpointMultipleQueryParameters.scala - :language: scala -``` -## Open a websocket using ZIO - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "zio" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/WebSocketZio.scala - :language: scala -``` - -## Open a websocket using FS2 streams - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "fs2" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/WebSocketStreamFs2.scala - :language: scala -``` - -## Test Monix websockets - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "monix" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples-ce2/src/main/scala/sttp/client4/examples/WebSocketTesting.scala - :language: scala -``` - -## Open a websocket using Akka - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "akka-http-backend" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/WebSocketAkka.scala - :language: scala -``` - -## Open a websocket using Pekko - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "pekko-http-backend" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/WebSocketPekko.scala - :language: scala -``` - -## Open a websocket using Monix - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "monix" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples-ce2/src/main/scala/sttp/client4/examples/WebSocketMonix.scala - :language: scala -``` - -## Stream request and response bodies using fs2 - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "fs2" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/StreamFs2.scala - :language: scala -``` - -## Stream request and response bodies using zio-stream - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "zio" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/StreamZio.scala - :language: scala -``` - -## Retry a request using ZIO - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "zio" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/RetryZio.scala - :language: scala -``` - -## GET parsed and raw response bodies - -Required dependencies: - -```scala -libraryDependencies ++= List("com.softwaremill.sttp.client4" %% "core" % "4.0.0-M20") -``` - -Example code: - -```eval_rst -.. literalinclude:: ../../examples/src/main/scala/sttp/client4/examples/GetRawResponseBodySynchronous.scala - :language: scala -``` \ No newline at end of file diff --git a/generated-docs/out/how.md b/generated-docs/out/how.md index 1814672bfc..47c33af485 100644 --- a/generated-docs/out/how.md +++ b/generated-docs/out/how.md @@ -11,7 +11,7 @@ A `RequestT` value contains both information on what to include in the request, To start describing a request, import the sttp client package and customise `basicRequest`: ```scala -import sttp.client4._ +import sttp.client4.* val myRequest: Request[_] = ??? // basicRequest.(...) ``` @@ -30,7 +30,7 @@ Backends manage the connection pool, thread pools for handling responses, depend For example, the following sends a synchronous request, using the default JVM backend: ```scala -import sttp.client4._ +import sttp.client4.* val myRequest: Request[String] = ??? val backend = DefaultSyncBackend() val response = myRequest.send(backend) diff --git a/generated-docs/out/includes/examples_list.md b/generated-docs/out/includes/examples_list.md new file mode 100644 index 0000000000..6d7213f9cf --- /dev/null +++ b/generated-docs/out/includes/examples_list.md @@ -0,0 +1,41 @@ +## Hello, World! + +* [POST JSON data](https://github.com/softwaremill/sttp/tree/master/examples-ce2/src/main/scala/sttp/client4/examples/PostSerializeJsonMonixHttpClientCirce.scala) HttpClient Monix +* [Post form data](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/PostFormSynchronous.scala) HttpClient Direct + +## JSON + +* [Receive & parse JSON using circe](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/GetAndParseJsonZioCirce.scala) HttpClient ZIO +* [Receive & parse JSON using circe](https://github.com/softwaremill/sttp/tree/master/examples-ce2/src/main/scala/sttp/client4/examples/GetAndParseJsonOrFailMonixCirce.scala) HttpClient Monix +* [Receive & parse JSON using json4s](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/getAndParseJsonPekkoHttpJson4s.scala) Pekko Future + +## Logging + +* [Add a logging backend wrapper, which uses slf4j](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/LogRequestsSlf4j.scala) HttpClient Direct + +## Other + +* [Handle the body by both parsing it to JSON and returning the raw string](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/GetRawResponseBodySynchronous.scala) HttpClient Direct + +## Resilience + +* [Retry sending a request](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/RetryZio.scala) HttpClient ZIO + +## Streaming + +* [Stream request & response bodies using ZIO-Streams](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/StreamZio.scala) HttpClient ZIO +* [Stream request & response bodies using fs2](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/StreamFs2.scala) HttpClient cats-effect + +## Testing + +* [Create a backend stub which simulates interactions using multiple query parameters](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/testEndpointMultipleQueryParameters.scala) +* [Create a backend stub which simulates interactions with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/WebSocketTesting.scala) HttpClient cats-effect + +## WebSocket + +* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/webSocketPekko.scala) Pekko Future +* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/WebSocketZio.scala) HttpClient ZIO +* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/WebSocketSynchronous.scala) HttpClient Direct +* [Connect to & interact with a WebSocket](https://github.com/softwaremill/sttp/tree/master/examples-ce2/src/main/scala/sttp/client4/examples/WebSocketMonix.scala) HttpClient Monix +* [Connect to & interact with a WebSocket, using Ox channels for streaming](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/wsOxExample.scala) HttpClient Direct +* [Connect to & interact with a WebSocket, using fs2 streaming](https://github.com/softwaremill/sttp/tree/master/examples/src/main/scala/sttp/client4/examples/WebSocketStreamFs2.scala) HttpClient cats-effect \ No newline at end of file diff --git a/generated-docs/out/index.md b/generated-docs/out/index.md index 2d8118476f..eb5b1eb5f1 100644 --- a/generated-docs/out/index.md +++ b/generated-docs/out/index.md @@ -12,7 +12,7 @@ Backend implementations include the HTTP client that is shipped with Java, as we Here's a quick example of sttp client in action: ```scala -import sttp.client4._ +import sttp.client4.* val query = "http language:scala" val sort: Option[String] = None @@ -66,7 +66,7 @@ We offer commercial support for sttp and related technologies, as well as develo # Table of contents -```eval_rst +```{eval-rst} .. toctree:: :maxdepth: 2 :caption: Getting started @@ -75,6 +75,11 @@ We offer commercial support for sttp and related technologies, as well as develo how goals community + +.. toctree:: + :maxdepth: 2 + :caption: How-to's + examples .. toctree:: diff --git a/generated-docs/out/json.md b/generated-docs/out/json.md index f91ce9fe81..3c05bdd7ff 100644 --- a/generated-docs/out/json.md +++ b/generated-docs/out/json.md @@ -16,7 +16,7 @@ The following variants of `asJson` methods are available: The type signatures vary depending on the underlying library (required implicits and error representation differs), but they obey the following pattern: ```scala -import sttp.client4._ +import sttp.client4.* // request bodies def asJson[B](b: B): StringBody = ??? @@ -52,8 +52,8 @@ Automatic and semi-automatic derivation of encoders is possible by using the [ci Response can be parsed into json using `asJson[T]`, provided there's an implicit `io.circe.Decoder[T]` in scope. The decoding result will be represented as either a http/deserialization error, or the parsed value. For example: ```scala -import sttp.client4._ -import sttp.client4.circe._ +import sttp.client4.* +import sttp.client4.circe.* val backend: SyncBackend = DefaultSyncBackend() @@ -86,15 +86,17 @@ Using this module it is possible to set request bodies and read response bodies Usage example: ```scala -import sttp.client4._ -import sttp.client4.json4s._ +import org.json4s.Formats +import org.json4s.Serialization +import sttp.client4.* +import sttp.client4.json4s.* val backend: SyncBackend = DefaultSyncBackend() val requestPayload = RequestPayload("some data") -implicit val serialization = org.json4s.native.Serialization -implicit val formats = org.json4s.DefaultFormats +given Serialization = org.json4s.native.Serialization +given Formats = org.json4s.DefaultFormats val response: Response[Either[ResponseException[String, Exception], ResponsePayload]] = basicRequest @@ -117,9 +119,9 @@ Using this module it is possible to set request bodies and read response bodies Usage example: ```scala -import sttp.client4._ -import sttp.client4.sprayJson._ -import spray.json._ +import sttp.client4.* +import sttp.client4.sprayJson.* +import spray.json.* val backend: SyncBackend = DefaultSyncBackend() @@ -173,9 +175,9 @@ To use, add an import: `import sttp.client4.ziojson._` (or extend `SttpZioJsonAp Usage example: ```scala -import sttp.client4._ -import sttp.client4.ziojson._ -import zio.json._ +import sttp.client4.* +import sttp.client4.ziojson.* +import zio.json.* val backend: SyncBackend = DefaultSyncBackend() @@ -212,10 +214,10 @@ However, an `implicit def` has been made for `Option` and is shipped in the `Stt Usage example: ```scala -import sttp.client4._ -import sttp.client4.jsoniter._ -import com.github.plokhotnyuk.jsoniter_scala.core._ -import com.github.plokhotnyuk.jsoniter_scala.macros._ +import sttp.client4.* +import sttp.client4.jsoniter.* +import com.github.plokhotnyuk.jsoniter_scala.core.* +import com.github.plokhotnyuk.jsoniter_scala.macros.* val backend: SyncBackend = DefaultSyncBackend() @@ -250,9 +252,9 @@ To use, add an import: `import sttp.client4.upicklejson.default._` and define an Usage example: ```scala -import sttp.client4._ -import sttp.client4.upicklejson.default._ -import upickle.default._ +import sttp.client4.* +import sttp.client4.upicklejson.default.* +import upickle.default.* val backend: SyncBackend = DefaultSyncBackend() @@ -276,12 +278,12 @@ That's needed as the `upickle.Api` contains the `read`/`write` methods to serial For example, if you want to use the `legacy` upickle configuration, the integration might look as follows: ```scala -import upickle.legacy._ // get access to ReadWriter type, macroRW derivation, etc. +import upickle.legacy.* // get access to ReadWriter type, macroRW derivation, etc. object legacyUpickle extends sttp.client4.upicklejson.SttpUpickleApi { override val upickleApi: upickle.legacy.type = upickle.legacy } -import legacyUpickle._ +import legacyUpickle.* // use upickle as in the above examples ``` \ No newline at end of file diff --git a/generated-docs/out/model/model.md b/generated-docs/out/model/model.md index a08a973d1a..a7505addb5 100644 --- a/generated-docs/out/model/model.md +++ b/generated-docs/out/model/model.md @@ -24,14 +24,13 @@ Example with objects: import sttp.client4._ import sttp.model._ -object Example { +object Example: val request = basicRequest.header(Header.contentType(MediaType.ApplicationJson)) .get(uri"https://httpbin.org") val backend = DefaultSyncBackend() val response = request.send(backend) - if (response.code == StatusCode.Ok) println("Ok!") -} + if response.code == StatusCode.Ok then println("Ok!") ``` Example with traits: @@ -40,14 +39,13 @@ Example with traits: import sttp.client4._ import sttp.model._ -object Example extends HeaderNames with MediaTypes with StatusCodes { +object Example extends HeaderNames with MediaTypes with StatusCodes: val request = basicRequest.header(ContentType, ApplicationJson.toString) .get(uri"https://httpbin.org") val backend = DefaultSyncBackend() val response = request.send(backend) - if (response.code == Ok) println("Ok!") -} + if response.code == Ok then println("Ok!") ``` For more information see diff --git a/generated-docs/out/model/uri.md b/generated-docs/out/model/uri.md index 62b75f91f4..739c1e2ce1 100644 --- a/generated-docs/out/model/uri.md +++ b/generated-docs/out/model/uri.md @@ -30,7 +30,7 @@ Any values embedded in the URI will be URL-encoded, taking into account the cont All components of the URI can be embedded from values: scheme, username/password, host, port, path, query and fragment. The embedded values won't be further parsed, except the `:` in the host part, which is commonly used to pass in both the host and port: ```scala -import sttp.client4._ +import sttp.client4.* // the embedded / is escaped println(uri"http://example.org/${"a/b"}") @@ -117,7 +117,7 @@ When sending requests using relative URIs, the [`ResolveRelativeUrisBackend`](.. A fully-featured example: ```scala -import sttp.client4._ +import sttp.client4.* val secure = true val scheme = if (secure) "https" else "http" val subdomains = List("sub1", "sub2") diff --git a/generated-docs/out/quickstart.md b/generated-docs/out/quickstart.md index cbb04ea916..f0a5782179 100644 --- a/generated-docs/out/quickstart.md +++ b/generated-docs/out/quickstart.md @@ -49,7 +49,7 @@ This brings into scope the starting point for defining requests and some helper And that's all you need to start using sttp client! To create and send your first request, import the above, type `basicRequest.` and see where your IDE's auto-complete gets you! Here's a simple request, using the synchronous backend: ```scala -import sttp.client4._ +import sttp.client4.* val backend = DefaultSyncBackend() val response = basicRequest @@ -78,9 +78,9 @@ dependency: Your code might then look as follows: ```scala -import sttp.client4._ -import sttp.client4.upicklejson.default._ -import upickle.default._ +import sttp.client4.* +import sttp.client4.upicklejson.default.* +import upickle.default.* val backend = DefaultSyncBackend() @@ -115,7 +115,7 @@ use slf4j, you'll need the following dependency: Then, you'll need to configure your client: ```scala -import sttp.client4._ +import sttp.client4.* import sttp.client4.logging.slf4j.Slf4jLoggingBackend val backend = Slf4jLoggingBackend(DefaultSyncBackend()) @@ -130,7 +130,7 @@ This backend instance is global (created on first access), can't be customised a The `send()` extension method allows sending requests using that `backend` instance: ```scala -import sttp.client4.quick._ +import sttp.client4.quick.* quickRequest.get(uri"http://httpbin.org/ip").send() ``` diff --git a/generated-docs/out/requests/authentication.md b/generated-docs/out/requests/authentication.md index 11a5e88466..785e2f763b 100644 --- a/generated-docs/out/requests/authentication.md +++ b/generated-docs/out/requests/authentication.md @@ -5,7 +5,7 @@ sttp supports basic, bearer-token based authentication and digest authentication Basic authentication, using which the username and password are encoded using Base64, can be added as follows: ```scala -import sttp.client4._ +import sttp.client4.* val username = "mary" val password = "p@assword" @@ -19,7 +19,7 @@ val token = "zMDjRfl76ZC9Ub0wnz4XsNiRVBChTYbJcE3F" basicRequest.auth.bearer(token) ``` -### Important Note on the `Authorization` Header and Redirects +## Important Note on the `Authorization` Header and Redirects The `Authorization` header is by default removed during redirects. See [redirects](../conf/redirects.md) for more details. @@ -65,10 +65,11 @@ You can use sttp with OAuth2. Looking at the [OAuth2 protocol flow](https://tool 1. (A)/(B) - Your UI needs to enable the user to authenticate. Your application will then receive a callback from the authentication server, which will include an authentication code. 2. (C)/(D) - You need to send a request to the authentication server, passing in the authentication code from step 1. You'll receive an access token in response (and optionally a refresh token). For example, if you were using GitHub as your authentication server, you'd need to take the values of `clientId` and `clientSecret` from the GitHub settings, then take the `authCode` received in step 1 above, and send a request like this: + ```scala -import sttp.client4.circe._ -import io.circe._ -import io.circe.generic.semiauto._ +import sttp.client4.circe.* +import io.circe.* +import io.circe.generic.semiauto.* val authCode = "SplxlOBeZQQYbYS6WxSbIA" val clientId = "myClient123" diff --git a/generated-docs/out/requests/basics.md b/generated-docs/out/requests/basics.md index 8d733e10a5..4020b8ce82 100644 --- a/generated-docs/out/requests/basics.md +++ b/generated-docs/out/requests/basics.md @@ -3,7 +3,7 @@ As mentioned in the [quickstart](../quickstart.md), the following import will be needed: ```scala -import sttp.client4._ +import sttp.client4.* ``` This brings into scope `basicRequest`, the starting request. This request can be customised, each time yielding a new, immutable request definition (unless a mutable body is set on the request, such as a byte array). As the request definition is immutable, it can be freely stored in values, shared across threads, and customized multiple times in various ways. @@ -42,7 +42,7 @@ val response: Response[Either[String, String]] = request.send(backend) The default backend uses the `Identity` effect to return responses, which is equivalent to a synchronous call (no effect at all). Other asynchronous backends use other effect types. See the section on [backends](../backends/summary.md) for more details. -```eval_rst +```{eval-rst} .. note:: Only requests with the request method and uri can be sent. If trying to send a request without these components specified, a compile-time error will be reported. On how this is implemented, see the documentation on the :doc:`type of request definitions `. diff --git a/generated-docs/out/requests/body.md b/generated-docs/out/requests/body.md index 037581877f..6e32bce21e 100644 --- a/generated-docs/out/requests/body.md +++ b/generated-docs/out/requests/body.md @@ -11,14 +11,14 @@ In its simplest form, the request's body can be set as a `String`. By default, t A `String` body can be set on a request as follows: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.body("Hello, world!") ``` It is also possible to use a different character encoding: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.body("Hello, world!", "utf-8") ``` @@ -27,7 +27,7 @@ basicRequest.body("Hello, world!", "utf-8") To set a binary-data body, the following methods are available: ```scala -import sttp.client4._ +import sttp.client4.* val bytes: Array[Byte] = ??? basicRequest.body(bytes) @@ -43,7 +43,7 @@ basicRequest.body(inputStream) If not specified before, these methods will set the content type to `application/octet-stream`. When using a byte array, additionally the content length will be set to the length of the array (unless specified explicitly). -```eval_rst +```{eval-rst} .. note:: While the object defining a request is immutable, setting a mutable request body will make the whole request definition mutable as well. With ``InputStream``, the request can be moreover sent only once, as input streams can be consumed once. @@ -54,7 +54,7 @@ If not specified before, these methods will set the content type to `application To upload a file, simply set the request body as a `File` or `Path`: ```scala -import sttp.client4._ +import sttp.client4.* import java.io.File basicRequest.body(new File("data.txt")) @@ -76,7 +76,7 @@ If you set the body as a `Map[String, String]` or `Seq[(String, String)]`, it wi By default, the `UTF-8` encoding is used, but can be also specified explicitly: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.body(Map("k1" -> "v1")) basicRequest.body(Map("k1" -> "v1"), "utf-8") basicRequest.body("k1" -> "v1", "k2" -> "v2") @@ -92,7 +92,7 @@ types: a `String`, byte array, an input stream, etc. For example, here's how to write a custom serializer for a case class, with serializer-specific default content type: ```scala -import sttp.client4._ +import sttp.client4.* import sttp.model.MediaType case class Person(name: String, surname: String, age: Int) diff --git a/generated-docs/out/requests/cookies.md b/generated-docs/out/requests/cookies.md index 62e7faea16..7ea14a5a0b 100644 --- a/generated-docs/out/requests/cookies.md +++ b/generated-docs/out/requests/cookies.md @@ -7,7 +7,7 @@ Cookies are currently only available on the JVM. Cookies can also be set using the following methods: ```scala -import sttp.client4._ +import sttp.client4.* import sttp.model.headers.CookieWithMeta basicRequest @@ -22,7 +22,7 @@ basicRequest It is often necessary to copy cookies from a response, e.g. after a login request is sent, and a successful response with the authentication cookie received. Having an object `response: Response[_]`, cookies on a request can be copied: ```scala -import sttp.client4._ +import sttp.client4.* val backend = DefaultSyncBackend() val loginRequest = basicRequest @@ -37,7 +37,7 @@ basicRequest.cookies(response) Or, it's also possible to store only the `sttp.model.CookieWithMeta` objects (a sequence of which can be obtained from a response), and set the on the request: ```scala -import sttp.client4._ +import sttp.client4.* val backend = DefaultSyncBackend() val loginRequest = basicRequest diff --git a/generated-docs/out/requests/headers.md b/generated-docs/out/requests/headers.md index 46fa656963..446d7e5ca3 100644 --- a/generated-docs/out/requests/headers.md +++ b/generated-docs/out/requests/headers.md @@ -3,7 +3,7 @@ Arbitrary headers can be set on the request using the `.header` method: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.header("User-Agent", "myapp") ``` @@ -18,8 +18,8 @@ While most headers should be set only once on a request, HTTP allows setting a h There are also variants of this method accepting a number of headers: ```scala -import sttp.client4._ -import sttp.model._ +import sttp.client4.* +import sttp.model.* basicRequest.header(Header("k1", "v1"), onDuplicate = DuplicateHeaderBehavior.Add) basicRequest.header("k2", "v2") @@ -33,7 +33,7 @@ basicRequest.headers(Header("k9", "v9"), Header("k10", "v10"), Header("k11", "v1 For some common headers, dedicated methods are provided: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.contentType("application/json") basicRequest.contentType("application/json", "iso-8859-1") diff --git a/generated-docs/out/requests/multipart.md b/generated-docs/out/requests/multipart.md index 14c6e6af93..1952fda478 100644 --- a/generated-docs/out/requests/multipart.md +++ b/generated-docs/out/requests/multipart.md @@ -27,8 +27,8 @@ basicRequest.multipartBody(multipart("p1", "v1"), multipart("p2", "v2")) For example: ```scala -import sttp.client4._ -import java.io._ +import sttp.client4.* +import java.io.* val someFile = new File("/sample/path") @@ -44,8 +44,8 @@ basicRequest.multipartBody( For each part, an optional filename can be specified, as well as a custom content type and additional headers. For example: ```scala -import sttp.client4._ -import java.io._ +import sttp.client4.* +import java.io.* val logoFile = new File("/sample/path/logo123.jpg") val docFile = new File("/sample/path/doc123.doc") diff --git a/generated-docs/out/requests/streaming.md b/generated-docs/out/requests/streaming.md index 305720f7f9..f5fbb5b275 100644 --- a/generated-docs/out/requests/streaming.md +++ b/generated-docs/out/requests/streaming.md @@ -2,7 +2,7 @@ Some backends (see [backends summary](../backends/summary.md)) support streaming bodies, as described by the `Streams[S]` capability. If that's the case, you can set a stream of the supported type as a request body using the `streamBody` method, instead of the usual `body` method. -```eval_rst +```{eval-rst} .. note:: Here, streaming refers to (usually) non-blocking, asynchronous streams of data. To send data which is available as an ``InputStream``, or a file from local storage (which is available as a ``File`` or ``Path``), no special backend support is needed. See the documenttation on :doc:`setting the request body `. @@ -13,21 +13,21 @@ An implementation of the `Streams[S]` capability must be passed to the `.streamB For example, using the [akka-http backend](../backends/akka.md), a request with a streaming body can be defined as follows: ```scala -import sttp.client4._ -import sttp.capabilities.akka.AkkaStreams +import sttp.client4.* +import sttp.capabilities.pekko.PekkoStreams -import akka.stream.scaladsl.Source -import akka.util.ByteString +import org.apache.pekko.stream.scaladsl.Source +import org.apache.pekko.util.ByteString val chunks = "Streaming test".getBytes("utf-8").grouped(10).to(Iterable) val source: Source[ByteString, Any] = Source.apply(chunks.toList.map(ByteString(_))) basicRequest .post(uri"...") - .streamBody(AkkaStreams)(source) + .streamBody(PekkoStreams)(source) ``` -```eval_rst +```{eval-rst} .. note:: A request with the body set as a stream can only be sent using a backend supporting exactly the given type of streams. ``` diff --git a/generated-docs/out/requirements.txt b/generated-docs/out/requirements.txt index 654b597d27..e8548070c2 100644 --- a/generated-docs/out/requirements.txt +++ b/generated-docs/out/requirements.txt @@ -1,5 +1,4 @@ -sphinx_rtd_theme==1.0.0 -recommonmark==0.7.1 -sphinx==4.2.0 -sphinx-autobuild==2021.3.14 -myst-parser==0.15.2 \ No newline at end of file +sphinx_rtd_theme==2.0.0 +sphinx==7.3.7 +sphinx-autobuild==2024.4.16 +myst-parser==2.0.0 diff --git a/generated-docs/out/resilience.md b/generated-docs/out/resilience.md index 9ae87bb2d1..a8af4cba8f 100644 --- a/generated-docs/out/resilience.md +++ b/generated-docs/out/resilience.md @@ -16,6 +16,7 @@ Still, the input for a particular resilience model might involve both the result Here's an incomplete list of libraries which can be used to manage retries in various Scala stacks: +* for synchornous/direct-style: [ox](https://github.com/softwaremill/ox) * for `Future`: [retry](https://github.com/softwaremill/retry) * for ZIO: [schedules](https://zio.dev/reference/schedule/), [rezilience](https://github.com/svroonland/rezilience) * for Monix/cats-effect: [cats-retry](https://github.com/cb372/cats-retry) @@ -32,7 +33,6 @@ Some backends have built-in retry mechanisms: * [akka-http](https://doc.akka.io/docs/akka-http/current/scala/http/client-side/host-level.html#retrying-a-request) * [OkHttp](http://square.github.io/okhttp) (see the builder's `retryOnConnectionFailure` method) -* async-http-client (deprecated): by default, the backend will attempt 5 retries in case an `IOException` is thrown during the connection. This can be changed by specifying the `org.asynchttpclient.maxRequestRetry` config option, or by providing custom configuration using when creating the backend (`setMaxRequestRetry`). ## Circuit breaking diff --git a/generated-docs/out/responses/basics.md b/generated-docs/out/responses/basics.md index caf8c04759..8357ec3015 100644 --- a/generated-docs/out/responses/basics.md +++ b/generated-docs/out/responses/basics.md @@ -4,7 +4,7 @@ Responses are represented as instances of the case class `Response[T]`, where `T If sending the request fails, either due to client or connection errors, an exception will be thrown (synchronous backends), or a failed effect will be returned (e.g. a failed future). -```eval_rst +```{eval-rst} .. note:: If the request completes, but results in a non-2xx return code, the request is still considered successful, that is, a ``Response[T]`` will be returned. See :doc:`response body specifications ` for details on how such cases are handled. ``` @@ -19,8 +19,8 @@ Response headers are available through the `.headers` property, which gives all Individual headers can be obtained using the methods: ```scala -import sttp.model._ -import sttp.client4._ +import sttp.model.* +import sttp.client4.* val backend = DefaultSyncBackend() val request = basicRequest diff --git a/generated-docs/out/responses/body.md b/generated-docs/out/responses/body.md index e39cacb274..dc93c85f87 100644 --- a/generated-docs/out/responses/body.md +++ b/generated-docs/out/responses/body.md @@ -24,7 +24,7 @@ When the above request is completely described and sent, it will result in a `Re Other possible response descriptions include: ```scala -import sttp.client4._ +import sttp.client4.* import java.io.File import java.nio.file.Path @@ -56,7 +56,7 @@ def asBothOption[A, B](l: ResponseAs[A], r: ResponseAs[B]): ResponseAs[(A, Optio Hence, to discard the response body, the request description should include the following: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.response(ignore) ``` @@ -64,14 +64,14 @@ basicRequest.response(ignore) And to save the response to a file: ```scala -import sttp.client4._ -import java.io._ +import sttp.client4.* +import java.io.* val someFile = new File("some/path") basicRequest.response(asFile(someFile)) ``` -```eval_rst +```{eval-rst} .. note:: As the handling of response is specified upfront, there's no need to "consume" the response body. It can be safely discarded if not needed. @@ -82,14 +82,14 @@ basicRequest.response(asFile(someFile)) Sometimes it's convenient to get a failed effect (or an exception thrown) when the response status code is not successful. In such cases, the response description can be modified using the `.orFail` combinator: ```scala -import sttp.client4._ +import sttp.client4.* basicRequest.response(asString.orFail): PartialRequest[String] ``` The combinator works in all cases where the response body is specified to be deserialized as an `Either`. If the left is already an exception, it will be thrown unchanged. Otherwise, the left-value will be wrapped in an `HttpError`. -```eval_rst +```{eval-rst} .. note:: While both ``asStringAlways`` and ``asString.orFail`` have the type ``ResponseAs[String, Any]``, they are different. The first will return the response body as a string always, regardless of the responses' status code. The second will return a failed effect / throw a ``HttpError`` exception for non-2xx status codes, and the string as body only for 2xx status codes. @@ -101,14 +101,14 @@ There's also a variant of the combinator, `.getEither`, which can be used to ext It's possible to define custom body deserializers by taking any of the built-in response descriptions and mapping over them. Each `ResponseAs` instance has `map` and `mapWithMetadata` methods, which can be used to transform it to a description for another type (optionally using response metadata, such as headers or the status code). Each such value is immutable and can be used multiple times. -```eval_rst +```{eval-rst} .. note:: Alternatively, response descriptions can be modified directly from the request description, by using the ``request.mapResponse(...)`` and ``request.mapResponseRight(...)`` methods (which is available, if the response body is deserialized to an either). That's equivalent to calling ``request.response(request.response.map(...))``, that is setting a new response description, to a modified old response description; but with shorter syntax. ``` As an example, to read the response body as an int, the following response description can be defined (warning: this ignores the possibility of exceptions!): ```scala -import sttp.client4._ +import sttp.client4.* val asInt: ResponseAs[Either[String, Int]] = asString.mapRight((_: String).toInt) @@ -120,7 +120,7 @@ basicRequest To integrate with a third-party JSON library, and always parse the response as JSON (regardless of the status code): ```scala -import sttp.client4._ +import sttp.client4.* type JsonError type JsonAST @@ -141,11 +141,11 @@ Using the `fromMetadata` combinator, it's possible to dynamically specify how th A more complex case, which uses Circe for deserializing JSON, choosing to which model to deserialize to depending on the status code, can look as following: ```scala -import sttp.client4._ -import sttp.model._ -import sttp.client4.circe._ -import io.circe._ -import io.circe.generic.auto._ +import sttp.client4.* +import sttp.model.* +import sttp.client4.circe.* +import io.circe.* +import io.circe.generic.auto.* sealed trait MyModel case class SuccessModel(name: String, age: Int) extends MyModel @@ -163,11 +163,11 @@ val myRequest: Request[Either[ResponseException[String, io.circe.Error], MyModel The above example assumes that success and error models are part of one hierarchy (`MyModel`). Sometimes http errors are modelled independently of success. In this case, we can use `asJsonEither`, which uses `asEitherDeserialized` under the covers: ```scala -import sttp.client4._ -import sttp.model._ -import sttp.client4.circe._ -import io.circe._ -import io.circe.generic.auto._ +import sttp.client4.* +import sttp.model.* +import sttp.client4.circe.* +import io.circe.* +import io.circe.generic.auto.* case class MyModel(p1: Int) @@ -195,7 +195,7 @@ If the backend used supports non-blocking, asynchronous streaming (see "Supporte ```scala import sttp.capabilities.{Effect, Streams} -import sttp.client4._ +import sttp.client4.* import sttp.model.ResponseMetadata def asStream[F[_], T, S](s: Streams[S])(f: s.BinaryStream => F[T]): @@ -225,22 +225,22 @@ The first two "safe" variants pass the response stream to the user-provided func The "unsafe" variants return the stream directly to the user, and then it's up to the user of the code to consume and close the stream, releasing any resources held by the HTTP connection. -For example, when using the [Akka backend](../backends/akka.md): +For example, when using the [Pekko backend](../backends/pekko.md): ```scala -import akka.stream.scaladsl.Source -import akka.util.ByteString +import org.apache.pekko.stream.scaladsl.Source +import org.apache.pekko.util.ByteString import scala.concurrent.Future -import sttp.capabilities.akka.AkkaStreams -import sttp.client4._ -import sttp.client4.akkahttp.AkkaHttpBackend +import sttp.capabilities.pekko.PekkoStreams +import sttp.client4.* +import sttp.client4.pekkohttp.PekkoHttpBackend -val backend: StreamBackend[Future, AkkaStreams] = AkkaHttpBackend() +val backend: StreamBackend[Future, PekkoStreams] = PekkoHttpBackend() val response: Future[Response[Either[String, Source[ByteString, Any]]]] = basicRequest .post(uri"...") - .response(asStreamUnsafe(AkkaStreams)) + .response(asStreamUnsafe(PekkoStreams)) .send(backend) ``` diff --git a/generated-docs/out/testing.md b/generated-docs/out/testing.md index 37f236022b..ac9f6266a7 100644 --- a/generated-docs/out/testing.md +++ b/generated-docs/out/testing.md @@ -19,9 +19,9 @@ An empty backend stub can be created using the following ways: Some code which will be reused among following examples: ```scala -import sttp.client4._ -import sttp.model._ -import sttp.client4.testing._ +import sttp.client4.* +import sttp.model.* +import sttp.client4.testing.* import java.io.File import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global @@ -68,7 +68,7 @@ val response2 = basicRequest.post(uri"http://example.org/partialAda").send(testi // response2.body will be Right("Ada") ``` -```eval_rst +```{eval-rst} .. note:: This approach to testing has one caveat: the responses are not type-safe. That is, the stub backend cannot match on or verify that the type of the response body matches the response body type, as it was requested. However, when a "raw" response is provided (a ``String``, ``Array[Byte]``, ``InputStream``, or a non-blocking stream wrapped in ``RawStream``), it will be handled as specified by the response specification - see below for details. @@ -210,18 +210,17 @@ If you actually want a file to be written you can set up the stub like this: ```scala import org.apache.commons.io.FileUtils -import cats.effect._ -import sttp.client4.impl.cats.implicits._ +import cats.effect.* +import sttp.client4.impl.cats.implicits.* import sttp.monad.MonadAsyncError val sourceFile = new File("path/to/file.ext") val destinationFile = new File("path/to/file.ext") BackendStub(implicitly[MonadAsyncError[IO]]) .whenRequestMatches(_ => true) - .thenRespondF { _ => + .thenRespondF: _ => FileUtils.copyFile(sourceFile, destinationFile) IO(ResponseStub(Right(destinationFile), StatusCode.Ok, "")) - } ``` ## Delegating to another backend @@ -274,10 +273,9 @@ val webSocketStub = WebSocketStub .initialReceive( List(WebSocketFrame.text("Hello from the server!")) ) - .thenRespondS(0) { + .thenRespondS(0): case (counter, tf: WebSocketFrame.Text) => (counter + 1, List(WebSocketFrame.text(s"echo: ${tf.payload}"))) case (counter, _) => (counter, List.empty) - } backend.whenAnyRequest.thenRespond(webSocketStub) ``` diff --git a/generated-docs/out/websockets.md b/generated-docs/out/websockets.md index 6da0bf5566..0a66e815c0 100644 --- a/generated-docs/out/websockets.md +++ b/generated-docs/out/websockets.md @@ -4,9 +4,9 @@ One of the optional capabilities (represented as `WebSockets`) that a backend ca A websocket request will be sent instead of a regular one if the response specification includes handling the response as a websocket. Depending on the backend you are using, there are three variants of websocket response specifications: synchronous, asynchronous and streaming. To use them, add one of the following imports: -* `import sttp.client4.ws.sync._` if you are using a synchronous backend (such as `DefaultSyncBackend`), without any effect wrappers -* `import sttp.client4.ws.async._` if you are using an asynchronous backend (e.g. based on `Future`s or `IO`s) -* `import sttp.client4.ws.stream._` if you want to handle web socket messages using a non-blocking stream (e.g. `fs2.Stream` or `akka.stream.scaladsl.Source`) +* `import sttp.client4.ws.sync.*` if you are using a synchronous backend (such as `DefaultSyncBackend`), without any effect wrappers +* `import sttp.client4.ws.async.*` if you are using an asynchronous backend (e.g. based on `Future`s or `IO`s) +* `import sttp.client4.ws.stream.*` if you want to handle web socket messages using a non-blocking stream (e.g. `fs2.Stream` or `akka.stream.scaladsl.Source`) The above imports will bring into scope a number of `asWebSocket(...)` methods, giving a couple of variants of working with websockets. @@ -22,12 +22,12 @@ The `SyncWebSocket` / `WebSocket` classes also contain other methods for receivi The following response specifications which use `SyncWebSocket` are available in the `sttp.client4.ws.sync` object (the second type parameter of `WebSocketResponseAs` specifies the type returned as the response body): ```scala -import sttp.client4._ +import sttp.client4.* import sttp.client4.ws.SyncWebSocket import sttp.model.ResponseMetadata import sttp.shared.Identity -// when using import sttp.client4.ws.sync._ +// when using import sttp.client4.ws.sync.* def asWebSocket[T](f: SyncWebSocket => T): WebSocketResponseAs[Identity, Either[String, T]] = ??? @@ -67,7 +67,7 @@ Another possibility is to work with websockets by providing a streaming stage, w The following response specifications are available: ```scala -import sttp.client4._ +import sttp.client4.* import sttp.capabilities.{Streams, WebSockets} import sttp.ws.WebSocketFrame @@ -86,7 +86,7 @@ When working with streams of websocket frames keep in mind that a text payload m sttp provides two useful methods (`fromTextPipe`, `fromTextPipeF`) for each backend to aggregate these fragments back into complete messages. These methods can be found in corresponding WebSockets classes for given effect type: -```eval_rst +```{eval-rst} ================ ========================================== effect type class name ================ ========================================== @@ -156,17 +156,3 @@ configuring individual backends for more information. ### akka-http backend Compression is not yet available, to track Akka developments in this area, see [this issue](https://github.com/akka/akka-http/issues/659). - -### async-http-client based backends (deprecated) - -```eval_rst -.. note:: Note that the async-http-client is no longer maintained, thus backends based on it should not be used in the new projects. -``` - -Web socket settings can be adjusted by providing a custom `AsyncHttpClientConfig`, which can be created using -`new DefaultAsyncHttpClientConfig.Builder()`. - -Some available settings: - -* maximum web socket frame size. Default: 10240, can be changed using `.setWebSocketMaxFrameSize`. -* compression. Default: false, can be changed using: `.setEnablewebSocketCompression`. diff --git a/generated-docs/out/xml.md b/generated-docs/out/xml.md index 96bea7e2e1..b89388c351 100644 --- a/generated-docs/out/xml.md +++ b/generated-docs/out/xml.md @@ -17,26 +17,22 @@ import sttp.model.MediaType import scala.xml.{NodeSeq, XML} -trait SttpScalaxbApi { +trait SttpScalaxbApi: case class XmlElementLabel(label: String) - def asXml[B](b: B)(implicit format: CanWriteXML[B], label: XmlElementLabel): StringBody = { + def asXml[B](b: B)(implicit format: CanWriteXML[B], label: XmlElementLabel): StringBody = val nodeSeq: NodeSeq = toXML[B](obj = b, elementLabel = label.label, scope = defaultScope) StringBody(nodeSeq.toString(), "utf-8", MediaType.ApplicationXml) - } - implicit def deserializeXml[B](implicit decoder: XMLFormat[B]): String => Either[Exception, B] = { (s: String) => - try { + implicit def deserializeXml[B](implicit decoder: XMLFormat[B]): String => Either[Exception, B] = (s: String) => + try Right(fromXML[B](XML.loadString(s))) - } catch { + catch case e: Exception => Left(e) - } - } def asXml[B: XMLFormat]: ResponseAs[Either[ResponseException[String, Exception], B], Any] = asString.mapWithMetadata(ResponseAs.deserializeRightWithError(deserializeXml[B])) .showAs("either(as string, as xml)") -} ``` This would add `asXml` methods needed for serialization and deserialization. Please notice, that `fromXML`, `toXML`, `CanWriteXML`, `XMLFormat` and `defaultScope` are members of code generated by scalaxb. diff --git a/project/FileUtils.scala b/project/FileUtils.scala new file mode 100644 index 0000000000..863e77d4ab --- /dev/null +++ b/project/FileUtils.scala @@ -0,0 +1,22 @@ +import java.io.File +import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor} +import java.nio.file.attribute.BasicFileAttributes + +object FileUtils { + def listScalaFiles(basePath: File): Seq[Path] = { + val dirPath = basePath.toPath + var result = Vector.empty[Path] + + val fileVisitor = new SimpleFileVisitor[Path] { + override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = { + if (file.toString.endsWith(".scala")) { + result = result :+ file + } + FileVisitResult.CONTINUE + } + } + + Files.walkFileTree(dirPath, fileVisitor) + result + } +} diff --git a/project/GenerateListOfExamples.scala b/project/GenerateListOfExamples.scala new file mode 100644 index 0000000000..571a542ef5 --- /dev/null +++ b/project/GenerateListOfExamples.scala @@ -0,0 +1,99 @@ +import java.io.{File, PrintWriter} +import sbt.Logger + +import java.nio.file.Path +import scala.io.Source + +/** Generates the generated-docs/out/includes/examples_list.md file, basing on the metadata included in the source of + * the examples. + */ +object GenerateListOfExamples { + private val MetadataPattern = """// \{(.+?)}: (.+)""".r + private val LinkBase = "https://github.com/softwaremill/sttp/tree/master" + + private case class Example(cat: String, otherMetadata: Map[String, String], description: String, path: Path) + + def apply(log: Logger, rootBasePath: File): Unit = { + log.info(s"[GenerateListOfExamples] Base path: $rootBasePath") + + // from each of those examples, we need to extract the metadata + val examples = FileUtils.listScalaFiles(rootBasePath.toPath.resolve("examples/src/main").toFile) ++ + FileUtils.listScalaFiles(rootBasePath.toPath.resolve("examples-ce2/src/main").toFile) + + val parsedExamples: Seq[Option[Example]] = for (example <- examples) yield { + val first = firstLine(example) + if (first.startsWith("// {")) { + first match { + case MetadataPattern(metadataStr, description) => + val metadata = metadataStr.trim + .split(";") + .map { entry => + val Array(key, value) = entry.split("=") + key.trim -> value.trim + } + .toMap + + Some(Example(metadata("cat"), metadata - "cat", description, example)) + } + } else { + log.warn(s"[GenerateListOfExamples] Skipping $example as it doesn't start with the required prefix") + None + } + } + + val examplesByCategory = parsedExamples.flatten.groupBy(_.cat) + val renderedCategories = examplesByCategory.toList + // we want the "Hello, World!" category to come first + .sortBy { case (k, _) => if (k == "Hello, World!") "00" else k.toLowerCase() } + .map { case (category, examples) => + val renderedExamplesList = examples + .sortBy(_.description) + // rendering a single line with the example's description & metadata + .map { example => + val relativeLink = relativePath(example.path.toFile, rootBasePath) + val tags = example.otherMetadata.toList + .sortBy(_._1) + .map { case (key, value) => + s"""$value""" + } + .mkString(" ") + + s"""* [${example.description}]($LinkBase$relativeLink) $tags""" + } + // combining all examples in category + .mkString("\n") + + s"""## $category + | + |$renderedExamplesList""".stripMargin + } + // separating categories + .mkString("\n\n") + + // writing the result + val targetPath = rootBasePath.toPath.resolve("generated-docs/out/includes/examples_list.md") + ensureExists(targetPath.getParent.toFile) + log.info(s"[GenerateListOfExamples] Writing rendered categories to $targetPath") + + val writer = new PrintWriter(targetPath.toFile) + try writer.write(renderedCategories) + finally writer.close() + } + + private def firstLine(p: Path): String = { + val source = Source.fromFile(p.toUri) + try source.getLines().next() + finally source.close() + } + + private def relativePath(file: File, relativeTo: File): String = { + val p1 = file.getAbsolutePath + val p2 = relativeTo.getAbsolutePath + if (!p1.startsWith(p2)) throw new IllegalArgumentException(s"$file is not relative to $relativeTo!") + else p1.substring(p2.length) + } + + private def ensureExists(dir: File): Unit = if (!dir.exists() && !dir.mkdirs()) { + throw new IllegalStateException("Cannot create directory: " + dir) + } +} diff --git a/project/VerifyExamplesCompileUsingScalaCli.scala b/project/VerifyExamplesCompileUsingScalaCli.scala new file mode 100644 index 0000000000..54bd6198d1 --- /dev/null +++ b/project/VerifyExamplesCompileUsingScalaCli.scala @@ -0,0 +1,24 @@ +import java.io.File +import sbt.Logger +import scala.sys.process.{Process, ProcessLogger} + +object VerifyExamplesCompileUsingScalaCli { + def apply(log: Logger, examplesSrcPath: File): Unit = { + val examples = FileUtils.listScalaFiles(examplesSrcPath) + log.info(s"Found ${examples.size} examples") + + for (example <- examples) { + log.info(s"Compiling: $example") + val errorOutput = new StringBuilder + val logger = ProcessLogger((o: String) => (), (e: String) => errorOutput.append(e + "\n")) + try { + val result = Process(List("scala-cli", "compile", example.toFile.getAbsolutePath), examplesSrcPath).!(logger) + if (result != 0) { + throw new Exception(s"""Compiling $example failed.\n$errorOutput""".stripMargin) + } + } finally { + Process(List("scala-cli", "clean", example.toFile.getAbsolutePath), examplesSrcPath).! + } + } + } +}