Permalink
Browse files

Add circe interop module (#52)

* add circe module

* add circe derived Show and Read based off Encoder and Decoder, respectively

* fixed Read + added tests

* address codacy issues

* amended README.md

* address codacy issue on README.md
  • Loading branch information...
sirocchj committed Nov 6, 2018
1 parent be17a67 commit 6194c89bdbc02501ccebb79a1205b879f99437e3
Showing with 110 additions and 0 deletions.
  1. +23 −0 README.md
  2. +24 −0 build.sbt
  3. +13 −0 circe/src/main/scala/laserdisc/interop/circe.scala
  4. +50 −0 circe/src/test/scala/laserdisc/interop/CirceSpec.scala
View
@@ -60,6 +60,29 @@ If you only need protocols (i.e. Redis commands and RESP wire format), you may s
libraryDependencies += "io.laserdisc" %% "laserdisc-core" % latestVersion
```
### Interoperability modules
Support for existing libraries is available via dedicated dependencies.
#### [Circe](https://circe.github.io/circe/)
When an `io.circe.Decoder[A]` and a `io.circe.Encoder[A]` are implicilty available,
instances of `Show[A]` and `Read[NonNullBulkString, A]` can be derived for free,
just add the following in your `build.sbt`:
```
libraryDependecies += "io.laserdisc" %% "laserdisc-circe" % latestVersion
```
then, to make use of them, at call site it should be sufficient to just:
```scala
import laserdisc.interop.circe._
```
*Note*: the derived `Show[A]` instance uses the most compact string representation
of the JSON data structure, i.e. no spacing is used
### Example usage
With a running Redis instance on `localhost:6379` try running the following:
```scala
View
@@ -5,6 +5,7 @@ val `scala 211` = "2.11.11-bin-typelevel-4"
val `scala 212` = "2.12.7"
val V = new {
val circe = "0.10.1"
val fs2 = "1.0.0"
val `kind-projector` = "0.9.8"
val kittens = "1.2.0"
@@ -19,6 +20,8 @@ val V = new {
val `log-effect-fs2` = "0.4.0"
}
val `circe-core` = Def.setting("io.circe" %%% "circe-core" % V.circe)
val `circe-parser` = Def.setting("io.circe" %%% "circe-parser" % V.circe)
val `fs2-core` = Def.setting("co.fs2" %%% "fs2-core" % V.fs2)
val `fs2-io` = Def.setting("co.fs2" %% "fs2-io" % V.fs2)
val kittens = Def.setting("org.typelevel" %%% "kittens" % V.kittens)
@@ -27,6 +30,7 @@ val `scodec-core` = Def.setting("org.scodec" %%% "scodec-core" % V.
val `scodec-stream` = Def.setting("org.scodec" %%% "scodec-stream" % V.`scodec-stream`)
val shapeless = Def.setting("com.chuusai" %%% "shapeless" % V.shapeless)
val `log-effect-fs2` = Def.setting("io.laserdisc" %%% "log-effect-fs2" % V.`log-effect-fs2`)
val `circe-generic` = Def.setting("io.circe" %%% "circe-generic" % V.circe % Test)
val scalacheck = Def.setting("org.scalacheck" %%% "scalacheck" % V.scalacheck % Test)
val scalatest = Def.setting("org.scalatest" %%% "scalatest" % V.scalatest % Test)
val refined = Def.setting {
@@ -69,6 +73,16 @@ val fs2Deps = Def.Initialize.join {
)
}
val circeDeps = Def.Initialize.join {
Seq(
`circe-core`,
`circe-parser`,
`circe-generic`,
scalacheck,
scalatest
)
}
val externalApiMappings = Def.task {
val fullClassPath = (Compile / fullClasspath).value
@@ -279,6 +293,16 @@ lazy val cli = project
libraryDependencies ++= fs2Deps.value
)
lazy val circe = crossProject(JSPlatform, JVMPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(CrossType.Pure)
.in(file("circe"))
.dependsOn(core)
.settings(
name := "laserdisc-circe",
libraryDependencies := circeDeps.value
)
lazy val laserdisc = project
.in(file("."))
.aggregate(coreJVM, coreJS, fs2, cli)
@@ -0,0 +1,13 @@
package laserdisc
package interop
import io.circe._
import io.circe.syntax._
import laserdisc.protocol.NonNullBulkString
object circe {
implicit final def encoderShow[A: Encoder]: Show[A] = Show.instance(_.asJson.noSpaces)
implicit final def decoderRead[A: Decoder]: Read[NonNullBulkString, A] = Read.instance {
case NonNullBulkString(s) => parser.decode(s).toOption
}
}
@@ -0,0 +1,50 @@
package laserdisc
package interop
import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto._
import laserdisc.interop.circe._
import laserdisc.protocol.NonNullBulkString
import laserdisc.protocol.RESP.bulk
import org.scalacheck.{Arbitrary, Gen}
import org.scalatest.prop.PropertyChecks
import org.scalatest.{MustMatchers, OptionValues, WordSpec}
final class CirceSpec extends WordSpec with MustMatchers with PropertyChecks with OptionValues {
private[this] sealed trait Foo extends Product with Serializable
private[this] final case class Bar(x: Int) extends Foo
private[this] final case class Baz(y: String, z: Foo) extends Foo
private[this] implicit val barDecoder: Decoder[Bar] = deriveDecoder
private[this] implicit val barEncoder: Encoder[Bar] = deriveEncoder
private[this] implicit val bazDecoder: Decoder[Baz] = deriveDecoder
private[this] implicit val bazEncoder: Encoder[Baz] = deriveEncoder
private[this] implicit val fooDecoder: Decoder[Foo] = deriveDecoder
private[this] implicit val fooEncoder: Encoder[Foo] = deriveEncoder
private[this] val barGen: Gen[Bar] = Arbitrary.arbitrary[Int].map(Bar.apply)
private[this] val bazGen: Gen[Baz] = for {
s <- Arbitrary.arbitrary[String]
foo <- fooGen
} yield Baz(s, foo)
private[this] def fooGen: Gen[Foo] = Gen.oneOf(barGen, bazGen)
private[this] implicit val barArbitrary: Arbitrary[Bar] = Arbitrary(barGen)
private[this] implicit val bazArbitrary: Arbitrary[Baz] = Arbitrary(bazGen)
"Circe interop" when {
"handling a simple type" must {
"round-trip with no errors" in forAll { bar: Bar =>
Read[NonNullBulkString, Bar].read(bulk(Show[Bar].show(bar))).value mustBe bar
}
}
"handling a recursive type" must {
"round-trip with no errors" in forAll { baz: Baz =>
Read[NonNullBulkString, Baz].read(bulk(Show[Baz].show(baz))).value mustBe baz
}
}
}
}

0 comments on commit 6194c89

Please sign in to comment.