Skip to content

Commit

Permalink
Add ninny-circe-compat (#69)
Browse files Browse the repository at this point in the history
* Add ninny-circe-compat

* add a nested field in test
  • Loading branch information
romanchelsea authored Dec 9, 2024
1 parent ef0dc7e commit ea439a9
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 0 deletions.
17 changes: 17 additions & 0 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ class Json4sCompat(val crossScalaVersion: String, val json4sMajor: Int)
}
}

object `circe-compat` extends mill.Cross[CirceCompat](`2.12`, `2.13`)
class CirceCompat(val crossScalaVersion: String) extends CrossScalaModule with PublishMod {
def artifactName = "ninny-circe-compat"

def moduleDeps = List(ninny(crossScalaVersion))
def ivyDeps = Agg(
ivy"io.circe::circe-core:0.14.3",
ivy"io.circe::circe-generic:0.14.3",
ivy"io.circe::circe-generic-extras:0.14.3",
)

object test extends Tests {
def testFrameworks = Seq("org.scalatest.tools.Framework")
def ivyDeps = Agg(scalaTest)
}
}

object ubjson extends ScalaModule with PublishMod {
def scalaVersion = `2.13`
def artifactName = "ninny-ubjson"
Expand Down
91 changes: 91 additions & 0 deletions circe-compat/src/nrktkt/ninny/compat/CirceCompat.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package nrktkt.ninny.compat

import scala.annotation.nowarn
import scala.language.implicitConversions
import scala.util.{Failure, Success}
import io.circe._
import io.circe.Decoder.Result
import nrktkt.ninny.{FromJson, ToSomeJson, ToSomeJsonValue}
import nrktkt.ninny.ast._

trait NinnyToCirce {
import CirceToNinny.asNinny

implicit def toSomeJsonEncoder[A](implicit
toSomeJson: ToSomeJson[A]
): Encoder[A] =
new Encoder[A] {
override def apply(a: A): Json = toSomeJson.toSome(a)
}

implicit def fromJsonDecoder[A](implicit
fromJson: FromJson[A]
): Decoder[A] =
new Decoder[A] {
override def apply(c: HCursor): Result[A] =
fromJson.from(c.root.value) match {
case Success(value) => Right(value)
case Failure(_) =>
Left(DecodingFailure("decode failure", { List.empty }))
}
}

implicit def asCirce(json: JsonValue): Json =
json match {
case JsonNull => Json.Null
case JsonBoolean(value) => Json.fromBoolean(value)
case JsonString(value) => Json.fromString(value)
case nrktkt.ninny.ast.JsonDouble(value) => {
if (value % 1 == 0) Json.fromInt(value.toInt)
else Json.fromDoubleOrNull(value)
}
case nrktkt.ninny.ast.JsonDecimal(value) => Json.fromBigDecimal(value)
case JsonArray(value) => Json.fromValues(value.map(asCirce))
case nrktkt.ninny.ast.JsonObject(value) =>
Json.fromJsonObject(io.circe.JsonObject.fromMap {
value.map { case (k, v) =>
(k, asCirce(v))
}
})
case blob: JsonBlob => Json.fromString(nrktkt.ninny.Json.render(blob))
}
}

object NinnyToCirce extends NinnyToCirce

trait CirceToNinny {
import NinnyToCirce.asCirce

implicit def encoderToSomeJson[A](implicit
encoder: Encoder[A]
): ToSomeJson[A] =
new ToSomeJsonValue[A, JsonValue] {
override def toSome(a: A): JsonValue = encoder.apply(a)
}

implicit def decoderFromJson[A](implicit decoder: Decoder[A]): FromJson[A] =
FromJson.fromSome(jsonValue => { decoder.decodeJson(jsonValue).toTry })

@nowarn
implicit def asNinny(json: Json): JsonValue = json match {
case _ if json.isNull => JsonNull
case _ if json.isBoolean => JsonBoolean(json.asBoolean.get)
case _ if json.isArray =>
JsonArray(json.asArray.get.map(asNinny).toIndexedSeq)
case _ if json.isString => JsonString(json.asString.get)
case _ if json.isNumber =>
json.asNumber match {
case Some(jsonNumber) =>
jsonNumber.toBigDecimal match {
case Some(bigDecimal) => nrktkt.ninny.ast.JsonDecimal(bigDecimal)
}
nrktkt.ninny.ast.JsonNumber(jsonNumber.toDouble)
}
case _ if json.isObject =>
nrktkt.ninny.ast.JsonObject(json.asObject.get.toMap.map { case (k, v) =>
(k, asNinny(v))
})
}
}

object CirceToNinny extends CirceToNinny
88 changes: 88 additions & 0 deletions circe-compat/test/src/nrktkt/ninny/compat/CirceCompatSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package nrktkt.ninny.compat

import io.circe._
import io.circe.generic.semiauto._
import io.circe.syntax._
import nrktkt.ninny.ast._
import nrktkt.ninny._
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should
import org.scalatest.OptionValues
import org.scalatest.TryValues

class CirceCompatSpec
extends AnyFlatSpec
with should.Matchers
with OptionValues
with TryValues {

case class Person1(name: String, age: Int, married: Boolean)
object Person1 {
implicit val decoder: Decoder[Person1] = deriveDecoder[Person1]
implicit val encoder: Encoder[Person1] = deriveEncoder[Person1]
}

case class Example1(foo: String, bar: Seq[Int], person: Person1)
object Example1 {
implicit val decoder: Decoder[Example1] = deriveDecoder[Example1]
implicit val encoder: Encoder[Example1] = deriveEncoder[Example1]
}

case class Person2(name: String, age: Int, married: Boolean)
object Person2 {
implicit val toJson = ToJson.auto[Person2]
implicit val fromJson = FromJson.auto[Person2]
}

case class Example2(foo: String, bar: Seq[Int], person: Person2)
object Example2 {
implicit val toJson = ToJson.auto[Example2]
implicit val fromJson = FromJson.auto[Example2]
}

val ex1 = Example1("baz", Seq(1, 2, 3), Person1("Alice", 27, false))
val ex2 = Example2("baz", Seq(1, 2, 3), Person2("Bob", 30, true))

val ex1json = nrktkt.ninny.obj("foo" ~> "baz", "bar" ~> Seq(1, 2, 3),
"person" ~> nrktkt.ninny.obj(
"name" ~> "Alice",
"age" ~> 27,
"married" ~> false,
))
val ex2json = io.circe.Json.obj(
"foo" -> io.circe.Json.fromString("baz"),
"bar" -> io.circe.Json.fromValues(Seq(1, 2, 3).map(io.circe.Json.fromInt)),
"person" -> io.circe.Json.obj(
"name" -> io.circe.Json.fromString("Bob"),
"age" -> io.circe.Json.fromInt(30),
"married" -> io.circe.Json.fromBoolean(true),
)
)

"Circe typeclasses" should "write ninny json" in {
import CirceToNinny._

val json = ex1.toSomeJson
json shouldEqual ex1json
}

it should "read ninny json" in {
import CirceToNinny._

val objekt = ex1json.to[Example1].success.value
objekt shouldEqual ex1
}

"ninny typeclasses" should "write circe json" in {
import NinnyToCirce._
val json = ex2.asJson
json shouldEqual ex2json
}

it should "read circe json" in {
import NinnyToCirce._

val objekt = ex2json.as[Example2].toOption
objekt shouldEqual Some(ex2)
}
}

0 comments on commit ea439a9

Please sign in to comment.