Skip to content

Commit

Permalink
Merge pull request #1716 from haoqin/1654
Browse files Browse the repository at this point in the history
1654
  • Loading branch information
adamw authored Feb 28, 2023
2 parents c5daada + d35cf56 commit 362019a
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ object ArmeriaZioBackend {
.map(runtime => apply(runtime, client, closeFactory = true))
)(_.close().ignore)

def layer(options: SttpBackendOptions = SttpBackendOptions.Default): Layer[Throwable, SttpBackend[Task, ZioStreams]] =
def layer(options: SttpBackendOptions = SttpBackendOptions.Default): Layer[Throwable, SttpClient] =
ZLayer.scoped(scoped(options))

def usingClient(client: WebClient): Task[SttpBackend[Task, ZioStreams]] =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package sttp.client3.armeria

import _root_.zio._
import sttp.capabilities.Effect
import sttp.capabilities.zio.ZioStreams
import sttp.client3._
import sttp.client3.impl.zio.ExtendEnv

package object zio {

/** Type alias to be used as the sttp ZIO service (mainly in ZIO environment). */
type SttpClient = SttpBackend[Task, ZioStreams]

/** Sends the request. Only requests for which the method & URI are specified can be sent.
*
* @return
* An effect resulting in a`Response`, containing the body, deserialized as specified by the request (see
* `RequestT.response`), if the request was successful (1xx, 2xx, 3xx response codes), or if there was a
* protocol-level failure (4xx, 5xx response codes).
*
* A failed effect, if an exception occurred when connecting to the target host, writing the request or reading the
* response.
*
* Known exceptions are converted to one of `SttpClientException`. Other exceptions are kept unchanged.
*/
def send[T](request: Request[T, Effect[Task] with ZioStreams]): ZIO[SttpClient, Throwable, Response[T]] =
ZIO.serviceWithZIO[SttpClient](_.send(request))

/** A variant of `send` which allows the effects that are part of the response handling specification (when using
* websockets or resource-safe streaming) to use an `R` environment.
*/
def sendR[T, R](
request: Request[T, Effect[RIO[R, *]] with ZioStreams]
): ZIO[SttpClient with R, Throwable, Response[T]] =
ZIO.serviceWithZIO[SttpClient](_.extendEnv[R].send(request))
}
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,5 @@ object AsyncHttpClientZioBackend {
*/
def stub: SttpBackendStub[Task, ZioStreams with WebSockets] =
SttpBackendStub(new RIOMonadAsyncError[Any])

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package sttp.client3.asynchttpclient

import _root_.zio._
import sttp.capabilities.Effect
import sttp.capabilities.zio.ZioStreams
import sttp.client3._
import sttp.client3.impl.zio.ExtendEnv

package object zio {

/** Type alias to be used as the sttp ZIO service (mainly in ZIO environment). */
type SttpClient = SttpBackend[Task, ZioStreams]

/** Sends the request. Only requests for which the method & URI are specified can be sent.
*
* @return
* An effect resulting in a`Response`, containing the body, deserialized as specified by the request (see
* `RequestT.response`), if the request was successful (1xx, 2xx, 3xx response codes), or if there was a
* protocol-level failure (4xx, 5xx response codes).
*
* A failed effect, if an exception occurred when connecting to the target host, writing the request or reading the
* response.
*
* Known exceptions are converted to one of `SttpClientException`. Other exceptions are kept unchanged.
*/
def send[T](request: Request[T, Effect[Task] with ZioStreams]): ZIO[SttpClient, Throwable, Response[T]] =
ZIO.serviceWithZIO[SttpClient](_.send(request))

/** A variant of `send` which allows the effects that are part of the response handling specification (when using
* websockets or resource-safe streaming) to use an `R` environment.
*/
def sendR[T, R](
request: Request[T, Effect[RIO[R, *]] with ZioStreams]
): ZIO[SttpClient with R, Throwable, Response[T]] =
ZIO.serviceWithZIO[SttpClient](_.extendEnv[R].send(request))
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import org.scalatest.matchers.should.Matchers
import sttp.client3._
import sttp.client3.impl.zio._
import sttp.client3.testing.{SttpBackendStub, TestStreams}
import zio._
import zio.{Task, ZIO}

class SttpBackendStubZioTests extends AnyFlatSpec with Matchers with ScalaFutures with ZioTestBase {

"backend stub" should "cycle through responses using a single sent request" in {
// given
val backend: SttpBackendStub[Task, Any] = SttpBackendStub(new RIOMonadAsyncError[Any])
val backend: SttpBackendStub[Task, Any] = AsyncHttpClientZioBackend.stub
.whenRequestMatches(_ => true)
.thenRespondCyclic("a", "b", "c")

// when
val r = basicRequest.get(uri"http://example.org/a/b/c").send(backend)

Expand All @@ -27,7 +28,7 @@ class SttpBackendStubZioTests extends AnyFlatSpec with Matchers with ScalaFuture

it should "cycle through responses when called concurrently" in {
// given
val backend: SttpBackendStub[Task, Any] = SttpBackendStub(new RIOMonadAsyncError[Any])
val backend: SttpBackendStub[Task, Any] = AsyncHttpClientZioBackend.stub
.whenRequestMatches(_ => true)
.thenRespondCyclic("a", "b", "c")

Expand All @@ -47,7 +48,7 @@ class SttpBackendStubZioTests extends AnyFlatSpec with Matchers with ScalaFuture

it should "lift errors due to mapping with impure functions into the response monad" in {
val backend: SttpBackendStub[Task, Any] =
SttpBackendStub(new RIOMonadAsyncError[Any]).whenAnyRequest.thenRespondOk()
AsyncHttpClientZioBackend.stub.whenAnyRequest.thenRespondOk()

val error = new IllegalStateException("boom")

Expand All @@ -63,8 +64,9 @@ class SttpBackendStubZioTests extends AnyFlatSpec with Matchers with ScalaFuture
}

it should "lift errors due to mapping stream with impure functions into the response monad" in {
val backend = SttpBackendStub[Task, TestStreams](new RIOMonadAsyncError[Any]).whenAnyRequest
.thenRespond(SttpBackendStub.RawStream(List(1: Byte)))
val backend: SttpBackendStub[Task, TestStreams] =
SttpBackendStub[Task, TestStreams](new RIOMonadAsyncError[Any]).whenAnyRequest
.thenRespond(SttpBackendStub.RawStream(List(1: Byte)))

val error = new IllegalStateException("boom")

Expand Down
32 changes: 29 additions & 3 deletions docs/backends/zio.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,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:
Expand Down Expand Up @@ -96,9 +95,9 @@ This backend is build on top of [Armeria](https://armeria.dev/docs/client-http).
Armeria's [ClientFactory](https://armeria.dev/docs/client-factory) manages connections and protocol-specific properties.
Please visit [the official documentation](https://armeria.dev/docs/client-factory) to learn how to configure it.

## ZIO layers
## ZIO layers + constructors

As an alternative to effectfully or resourcefully creating backend instances, ZIO layers can be used. In this scenario, the lifecycle of a `SttpBackend` service is described by `ZLayer`s, which can be created using the `.layer`/`.layerUsingConfig`/... methods on `HttpClientZioBackend` / `ArmeriaZioBackend`.
When using constructors to express service dependencies, ZIO layers can be used to provide the `SttpBackend` instance, instead of creating one by hand. In this scenario, the lifecycle of a `SttpBackend` service is described by `ZLayer`s, which can be created using the `.layer`/`.layerUsingConfig`/... methods on `HttpClientZioBackend` / `ArmeriaZioBackend`.

The layers can be used to provide an implementation of the `SttpBackend` dependency when creating services. For example:

Expand All @@ -121,6 +120,33 @@ object MyService {
ZLayer.make[MyService](MyService.live, HttpClientZioBackend.layer())
```

## ZIO environment

As yet another alternative to effectfully or resourcefully creating backend instances, ZIO environment can be used. There are top-level `send` and `sendR` top-level methods which require a `SttpClient` to be available in the environment. The `SttpClient` itself is a type alias:

```scala
package sttp.client3.httpclient.zio
type SttpClient = SttpBackend[Task, ZioStreams with WebSockets]

// or, when using Armeria
package sttp.client3.armeria.zio
type SttpClient = SttpBackend[Task, ZioStreams]
```

The lifecycle of the `SttpClient` service is described by `ZLayer`s, which can be created using the `.layer`/`.layerUsingConfig`/... methods on `HttpClientZioBackend` / `ArmeriaZioBackend`.

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.client3._
import sttp.client3.httpclient.zio._
import zio._

val request = basicRequest.get(uri"https://httpbin.org/get")
val sent: ZIO[SttpClient, Throwable, Response[Either[String, String]]] =
send(request)
```

## Streaming

The ZIO based backends support streaming using zio-streams. The following example is using the `HttpClientZioBackend`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ object HttpClientZioBackend {
options: SttpBackendOptions = SttpBackendOptions.Default,
customizeRequest: HttpRequest => HttpRequest = identity,
customEncodingHandler: ZioEncodingHandler = PartialFunction.empty
): ZLayer[Any, Throwable, SttpBackend[Task, ZioStreams with WebSockets]] = {
): ZLayer[Any, Throwable, SttpClient] = {
ZLayer.scoped(
(for {
backend <- HttpClientZioBackend(
Expand Down Expand Up @@ -178,7 +178,7 @@ object HttpClientZioBackend {
client: HttpClient,
customizeRequest: HttpRequest => HttpRequest = identity,
customEncodingHandler: ZioEncodingHandler = PartialFunction.empty
): ZLayer[Any, Throwable, SttpBackend[Task, ZioStreams with WebSockets]] = {
): ZLayer[Any, Throwable, SttpClient] = {
ZLayer.scoped(
ZIO
.acquireRelease(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package sttp.client3.httpclient

import _root_.zio._
import sttp.capabilities.{Effect, WebSockets}
import sttp.capabilities.zio.ZioStreams
import sttp.client3._
import sttp.client3.impl.zio.ExtendEnv

package object zio {
/** Type alias to be used as the sttp ZIO service (mainly in ZIO environment). */
type SttpClient = SttpBackend[Task, ZioStreams with WebSockets]

/** Sends the request. Only requests for which the method & URI are specified can be sent.
*
* @return
* An effect resulting in a`Response`, containing the body, deserialized as specified by the request (see
* `RequestT.response`), if the request was successful (1xx, 2xx, 3xx response codes), or if there was a
* protocol-level failure (4xx, 5xx response codes).
*
* A failed effect, if an exception occurred when connecting to the target host, writing the request or reading the
* response.
*
* Known exceptions are converted to one of `SttpClientException`. Other exceptions are kept unchanged.
*/
def send[T](
request: Request[T, Effect[Task] with ZioStreams with WebSockets]
): ZIO[SttpClient, Throwable, Response[T]] =
ZIO.serviceWithZIO[SttpClient](_.send(request))

/** A variant of `send` which allows the effects that are part of the response handling specification (when using
* websockets or resource-safe streaming) to use an `R` environment.
*/
def sendR[T, R](
request: Request[T, Effect[RIO[R, *]] with ZioStreams with WebSockets]
): ZIO[SttpClient with R, Throwable, Response[T]] =
ZIO.serviceWithZIO[SttpClient](_.extendEnv[R].send(request))
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ class SttpBackendStubZioTests extends AnyFlatSpec with Matchers with ScalaFuture

"backend stub" should "cycle through responses using a single sent request" in {
// given
val backend: SttpBackendStub[Task, Any] = SttpBackendStub(new RIOMonadAsyncError[Any])
val backend: SttpBackendStub[Task, Any] = HttpClientZioBackend.stub
.whenRequestMatches(_ => true)
.thenRespondCyclic("a", "b", "c")

// when
val r = basicRequest.get(uri"http://example.org/a/b/c").send(backend)

Expand All @@ -27,7 +28,7 @@ class SttpBackendStubZioTests extends AnyFlatSpec with Matchers with ScalaFuture

it should "cycle through responses when called concurrently" in {
// given
val backend: SttpBackendStub[Task, Any] = SttpBackendStub(new RIOMonadAsyncError[Any])
val backend: SttpBackendStub[Task, Any] = HttpClientZioBackend.stub
.whenRequestMatches(_ => true)
.thenRespondCyclic("a", "b", "c")

Expand All @@ -47,7 +48,7 @@ class SttpBackendStubZioTests extends AnyFlatSpec with Matchers with ScalaFuture

it should "lift errors due to mapping with impure functions into the response monad" in {
val backend: SttpBackendStub[Task, Any] =
SttpBackendStub(new RIOMonadAsyncError[Any]).whenAnyRequest.thenRespondOk()
HttpClientZioBackend.stub.whenAnyRequest.thenRespondOk()

val error = new IllegalStateException("boom")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,22 @@ package sttp.client3.examples
import io.circe.generic.auto._
import sttp.client3._
import sttp.client3.circe._
import sttp.client3.httpclient.zio.HttpClientZioBackend
import sttp.client3.httpclient.zio.{HttpClientZioBackend, send}
import zio._

object GetAndParseJsonZioCirce extends ZIOAppDefault {

override def run = {

case class HttpBinResponse(origin: String, headers: Map[String, String])

val request = basicRequest
.get(uri"https://httpbin.org/get")
.response(asJson[HttpBinResponse])

// create a description of a program, which requires SttpClient dependency in the environment
def sendAndPrint(backend: SttpBackend[Task, Any]): Task[Unit] = for {
response <- backend.send(request)
for {
response <- send(request)
_ <- Console.printLine(s"Got response code: ${response.code}")
_ <- Console.printLine(response.body.toString)
} yield ()

// provide an implementation for the SttpClient dependency
HttpClientZioBackend.scoped().flatMap(sendAndPrint)
}
}.provideLayer(HttpClientZioBackend.layer())
}
40 changes: 16 additions & 24 deletions examples/src/main/scala/sttp/client3/examples/StreamZio.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,29 @@ package sttp.client3.examples

import sttp.capabilities.zio.ZioStreams
import sttp.client3._
import sttp.client3.httpclient.zio.HttpClientZioBackend
import zio.Console._
import zio._
import zio.stream._
import sttp.client3.httpclient.zio.{HttpClientZioBackend, SttpClient, send}

object StreamZio extends ZIOAppDefault {
def streamRequestBody(backend: SttpBackend[Task, ZioStreams]): Task[Unit] = {
val stream: Stream[Throwable, Byte] = ZStream("Hello, world".getBytes: _*)

backend
.send(
basicRequest
.streamBody(ZioStreams)(stream)
.post(uri"https://httpbin.org/post")
)
.flatMap { response => printLine(s"RECEIVED:\n${response.body}") }
def streamRequestBody: RIO[SttpClient, Unit] = {
val stream: Stream[Throwable, Byte] = ZStream("Hello, world".getBytes.toIndexedSeq: _*)
send(
basicRequest
.streamBody(ZioStreams)(stream)
.post(uri"https://httpbin.org/post")
).flatMap { response => printLine(s"RECEIVED:\n${response.body}") }
}

def streamResponseBody(backend: SttpBackend[Task, ZioStreams]): Task[Unit] = {
backend
.send(
basicRequest
.body("I want a stream!")
.post(uri"https://httpbin.org/post")
.response(asStreamAlways(ZioStreams)(_.via(ZPipeline.utf8Decode).runFold("")(_ + _)))
)
.flatMap { response => printLine(s"RECEIVED:\n${response.body}") }
}
def streamResponseBody: RIO[SttpClient, Unit] =
send(
basicRequest
.body("I want a stream!")
.post(uri"https://httpbin.org/post")
.response(asStreamAlways(ZioStreams)(_.via(ZPipeline.utf8Decode).runFold("")(_ + _)))
).flatMap { response => printLine(s"RECEIVED:\n${response.body}") }

override def run =
HttpClientZioBackend.scoped().flatMap { backend =>
streamRequestBody(backend) *> streamResponseBody(backend)
}
(streamRequestBody *> streamResponseBody).provide(HttpClientZioBackend.layer())
}

0 comments on commit 362019a

Please sign in to comment.