Skip to content

Commit 94217b5

Browse files
kellydavidhowardjohn
authored andcommitted
Added ZIO http4s handler (howardjohn#11)
* Added ZIO http4s handler Extracted as much code where the effect type could be generalised into the trait: `Http4sLambdaHandlerK`. Added a ZIO http4s handler class. * Fixed http4s-lambda-zio module build.sbt * http4s http4s-zio append scalac options * Revert case class handler
1 parent 67c7484 commit 94217b5

File tree

7 files changed

+172
-78
lines changed

7 files changed

+172
-78
lines changed

build.sbt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ lazy val root = project
1818
.in(file("."))
1919
.settings(commonSettings)
2020
.settings(noPublishSettings)
21-
.aggregate(common, tests, http4s, akka, exampleHttp4s, exampleAkka)
21+
.aggregate(common, tests, http4s, http4sZio, akka, exampleHttp4s, exampleAkka)
2222

2323
lazy val CirceVersion = "0.12.1"
2424
lazy val ScalaTestVersion = "3.1.0"
@@ -58,7 +58,7 @@ lazy val http4s = project
5858
.settings(
5959
name := "http4s-lambda",
6060
moduleName := "http4s-lambda",
61-
scalacOptions := scalacVersionOptions(scalaVersion.value),
61+
scalacOptions ++= scalacVersionOptions(scalaVersion.value),
6262
libraryDependencies ++= {
6363
Seq(
6464
"org.http4s" %% "http4s-core" % Http4sVersion,
@@ -71,6 +71,29 @@ lazy val http4s = project
7171
.dependsOn(common)
7272
.dependsOn(tests % "test")
7373

74+
lazy val http4sZio = project
75+
.in(file("http4s-lambda-zio"))
76+
.settings(publishSettings)
77+
.settings(commonSettings)
78+
.settings(
79+
name := "http4s-lambda-zio",
80+
moduleName := "http4s-lambda-zio",
81+
scalacOptions ++= scalacVersionOptions(scalaVersion.value),
82+
libraryDependencies ++= {
83+
Seq(
84+
"org.http4s" %% "http4s-core" % Http4sVersion,
85+
"org.scalatest" %% "scalatest" % ScalaTestVersion % "test",
86+
"org.http4s" %% "http4s-dsl" % Http4sVersion % "test",
87+
"org.http4s" %% "http4s-circe" % Http4sVersion % "test",
88+
"dev.zio" %% "zio" % "1.0.0-RC14",
89+
"dev.zio" %% "zio-interop-cats" % "2.0.0.0-RC5"
90+
)
91+
}
92+
)
93+
.dependsOn(common)
94+
.dependsOn(tests % "test")
95+
.dependsOn(http4s % "test->test;compile->compile")
96+
7497
lazy val akka = project
7598
.in(file("akka-http-lambda"))
7699
.settings(publishSettings)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.github.howardjohn.lambda.http4szio
2+
3+
import io.github.howardjohn.lambda.ProxyEncoding._
4+
import io.github.howardjohn.lambda.http4s.Http4sLambdaHandlerK
5+
import org.http4s._
6+
import zio._
7+
import zio.interop.catz._
8+
9+
class Http4sLambdaHandlerZIO(val service: HttpRoutes[Task]) extends Http4sLambdaHandlerK[Task] {
10+
val runtime: DefaultRuntime = new DefaultRuntime {}
11+
12+
def handleRequest(request: ProxyRequest): ProxyResponse =
13+
parseRequest(request)
14+
.map(runRequest)
15+
.flatMap(request => runtime.unsafeRun(request.either))
16+
.fold(errorResponse, identity)
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.github.howardjohn.lambda.http4szio
2+
3+
import io.circe.generic.auto._
4+
import io.github.howardjohn.lambda.LambdaHandlerBehavior
5+
import io.github.howardjohn.lambda.LambdaHandlerBehavior._
6+
import io.github.howardjohn.lambda.http4s.TestRoutes
7+
import org.http4s.circe._
8+
import org.http4s.EntityDecoder
9+
import org.scalatest.{FeatureSpec, GivenWhenThen}
10+
import zio._
11+
import zio.interop.catz._
12+
import zio.interop.catz.implicits._
13+
14+
class Http4sLambdaHandlerZIOSpec extends FeatureSpec with LambdaHandlerBehavior with GivenWhenThen {
15+
implicit val jsonDecoder: EntityDecoder[Task, JsonBody] = jsonOf[Task, JsonBody]
16+
17+
val handler = new Http4sLambdaHandlerZIO(new TestRoutes[Task].routes)
18+
19+
scenariosFor(behavior(handler))
20+
}
Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,13 @@
11
package io.github.howardjohn.lambda.http4s
22

33
import cats.effect.IO
4-
import fs2.{text, Stream}
54
import io.github.howardjohn.lambda.ProxyEncoding._
6-
import io.github.howardjohn.lambda.{LambdaHandler, ProxyEncoding}
75
import org.http4s._
86

9-
import scala.util.Try
10-
11-
class Http4sLambdaHandler(service: HttpRoutes[IO]) extends LambdaHandler {
12-
import Http4sLambdaHandler._
13-
14-
override def handleRequest(request: ProxyRequest): ProxyResponse =
7+
class Http4sLambdaHandler(val service: HttpRoutes[IO]) extends Http4sLambdaHandlerK[IO] {
8+
def handleRequest(request: ProxyRequest): ProxyResponse =
159
parseRequest(request)
1610
.map(runRequest)
1711
.flatMap(_.attempt.unsafeRunSync())
1812
.fold(errorResponse, identity)
19-
20-
private def runRequest(request: Request[IO]): IO[ProxyResponse] =
21-
Try {
22-
service
23-
.run(request)
24-
.getOrElse(Response.notFound)
25-
.flatMap(asProxyResponse)
26-
}.fold(errorResponse.andThen(e => IO(e)), identity)
27-
}
28-
29-
private object Http4sLambdaHandler {
30-
private val errorResponse = (err: Throwable) => ProxyResponse(500, Map.empty, err.getMessage)
31-
32-
private def asProxyResponse(resp: Response[IO]): IO[ProxyResponse] =
33-
resp
34-
.as[String]
35-
.map { body =>
36-
ProxyResponse(
37-
resp.status.code,
38-
resp.headers.toList
39-
.map(h => h.name.value -> h.value)
40-
.toMap,
41-
body)
42-
}
43-
44-
private def parseRequest(request: ProxyRequest): Either[ParseFailure, Request[IO]] =
45-
for {
46-
uri <- Uri.fromString(ProxyEncoding.reconstructPath(request))
47-
method <- Method.fromString(request.httpMethod)
48-
} yield
49-
Request[IO](
50-
method,
51-
uri,
52-
headers = request.headers.map(toHeaders).getOrElse(Headers.empty),
53-
body = request.body.map(encodeBody).getOrElse(EmptyBody)
54-
)
55-
56-
private def toHeaders(headers: Map[String, String]): Headers =
57-
Headers {
58-
headers.map {
59-
case (k, v) => Header(k, v)
60-
}.toList
61-
}
62-
63-
private def encodeBody(body: String) = Stream(body).through(text.utf8Encode)
6413
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package io.github.howardjohn.lambda.http4s
2+
3+
import cats.{Applicative, MonadError}
4+
import io.github.howardjohn.lambda.{LambdaHandler, ProxyEncoding}
5+
import io.github.howardjohn.lambda.ProxyEncoding.{ProxyRequest, ProxyResponse}
6+
import org.http4s._
7+
import fs2.{Pure, Stream, text}
8+
import cats.implicits._
9+
10+
import scala.util.Try
11+
12+
trait Http4sLambdaHandlerK[F[_]] extends LambdaHandler {
13+
val service: HttpRoutes[F]
14+
15+
def handleRequest(request: ProxyRequest): ProxyResponse
16+
17+
def runRequest(request: Request[F])(implicit F: MonadError[F, Throwable],
18+
decoder: EntityDecoder[F, String]): F[ProxyResponse] =
19+
Try {
20+
service
21+
.run(request)
22+
.getOrElse(Response.notFound)
23+
.flatMap(asProxyResponse)
24+
}.fold(errorResponse.andThen(e => Applicative[F].pure(e)), identity)
25+
26+
protected val errorResponse = (err: Throwable) => ProxyResponse(500, Map.empty, err.getMessage)
27+
28+
protected def asProxyResponse(resp: Response[F])(implicit F: MonadError[F, Throwable],
29+
decoder: EntityDecoder[F, String]): F[ProxyResponse] =
30+
resp
31+
.as[String]
32+
.map { body =>
33+
ProxyResponse(
34+
resp.status.code,
35+
resp.headers.toList
36+
.map(h => h.name.value -> h.value)
37+
.toMap,
38+
body)
39+
}
40+
41+
protected def parseRequest(request: ProxyRequest): Either[ParseFailure, Request[F]] =
42+
for {
43+
uri <- Uri.fromString(ProxyEncoding.reconstructPath(request))
44+
method <- Method.fromString(request.httpMethod)
45+
} yield
46+
Request[F](
47+
method,
48+
uri,
49+
headers = request.headers.map(toHeaders).getOrElse(Headers.empty),
50+
body = request.body.map(encodeBody).getOrElse(EmptyBody)
51+
)
52+
53+
protected def toHeaders(headers: Map[String, String]): Headers =
54+
Headers {
55+
headers.map {
56+
case (k, v) => Header(k, v)
57+
}.toList
58+
}
59+
60+
protected def encodeBody(body: String): Stream[Pure, Byte] = Stream(body).through(text.utf8Encode)
61+
}
62+

http4s-lambda/src/test/scala/io/github/howardjohn/lambda/http4s/Http4sLambdaHandlerSpec.scala

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,16 @@ package io.github.howardjohn.lambda.http4s
22

33
import cats.effect.IO
44
import io.circe.generic.auto._
5-
import io.circe.syntax._
65
import io.github.howardjohn.lambda.LambdaHandlerBehavior
76
import io.github.howardjohn.lambda.LambdaHandlerBehavior._
7+
import org.http4s.EntityDecoder
88
import org.http4s.circe._
9-
import org.http4s.dsl.io._
10-
import org.http4s.{EntityDecoder, Header, HttpRoutes}
119
import org.scalatest.{FeatureSpec, GivenWhenThen}
1210

1311
class Http4sLambdaHandlerSpec extends FeatureSpec with LambdaHandlerBehavior with GivenWhenThen {
1412
implicit val jsonDecoder: EntityDecoder[IO, JsonBody] = jsonOf[IO, JsonBody]
1513

16-
object TimesQueryMatcher extends OptionalQueryParamDecoderMatcher[Int]("times")
17-
18-
val route: HttpRoutes[IO] = HttpRoutes.of[IO] {
19-
case GET -> Root / "hello" :? TimesQueryMatcher(times) =>
20-
Ok {
21-
Seq
22-
.fill(times.getOrElse(1))("Hello World!")
23-
.mkString(" ")
24-
}
25-
case GET -> Root / "long" => IO(Thread.sleep(1000)).flatMap(_ => Ok("Hello World!"))
26-
case GET -> Root / "exception" => throw RouteException()
27-
case GET -> Root / "error" => InternalServerError()
28-
case req @ GET -> Root / "header" =>
29-
val header = req.headers.find(h => h.name.value == inputHeader).map(_.value).getOrElse("Header Not Found")
30-
Ok(header, Header(outputHeader, outputHeaderValue))
31-
case req @ POST -> Root / "post" => req.as[String].flatMap(s => Ok(s))
32-
case req @ POST -> Root / "json" => req.as[JsonBody].flatMap(s => Ok(LambdaHandlerBehavior.jsonReturn.asJson))
33-
}
34-
35-
val handler = new Http4sLambdaHandler(route)
14+
val handler = new Http4sLambdaHandler(new TestRoutes[IO].routes)
3615

3716
scenariosFor(behavior(handler))
3817
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.github.howardjohn.lambda.http4s
2+
3+
import cats.{Applicative, MonadError}
4+
import cats.effect.Sync
5+
import cats.implicits._
6+
import io.github.howardjohn.lambda.LambdaHandlerBehavior
7+
import io.github.howardjohn.lambda.LambdaHandlerBehavior._
8+
import org.http4s.dsl.Http4sDsl
9+
import org.http4s.{EntityDecoder, Header, HttpRoutes}
10+
import org.http4s.circe._
11+
import io.circe.generic.auto._
12+
import io.circe.syntax._
13+
import org.http4s.dsl.impl.OptionalQueryParamDecoderMatcher
14+
15+
class TestRoutes[F[_]] {
16+
17+
object TimesQueryMatcher extends OptionalQueryParamDecoderMatcher[Int]("times")
18+
19+
val dsl = Http4sDsl[F]
20+
21+
import dsl._
22+
23+
def routes(implicit sync: Sync[F],
24+
jsonDecoder: EntityDecoder[F, JsonBody],
25+
me: MonadError[F, Throwable],
26+
stringDecoder: EntityDecoder[F, String],
27+
ap: Applicative[F]): HttpRoutes[F] = HttpRoutes.of[F] {
28+
case GET -> Root / "hello" :? TimesQueryMatcher(times) =>
29+
Ok {
30+
Seq
31+
.fill(times.getOrElse(1))("Hello World!")
32+
.mkString(" ")
33+
}
34+
case GET -> Root / "long" => Applicative[F].pure(Thread.sleep(1000)).flatMap(_ => Ok("Hello World!"))
35+
case GET -> Root / "exception" => throw RouteException()
36+
case GET -> Root / "error" => InternalServerError()
37+
case req@GET -> Root / "header" =>
38+
val header = req.headers.find(h => h.name.value == inputHeader).map(_.value).getOrElse("Header Not Found")
39+
Ok(header, Header(outputHeader, outputHeaderValue))
40+
case req@POST -> Root / "post" => req.as[String].flatMap(s => Ok(s))
41+
case req@POST -> Root / "json" => req.as[JsonBody].flatMap(s => Ok(LambdaHandlerBehavior.jsonReturn.asJson))
42+
}
43+
44+
}

0 commit comments

Comments
 (0)