Skip to content

Commit

Permalink
Added zio-json instances for http4s (zio#241)
Browse files Browse the repository at this point in the history
* Fixed zio#240 - Added zio-json instances for http4s EntityDecoder/EntityEncoder

* added interop to zio-json http4s package
  • Loading branch information
Adriani-Furtado authored Apr 7, 2021
1 parent ab58a93 commit 6eb41b5
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 2 deletions.
23 changes: 21 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ addCommandAlias("fixCheck", "scalafixAll --check")
addCommandAlias("fmt", "all scalafmtSbt scalafmtAll")
addCommandAlias("fmtCheck", "all scalafmtSbtCheck scalafmtCheckAll")
addCommandAlias("prepare", "fix; fmt")
addCommandAlias("testJVM", "zioJsonJVM/test; zioJsonYaml/test")
addCommandAlias("testJVM", "zioJsonJVM/test; zioJsonYaml/test; zioJsonInteropHttp4s/test")
addCommandAlias("testJS", "zioJsonJS/test")

val zioVersion = "1.0.5"
Expand All @@ -43,7 +43,8 @@ lazy val root = project
.aggregate(
zioJsonJVM,
zioJsonJS,
zioJsonYaml
zioJsonYaml,
zioJsonInteropHttp4s
)

val circeVersion = "0.13.0"
Expand Down Expand Up @@ -195,6 +196,24 @@ lazy val zioJsonYaml = project
)
.dependsOn(zioJsonJVM)

lazy val zioJsonInteropHttp4s = project
.in(file("zio-json-interop-http4s"))
.settings(stdSettings("zio-json-interop-http4s"))
.settings(buildInfoSettings("zio.json.interop.http4s"))
.enablePlugins(NeoJmhPlugin)
.settings(
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-dsl" % "0.21.21",
"dev.zio" %% "zio" % zioVersion,
"org.typelevel" %% "cats-effect" % "2.4.0",
"dev.zio" %% "zio-interop-cats" % "2.4.0.0" % "test",
"dev.zio" %% "zio-test" % zioVersion % "test",
"dev.zio" %% "zio-test-sbt" % zioVersion % "test"
),
testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")
)
.dependsOn(zioJsonJVM)

lazy val docs = project
.in(file("zio-json-docs"))
.dependsOn(zioJsonJVM)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package zio.json.interop.http4s

object ZIOEntityCodec extends ZIOEntityEncoder with ZIOEntityDecoder
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package zio.json.interop.http4s

import cats.effect.Concurrent
import org.http4s.EntityDecoder

import zio.json.JsonDecoder

trait ZIOEntityDecoder {
implicit def zioEntityDecoder[F[_]: Concurrent, A: JsonDecoder]: EntityDecoder[F, A] =
jsonOf[F, A]
}

object ZIOEntityDecoder extends ZIOEntityDecoder
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package zio.json.interop.http4s

import org.http4s.EntityEncoder

import zio.json.JsonEncoder

trait ZIOEntityEncoder {
implicit def zioEntityEncoder[F[_], A: JsonEncoder]: EntityEncoder[F, A] = jsonEncoderOf[F, A]
}

object ZIOEntityEncoder extends ZIOEntityEncoder
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package zio.json.interop.http4s

import java.nio.charset.StandardCharsets

import cats.effect.Concurrent
import org.http4s.headers.`Content-Type`
import org.http4s.{ EntityDecoder, EntityEncoder, MalformedMessageBodyFailure, MediaType }

import zio.json._

trait ZIOJsonInstances {
def jsonOf[F[_]: Concurrent, A: JsonDecoder]: EntityDecoder[F, A] =
EntityDecoder.decodeBy[F, A](MediaType.application.json) { m =>
EntityDecoder.collectBinary(m).subflatMap { chunk =>
val str = new String(chunk.toArray, StandardCharsets.UTF_8)
if (str.nonEmpty)
str.fromJson.fold(e => Left(MalformedMessageBodyFailure(e, None)), Right(_))
else
Left(MalformedMessageBodyFailure("Invalid JSON: empty body"))
}
}

def jsonEncoderOf[F[_], A: JsonEncoder]: EntityEncoder[F, A] = EntityEncoder
.stringEncoder[F]
.contramap[A](_.toJson)
.withContentType(`Content-Type`(MediaType.application.json))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package zio.json.interop

package object http4s extends ZIOJsonInstances
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package zio.json.interop.http4s

import java.nio.charset.StandardCharsets

import org.http4s._

import zio.Task
import zio.interop.catz._
import zio.json._
import zio.test.Assertion._
import zio.test.{ DefaultRunnableSpec, _ }

object ZIOJsonInstancesSpec extends DefaultRunnableSpec {
final case class Test(string: String, int: Int)
private implicit val decoder: JsonCodec[Test] = DeriveJsonCodec.gen[Test]

def spec: ZSpec[Environment, Failure] = suite("json instances")(
suite("jsonEncoderOf") {
testM("returns an EntityEncoder that can encode for the given effect and type") {
checkM(Gen.anyString, Gen.anyInt) { (s, i) =>
val result = jsonEncoderOf[Task, Test]
.toEntity(Test(s, i))
.body
.compile
.toList
.map(v => new String(v.toArray, StandardCharsets.UTF_8))

assertM(result)(equalTo(s"""{"string":"$s","int":$i}"""))
}
}
},
suite("jsonOf")(
testM("returns an EntityDecoder that can decode for the given effect and type")(
checkM(Gen.anyString, Gen.anyInt) { (s, i) =>
val media = Request[Task]()
.withEntity(s"""{"string":"$s","int":$i}""")
.withHeaders(Header("Content-Type", "application/json"))

assertM(jsonOf[Task, Test].decode(media, true).value)(isRight(equalTo(Test(s, i))))
}
),
testM("returns MalformedMessageBodyFailure when json is empty") {
val media = Request[Task]()
.withEntity("")
.withHeaders(Header("Content-Type", "application/json"))

assertM(jsonOf[Task, Test].decode(media, true).value)(
isLeft(equalTo(MalformedMessageBodyFailure("Invalid JSON: empty body")))
)
},
testM("returns MalformedMessageBodyFailure when json is invalid") {
val media = Request[Task]()
.withEntity("""{"bad" "json"}""")
.withHeaders(Header("Content-Type", "application/json"))

assertM(jsonOf[Task, Test].decode(media, true).value)(
isLeft(equalTo(MalformedMessageBodyFailure("(expected ':' got '\"')")))
)
},
testM("returns MalformedMessageBodyFailure when message body is not a json") {
val media = Request[Task]()
.withEntity("not a json")
.withHeaders(Header("Content-Type", "text/plain"))

assertM(jsonOf[Task, Test].decode(media, true).value)(isLeft(isSubtype[MediaTypeMismatch](anything)))
}
)
)
}

0 comments on commit 6eb41b5

Please sign in to comment.