Skip to content

Commit 412a6ef

Browse files
karadzhovPetar Karadzhov
and
Petar Karadzhov
authored
feat(http4s): ember client and server (#961)
Co-authored-by: Petar Karadzhov <[email protected]>
1 parent 64de91d commit 412a6ef

File tree

20 files changed

+459
-10
lines changed

20 files changed

+459
-10
lines changed

build.sbt

+102-10
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ def pureconfig = libraryDependencies ++= {
1616
lazy val root = project
1717
.in(file("."))
1818
.aggregate(
19+
appMonix,
20+
appZio,
1921
bundleMonixHttp4sBlaze,
22+
bundleMonixHttp4sEmber,
2023
bundleZioHttp4sBlaze,
24+
bundleZioHttp4sEmber,
2125
cassandraDatastaxDriver,
2226
cassandraDatastaxDriverPureConfig,
2327
catsEffect,
@@ -33,10 +37,14 @@ lazy val root = project
3337
grpcServerPureConfig,
3438
http4sClientBlaze,
3539
http4sClientBlazePureConfig,
40+
http4sClientEmber,
41+
http4sClientEmberPureConfig,
3642
http4sClientMonixCatnap,
3743
http4sServer,
3844
http4sServerBlaze,
3945
http4sServerBlazePureConfig,
46+
http4sServerEmber,
47+
http4sServerEmberPureConfig,
4048
http4sServerMicrometer,
4149
jdkHttpClient,
4250
jdkHttpClientPureConfig,
@@ -67,45 +75,93 @@ lazy val root = project
6775
publish / skip := true
6876
)
6977

70-
lazy val bundleMonixHttp4sBlaze = project
71-
.in(file("bundle-monix-http4s-blaze"))
78+
lazy val appMonix = project
79+
.in(file("app-monix"))
7280
.dependsOn(
73-
http4sClientBlaze,
74-
http4sClientBlazePureConfig,
75-
http4sServerBlaze,
76-
http4sServerBlazePureConfig,
7781
http4sServerMicrometer,
7882
jvmMicrometer,
7983
jvmPureConfig,
8084
pureConfig
8185
)
8286
.settings(BuildSettings.common)
8387
.settings(
84-
name := "sst-bundle-monix-http4s-blaze",
88+
name := "sst-app-monix",
8589
libraryDependencies += Dependencies.monixEval
8690
)
8791

88-
lazy val bundleZioHttp4sBlaze = project
89-
.in(file("bundle-zio-http4s-blaze"))
92+
lazy val bundleMonixHttp4sBlaze = project
93+
.in(file("bundle-monix-http4s-blaze"))
9094
.dependsOn(
9195
http4sClientBlaze,
9296
http4sClientBlazePureConfig,
9397
http4sServerBlaze,
9498
http4sServerBlazePureConfig,
99+
appMonix
100+
)
101+
.settings(BuildSettings.common)
102+
.settings(
103+
name := "sst-bundle-monix-http4s-blaze"
104+
)
105+
106+
lazy val bundleMonixHttp4sEmber = project
107+
.in(file("bundle-monix-http4s-ember"))
108+
.dependsOn(
109+
http4sClientEmber,
110+
http4sClientEmberPureConfig,
111+
http4sServerEmber,
112+
http4sServerEmberPureConfig,
113+
appMonix
114+
)
115+
.settings(BuildSettings.common)
116+
.settings(
117+
name := "sst-bundle-monix-http4s-ember"
118+
)
119+
120+
lazy val appZio = project
121+
.in(file("app-zio"))
122+
.dependsOn(
95123
http4sServerMicrometer,
96124
jvmMicrometer,
97125
jvmPureConfig,
98126
pureConfig
99127
)
100128
.settings(BuildSettings.common)
101129
.settings(
102-
name := "sst-bundle-zio-http4s-blaze",
130+
name := "sst-app-zio",
103131
libraryDependencies ++= Seq(
104132
Dependencies.zio,
105133
Dependencies.zioInteropCats
106134
)
107135
)
108136

137+
lazy val bundleZioHttp4sBlaze = project
138+
.in(file("bundle-zio-http4s-blaze"))
139+
.dependsOn(
140+
http4sClientBlaze,
141+
http4sClientBlazePureConfig,
142+
http4sServerBlaze,
143+
http4sServerBlazePureConfig,
144+
appZio
145+
)
146+
.settings(BuildSettings.common)
147+
.settings(
148+
name := "sst-bundle-zio-http4s-blaze"
149+
)
150+
151+
lazy val bundleZioHttp4sEmber = project
152+
.in(file("bundle-zio-http4s-ember"))
153+
.dependsOn(
154+
http4sClientEmber,
155+
http4sClientEmberPureConfig,
156+
http4sServerEmber,
157+
http4sServerEmberPureConfig,
158+
appZio
159+
)
160+
.settings(BuildSettings.common)
161+
.settings(
162+
name := "sst-bundle-zio-http4s-ember"
163+
)
164+
109165
lazy val cassandraDatastaxDriver = project
110166
.in(file("cassandra-datastax-driver"))
111167
.settings(BuildSettings.common)
@@ -256,12 +312,26 @@ lazy val http4sClientBlaze = project
256312
libraryDependencies += Dependencies.http4sBlazeClient
257313
)
258314

315+
lazy val http4sClientEmber = project
316+
.in(file("http4s-client-ember"))
317+
.settings(BuildSettings.common)
318+
.settings(
319+
name := "sst-http4s-client-ember",
320+
libraryDependencies += Dependencies.http4sEmberClient
321+
)
322+
259323
lazy val http4sClientBlazePureConfig = project
260324
.in(file("http4s-client-blaze-pureconfig"))
261325
.dependsOn(http4sClientBlaze, jvmPureConfig)
262326
.settings(BuildSettings.common)
263327
.settings(name := "sst-http4s-client-blaze-pureconfig")
264328

329+
lazy val http4sClientEmberPureConfig = project
330+
.in(file("http4s-client-ember-pureconfig"))
331+
.dependsOn(http4sClientEmber, jvmPureConfig)
332+
.settings(BuildSettings.common)
333+
.settings(name := "sst-http4s-client-ember-pureconfig")
334+
265335
lazy val http4sClientMonixCatnap = project
266336
.in(file("http4s-client-monix-catnap"))
267337
.dependsOn(monixCatnapMicrometer)
@@ -297,6 +367,19 @@ lazy val http4sServerBlaze = project
297367
)
298368
)
299369

370+
lazy val http4sServerEmber = project
371+
.in(file("http4s-server-ember"))
372+
.dependsOn(http4sServer, http4sClientEmber % Test)
373+
.settings(BuildSettings.common)
374+
.settings(
375+
name := "sst-http4s-server-ember",
376+
libraryDependencies ++= Seq(
377+
Dependencies.http4sEmberServer,
378+
Dependencies.http4sDsl,
379+
Dependencies.slf4jApi
380+
)
381+
)
382+
300383
lazy val http4sServerBlazePureConfig = project
301384
.in(file("http4s-server-blaze-pureconfig"))
302385
.dependsOn(http4sServerBlaze)
@@ -306,6 +389,15 @@ lazy val http4sServerBlazePureConfig = project
306389
pureconfig
307390
)
308391

392+
lazy val http4sServerEmberPureConfig = project
393+
.in(file("http4s-server-ember-pureconfig"))
394+
.dependsOn(http4sServerEmber)
395+
.settings(BuildSettings.common)
396+
.settings(
397+
name := "sst-http4s-server-ember-pureconfig",
398+
pureconfig
399+
)
400+
309401
lazy val http4sServerMicrometer = project
310402
.in(file("http4s-server-micrometer"))
311403
.dependsOn(http4sServer)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.avast.sst.http4s.client.pureconfig.ember
2+
3+
import cats.syntax.either.*
4+
import com.avast.sst.http4s.client.Http4sEmberClientConfig
5+
import com.avast.sst.http4s.client.Http4sEmberClientConfig.SocketOptions
6+
import org.http4s.headers.`User-Agent`
7+
import pureconfig.ConfigReader
8+
import pureconfig.error.CannotConvert
9+
import pureconfig.generic.ProductHint
10+
import pureconfig.generic.semiauto.*
11+
12+
trait ConfigReaders {
13+
implicit protected def hint[T]: ProductHint[T] = ProductHint.default
14+
15+
implicit val http4sClientUserAgentReader: ConfigReader[`User-Agent`] = ConfigReader[String].emap { value =>
16+
`User-Agent`.parse(value).leftMap { parseFailure => CannotConvert(value, "User-Agent HTTP header", parseFailure.message) }
17+
}
18+
19+
implicit val http4sClientSocketOptionsReader: ConfigReader[SocketOptions] = deriveReader[SocketOptions]
20+
21+
implicit val http4sClientHttp4sEmberClientConfigReader: ConfigReader[Http4sEmberClientConfig] = deriveReader[Http4sEmberClientConfig]
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.avast.sst.http4s.client.pureconfig.ember
2+
3+
import pureconfig.ConfigFieldMapping
4+
import pureconfig.generic.ProductHint
5+
6+
/** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */
7+
object implicits extends ConfigReaders {
8+
9+
/** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention.
10+
*
11+
* This is alias for the default `implicits._` import.
12+
*/
13+
object KebabCase extends ConfigReaders
14+
15+
/** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */
16+
object CamelCase extends ConfigReaders {
17+
implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase))
18+
}
19+
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.avast.sst.http4s.client.pureconfig.ember
2+
3+
import cats.syntax.either.*
4+
import com.avast.sst.http4s.client.Http4sEmberClientConfig
5+
import org.http4s.headers.`User-Agent`
6+
import pureconfig.ConfigReader
7+
import pureconfig.error.CannotConvert
8+
import pureconfig.generic.derivation.default.*
9+
10+
trait ConfigReaders {
11+
12+
implicit val http4sClientUserAgentReader: ConfigReader[`User-Agent`] = ConfigReader[String].emap { value =>
13+
`User-Agent`.parse(value).leftMap { parseFailure => CannotConvert(value, "User-Agent HTTP header", parseFailure.message) }
14+
}
15+
16+
implicit val http4sClientHttp4sEmberClientConfigReader: ConfigReader[Http4sEmberClientConfig] =
17+
ConfigReader.derived
18+
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.avast.sst.http4s.client.pureconfig.ember
2+
3+
import pureconfig.ConfigFieldMapping
4+
5+
/** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */
6+
object implicits extends ConfigReaders {
7+
8+
/** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention.
9+
*
10+
* This is alias for the default `implicits._` import.
11+
*/
12+
object KebabCase extends ConfigReaders
13+
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.avast.sst.http4s.client
2+
3+
import com.avast.sst.http4s.client.Http4sEmberClientConfig.{Defaults, SocketOptions}
4+
import org.http4s.ProductId
5+
import org.http4s.client.defaults
6+
import org.http4s.headers.`User-Agent`
7+
8+
import scala.concurrent.duration.{DurationInt, FiniteDuration}
9+
10+
final case class Http4sEmberClientConfig(
11+
maxTotal: Int = Defaults.maxTotal,
12+
maxPerKey: Int = Defaults.maxPerKey,
13+
idleTimeInPool: FiniteDuration = Defaults.idleTimeInPool,
14+
chunkSize: Int = Defaults.chunkSize,
15+
maxResponseHeaderSize: Int = Defaults.maxResponseHeaderSize,
16+
idleConnectionTime: FiniteDuration = Defaults.idleConnectionTime,
17+
timeout: FiniteDuration = Defaults.timeout,
18+
socketOptions: SocketOptions = SocketOptions(),
19+
userAgent: `User-Agent` = Defaults.userAgent,
20+
checkEndpointIdentification: Boolean = Defaults.checkEndpointIdentification
21+
)
22+
23+
object Http4sEmberClientConfig {
24+
final case class SocketOptions(
25+
reuseAddress: Boolean = true,
26+
sendBufferSize: Int = 256 * 1024,
27+
receiveBufferSize: Int = 256 * 1024,
28+
keepAlive: Boolean = false,
29+
noDelay: Boolean = false
30+
)
31+
32+
object Defaults {
33+
val maxTotal = 100
34+
val maxPerKey = 100
35+
val idleTimeInPool: FiniteDuration = 30.seconds
36+
val chunkSize: Int = 32 * 1024
37+
val maxResponseHeaderSize: Int = 4096
38+
val idleConnectionTime: FiniteDuration = defaults.RequestTimeout
39+
val timeout: FiniteDuration = defaults.RequestTimeout
40+
val userAgent: `User-Agent` = `User-Agent`(ProductId("http4s-ember", Some(org.http4s.BuildInfo.version)))
41+
val checkEndpointIdentification = true
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.avast.sst.http4s.client
2+
3+
import cats.effect.{Blocker, Concurrent, ContextShift, Resource, Timer}
4+
import com.avast.sst.http4s.client.Http4sEmberClientConfig.SocketOptions
5+
import fs2.io.tcp.SocketOptionMapping
6+
import fs2.io.tls.TLSContext
7+
import org.http4s.client.Client
8+
import org.http4s.ember.client.EmberClientBuilder
9+
10+
import java.net.StandardSocketOptions
11+
12+
object Http4sEmberClientModule {
13+
def make[F[_]: Concurrent: Timer: ContextShift](
14+
config: Http4sEmberClientConfig,
15+
blocker: Option[Blocker] = None,
16+
tlsContext: Option[TLSContext] = None
17+
): Resource[F, Client[F]] = {
18+
val builder = EmberClientBuilder
19+
.default[F]
20+
.withMaxTotal(config.maxTotal)
21+
.withMaxPerKey(Function.const(config.maxPerKey))
22+
.withIdleTimeInPool(config.idleTimeInPool)
23+
.withChunkSize(config.chunkSize)
24+
.withMaxResponseHeaderSize(config.maxResponseHeaderSize)
25+
.withIdleConnectionTime(config.idleConnectionTime)
26+
.withTimeout(config.timeout)
27+
.withAdditionalSocketOptions(socketOptionMapping(config.socketOptions))
28+
.withUserAgent(config.userAgent)
29+
.withCheckEndpointAuthentication(config.checkEndpointIdentification)
30+
31+
val builderWithMaybeBlocker = blocker.fold(builder)(builder.withBlocker)
32+
val builderWithMaybeTSL = tlsContext.fold(builderWithMaybeBlocker)(builderWithMaybeBlocker.withTLSContext)
33+
34+
builderWithMaybeTSL.build
35+
}
36+
37+
def socketOptionMapping(socketOptions: SocketOptions) =
38+
List(
39+
SocketOptionMapping[java.lang.Boolean](StandardSocketOptions.SO_REUSEADDR, socketOptions.reuseAddress),
40+
SocketOptionMapping[java.lang.Integer](StandardSocketOptions.SO_SNDBUF, socketOptions.sendBufferSize),
41+
SocketOptionMapping[java.lang.Integer](StandardSocketOptions.SO_RCVBUF, socketOptions.receiveBufferSize),
42+
SocketOptionMapping[java.lang.Boolean](StandardSocketOptions.SO_KEEPALIVE, socketOptions.keepAlive),
43+
SocketOptionMapping[java.lang.Boolean](StandardSocketOptions.TCP_NODELAY, socketOptions.noDelay)
44+
)
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.avast.sst.http4s.client
2+
3+
import cats.effect.*
4+
import org.http4s.headers.*
5+
import org.http4s.{ProductComment, ProductId}
6+
import org.scalatest.funsuite.AsyncFunSuite
7+
8+
import java.util.concurrent.Executors
9+
import scala.concurrent.ExecutionContext
10+
11+
class Http4SEmberClientTest extends AsyncFunSuite {
12+
13+
implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
14+
implicit private val timer: Timer[IO] = IO.timer(ExecutionContext.global)
15+
16+
test("Initialization of HTTP client and simple GET") {
17+
val expected = """|{
18+
| "user-agent": "http4s-client/1.2.3 (Test)"
19+
|}
20+
|""".stripMargin
21+
22+
val test = for {
23+
client <- Http4sEmberClientModule.make[IO](
24+
Http4sEmberClientConfig(
25+
userAgent = `User-Agent`(ProductId("http4s-client", Some("1.2.3")), List(ProductComment("Test")))
26+
),
27+
Some(Blocker.liftExecutionContext(ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())))
28+
)
29+
response <- Resource.eval(client.expect[String]("https://httpbin.org/user-agent"))
30+
} yield assert(response === expected)
31+
32+
test.use(IO.pure).unsafeToFuture()
33+
}
34+
35+
}

0 commit comments

Comments
 (0)