Skip to content

Commit d656637

Browse files
committed
#1050: http4s server and client migrated to cats-effect 3 / http4s 1.x, some other modules partially as well
1 parent 6453fc3 commit d656637

File tree

40 files changed

+1047
-197
lines changed

40 files changed

+1047
-197
lines changed

build.sbt

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq(
3434
// slow down for CI
3535
Test / parallelExecution := false,
3636
// remove false alarms about unused implicit definitions in macros
37-
scalacOptions += "-Ywarn-macros:after"
37+
scalacOptions += "-Ywarn-macros:after",
38+
evictionErrorLevel := Level.Info
3839
)
3940

4041
val commonJvmSettings: Seq[Def.Setting[_]] = commonSettings
@@ -137,12 +138,12 @@ lazy val rootProject = (project in file("."))
137138
.settings(
138139
publishArtifact := false,
139140
name := "tapir",
140-
testJVM := (test in Test).all(filterProject(p => !p.contains("JS"))).value,
141-
testJS := (test in Test).all(filterProject(_.contains("JS"))).value,
142-
testDocs := (test in Test).all(filterProject(p => p.contains("Docs") || p.contains("openapi") || p.contains("asyncapi"))).value,
143-
testServers := (test in Test).all(filterProject(p => p.contains("Server"))).value,
144-
testClients := (test in Test).all(filterProject(p => p.contains("Client"))).value,
145-
testOther := (test in Test)
141+
testJVM := (Test / test).all(filterProject(p => !p.contains("JS"))).value,
142+
testJS := (Test / test).all(filterProject(_.contains("JS"))).value,
143+
testDocs := (Test / test).all(filterProject(p => p.contains("Docs") || p.contains("openapi") || p.contains("asyncapi"))).value,
144+
testServers := (Test / test).all(filterProject(p => p.contains("Server"))).value,
145+
testClients := (Test / test).all(filterProject(p => p.contains("Client"))).value,
146+
testOther := (Test / test)
146147
.all(
147148
filterProject(p =>
148149
!p.contains("Server") && !p.contains("Client") && !p.contains("Docs") && !p.contains("openapi") && !p.contains("asyncapi")
@@ -155,14 +156,14 @@ lazy val rootProject = (project in file("."))
155156
// start a test server before running tests of a client interpreter; this is required both for JS tests run inside a
156157
// nodejs/browser environment, as well as for JVM tests where akka-http isn't available (e.g. dotty).
157158
val clientTestServerSettings = Seq(
158-
test in Test := (test in Test)
159-
.dependsOn(startClientTestServer in clientTestServer2_13)
159+
Test / test := (Test / test)
160+
.dependsOn(clientTestServer2_13 / startClientTestServer)
160161
.value,
161-
testOnly in Test := (testOnly in Test)
162-
.dependsOn(startClientTestServer in clientTestServer2_13)
162+
Test / testOnly := (Test / testOnly)
163+
.dependsOn(clientTestServer2_13 / startClientTestServer)
163164
.evaluated,
164-
testOptions in Test += Tests.Setup(() => {
165-
val port = (clientTestServerPort in clientTestServer2_13).value
165+
Test / testOptions += Tests.Setup(() => {
166+
val port = (clientTestServer2_13 / clientTestServerPort).value
166167
PollingUtils.waitUntilServerAvailable(new URL(s"http://localhost:$port"))
167168
})
168169
)
@@ -171,16 +172,16 @@ lazy val clientTestServer = (projectMatrix in file("client/testserver"))
171172
.settings(commonJvmSettings)
172173
.settings(
173174
name := "testing-server",
174-
skip in publish := true,
175+
publish / skip := true,
175176
libraryDependencies ++= loggerDependencies ++ Seq(
176177
"org.http4s" %% "http4s-dsl" % Versions.http4s,
177178
"org.http4s" %% "http4s-blaze-server" % Versions.http4s,
178179
"org.http4s" %% "http4s-circe" % Versions.http4s
179180
),
180181
// the test server needs to be started before running any client tests
181-
mainClass in reStart := Some("sttp.tapir.client.tests.HttpServer"),
182-
reStartArgs in reStart := Seq(s"${(clientTestServerPort in Test).value}"),
183-
fullClasspath in reStart := (fullClasspath in Test).value,
182+
reStart / mainClass := Some("sttp.tapir.client.tests.HttpServer"),
183+
reStart / reStartArgs := Seq(s"${(Test / clientTestServerPort).value}"),
184+
reStart / fullClasspath := (reStart / fullClasspath).value,
184185
clientTestServerPort := 51823,
185186
startClientTestServer := reStart.toTask("").value
186187
)
@@ -205,17 +206,17 @@ lazy val core: ProjectMatrix = (projectMatrix in file("core"))
205206
scalaTestPlusScalaCheck.value % Test,
206207
"com.47deg" %%% "scalacheck-toolbox-datetime" % "0.5.0" % Test
207208
),
208-
unmanagedSourceDirectories in Compile += {
209-
val sourceDir = (sourceDirectory in Compile).value
209+
Compile / unmanagedSourceDirectories += {
210+
val sourceDir = (Compile / sourceDirectory).value
210211
CrossVersion.partialVersion(scalaVersion.value) match {
211212
case Some((2, n)) if n >= 13 => sourceDir / "scala-2.13+"
212213
case _ => sourceDir / "scala-2.13-"
213214
}
214215
},
215216
// Until https://youtrack.jetbrains.com/issue/SCL-18636 is fixed and IntelliJ properly imports projects with
216217
// generated sources, they are explicitly added to git. See also below: commented out plugin.
217-
unmanagedSourceDirectories in Compile += {
218-
(sourceDirectory in Compile).value / "boilerplate-gen"
218+
Compile / unmanagedSourceDirectories += {
219+
(Compile / sourceDirectory).value / "boilerplate-gen"
219220
}
220221
)
221222
.jvmPlatform(
@@ -748,11 +749,7 @@ lazy val finatraServerCats: ProjectMatrix =
748749
.settings(commonJvmSettings)
749750
.settings(
750751
name := "tapir-finatra-server-cats",
751-
libraryDependencies ++= Seq(
752-
"org.typelevel" %% "cats-effect" % Versions.catsEffect,
753-
"io.catbird" %% "catbird-finagle" % Versions.catbird,
754-
"io.catbird" %% "catbird-effect" % Versions.catbird
755-
)
752+
libraryDependencies ++= Seq("org.typelevel" %% "cats-effect" % Versions.catsEffect)
756753
)
757754
.jvmPlatform(scalaVersions = scala2_12Versions)
758755
.dependsOn(finatraServer % "compile->compile;test->test", serverTests % Test)

client/http4s-client/src/main/scala/sttp/tapir/client/http4s/EndpointToHttp4sClient.scala

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package sttp.tapir.client.http4s
22

33
import cats.Applicative
4-
import cats.effect.{Blocker, ContextShift, Effect, Sync}
4+
import cats.effect.Async
55
import cats.implicits._
66
import fs2.Chunk
7+
import fs2.io.file.Files
78
import org.http4s._
89
import org.http4s.headers.`Content-Type`
10+
import org.typelevel.ci.CIString
911
import sttp.capabilities.Streams
1012
import sttp.capabilities.fs2.Fs2Streams
1113
import sttp.model.ResponseMetadata
@@ -29,9 +31,9 @@ import sttp.tapir.{
2931
import java.io.{ByteArrayInputStream, File, InputStream}
3032
import java.nio.ByteBuffer
3133

32-
private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Http4sClientOptions) {
34+
private[http4s] class EndpointToHttp4sClient(clientOptions: Http4sClientOptions) {
3335

34-
def toHttp4sRequest[I, E, O, R, F[_]: ContextShift: Effect](
36+
def toHttp4sRequest[I, E, O, R, F[_]: Async](
3537
e: Endpoint[I, E, O, R],
3638
baseUriStr: Option[String]
3739
): I => (Request[F], Response[F] => F[DecodeResult[Either[E, O]]]) = { params =>
@@ -46,7 +48,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
4648
(request, responseParser)
4749
}
4850

49-
def toHttp4sRequestUnsafe[I, E, O, R, F[_]: ContextShift: Effect](
51+
def toHttp4sRequestUnsafe[I, E, O, R, F[_]: Async](
5052
e: Endpoint[I, E, O, R],
5153
baseUriStr: Option[String]
5254
): I => (Request[F], Response[F] => F[Either[E, O]]) = { params =>
@@ -63,7 +65,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
6365
}
6466

6567
@scala.annotation.tailrec
66-
private def setInputParams[I, F[_]: ContextShift: Effect](
68+
private def setInputParams[I, F[_]: Async](
6769
input: EndpointInput[I],
6870
params: Params,
6971
req: Request[F]
@@ -95,12 +97,12 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
9597
case EndpointIO.StreamBodyWrapper(StreamBodyIO(streams, _, _, _)) =>
9698
setStreamingBody(streams)(value.asInstanceOf[streams.BinaryStream], req)
9799
case EndpointIO.Header(name, codec, _) =>
98-
val headers = codec.encode(value).map(value => Header(name, value))
100+
val headers = codec.encode(value).map(value => Header.Raw(CIString(name), value): Header.ToRaw)
99101
req.putHeaders(headers: _*)
100102
case EndpointIO.Headers(codec, _) =>
101-
val headers = codec.encode(value).map(h => Header(h.name, h.value))
103+
val headers = codec.encode(value).map(h => Header.Raw(CIString(h.name), h.value): Header.ToRaw)
102104
req.putHeaders(headers: _*)
103-
case EndpointIO.FixedHeader(h, _, _) => req.putHeaders(Header(h.name, h.value))
105+
case EndpointIO.FixedHeader(h, _, _) => req.putHeaders(Header.Raw(CIString(h.name), h.value))
104106
case EndpointInput.ExtractFromRequest(_, _) => req // ignoring
105107
case a: EndpointInput.Auth[_] => setInputParams(a.input, params, req)
106108
case EndpointInput.Pair(left, right, _, split) => handleInputPair(left, right, params, split, req)
@@ -110,7 +112,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
110112
}
111113
}
112114

113-
private def setBody[R, T, CF <: CodecFormat, F[_]: ContextShift: Effect](
115+
private def setBody[R, T, CF <: CodecFormat, F[_]: Async](
114116
value: T,
115117
bodyType: RawBodyType[R],
116118
codec: Codec[R, T, CF],
@@ -128,10 +130,10 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
128130
val entityEncoder = EntityEncoder.chunkEncoder[F].contramap(Chunk.byteBuffer)
129131
req.withEntity(encoded.asInstanceOf[ByteBuffer])(entityEncoder)
130132
case RawBodyType.InputStreamBody =>
131-
val entityEncoder = EntityEncoder.inputStreamEncoder[F, InputStream](blocker)
133+
val entityEncoder = EntityEncoder.inputStreamEncoder[F, InputStream]
132134
req.withEntity(Applicative[F].pure(encoded.asInstanceOf[InputStream]))(entityEncoder)
133135
case RawBodyType.FileBody =>
134-
val entityEncoder = EntityEncoder.fileEncoder[F](blocker)
136+
val entityEncoder = EntityEncoder.fileEncoder[F]
135137
req.withEntity(encoded.asInstanceOf[File])(entityEncoder)
136138
case _: RawBodyType.MultipartBody =>
137139
throw new IllegalArgumentException("Multipart body isn't supported yet")
@@ -151,7 +153,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
151153
throw new IllegalArgumentException("Only Fs2Streams streaming is supported")
152154
}
153155

154-
private def handleInputPair[I, F[_]: ContextShift: Effect](
156+
private def handleInputPair[I, F[_]: Async](
155157
left: EndpointInput[_],
156158
right: EndpointInput[_],
157159
params: Params,
@@ -164,15 +166,15 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
164166
setInputParams(right.asInstanceOf[EndpointInput[Any]], rightParams, req2)
165167
}
166168

167-
private def handleMapped[II, T, F[_]: ContextShift: Effect](
169+
private def handleMapped[II, T, F[_]: Async](
168170
tuple: EndpointInput[II],
169171
codec: Mapping[T, II],
170172
params: Params,
171173
req: Request[F]
172174
): Request[F] =
173175
setInputParams(tuple.asInstanceOf[EndpointInput[Any]], ParamsAsAny(codec.encode(params.asAny.asInstanceOf[II])), req)
174176

175-
private def parseHttp4sResponse[I, E, O, R, F[_]: Sync: ContextShift](
177+
private def parseHttp4sResponse[I, E, O, R, F[_]: Async](
176178
e: Endpoint[I, E, O, R]
177179
): Response[F] => F[DecodeResult[Either[E, O]]] = { response =>
178180
val code = sttp.model.StatusCode(response.status.code)
@@ -181,15 +183,15 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
181183
val output = if (code.isSuccess) e.output else e.errorOutput
182184

183185
// headers with cookies
184-
val headers = response.headers.toList.map(h => sttp.model.Header(h.name.toString(), h.value)).toVector
186+
val headers = response.headers.headers.map(h => sttp.model.Header(h.name.toString, h.value)).toVector
185187

186188
parser(response).map { responseBody =>
187189
val params = clientOutputParams(output, responseBody, ResponseMetadata(code, response.status.reason, headers))
188190
params.map(_.asAny).map(p => if (code.isSuccess) Right(p.asInstanceOf[O]) else Left(p.asInstanceOf[E]))
189191
}
190192
}
191193

192-
private def responseFromOutput[F[_]: Sync: ContextShift](out: EndpointOutput[_]): Response[F] => F[Any] = { response =>
194+
private def responseFromOutput[F[_]: Async](out: EndpointOutput[_]): Response[F] => F[Any] = { response =>
193195
bodyIsStream(out) match {
194196
case Some(streams) =>
195197
streams match {
@@ -211,7 +213,7 @@ private[http4s] class EndpointToHttp4sClient(blocker: Blocker, clientOptions: Ht
211213
response.body.compile.toVector.map(_.toArray).map(new ByteArrayInputStream(_)).map(_.asInstanceOf[Any])
212214
case RawBodyType.FileBody =>
213215
val file = clientOptions.createFile()
214-
response.body.through(fs2.io.file.writeAll(file.toPath, blocker)).compile.drain.map(_ => file.asInstanceOf[Any])
216+
response.body.through(Files[F].writeAll(file.toPath)).compile.drain.map(_ => file.asInstanceOf[Any])
215217
case RawBodyType.MultipartBody(_, _) => throw new IllegalArgumentException("Multipart bodies aren't supported in responses")
216218
}
217219
.getOrElse[F[Any]](((): Any).pure[F])
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package sttp.tapir.client.http4s
22

3-
import cats.effect.{Blocker, ContextShift, Effect}
3+
import cats.effect.Async
44
import org.http4s.{Request, Response}
55
import sttp.tapir.{DecodeResult, Endpoint}
66

7-
abstract class Http4sClientInterpreter[F[_]: ContextShift: Effect] {
7+
abstract class Http4sClientInterpreter[F[_]: Async] {
88

99
/** Interprets the endpoint as a client call, using the given `baseUri` as the starting point to create the target
1010
* uri. If `baseUri` is not provided, the request will be a relative one.
@@ -15,10 +15,9 @@ abstract class Http4sClientInterpreter[F[_]: ContextShift: Effect] {
1515
* - a response parser that extracts the expected entity from the received `org.http4s.Response[F]`.
1616
*/
1717
def toRequest[I, E, O, R](e: Endpoint[I, E, O, R], baseUri: Option[String])(implicit
18-
blocker: Blocker,
1918
clientOptions: Http4sClientOptions
2019
): I => (Request[F], Response[F] => F[DecodeResult[Either[E, O]]]) =
21-
new EndpointToHttp4sClient(blocker, clientOptions).toHttp4sRequest[I, E, O, R, F](e, baseUri)
20+
new EndpointToHttp4sClient(clientOptions).toHttp4sRequest[I, E, O, R, F](e, baseUri)
2221

2322
/** Interprets the endpoint as a client call, using the given `baseUri` as the starting point to create the target
2423
* uri. If `baseUri` is not provided, the request will be a relative one.
@@ -29,12 +28,11 @@ abstract class Http4sClientInterpreter[F[_]: ContextShift: Effect] {
2928
* - a response parser that extracts the expected entity from the received `org.http4s.Response[F]`.
3029
*/
3130
def toRequestUnsafe[I, E, O, R](e: Endpoint[I, E, O, R], baseUri: Option[String])(implicit
32-
blocker: Blocker,
3331
clientOptions: Http4sClientOptions
3432
): I => (Request[F], Response[F] => F[Either[E, O]]) =
35-
new EndpointToHttp4sClient(blocker, clientOptions).toHttp4sRequestUnsafe[I, E, O, R, F](e, baseUri)
33+
new EndpointToHttp4sClient(clientOptions).toHttp4sRequestUnsafe[I, E, O, R, F](e, baseUri)
3634
}
3735

3836
object Http4sClientInterpreter {
39-
def apply[F[_]: ContextShift: Effect]: Http4sClientInterpreter[F] = new Http4sClientInterpreter[F] {}
37+
def apply[F[_]: Async]: Http4sClientInterpreter[F] = new Http4sClientInterpreter[F] {}
4038
}

client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4ClientStreamingTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sttp.tapir.client.http4s
22

33
import cats.effect.IO
4+
import cats.effect.unsafe.implicits.global
45
import fs2.text
56
import sttp.capabilities.fs2.Fs2Streams
67
import sttp.tapir.client.tests.ClientStreamingTests

client/http4s-client/src/test/scala/sttp/tapir/client/http4s/Http4sClientTests.scala

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package sttp.tapir.client.http4s
22

3-
import cats.effect.{Blocker, ContextShift, IO, Timer}
3+
import cats.effect.IO
44
import org.http4s.client.blaze.BlazeClientBuilder
55
import org.http4s.{Request, Response}
66
import sttp.tapir.client.tests.ClientTests
@@ -9,10 +9,6 @@ import sttp.tapir.{DecodeResult, Endpoint}
99
import scala.concurrent.ExecutionContext.global
1010

1111
abstract class Http4sClientTests[R] extends ClientTests[R] {
12-
implicit val cs: ContextShift[IO] = IO.contextShift(global)
13-
implicit val timer: Timer[IO] = IO.timer(global)
14-
implicit val blocker: Blocker = Blocker.liftExecutionContext(global)
15-
1612
override def send[I, E, O, FN[_]](e: Endpoint[I, E, O, R], port: Port, args: I, scheme: String = "http"): IO[Either[E, O]] = {
1713
val (request, parseResponse) = Http4sClientInterpreter[IO].toRequestUnsafe(e, Some(s"http://localhost:$port")).apply(args)
1814

client/tests/src/main/scala/sttp/tapir/client/tests/ClientBasicTests.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package sttp.tapir.client.tests
22

3+
import cats.effect.unsafe.implicits.global
4+
35
import sttp.model.{QueryParams, StatusCode}
46
import sttp.tapir._
57
import sttp.tapir.model.UsernamePassword

client/tests/src/main/scala/sttp/tapir/client/tests/ClientMultipartTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package sttp.tapir.client.tests
22

3+
import cats.effect.unsafe.implicits.global
34
import sttp.tapir.tests._
45

56
trait ClientMultipartTests { this: ClientTests[Any] =>

client/tests/src/main/scala/sttp/tapir/client/tests/ClientStreamingTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package sttp.tapir.client.tests
22

3+
import cats.effect.unsafe.implicits.global
34
import sttp.capabilities.Streams
45
import sttp.tapir.DecodeResult
56
import sttp.tapir.tests.{in_stream_out_stream, not_existing_endpoint}

client/tests/src/main/scala/sttp/tapir/client/tests/ClientTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package sttp.tapir.client.tests
33
import java.io.InputStream
44

55
import cats.effect._
6+
import cats.effect.unsafe.implicits.global
67
import cats.implicits._
78
import org.scalatest.BeforeAndAfterAll
89
import org.scalatest.funsuite.AsyncFunSuite

client/tests/src/main/scala/sttp/tapir/client/tests/ClientWebSocketTests.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package sttp.tapir.client.tests
22

33
import cats.effect.IO
4+
import cats.effect.unsafe.implicits.global
45
import sttp.capabilities.{Streams, WebSockets}
56
import sttp.tapir._
67
import sttp.tapir.json.circe._
@@ -43,7 +44,9 @@ trait ClientWebSocketTests[S] { this: ClientTests[S with WebSockets] =>
4344

4445
test("web sockets, client-terminated echo using fragmented frames") {
4546
send(
46-
endpoint.get.in("ws" / "echo" / "fragmented").out(webSocketBody[String, CodecFormat.TextPlain, WebSocketFrame, CodecFormat.TextPlain].apply(streams)),
47+
endpoint.get
48+
.in("ws" / "echo" / "fragmented")
49+
.out(webSocketBody[String, CodecFormat.TextPlain, WebSocketFrame, CodecFormat.TextPlain].apply(streams)),
4750
port,
4851
(),
4952
"ws"

0 commit comments

Comments
 (0)