From ea439a92039a9743acee9254cf9474dc48cfd1a3 Mon Sep 17 00:00:00 2001 From: xwang <6080971+romanchelsea@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:31:45 -0800 Subject: [PATCH] Add ninny-circe-compat (#69) * Add ninny-circe-compat * add a nested field in test --- build.sc | 17 ++++ .../src/nrktkt/ninny/compat/CirceCompat.scala | 91 +++++++++++++++++++ .../nrktkt/ninny/compat/CirceCompatSpec.scala | 88 ++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 circe-compat/src/nrktkt/ninny/compat/CirceCompat.scala create mode 100644 circe-compat/test/src/nrktkt/ninny/compat/CirceCompatSpec.scala diff --git a/build.sc b/build.sc index 2e13b74..9af4cdd 100644 --- a/build.sc +++ b/build.sc @@ -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" diff --git a/circe-compat/src/nrktkt/ninny/compat/CirceCompat.scala b/circe-compat/src/nrktkt/ninny/compat/CirceCompat.scala new file mode 100644 index 0000000..30be9af --- /dev/null +++ b/circe-compat/src/nrktkt/ninny/compat/CirceCompat.scala @@ -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 diff --git a/circe-compat/test/src/nrktkt/ninny/compat/CirceCompatSpec.scala b/circe-compat/test/src/nrktkt/ninny/compat/CirceCompatSpec.scala new file mode 100644 index 0000000..fe3ce3c --- /dev/null +++ b/circe-compat/test/src/nrktkt/ninny/compat/CirceCompatSpec.scala @@ -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) + } +}