Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into redis-cache
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Jan 8, 2025
2 parents f2ba934 + 848ce13 commit 947e98c
Show file tree
Hide file tree
Showing 34 changed files with 196 additions and 98 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import sttp.client4.quick.*
println(quickRequest.get(uri"http://httpbin.org/ip").send())
```

sttp client addresses common HTTP client use cases, such as interacting with JSON APIs (with automatic serialization of request bodies and deserialization of response bodies), uploading and downloading files, submitting form data, handling multi-part requests, and working with WebSockets.
sttp client addresses common HTTP client use cases, such as interacting with JSON APIs (with automatic serialization of request bodies and deserialization of response bodies), uploading and downloading files, submitting form data, handling multipart requests, and working with WebSockets.

The driving principle of sttp client's design is to provide a clean, programmer-friendly API to describe HTTP requests, along with response handling. This ensures that resources, such as HTTP connections, are used safely, also in the presence of errors.

Expand Down Expand Up @@ -118,10 +118,11 @@ Type `basicRequest.` and see where your IDE’s auto-complete gets you!
sttp is a family of Scala HTTP-related projects, and currently includes:

* sttp client: this project
* [sttp tapir](https://github.com/softwaremill/tapir): Typed API descRiptions
* [sttp tapir](https://github.com/softwaremill/tapir): rapid development of self-documenting APIs
* [sttp model](https://github.com/softwaremill/sttp-model): simple HTTP model classes (used by client & tapir)
* [sttp shared](https://github.com/softwaremill/sttp-shared): shared web socket, FP abstractions, capabilities and streaming code.
* [sttp apispec](https://github.com/softwaremill/sttp-apispec): OpenAPI, AsyncAPI and JSON Schema models.
* [sttp openai](https://github.com/softwaremill/sttp-openai): Scala client wrapper for OpenAI and OpenAI-compatible APIs. Use the power of ChatGPT inside your code!

## Contributing

Expand Down
58 changes: 21 additions & 37 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,8 @@ val compileScoped =
val testScoped =
inputKey[Unit](s"Run tests in the given scope. Usage: testScoped [scala version] [platform]. $scopesDescription")

val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq(
val commonSettings = commonSmlBuildSettings ++ Seq(
organization := "com.softwaremill.sttp.client4",
updateDocs := Def.taskDyn {
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,
ideSkipProject := (scalaVersion.value != ideScalaVersion)
|| thisProjectRef.value.project.contains("JS") || thisProjectRef.value.project.contains("Native"),
bspEnabled := !ideSkipProject.value,
Expand Down Expand Up @@ -129,7 +119,7 @@ val testServerSettings = Seq(

val circeVersion: String = "0.14.10"

val jsoniterVersion = "2.32.0"
val jsoniterVersion = "2.33.0"

val play29JsonVersion = "2.10.6"

Expand Down Expand Up @@ -169,16 +159,14 @@ val logback = "ch.qos.logback" % "logback-classic" % "1.5.14"
val jaegerClientVersion = "1.8.1"
val braveOpentracingVersion = "1.0.1"
val zipkinSenderOkHttpVersion = "3.4.3"
val resilience4jVersion = "2.2.0"
val resilience4jVersion = "2.3.0"
val http4s_ce2_version = "0.22.15"
val http4s_ce3_version = "0.23.30"

val tethysVersion = "0.29.3"

val openTelemetryVersion = "1.45.0"

val slf4jVersion = "1.7.36"

val compileAndTest = "compile->compile;test->test"

lazy val loomProjects: Seq[String] = Seq(ox, examples).flatMap(_.projectRefs).flatMap(projectId)
Expand Down Expand Up @@ -254,7 +242,6 @@ lazy val rawAllAggregates =
armeriaFs2Backend.projectRefs ++
scribeBackend.projectRefs ++
slf4jBackend.projectRefs ++
caching.projectRefs ++
examplesCe2.projectRefs ++
examples.projectRefs ++
docs.projectRefs ++
Expand All @@ -277,7 +264,8 @@ def filterByVersionAndPlatform(scalaVersionFilter: String, platformFilter: Strin
}

lazy val rootProject = (project in file("."))
.settings(commonSettings: _*)
.settings(commonSettings)
.settings(ossPublishSettings)
.settings(
publish / skip := true,
name := "sttp",
Expand All @@ -290,7 +278,17 @@ lazy val rootProject = (project in file("."))
Def.taskDyn((Test / test).all(filterByVersionAndPlatform(args.head, args(1))))
}.evaluated,
ideSkipProject := false,
scalaVersion := scala2_13
scalaVersion := scala2_13,
updateDocs := Def.taskDyn {
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
)
.aggregate(allAggregates: _*)

Expand Down Expand Up @@ -767,7 +765,7 @@ lazy val zioJson = (projectMatrix in file("json/zio-json"))
.settings(
name := "zio-json",
libraryDependencies ++= Seq(
"dev.zio" %%% "zio-json" % "0.7.3",
"dev.zio" %%% "zio-json" % "0.7.4",
"com.softwaremill.sttp.shared" %%% "zio" % sttpSharedVersion
),
scalaTest
Expand Down Expand Up @@ -918,7 +916,7 @@ lazy val openTelemetryTracingZioBackend = (projectMatrix in file("observability/
.settings(
name := "opentelemetry-tracing-zio-backend",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-opentelemetry" % "3.1.0",
"dev.zio" %% "zio-opentelemetry" % "3.1.1",
"io.opentelemetry.semconv" % "opentelemetry-semconv" % "1.26.0-alpha",
"io.opentelemetry" % "opentelemetry-api" % openTelemetryVersion,
"io.opentelemetry" % "opentelemetry-sdk-testing" % openTelemetryVersion % Test
Expand Down Expand Up @@ -947,26 +945,13 @@ lazy val slf4jBackend = (projectMatrix in file("logging/slf4j"))
.settings(
name := "slf4j-backend",
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % slf4jVersion
"org.slf4j" % "slf4j-api" % "1.7.36"
),
scalaTest
)
.jvmPlatform(scalaVersions = scala2And3)
.dependsOn(core)

lazy val caching = (projectMatrix in file("caching"))
.settings(commonJvmSettings)
.settings(
name := "caching-backend",
scalaTest,
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % slf4jVersion,
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % jsoniterVersion % Compile
)
)
.jvmPlatform(scalaVersions = scala2And3)
.dependsOn(core, jsoniter)

lazy val examplesCe2 = (projectMatrix in file("examples-ce2"))
.settings(commonJvmSettings)
.settings(
Expand All @@ -991,7 +976,6 @@ lazy val examples = (projectMatrix in file("examples"))
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % jsoniterVersion,
"io.github.resilience4j" % "resilience4j-circuitbreaker" % resilience4jVersion,
"io.github.resilience4j" % "resilience4j-ratelimiter" % resilience4jVersion,
"redis.clients" % "jedis" % "5.2.0",
pekkoStreams,
logback
),
Expand All @@ -1007,10 +991,10 @@ lazy val examples = (projectMatrix in file("examples"))
circe,
upickle,
jsoniter,
zioJson,
scribeBackend,
slf4jBackend,
ox,
caching
ox
)

//TODO this should be invoked by compilation process, see #https://github.com/scalameta/mdoc/issues/355
Expand Down
1 change: 0 additions & 1 deletion core/src/main/scala/sttp/client4/testing/BackendStub.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import scala.concurrent.ExecutionContext
* Predicates can match requests basing on the URI or headers. A [[ClassCastException]] might occur if for a given
* request, a response is specified with the incorrect or inconvertible body type.
*/

class BackendStub[F[_]](
monad: MonadError[F],
matchers: PartialFunction[GenericRequest[_, _], F[Response[_]]],
Expand Down
2 changes: 1 addition & 1 deletion docs/backends/javascript/fetch.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# JavaScript (Fetch) backend
# Scala.js (Fetch) backend

A JavaScript backend with web socket support. Implemented using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).

Expand Down
25 changes: 23 additions & 2 deletions docs/backends/native/curl.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Curl backend
# Scala Native (curl) backend

A Scala Native backend implemented using [Curl](https://github.com/curl/curl/blob/master/include/curl/curl.h).
A Scala Native (0.5.x) backend implemented using [Curl](https://github.com/curl/curl/blob/master/include/curl/curl.h).

To use, add the following dependency to your project:

Expand All @@ -11,9 +11,30 @@ To use, add the following dependency to your project:
and initialize one of the backends:

```scala
import sttp.client4.curl.*

val backend = CurlBackend()
val tryBackend = CurlTryBackend()
```

You need to have an environment with Scala Native [setup](https://scala-native.readthedocs.io/en/latest/user/setup.html)
with additionally installed `libcrypto` (included in OpenSSL) and `curl` in version `7.56.0` or newer.

## scala-cli example

Try the following example:

```scala
// hello.scala

//> using platform native
//> using dep com.softwaremill.sttp.client4::core_native0.5:@VERSION@

import sttp.client4.*
import sttp.client4.curl.CurlBackend

@main def run(): Unit =
val backend = CurlBackend()
println(basicRequest.get(uri"http://httpbin.org/ip").send(backend))
```

9 changes: 8 additions & 1 deletion docs/backends/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ As a starting point, the default backends are good choice. Depending on the plat

These default backends provide limited customisation options, hence for any more advanced use-cases, simply substitute them with a specific implementation. E.g. the `HttpClientSyncBackend` backend, which is the underlying implementation of `DefaultSyncBackend`, offers customisation options not available in the default one.

## Backends overview
## JVM backends

Which one to choose?

Expand Down Expand Up @@ -61,6 +61,8 @@ Backends supporting cats-effect are available in versions for cats-effect 2.x (d

All backends that support asynchronous/non-blocking streams, also support server-sent events.

## Backend wrappers

There are also backends which wrap other backends to provide additional functionality. These include:

* `TryBackend`, which safely wraps any exceptions thrown by a synchronous backend in `scala.util.Try`
Expand All @@ -72,6 +74,9 @@ There are also backends which wrap other backends to provide additional function
* `ResolveRelativeUrisBackend` to resolve relative URIs given a base URI, or an arbitrary effectful function
* `ListenerBackend` to listen for backend lifecycle events. See the [dedicated section](wrappers/custom.md).
* `FollowRedirectsBackend`, which handles redirects. All implementation backends are created wrapped with this one.
* `CachingBackend`, which caches responses. See the [dedicated section](wrappers/caching.md).

## Scala.JS backends

In addition, there are also backends for Scala.JS:

Expand All @@ -87,6 +92,8 @@ Class Effect type Supported stre
================================ ================================ ========================================= ===================
```

## Scala Native backends

And a backend for scala-native:

```{eval-rst}
Expand Down
2 changes: 1 addition & 1 deletion docs/backends/zio.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ The `HttpClient` ZIO backend supports both regular and streaming [websockets](..
A stub backend can be created through the `.stub` method on the companion object, and configured as described in the
[testing](../testing/stub.md) section.

A layer with the stub `SttpBackend` can be then created by simply calling `ZLayer.succeed(sttpBackendStub)`.
A layer with the stub `Backend` can be then created by simply calling `ZLayer.succeed(backendStub)`.

## Server-sent events

Expand Down
6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import sttp.client4.quick.*
println(quickRequest.get(uri"http://httpbin.org/ip").send())
```

sttp client addresses common HTTP client use cases, such as interacting with JSON APIs (with automatic serialization of request bodies and deserialization of response bodies), uploading and downloading files, submitting form data, handling multi-part requests, and working with WebSockets.
sttp client addresses common HTTP client use cases, such as interacting with JSON APIs (with automatic serialization of request bodies and deserialization of response bodies), uploading and downloading files, submitting form data, handling multipart requests, and working with WebSockets.

The driving principle of sttp client's design is to provide a clean, programmer-friendly API to describe HTTP requests, along with response handling. This ensures that resources, such as HTTP connections, are used safely, also in the presence of errors.

Expand Down Expand Up @@ -64,7 +64,7 @@ sttp client is licensed under Apache2, the source code is [available on GitHub](
sttp is a family of Scala HTTP-related projects, and currently includes:

* sttp client: this project
* [sttp tapir](https://github.com/softwaremill/tapir): Typed API descRiptions
* [sttp tapir](https://github.com/softwaremill/tapir): rapid development of self-documenting APIs
* [sttp model](https://github.com/softwaremill/sttp-model): simple HTTP model classes (used by client & tapir)
* [sttp shared](https://github.com/softwaremill/sttp-shared): shared web socket, FP abstractions, capabilities and streaming code.
* [sttp apispec](https://github.com/softwaremill/sttp-apispec): OpenAPI, AsyncAPI and JSON Schema models.
Expand Down Expand Up @@ -135,6 +135,7 @@ Third party projects:
other/xml
other/resilience
other/openapi
other/sse
.. toctree::
:maxdepth: 2
Expand Down Expand Up @@ -163,7 +164,6 @@ Third party projects:
backends/wrappers/opentelemetry
backends/wrappers/prometheus
backends/wrappers/logging
backends/wrappers/cache
backends/wrappers/custom
.. toctree::
Expand Down
5 changes: 5 additions & 0 deletions docs/other/sse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Server-sent events

All backends that support [streaming](../requests/streaming.md) also support [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). Moreover, synchronous backends can also support SSE, when combined with [Ox](https://ox.softwaremill.com).

Refer to the documentation of individual backends, for more information on how to use SSE with a given backend, as well as usage examples.
12 changes: 7 additions & 5 deletions docs/other/websockets.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WebSockets

One of the optional capabilities (represented as `WebSockets`) that a backend can support are websockets (see [backends summary](../backends/summary.md)). Websocket requests are described exactly the same as regular requests, starting with `basicRequest`, adding headers, specifying the request method and uri.
One of the optional capabilities that a backend can support are websockets (see [backends summary](../backends/summary.md)). Websocket requests are described exactly the same as regular requests, starting with `basicRequest`, adding headers, specifying the request method and uri.

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:

Expand All @@ -10,9 +10,11 @@ A websocket request will be sent instead of a regular one if the response specif

The above imports will bring into scope a number of `asWebSocket(...)` methods, giving a couple of variants of working with websockets. Alternatively, you can extend the `SttpWebSocketSyncApi`, `SttpWebSocketAsyncApi` or `SttpWebSocketStreamApi` traits, to group all used sttp client features within a single object.

Refer to the documentation of individual backends for additional notes, or restrictions, when using WebSockets.

## Using `WebSocket`

The first variant of interacting with web sockets is using `sttp.client4.ws.SyncWebSocket` (sync variant), or `sttp.ws.WebSocket[F]` (async variant), where `F` is the backend-specific effects wrapper, such as `Future` or `IO`. These classes contain two basic methods:
The first possibility of interacting with web sockets is using `sttp.client4.ws.SyncWebSocket` (sync variant), or `sttp.ws.WebSocket[F]` (async variant), where `F` is the backend-specific effects wrapper, such as `Future` or `IO`. These classes contain two basic methods:

* `def receive: WebSocketFrame` (optionally wrapped with `F[_]` in the async variant) which will complete once a message is available, and return the next incoming frame (which can be a data, ping, pong or close)
* `def send(f: WebSocketFrame, isContinuation: Boolean = false): Unit` (again optionally wrapped with `F[_]`), which sends a message to the websocket. The `WebSocketFrame` companion object contains methods for creating binary/text messages. When using fragmentation, the first message should be sent using `finalFragment = false`, and subsequent messages using `isContinuation = true`.
Expand Down Expand Up @@ -92,7 +94,7 @@ effect type class name
================ ==========================================
```

## Using blocking, sycnhronous Ox streams
## Using blocking, synchronous Ox streams

[Ox](https://ox.softwaremill.com) is a Scala 3 toolkit that allows you to handle concurrency and resiliency in direct-style, leveraging Java 21 virtual threads.
If you're using Ox with `sttp`, you can use the `DefaultSyncBackend` from `sttp-core` for HTTP communication. An additional `ox` module allows handling WebSockets
Expand Down Expand Up @@ -128,14 +130,14 @@ basicRequest

See the [full example here](https://github.com/softwaremill/sttp/blob/master/examples/src/main/scala/sttp/client4/examples/ws/wsOxExample.scala).

Make sure that the `Source` is contiunually read. This will guarantee that server-side `Close` signal is received and handled.
Make sure that the `Source` is continually read. This will guarantee that server-side `Close` signal is received and handled.
If you don't want to process frames from the server, you can at least handle it with a `fork { source.drain() }`.

You don't need to manually call `ws.close()` when using this approach, this will be handled automatically underneath,
according to following rules:
- If the request `Sink` is closed due to an upstream error, a `Close` frame is sent, and the `Source` with incoming responses gets completed as `Done`.
- If the request `Sink` completes as `Done`, a `Close` frame is sent, and the response `Sink` keeps receiving responses until the server closes communication.
- If the response `Source` is closed by a `Close` frome from the server or due to an error, the request Sink is closed as `Done`, which will still send all outstanding buffered frames, and then finish.
- If the response `Source` is closed by a `Close` frame from the server or due to an error, the request Sink is closed as `Done`, which will still send all outstanding buffered frames, and then finish.

Read more about Ox, structured concurrency, Sources and Sinks on the [project website](https://ox.softwaremill.com).

Expand Down
Loading

0 comments on commit 947e98c

Please sign in to comment.