Skip to content

Commit afd3077

Browse files
committed
Migrate fs2 & partly zio backend
1 parent 4a00a87 commit afd3077

23 files changed

Lines changed: 257 additions & 409 deletions

File tree

async-http-client-backend/cats/src/main/scala/sttp/client/asynchttpclient/cats/AsyncHttpClientCatsBackend.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class AsyncHttpClientCatsBackend[F[_]: Concurrent: ContextShift] private (
6161
override protected def streamToPublisher(s: Nothing): Publisher[ByteBuf] = s // nothing is everything
6262
}
6363

64-
override protected def createAsyncQueue[T]: AsyncQueue[F, T] =
64+
override protected def createAsyncQueue[T]: F[AsyncQueue[F, T]] =
6565
throw new IllegalStateException("Web sockets are not supported!")
6666
}
6767

async-http-client-backend/fs2/src/main/scala/sttp/client/asynchttpclient/fs2/AsyncHttpClientFs2Backend.scala

Lines changed: 87 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,30 @@ import java.nio.ByteBuffer
66
import cats.effect._
77
import cats.effect.implicits._
88
import cats.implicits._
9-
import fs2.{Chunk, Stream}
9+
import fs2.concurrent.InspectableQueue
10+
import fs2.{Chunk, Pipe, Stream}
1011
import fs2.interop.reactivestreams._
1112
import io.netty.buffer.{ByteBuf, Unpooled}
1213
import org.asynchttpclient.{Request => _, Response => _, _}
1314
import org.reactivestreams.Publisher
14-
import sttp.client.asynchttpclient.{AsyncHttpClientBackend, WebSocketHandler}
15+
import sttp.client.asynchttpclient.{AsyncHttpClientBackend, BodyFromAHC, BodyToAHC}
1516
import sttp.client.impl.cats.CatsMonadAsyncError
16-
import sttp.client.impl.fs2.Fs2Streams
17+
import sttp.client.impl.fs2.{Fs2AsyncQueue, Fs2Streams, Fs2WebSockets}
1718
import sttp.client.internal._
19+
import sttp.client.monad.MonadAsyncError
1820
import sttp.client.testing.SttpBackendStub
21+
import sttp.client.ws.WebSocket
22+
import sttp.client.ws.internal.AsyncQueue
1923
import sttp.client.{FollowRedirectsBackend, SttpBackend, SttpBackendOptions, _}
24+
import sttp.model.ws.WebSocketFrame
2025

2126
import scala.concurrent.ExecutionContext
2227

2328
class AsyncHttpClientFs2Backend[F[_]: ConcurrentEffect: ContextShift] private (
2429
asyncHttpClient: AsyncHttpClient,
2530
closeClient: Boolean,
26-
customizeRequest: BoundRequestBuilder => BoundRequestBuilder
31+
customizeRequest: BoundRequestBuilder => BoundRequestBuilder,
32+
webSocketBufferCapacity: Option[Int]
2733
) extends AsyncHttpClientBackend[F, Fs2Streams[F], Fs2Streams[F] with WebSockets](
2834
asyncHttpClient,
2935
new CatsMonadAsyncError,
@@ -33,89 +39,124 @@ class AsyncHttpClientFs2Backend[F[_]: ConcurrentEffect: ContextShift] private (
3339

3440
override val streams: Fs2Streams[F] = Fs2Streams[F]
3541

36-
override def send[T, R >: Fs2Streams[F] with sttp.client.Effect[F]](r: Request[T, R]): F[Response[T]] = {
42+
override def send[T, R >: sttp.client.Effect[F] with Fs2Streams[F] with WebSockets](
43+
r: Request[T, R]
44+
): F[Response[T]] = {
3745
super.send(r).guarantee(implicitly[ContextShift[F]].shift)
3846
}
3947

40-
override def openWebsocket[T, WS_RESULT, R >: Fs2Streams[F] with sttp.client.Effect[F]](
41-
r: Request[T, R],
42-
handler: WebSocketHandler[WS_RESULT]
43-
): F[WebSocketResponse[WS_RESULT]] = super.openWebsocket(r, handler).guarantee(ContextShift[F].shift)
44-
45-
override protected def streamBodyToPublisher(s: Stream[F, Byte]): Publisher[ByteBuf] =
46-
s.chunks.map(c => Unpooled.wrappedBuffer(c.toArray)).toUnicastPublisher
47-
48-
override protected def publisherToStreamBody(p: Publisher[ByteBuffer]): Stream[F, Byte] =
49-
p.toStream[F].flatMap(buf => Stream.chunk(Chunk.byteBuffer(buf)))
50-
51-
override protected def publisherToBytes(p: Publisher[ByteBuffer]): F[Array[Byte]] = {
52-
p.toStream[F]
53-
.compile
54-
.fold(ByteBuffer.allocate(0))(concatByteBuffers)
55-
.map(_.array())
48+
override protected val bodyFromAHC: BodyFromAHC[F, Fs2Streams[F]] =
49+
new BodyFromAHC[F, Fs2Streams[F]] {
50+
override val streams: Fs2Streams[F] = Fs2Streams[F]
51+
override implicit val monad: MonadAsyncError[F] = new CatsMonadAsyncError
52+
53+
override def publisherToStream(p: Publisher[ByteBuffer]): Stream[F, Byte] =
54+
p.toStream[F].flatMap(buf => Stream.chunk(Chunk.byteBuffer(buf)))
55+
56+
override def publisherToBytes(p: Publisher[ByteBuffer]): F[Array[Byte]] = {
57+
p.toStream[F]
58+
.compile
59+
.fold(ByteBuffer.allocate(0))(concatByteBuffers)
60+
.map(_.array())
61+
}
62+
63+
override def publisherToFile(p: Publisher[ByteBuffer], f: File): F[Unit] = {
64+
p.toStream[F]
65+
.flatMap(b => Stream.emits(b.array()))
66+
.through(fs2.io.file.writeAll(f.toPath, Blocker.liftExecutionContext(ExecutionContext.global)))
67+
.compile
68+
.drain
69+
}
70+
71+
override def compileWebSocketPipe(
72+
ws: WebSocket[F],
73+
pipe: Pipe[F, WebSocketFrame.Data[_], WebSocketFrame]
74+
): F[Unit] = Fs2WebSockets.handleThroughPipe(ws)(pipe)
75+
}
76+
77+
override protected val bodyToAHC: BodyToAHC[F, Fs2Streams[F]] = new BodyToAHC[F, Fs2Streams[F]] {
78+
override val streams: Fs2Streams[F] = Fs2Streams[F]
79+
80+
override protected def streamToPublisher(s: Stream[F, Byte]): Publisher[ByteBuf] =
81+
s.chunks.map(c => Unpooled.wrappedBuffer(c.toArray)).toUnicastPublisher
5682
}
5783

58-
override protected def publisherToFile(p: Publisher[ByteBuffer], f: File): F[Unit] = {
59-
p.toStream[F]
60-
.flatMap(b => Stream.emits(b.array()))
61-
.through(fs2.io.file.writeAll(f.toPath, Blocker.liftExecutionContext(ExecutionContext.global)))
62-
.compile
63-
.drain
64-
}
84+
override protected def createAsyncQueue[T]: F[AsyncQueue[F, T]] =
85+
webSocketBufferCapacity
86+
.fold(InspectableQueue.unbounded[F, T])(InspectableQueue.bounded)
87+
.map(new Fs2AsyncQueue(_))
6588
}
6689

6790
object AsyncHttpClientFs2Backend {
6891
private def apply[F[_]: ConcurrentEffect: ContextShift](
6992
asyncHttpClient: AsyncHttpClient,
7093
closeClient: Boolean,
71-
customizeRequest: BoundRequestBuilder => BoundRequestBuilder
94+
customizeRequest: BoundRequestBuilder => BoundRequestBuilder,
95+
webSocketBufferCapacity: Option[Int]
7296
): SttpBackend[F, Fs2Streams[F] with WebSockets] =
73-
new FollowRedirectsBackend(new AsyncHttpClientFs2Backend(asyncHttpClient, closeClient, customizeRequest))
97+
new FollowRedirectsBackend(
98+
new AsyncHttpClientFs2Backend(asyncHttpClient, closeClient, customizeRequest, webSocketBufferCapacity)
99+
)
74100

75101
def apply[F[_]: ConcurrentEffect: ContextShift](
76102
options: SttpBackendOptions = SttpBackendOptions.Default,
77-
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity
103+
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity,
104+
webSocketBufferCapacity: Option[Int] = AsyncHttpClientBackend.DefaultWebSocketBufferCapacity
78105
): F[SttpBackend[F, Fs2Streams[F] with WebSockets]] =
79106
implicitly[Sync[F]]
80-
.delay(apply[F](AsyncHttpClientBackend.defaultClient(options), closeClient = true, customizeRequest))
107+
.delay(
108+
apply[F](
109+
AsyncHttpClientBackend.defaultClient(options),
110+
closeClient = true,
111+
customizeRequest,
112+
webSocketBufferCapacity
113+
)
114+
)
81115

82116
/**
83117
* Makes sure the backend is closed after usage.
84118
*/
85119
def resource[F[_]: ConcurrentEffect: ContextShift](
86120
options: SttpBackendOptions = SttpBackendOptions.Default,
87-
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity
121+
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity,
122+
webSocketBufferCapacity: Option[Int] = AsyncHttpClientBackend.DefaultWebSocketBufferCapacity
88123
): Resource[F, SttpBackend[F, Fs2Streams[F] with WebSockets]] =
89-
Resource.make(apply(options, customizeRequest))(_.close())
124+
Resource.make(apply(options, customizeRequest, webSocketBufferCapacity))(_.close())
90125

91126
def usingConfig[F[_]: ConcurrentEffect: ContextShift](
92127
cfg: AsyncHttpClientConfig,
93-
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity
128+
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity,
129+
webSocketBufferCapacity: Option[Int] = AsyncHttpClientBackend.DefaultWebSocketBufferCapacity
94130
): F[SttpBackend[F, Fs2Streams[F] with WebSockets]] =
95-
implicitly[Sync[F]].delay(apply[F](new DefaultAsyncHttpClient(cfg), closeClient = true, customizeRequest))
131+
implicitly[Sync[F]].delay(
132+
apply[F](new DefaultAsyncHttpClient(cfg), closeClient = true, customizeRequest, webSocketBufferCapacity)
133+
)
96134

97135
/**
98136
* Makes sure the backend is closed after usage.
99137
*/
100138
def resourceUsingConfig[F[_]: ConcurrentEffect: ContextShift](
101139
cfg: AsyncHttpClientConfig,
102-
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity
140+
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity,
141+
webSocketBufferCapacity: Option[Int] = AsyncHttpClientBackend.DefaultWebSocketBufferCapacity
103142
): Resource[F, SttpBackend[F, Fs2Streams[F] with WebSockets]] =
104-
Resource.make(usingConfig(cfg, customizeRequest))(_.close())
143+
Resource.make(usingConfig(cfg, customizeRequest, webSocketBufferCapacity))(_.close())
105144

106145
/**
107146
* @param updateConfig A function which updates the default configuration (created basing on `options`).
108147
*/
109148
def usingConfigBuilder[F[_]: ConcurrentEffect: ContextShift](
110149
updateConfig: DefaultAsyncHttpClientConfig.Builder => DefaultAsyncHttpClientConfig.Builder,
111150
options: SttpBackendOptions = SttpBackendOptions.Default,
112-
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity
151+
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity,
152+
webSocketBufferCapacity: Option[Int] = AsyncHttpClientBackend.DefaultWebSocketBufferCapacity
113153
): F[SttpBackend[F, Fs2Streams[F] with WebSockets]] =
114154
implicitly[Sync[F]].delay(
115155
AsyncHttpClientFs2Backend[F](
116156
AsyncHttpClientBackend.clientWithModifiedOptions(options, updateConfig),
117157
closeClient = true,
118-
customizeRequest
158+
customizeRequest,
159+
webSocketBufferCapacity
119160
)
120161
)
121162

@@ -126,15 +167,17 @@ object AsyncHttpClientFs2Backend {
126167
def resourceUsingConfigBuilder[F[_]: ConcurrentEffect: ContextShift](
127168
updateConfig: DefaultAsyncHttpClientConfig.Builder => DefaultAsyncHttpClientConfig.Builder,
128169
options: SttpBackendOptions = SttpBackendOptions.Default,
129-
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity
170+
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity,
171+
webSocketBufferCapacity: Option[Int] = AsyncHttpClientBackend.DefaultWebSocketBufferCapacity
130172
): Resource[F, SttpBackend[F, Fs2Streams[F] with WebSockets]] =
131-
Resource.make(usingConfigBuilder(updateConfig, options, customizeRequest))(_.close())
173+
Resource.make(usingConfigBuilder(updateConfig, options, customizeRequest, webSocketBufferCapacity))(_.close())
132174

133175
def usingClient[F[_]: ConcurrentEffect: ContextShift](
134176
client: AsyncHttpClient,
135-
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity
177+
customizeRequest: BoundRequestBuilder => BoundRequestBuilder = identity,
178+
webSocketBufferCapacity: Option[Int] = AsyncHttpClientBackend.DefaultWebSocketBufferCapacity
136179
): SttpBackend[F, Fs2Streams[F] with WebSockets] =
137-
apply[F](client, closeClient = false, customizeRequest)
180+
apply[F](client, closeClient = false, customizeRequest, webSocketBufferCapacity)
138181

139182
/**
140183
* Create a stub backend for testing, which uses the `F` response wrapper, and supports `Stream[F, ByteBuffer]`

async-http-client-backend/fs2/src/main/scala/sttp/client/asynchttpclient/fs2/Fs2WebSocketHandler.scala

Lines changed: 0 additions & 51 deletions
This file was deleted.

async-http-client-backend/fs2/src/test/scala/sttp/client/asynchttpclient/fs2/AsyncHttpClientFs2HttpStreamingTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package sttp.client.asynchttpclient.fs2
22

33
import cats.effect.{ContextShift, IO}
44
import sttp.client.impl.fs2.{Fs2StreamingTest, Fs2Streams}
5-
import sttp.client.{NothingT, SttpBackend}
5+
import sttp.client.SttpBackend
66

77
import scala.concurrent.ExecutionContext
88

99
class AsyncHttpClientFs2HttpStreamingTest extends Fs2StreamingTest {
1010
private implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
1111

12-
override implicit val backend: SttpBackend[IO, Fs2Streams[IO], NothingT] =
12+
override implicit val backend: SttpBackend[IO, Fs2Streams[IO]] =
1313
AsyncHttpClientFs2Backend[IO]().unsafeRunSync()
1414
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package sttp.client.asynchttpclient.fs2
22

33
import cats.effect.IO
4-
import sttp.client.{NothingT, SttpBackend}
4+
import sttp.client.SttpBackend
55
import sttp.client.impl.cats.CatsTestBase
66
import sttp.client.testing.HttpTest
77

88
class AsyncHttpClientFs2HttpTest extends HttpTest[IO] with CatsTestBase {
9-
override implicit val backend: SttpBackend[IO, Any, NothingT] = AsyncHttpClientFs2Backend[IO]().unsafeRunSync()
9+
override implicit val backend: SttpBackend[IO, Any] = AsyncHttpClientFs2Backend[IO]().unsafeRunSync()
1010

1111
override def throwsExceptionOnUnsupportedEncoding = false
1212
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package sttp.client.asynchttpclient.fs2
2+
3+
import cats.effect.IO
4+
import cats.implicits._
5+
import sttp.client._
6+
import sttp.client.asynchttpclient.AsyncHttpClientWebSocketTest
7+
import sttp.client.impl.cats.CatsTestBase
8+
import sttp.client.impl.fs2.Fs2Streams
9+
import sttp.model.ws.WebSocketFrame
10+
11+
import scala.concurrent.duration._
12+
13+
class AsyncHttpClientFs2WebSocketTest extends AsyncHttpClientWebSocketTest[IO, Fs2Streams[IO]] with CatsTestBase {
14+
implicit val backend: SttpBackend[IO, Fs2Streams[IO] with WebSockets] =
15+
AsyncHttpClientFs2Backend[IO]().unsafeRunSync()
16+
17+
override val streams: Fs2Streams[IO] = new Fs2Streams[IO] {}
18+
19+
override def functionToPipe(
20+
f: WebSocketFrame.Data[_] => WebSocketFrame
21+
): fs2.Pipe[IO, WebSocketFrame.Data[_], WebSocketFrame] = _.map(f)
22+
23+
override def eventually[T](interval: FiniteDuration, attempts: Int)(f: => IO[T]): IO[T] = {
24+
def tryWithCounter(i: Int): IO[T] = {
25+
(IO.sleep(interval) >> f).recoverWith {
26+
case _: Exception if i < attempts => tryWithCounter(i + 1)
27+
}
28+
}
29+
tryWithCounter(0)
30+
}
31+
}

async-http-client-backend/fs2/src/test/scala/sttp/client/asynchttpclient/fs2/AsyncHttpClientHighLevelFs2WebsocketTest.scala

Lines changed: 0 additions & 25 deletions
This file was deleted.

async-http-client-backend/fs2/src/test/scala/sttp/client/asynchttpclient/fs2/AsyncHttpClientLowLevelFs2WebsocketTest.scala

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)