Skip to content
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

Add module with Http4s Blaze server #51

Draft
wants to merge 2 commits into
base: async-instance
Choose a base branch
from
Draft
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
15 changes: 14 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ lazy val root =
`http-scala-fx`,
documentation,
`sttp-scala-fx`,
`java-net-multipart-body-publisher`
`java-net-multipart-body-publisher`,
`http4s-scala-fx`
)

lazy val `scala-fx` = project.settings(scalafxSettings: _*)
Expand Down Expand Up @@ -79,6 +80,14 @@ lazy val `sttp-scala-fx` = (project in file("./sttp-scala-fx"))
`http-scala-fx`,
`munit-scala-fx` % "test -> compile")

lazy val `http4s-scala-fx` = (project in file("./http4s-scala-fx"))
.settings(http4sScalaFXSettings)
.dependsOn(
`cats-scala-fx`,
`scala-fx`,
`http-scala-fx` % "test -> test",
`munit-scala-fx` % "test -> compile")

lazy val commonSettings = Seq(
javaOptions ++= javaOptionsSettings,
autoAPIMappings := true,
Expand Down Expand Up @@ -145,6 +154,10 @@ lazy val sttpScalaFXSettings = commonSettings ++ Seq(
libraryDependencies += hedgehog % Test
)

lazy val http4sScalaFXSettings = commonSettings ++ Seq(
libraryDependencies += http4sBlaze
)

lazy val javaOptionsSettings = Seq(
"-XX:+IgnoreUnrecognizedVMOptions",
"-XX:-DetectLocksInCompiledFrames",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package http4s
package fx

import _root_.fx.handle
import _root_.fx.instances.StructuredF
import _root_.fx.instances.FxAsync.asyncInstance
import _root_.fx.{Control, Errors, Nullable, Resource, Resources, Structured}
import org.http4s.HttpApp
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.server.{defaults, Server}

class BlazeServerFXBackend private (
app: HttpApp[StructuredF]
)(
using structured: Structured,
control: Control[Throwable],
config: HttpServerConfig
) {
private var serverFinalizers: Nullable[(Server, StructuredF[Unit])] = Nullable.none

private val builder: BlazeServerBuilder[StructuredF] =
BlazeServerBuilder[StructuredF]
.bindHttp(
config.port.getOrElse(defaults.HttpPort),
config.host.getOrElse(defaults.IPv4Host))
.withIdleTimeout(config.idleTimeout.getOrElse(defaults.IdleTimeout))
.withSelectorThreadFactory(structured.threadFactory)
.withHttpApp(app)

def start(): BlazeServerFXBackend =
serverFinalizers = handle(
Nullable(builder.resource.allocated)
)((e: Throwable) => e.shift)
this

def close(): Unit =
serverFinalizers.map { case (_, finalizers) => finalizers }.getOrElse(())

def server(): Server =
handle(
serverFinalizers.map { case (server, _) => server }.value
)((e: Throwable) => e.shift)
}

object BlazeServerFXBackend:
def apply(app: HttpApp[StructuredF])(
using structured: Structured,
control: Control[Throwable],
config: HttpServerConfig
): Resource[BlazeServerFXBackend] =
Resource(new BlazeServerFXBackend(app).start(), (server, _) => server.close())
20 changes: 20 additions & 0 deletions http4s-scala-fx/src/main/scala/http4s/fx/HttpServerConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package http4s
package fx

import _root_.fx.Nullable

import java.net.InetSocketAddress
import scala.concurrent.duration.Duration

/**
* Defines the server configuration options for an http server. Not open for extension.
*/
final class HttpServerConfig(
val port: Nullable[Int],
val host: Nullable[String],
val idleTimeout: Nullable[Duration]
)

object HttpServerConfig:
given HttpServerConfig =
HttpServerConfig(Nullable.none, Nullable.none, Nullable.none)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package http4s
package fx

import _root_.fx.{defaultRetryPolicy, parallel, structured, GET, POST, Structured}
import munit.fx.ScalaFXSuite

import java.net.URI
import java.net.http.HttpResponse

class BlazeServerFXBackendSuite extends ScalaFXSuite, HttpServerFixtures:

httpServerHttp4s(pingPongApp).testFX(
"GET requests should be returned in non-blocking fibers") { serverResource =>
assertEqualsFX(
structured {
serverResource.use { baseServerAddress =>
parallel(
() => URI.create(s"$baseServerAddress/ping/1").GET[String]().body,
() => URI.create(s"$baseServerAddress/ping/2").GET[String]().body,
() => URI.create(s"$baseServerAddress/ping/3").GET[String]().body
)
}
},
("pong", "pong", "pong")
)
}

httpServerHttp4s(echoApp).testFX("POST requests should be returned") { serverResource =>
val response: HttpResponse[String] = structured {
serverResource.use { baseServerAddress =>
URI.create(s"$baseServerAddress/echo").POST[String, String]("hello")
}
}

assertEqualsFX(response.body, "hello")
assertEqualsFX(response.statusCode, 200)
}
33 changes: 33 additions & 0 deletions http4s-scala-fx/src/test/scala/http4s/fx/HttpServerFixtures.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package http4s
package fx

import _root_.fx.{handle, structured, toEither, Control, Resource, Structured}
import _root_.fx.instances.FxAsync.asyncInstance
import _root_.fx.instances.StructuredF
import _root_.fx.HttpExecutionException
import fx.BlazeServerFXBackend
import munit.FunSuite
import org.http4s.Method.{GET, POST}
import org.http4s.{HttpApp, HttpRoutes, Response}

gregorath marked this conversation as resolved.
Show resolved Hide resolved
trait HttpServerFixtures:
self: FunSuite =>

val pingPongApp: HttpApp[StructuredF] =
HttpApp.apply {
case r if r.method == GET && r.uri.path.renderString.contains("ping") =>
Response[StructuredF]().withEntity[String]("pong")
}

val echoApp: HttpApp[StructuredF] =
HttpApp.apply {
case r if r.method == POST && r.uri.path.renderString.contains("echo") =>
Response[StructuredF]().withBodyStream(r.body)
}

def httpServerHttp4s(app: HttpApp[StructuredF])
: FunFixture[(Structured, Control[Throwable]) ?=> Resource[String]] =
FunFixture(
setup = _ => BlazeServerFXBackend(app).map(server => s"http:/${server.server().address}"),
teardown = _ => ()
)
3 changes: 2 additions & 1 deletion project/project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ object Dependencies {
val sttp = "3.6.2"
val httpCore5 = "5.1.4"
val hedgehog = "0.9.0"
val http4sBlaze = "0.23.12"
}

object Compile {
Expand All @@ -47,7 +48,7 @@ object Dependencies {
val logback = "ch.qos.logback" % "logback-classic" % Versions.logback
val sttp = "com.softwaremill.sttp.client3" %% "core" % Versions.sttp
val httpCore5 = "org.apache.httpcomponents.core5" % "httpcore5" % Versions.httpCore5

val http4sBlaze = "org.http4s" %% "http4s-blaze-server" % Versions.http4sBlaze
}

object Test {
Expand Down
4 changes: 2 additions & 2 deletions scala-fx/src/main/scala/fx/Continuation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import scala.annotation.implicitNotFound
import scala.util.control.ControlThrowable
import java.util.UUID
import scala.util.control.NonFatal
import java.util.concurrent.ExecutionException
import java.util.concurrent.{CompletionException, ExecutionException}
import scala.annotation.tailrec

object Continuation:
Expand All @@ -30,7 +30,7 @@ object Continuation:

@tailrec def handleControl(control: Control[_], e: Throwable): Any =
e match
case e: ExecutionException =>
case e: (ExecutionException | CompletionException) =>
jackcviers marked this conversation as resolved.
Show resolved Hide resolved
handleControl(control, e.getCause)
case e @ ControlToken(token, shifted, recover) =>
if (control.token == token)
Expand Down