Skip to content

Ember support #961

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 102 additions & 10 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ def pureconfig = libraryDependencies ++= {
lazy val root = project
.in(file("."))
.aggregate(
appMonix,
appZio,
bundleMonixHttp4sBlaze,
bundleMonixHttp4sEmber,
bundleZioHttp4sBlaze,
bundleZioHttp4sEmber,
cassandraDatastaxDriver,
cassandraDatastaxDriverPureConfig,
catsEffect,
Expand All @@ -33,10 +37,14 @@ lazy val root = project
grpcServerPureConfig,
http4sClientBlaze,
http4sClientBlazePureConfig,
http4sClientEmber,
http4sClientEmberPureConfig,
http4sClientMonixCatnap,
http4sServer,
http4sServerBlaze,
http4sServerBlazePureConfig,
http4sServerEmber,
http4sServerEmberPureConfig,
http4sServerMicrometer,
jdkHttpClient,
jdkHttpClientPureConfig,
Expand Down Expand Up @@ -67,45 +75,93 @@ lazy val root = project
publish / skip := true
)

lazy val bundleMonixHttp4sBlaze = project
.in(file("bundle-monix-http4s-blaze"))
lazy val appMonix = project
.in(file("app-monix"))
.dependsOn(
http4sClientBlaze,
http4sClientBlazePureConfig,
http4sServerBlaze,
http4sServerBlazePureConfig,
http4sServerMicrometer,
jvmMicrometer,
jvmPureConfig,
pureConfig
)
.settings(BuildSettings.common)
.settings(
name := "sst-bundle-monix-http4s-blaze",
name := "sst-app-monix",
libraryDependencies += Dependencies.monixEval
)

lazy val bundleZioHttp4sBlaze = project
.in(file("bundle-zio-http4s-blaze"))
lazy val bundleMonixHttp4sBlaze = project
.in(file("bundle-monix-http4s-blaze"))
.dependsOn(
http4sClientBlaze,
http4sClientBlazePureConfig,
http4sServerBlaze,
http4sServerBlazePureConfig,
appMonix
)
.settings(BuildSettings.common)
.settings(
name := "sst-bundle-monix-http4s-blaze"
)

lazy val bundleMonixHttp4sEmber = project
.in(file("bundle-monix-http4s-ember"))
.dependsOn(
http4sClientEmber,
http4sClientEmberPureConfig,
http4sServerEmber,
http4sServerEmberPureConfig,
appMonix
)
.settings(BuildSettings.common)
.settings(
name := "sst-bundle-monix-http4s-ember"
)

lazy val appZio = project
.in(file("app-zio"))
.dependsOn(
http4sServerMicrometer,
jvmMicrometer,
jvmPureConfig,
pureConfig
)
.settings(BuildSettings.common)
.settings(
name := "sst-bundle-zio-http4s-blaze",
name := "sst-app-zio",
libraryDependencies ++= Seq(
Dependencies.zio,
Dependencies.zioInteropCats
)
)

lazy val bundleZioHttp4sBlaze = project
.in(file("bundle-zio-http4s-blaze"))
.dependsOn(
http4sClientBlaze,
http4sClientBlazePureConfig,
http4sServerBlaze,
http4sServerBlazePureConfig,
appZio
)
.settings(BuildSettings.common)
.settings(
name := "sst-bundle-zio-http4s-blaze"
)

lazy val bundleZioHttp4sEmber = project
.in(file("bundle-zio-http4s-ember"))
.dependsOn(
http4sClientEmber,
http4sClientEmberPureConfig,
http4sServerEmber,
http4sServerEmberPureConfig,
appZio
)
.settings(BuildSettings.common)
.settings(
name := "sst-bundle-zio-http4s-ember"
)

lazy val cassandraDatastaxDriver = project
.in(file("cassandra-datastax-driver"))
.settings(BuildSettings.common)
Expand Down Expand Up @@ -256,12 +312,26 @@ lazy val http4sClientBlaze = project
libraryDependencies += Dependencies.http4sBlazeClient
)

lazy val http4sClientEmber = project
.in(file("http4s-client-ember"))
.settings(BuildSettings.common)
.settings(
name := "sst-http4s-client-ember",
libraryDependencies += Dependencies.http4sEmberClient
)

lazy val http4sClientBlazePureConfig = project
.in(file("http4s-client-blaze-pureconfig"))
.dependsOn(http4sClientBlaze, jvmPureConfig)
.settings(BuildSettings.common)
.settings(name := "sst-http4s-client-blaze-pureconfig")

lazy val http4sClientEmberPureConfig = project
.in(file("http4s-client-ember-pureconfig"))
.dependsOn(http4sClientEmber, jvmPureConfig)
.settings(BuildSettings.common)
.settings(name := "sst-http4s-client-ember-pureconfig")

lazy val http4sClientMonixCatnap = project
.in(file("http4s-client-monix-catnap"))
.dependsOn(monixCatnapMicrometer)
Expand Down Expand Up @@ -297,6 +367,19 @@ lazy val http4sServerBlaze = project
)
)

lazy val http4sServerEmber = project
.in(file("http4s-server-ember"))
.dependsOn(http4sServer, http4sClientEmber % Test)
.settings(BuildSettings.common)
.settings(
name := "sst-http4s-server-ember",
libraryDependencies ++= Seq(
Dependencies.http4sEmberServer,
Dependencies.http4sDsl,
Dependencies.slf4jApi
)
)

lazy val http4sServerBlazePureConfig = project
.in(file("http4s-server-blaze-pureconfig"))
.dependsOn(http4sServerBlaze)
Expand All @@ -306,6 +389,15 @@ lazy val http4sServerBlazePureConfig = project
pureconfig
)

lazy val http4sServerEmberPureConfig = project
.in(file("http4s-server-ember-pureconfig"))
.dependsOn(http4sServerEmber)
.settings(BuildSettings.common)
.settings(
name := "sst-http4s-server-ember-pureconfig",
pureconfig
)

lazy val http4sServerMicrometer = project
.in(file("http4s-server-micrometer"))
.dependsOn(http4sServer)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.avast.sst.http4s.client.pureconfig.ember

import cats.syntax.either.*
import com.avast.sst.http4s.client.Http4sEmberClientConfig
import com.avast.sst.http4s.client.Http4sEmberClientConfig.SocketOptions
import org.http4s.headers.`User-Agent`
import pureconfig.ConfigReader
import pureconfig.error.CannotConvert
import pureconfig.generic.ProductHint
import pureconfig.generic.semiauto.*

trait ConfigReaders {
implicit protected def hint[T]: ProductHint[T] = ProductHint.default

implicit val http4sClientUserAgentReader: ConfigReader[`User-Agent`] = ConfigReader[String].emap { value =>
`User-Agent`.parse(value).leftMap { parseFailure => CannotConvert(value, "User-Agent HTTP header", parseFailure.message) }
}

implicit val http4sClientSocketOptionsReader: ConfigReader[SocketOptions] = deriveReader[SocketOptions]

implicit val http4sClientHttp4sEmberClientConfigReader: ConfigReader[Http4sEmberClientConfig] = deriveReader[Http4sEmberClientConfig]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.avast.sst.http4s.client.pureconfig.ember

import pureconfig.ConfigFieldMapping
import pureconfig.generic.ProductHint

/** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */
object implicits extends ConfigReaders {

/** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention.
*
* This is alias for the default `implicits._` import.
*/
object KebabCase extends ConfigReaders

/** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */
object CamelCase extends ConfigReaders {
implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.avast.sst.http4s.client.pureconfig.ember

import cats.syntax.either.*
import com.avast.sst.http4s.client.Http4sEmberClientConfig
import org.http4s.headers.`User-Agent`
import pureconfig.ConfigReader
import pureconfig.error.CannotConvert
import pureconfig.generic.derivation.default.*

trait ConfigReaders {

implicit val http4sClientUserAgentReader: ConfigReader[`User-Agent`] = ConfigReader[String].emap { value =>
`User-Agent`.parse(value).leftMap { parseFailure => CannotConvert(value, "User-Agent HTTP header", parseFailure.message) }
}

implicit val http4sClientHttp4sEmberClientConfigReader: ConfigReader[Http4sEmberClientConfig] =
ConfigReader.derived

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.avast.sst.http4s.client.pureconfig.ember

import pureconfig.ConfigFieldMapping

/** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */
object implicits extends ConfigReaders {

/** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention.
*
* This is alias for the default `implicits._` import.
*/
object KebabCase extends ConfigReaders

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.avast.sst.http4s.client

import com.avast.sst.http4s.client.Http4sEmberClientConfig.{Defaults, SocketOptions}
import org.http4s.ProductId
import org.http4s.client.defaults
import org.http4s.headers.`User-Agent`

import scala.concurrent.duration.{DurationInt, FiniteDuration}

final case class Http4sEmberClientConfig(
maxTotal: Int = Defaults.maxTotal,
maxPerKey: Int = Defaults.maxPerKey,
idleTimeInPool: FiniteDuration = Defaults.idleTimeInPool,
chunkSize: Int = Defaults.chunkSize,
maxResponseHeaderSize: Int = Defaults.maxResponseHeaderSize,
idleConnectionTime: FiniteDuration = Defaults.idleConnectionTime,
timeout: FiniteDuration = Defaults.timeout,
socketOptions: SocketOptions = SocketOptions(),
userAgent: `User-Agent` = Defaults.userAgent,
checkEndpointIdentification: Boolean = Defaults.checkEndpointIdentification
)

object Http4sEmberClientConfig {
final case class SocketOptions(
reuseAddress: Boolean = true,
sendBufferSize: Int = 256 * 1024,
receiveBufferSize: Int = 256 * 1024,
keepAlive: Boolean = false,
noDelay: Boolean = false
)

object Defaults {
val maxTotal = 100
val maxPerKey = 100
val idleTimeInPool: FiniteDuration = 30.seconds
val chunkSize: Int = 32 * 1024
val maxResponseHeaderSize: Int = 4096
val idleConnectionTime: FiniteDuration = defaults.RequestTimeout
val timeout: FiniteDuration = defaults.RequestTimeout
val userAgent: `User-Agent` = `User-Agent`(ProductId("http4s-ember", Some(org.http4s.BuildInfo.version)))
val checkEndpointIdentification = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.avast.sst.http4s.client

import cats.effect.{Blocker, Concurrent, ContextShift, Resource, Timer}
import com.avast.sst.http4s.client.Http4sEmberClientConfig.SocketOptions
import fs2.io.tcp.SocketOptionMapping
import fs2.io.tls.TLSContext
import org.http4s.client.Client
import org.http4s.ember.client.EmberClientBuilder

import java.net.StandardSocketOptions

object Http4sEmberClientModule {
def make[F[_]: Concurrent: Timer: ContextShift](
config: Http4sEmberClientConfig,
blocker: Option[Blocker] = None,
tlsContext: Option[TLSContext] = None
): Resource[F, Client[F]] = {
val builder = EmberClientBuilder
.default[F]
.withMaxTotal(config.maxTotal)
.withMaxPerKey(Function.const(config.maxPerKey))
.withIdleTimeInPool(config.idleTimeInPool)
.withChunkSize(config.chunkSize)
.withMaxResponseHeaderSize(config.maxResponseHeaderSize)
.withIdleConnectionTime(config.idleConnectionTime)
.withTimeout(config.timeout)
.withAdditionalSocketOptions(socketOptionMapping(config.socketOptions))
.withUserAgent(config.userAgent)
.withCheckEndpointAuthentication(config.checkEndpointIdentification)

val builderWithMaybeBlocker = blocker.fold(builder)(builder.withBlocker)
val builderWithMaybeTSL = tlsContext.fold(builderWithMaybeBlocker)(builderWithMaybeBlocker.withTLSContext)

builderWithMaybeTSL.build
}

def socketOptionMapping(socketOptions: SocketOptions) =
List(
SocketOptionMapping[java.lang.Boolean](StandardSocketOptions.SO_REUSEADDR, socketOptions.reuseAddress),
SocketOptionMapping[java.lang.Integer](StandardSocketOptions.SO_SNDBUF, socketOptions.sendBufferSize),
SocketOptionMapping[java.lang.Integer](StandardSocketOptions.SO_RCVBUF, socketOptions.receiveBufferSize),
SocketOptionMapping[java.lang.Boolean](StandardSocketOptions.SO_KEEPALIVE, socketOptions.keepAlive),
SocketOptionMapping[java.lang.Boolean](StandardSocketOptions.TCP_NODELAY, socketOptions.noDelay)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.avast.sst.http4s.client

import cats.effect.*
import org.http4s.headers.*
import org.http4s.{ProductComment, ProductId}
import org.scalatest.funsuite.AsyncFunSuite

import java.util.concurrent.Executors
import scala.concurrent.ExecutionContext

class Http4SEmberClientTest extends AsyncFunSuite {

implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
implicit private val timer: Timer[IO] = IO.timer(ExecutionContext.global)

test("Initialization of HTTP client and simple GET") {
val expected = """|{
| "user-agent": "http4s-client/1.2.3 (Test)"
|}
|""".stripMargin

val test = for {
client <- Http4sEmberClientModule.make[IO](
Http4sEmberClientConfig(
userAgent = `User-Agent`(ProductId("http4s-client", Some("1.2.3")), List(ProductComment("Test")))
),
Some(Blocker.liftExecutionContext(ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())))
)
response <- Resource.eval(client.expect[String]("https://httpbin.org/user-agent"))
} yield assert(response === expected)

test.use(IO.pure).unsafeToFuture()
}

}
Loading