From 31a6524051cc147d9fd3225b6e5afaf4107a652e Mon Sep 17 00:00:00 2001 From: Nathan Fischer Date: Fri, 6 Jan 2023 16:38:15 -0800 Subject: [PATCH] JsonValue type as type member --- USERGUIDE.md | 2 +- build.sc | 4 +- forProductN.sc | 12 ++-- ninny/src-2/nrktkt/ninny/ToJsonAutoImpl.scala | 4 +- .../VersionSpecificToJsonInstances.scala | 3 +- ninny/src/io/github/kag0/ninny/package.scala | 10 ++- ninny/src/nrktkt/ninny/ToAndFromJson.scala | 1 + ninny/src/nrktkt/ninny/ToJson.scala | 44 +++++++++--- ninny/src/nrktkt/ninny/ToJsonInstances.scala | 69 ++++++++++--------- ninny/src/nrktkt/ninny/package.scala | 15 ++-- .../src-2/io/github/kag0/ninny/JsonSpec.scala | 25 +++---- .../github/kag0/ninny/example/Userguide.scala | 10 +-- .../kag0/ninny/userguide/DomainTo.scala | 10 +-- .../kag0/ninny/userguide/ForProductN.scala | 2 +- 14 files changed, 122 insertions(+), 89 deletions(-) diff --git a/USERGUIDE.md b/USERGUIDE.md index 3589414..c0d11c4 100644 --- a/USERGUIDE.md +++ b/USERGUIDE.md @@ -70,7 +70,7 @@ You can use ninny's dynamic update syntax easly to replace values way down in th repo='kag0/ninny-json' lang='scala' file='ninny/test/src-2/io/github/kag0/ninny/userguide/DomainTo.scala' - lines='7:51' + lines='7:53' > # Converting JSON to domain objects diff --git a/build.sc b/build.sc index 9ff7e0f..b36dfe0 100644 --- a/build.sc +++ b/build.sc @@ -7,8 +7,8 @@ import $ivy.`com.lihaoyi::mill-contrib-bloop:$MILL_VERSION` import mill.define.{Segment, Segments} import $file.forProductN -val `2.12` = "2.12.15" -val `2.13` = "2.13.8" +val `2.12` = "2.12.17" +val `2.13` = "2.13.10" val `3` = "3.1.0" val scalaTest = ivy"org.scalatest::scalatest:3.2.10" diff --git a/forProductN.sc b/forProductN.sc index 18dc3f2..22a84b1 100644 --- a/forProductN.sc +++ b/forProductN.sc @@ -17,13 +17,13 @@ def generateProductToJson = { .map(j => s"a${j}ToJson: ToJson[A$j]") .mkString(", ") - out ++= "): ToSomeJsonObject[Target] = target => {\n" + out ++= "): ToSomeJsonObject[Target] = ToJson(target => {\n" out ++= "val (" out ++= (0 until i).map(j => s"a$j").mkString(", ") out ++= ") = f(target)\n" out ++= "obj(" out ++= (0 until i).map(j => s"(nameA$j, a$j)").mkString(", ") - out ++= ")\n}\n" + out ++= ")\n})\n" } out += '}' @@ -65,6 +65,7 @@ def generateProductFromJson = { def generateProductToAndFromJson = { val out = new StringBuilder() out ++= "package nrktkt.ninny\n" + out ++= "import scala.language.existentials\n" out ++= "import nrktkt.ninny.ast._\n" out ++= "trait ProductToAndFromJson {\n" for (i <- 1 to 22) { @@ -84,16 +85,19 @@ def generateProductToAndFromJson = { .map(j => s"a${j}ToAndFromJson: ToAndFromJson[A$j]") .mkString(", ") - out ++= ") = new ToAndFromJson[Target] {\n" + out ++= ") = {\n" + val namesCsv = (0 until i).map(j => s"nameA$j").mkString(", ") out ++= s"val _toJson: ToSomeJson[Target] = ToJson.forProduct$i($namesCsv)(fTo)\n" out ++= s"val _fromJson: FromJson[Target] = FromJson.forProduct$i($namesCsv)(fFrom)\n" + out ++= "new ToAndFromJson[Target] {\n" + out ++= "type Json = _toJson.Json\n" out ++= """ def from(json: Option[JsonValue]) = _fromJson.from(json) def toSome(target: Target) = _toJson.toSome(target) """ - out ++= "}\n" + out ++= "}}\n" } out += '}' diff --git a/ninny/src-2/nrktkt/ninny/ToJsonAutoImpl.scala b/ninny/src-2/nrktkt/ninny/ToJsonAutoImpl.scala index dc79a54..55b1fdd 100644 --- a/ninny/src-2/nrktkt/ninny/ToJsonAutoImpl.scala +++ b/ninny/src-2/nrktkt/ninny/ToJsonAutoImpl.scala @@ -28,12 +28,12 @@ trait ToJsonAutoImpl { toJson: Lazy[ToSomeJsonObject[UpdatedRecord]] ): ToJsonAuto[A] = { val replacedNames = replaceNames(fields.keys, annotations()) - new ToJsonAuto[A](a => { + new ToJsonAuto[A](ToJson(a => { val record = generic.to(a) val updatedRecord = zipWithNames( replacedNames :: fields.values(record) :: HNil ) toJson.value.toSome(updatedRecord) - }) + })) } } diff --git a/ninny/src-2/nrktkt/ninny/VersionSpecificToJsonInstances.scala b/ninny/src-2/nrktkt/ninny/VersionSpecificToJsonInstances.scala index 633d795..3320a40 100644 --- a/ninny/src-2/nrktkt/ninny/VersionSpecificToJsonInstances.scala +++ b/ninny/src-2/nrktkt/ninny/VersionSpecificToJsonInstances.scala @@ -20,5 +20,6 @@ trait VersionSpecificToJsonInstances { } } - implicit val hNilToJson: ToSomeJsonObject[HNil] = _ => JsonObject(Map.empty) + implicit val hNilToJson: ToSomeJsonObject[HNil] = + ToJson(_ => JsonObject(Map.empty)) } diff --git a/ninny/src/io/github/kag0/ninny/package.scala b/ninny/src/io/github/kag0/ninny/package.scala index 9bb3497..fea3741 100644 --- a/ninny/src/io/github/kag0/ninny/package.scala +++ b/ninny/src/io/github/kag0/ninny/package.scala @@ -13,13 +13,11 @@ package object ninny extends VersionSpecificPackage with nrktkt.ninny.MagneticMethods { val Json = nrktkt.ninny.Json - type ToJson[A] = nrktkt.ninny.ToJsonValue[A, JsonValue] + type ToJson[A] = nrktkt.ninny.ToJson[A] val ToJson = nrktkt.ninny.ToJson - type ToJsonValue[A, J <: JsonValue] = nrktkt.ninny.ToJsonValue[A, J] - val ToJsonValue = nrktkt.ninny.ToJsonValue - type ToJsonObject[A] = nrktkt.ninny.ToJsonValue[A, JsonObject] - type ToSomeJson[A] = nrktkt.ninny.ToSomeJsonValue[A, JsonValue] - type ToSomeJsonObject[A] = nrktkt.ninny.ToSomeJsonValue[A, JsonObject] + type ToJsonValue[A] = nrktkt.ninny.ToJson[A] + type ToSomeJson[A] = nrktkt.ninny.ToSomeJson[A] + type ToSomeJsonObject[A] = nrktkt.ninny.ToSomeJsonObject[A] type FromJson[A] = nrktkt.ninny.FromJson[A] val FromJson = nrktkt.ninny.FromJson type ToAndFromJson[A] = nrktkt.ninny.ToAndFromJson[A] diff --git a/ninny/src/nrktkt/ninny/ToAndFromJson.scala b/ninny/src/nrktkt/ninny/ToAndFromJson.scala index c03f5a9..d6d420d 100644 --- a/ninny/src/nrktkt/ninny/ToAndFromJson.scala +++ b/ninny/src/nrktkt/ninny/ToAndFromJson.scala @@ -9,6 +9,7 @@ object ToAndFromJson extends ProductToAndFromJson { fromJson: FromJson[A] ): ToAndFromJson[A] = new ToAndFromJson[A] { + type Json = toJson.Json def toSome(a: A) = toJson.toSome(a) def from(maybeJson: Option[JsonValue]) = fromJson.from(maybeJson) } diff --git a/ninny/src/nrktkt/ninny/ToJson.scala b/ninny/src/nrktkt/ninny/ToJson.scala index c1cd2c1..be9e20b 100644 --- a/ninny/src/nrktkt/ninny/ToJson.scala +++ b/ninny/src/nrktkt/ninny/ToJson.scala @@ -2,28 +2,52 @@ package nrktkt.ninny import nrktkt.ninny.ast.JsonValue -trait ToJsonValue[A, +Json <: JsonValue] { - def to(a: A): Option[Json] +trait ToJson[A] { + type Json <: JsonValue - def contramap[B](f: B => A): ToJsonValue[B, Json] = b => to(f(b)) + def to(a: A): Option[Json] + def contramap[B](f: B => A): ToJson[B] = ToJson((b: B) => to(f(b))) } object ToJsonValue extends ToJsonInstances -object ToJson extends ProductToJson { +object ToJson + extends ToJsonInstances + with ProductToJson + with ToSomeJsonConstructor { - def apply[A: ToJson]: ToJson[A] = implicitly[ToJson[A]] + type Aux[A, +J <: JsonValue] = ToJson[A] { + type Json <: J + } - def apply[A, Json <: JsonValue](fn: A => Option[Json]): ToJsonValue[A, Json] = - (a: A) => fn(a) + def apply[A: ToJson]: ToJson[A] = implicitly[ToJson[A]] - def apply[A, Json <: JsonValue](fn: A => Json): ToSomeJsonValue[A, Json] = - (a: A) => fn(a) + def apply[A, J <: JsonValue]( + fn: A => Option[J] + ): ToJson.Aux[A, J] = new ToJson[A] { + type Json = J + def to(a: A): Option[Json] = fn(a) + } def auto[A: ToJsonAuto] = implicitly[ToJsonAuto[A]].toJson } -trait ToSomeJsonValue[A, +Json <: JsonValue] extends ToJsonValue[A, Json] { +trait ToSomeJson[A] extends ToJson[A] { def toSome(a: A): Json override def to(a: A) = Some(toSome(a)) } + +object ToSomeJson extends ToSomeJsonConstructor { + type Aux[A, +J <: JsonValue] = ToSomeJson[A] { + type Json <: J + } +} + +private[ninny] trait ToSomeJsonConstructor { + def apply[A, J <: JsonValue]( + fn: A => J + ): ToSomeJson[A] { type Json = J } = new ToSomeJson[A] { + type Json = J + def toSome(a: A): J = fn(a) + } +} diff --git a/ninny/src/nrktkt/ninny/ToJsonInstances.scala b/ninny/src/nrktkt/ninny/ToJsonInstances.scala index e9de52d..d92a3da 100644 --- a/ninny/src/nrktkt/ninny/ToJsonInstances.scala +++ b/ninny/src/nrktkt/ninny/ToJsonInstances.scala @@ -10,55 +10,56 @@ import scala.collection.compat.immutable.ArraySeq trait ToJsonInstances extends VersionSpecificToJsonInstances with LowPriorityToJsonInstances { - implicit val stringToJson: ToSomeJsonValue[String, JsonString] = + implicit val stringToJson: ToSomeJson[String] = ToJson(JsonString(_)) - implicit val booleanToJson: ToSomeJsonValue[Boolean, JsonBoolean] = - JsonBoolean(_) + implicit val booleanToJson: ToSomeJson[Boolean] = + ToJson(JsonBoolean(_)) - implicit val nullToJson: ToSomeJsonValue[Null, JsonNull.type] = _ => JsonNull + implicit val nullToJson: ToSomeJson[Null] = ToJson(_ => JsonNull) - implicit val bigDecimalToJson: ToSomeJsonValue[BigDecimal, JsonDecimal] = - JsonDecimal(_) + implicit val bigDecimalToJson: ToSomeJson[BigDecimal] = + ToJson(JsonDecimal(_)) - implicit val bigIntToJson: ToSomeJsonValue[BigInt, JsonDecimal] = i => - JsonDecimal(BigDecimal(i, MathContext.UNLIMITED)) + implicit val bigIntToJson: ToSomeJson.Aux[BigInt, JsonDecimal] = + ToJson(i => JsonDecimal(BigDecimal(i, MathContext.UNLIMITED))) - implicit val longToJson: ToSomeJsonValue[Long, JsonDecimal] = l => - JsonDecimal(BigDecimal(l)) + implicit val longToJson: ToSomeJson.Aux[Long, JsonDecimal] = + ToJson(l => JsonDecimal(BigDecimal(l))) - implicit val arraySeqToJson: ToSomeJsonValue[ArraySeq[Byte], JsonBlob] = - JsonBlob(_) + implicit val arraySeqToJson: ToSomeJson.Aux[ArraySeq[Byte], JsonBlob] = + ToJson(JsonBlob(_)) - implicit def arrayToJson[A: ToSomeJson] - : ToSomeJsonValue[Array[A], JsonArray] = - arr => { + implicit def arrayToJson[A: ToSomeJson]: ToSomeJson.Aux[Array[A], JsonArray] = + ToJson(arr => { val builder = immutable.Seq.newBuilder[JsonValue] builder.sizeHint(arr.length) for (i <- 0 until arr.length) { builder += (arr(i).toSomeJson) } JsonArray(builder.result()) - } + }) - implicit def jsonToJson[J <: JsonValue]: ToSomeJson[J] = j => j + implicit def jsonToJson[J <: JsonValue]: ToSomeJson[J] = ToJson(j => j) /** represents unit as an empty JSON array. because a tuple is a heterogeneous * list; (5, "foo") => [5, "foo"] and unit is an empty tuple; () => [] */ - implicit val unitToJson: ToSomeJson[Unit] = _ => JsonArray(Nil) + implicit val unitToJson: ToSomeJson[Unit] = ToJson(_ => JsonArray(Nil)) implicit def mapToJson[A: ToJson]: ToSomeJson[Map[String, A]] = - m => + ToJson(m => JsonObject(m.collect(Function.unlift { case (k, v) => v.toJson.map(k -> _) })) + ) implicit def optionToJson[A: ToJson]: ToJson[Option[A]] = - a => a.flatMap(ToJson[A].to(_)) + ToJson((a: Option[A]) => a.flatMap(ToJson[A].to(_))) - implicit val noneToJson: ToJson[None.type] = _ => None - implicit def someToJson[A: ToJson]: ToJson[Some[A]] = optionToJson[A].to(_) - implicit def someToSomeJson[A: ToSomeJson]: ToSomeJson[Some[A]] = - _.value.toSomeJson + implicit val noneToJson: ToJson[None.type] = _ => None + implicit def someToJson[A: ToJson]: ToJson[Some[A]] = + ToJson((some: Some[A]) => optionToJson[A].to(some)) + implicit def someToSomeJson[A: ToSomeJson]: ToSomeJson[Some[A]] = + ToJson(_.value.toSomeJson) implicit def leftToJson[L: ToJson, R]: ToJson[Left[L, R]] = _.value.toJson implicit def rightToJson[L, R: ToJson]: ToJson[Right[L, R]] = _.value.toJson @@ -66,16 +67,16 @@ trait ToJsonInstances _.fold(_.toJson, _.toJson) implicit val instantToJson: ToSomeJson[Instant] = - i => JsonNumber(i.getEpochSecond.toDouble) + ToJson(i => JsonNumber(i.getEpochSecond.toDouble)) implicit val offsetDateTimeToJson: ToSomeJson[OffsetDateTime] = - time => JsonString(time.toString) + ToJson(time => JsonString(time.toString)) implicit val zonedDateTimeToJson: ToSomeJson[ZonedDateTime] = - time => JsonString(time.toString) + ToJson(time => JsonString(time.toString)) - implicit val uuidToJson: ToSomeJsonValue[UUID, JsonString] = - uuid => JsonString(uuid.toString) + implicit val uuidToJson: ToSomeJson.Aux[UUID, JsonString] = + ToJson(uuid => JsonString(uuid.toString)) } object ToJsonInstances extends ToJsonInstances @@ -84,15 +85,15 @@ trait LowPriorityToJsonInstances { implicit def iterableToJson[I, A](implicit ev: I <:< Iterable[A], toJson: ToSomeJson[A] - ): ToSomeJsonValue[I, JsonArray] = - xs => { + ): ToSomeJson.Aux[I, JsonArray] = + ToJson(xs => { val builder = immutable.Seq.newBuilder[JsonValue] xs.foreach(builder += _.toSomeJson) JsonArray(builder.result()) - } + }) - implicit def numericToJson[A: Numeric]: ToSomeJsonValue[A, JsonDouble] = - a => JsonDouble(implicitly[Numeric[A]].toDouble(a)) + implicit def numericToJson[A: Numeric]: ToSomeJson.Aux[A, JsonDouble] = + ToJson(a => JsonDouble(implicitly[Numeric[A]].toDouble(a))) } class ToJsonAuto[A](val toJson: ToSomeJsonObject[A]) extends AnyVal diff --git a/ninny/src/nrktkt/ninny/package.scala b/ninny/src/nrktkt/ninny/package.scala index ba8c655..67342ba 100644 --- a/ninny/src/nrktkt/ninny/package.scala +++ b/ninny/src/nrktkt/ninny/package.scala @@ -56,12 +56,13 @@ package object ninny { } implicit class AnySyntax[A](val _a: A) extends AnyVal { - def toJson[Json <: JsonValue](implicit toJson: ToJsonValue[A, Json]) = + def toJson(implicit toJson: ToJson[A]): Option[toJson.Json] = toJson.to(_a) - def toSomeJson[Json <: JsonValue](implicit - toJson: ToSomeJsonValue[A, Json] - ) = toJson.toSome(_a) + def toSomeJson(implicit + toJson: ToSomeJson[A] + ): toJson.Json = toJson.toSome(_a) + } implicit class ArrowSyntax(val s: String) extends AnyVal { @@ -70,10 +71,8 @@ package object ninny { def ~>[A: ToJson](a: A) = s -> JsonMagnet(a) } - type ToJson[A] = ToJsonValue[A, JsonValue] - type ToJsonObject[A] = ToJsonValue[A, JsonObject] - type ToSomeJson[A] = ToSomeJsonValue[A, JsonValue] - type ToSomeJsonObject[A] = ToSomeJsonValue[A, JsonObject] + type ToJsonObject[A] = ToJson.Aux[A, JsonObject] + type ToSomeJsonObject[A] = ToSomeJson.Aux[A, JsonObject] // @deprecated( // message = diff --git a/ninny/test/src-2/io/github/kag0/ninny/JsonSpec.scala b/ninny/test/src-2/io/github/kag0/ninny/JsonSpec.scala index 7865ab0..deaa05d 100644 --- a/ninny/test/src-2/io/github/kag0/ninny/JsonSpec.scala +++ b/ninny/test/src-2/io/github/kag0/ninny/JsonSpec.scala @@ -27,13 +27,13 @@ class JsonSpec it should "work" in { val sampleValues = obj( - "string" -> """¯\_(ツ)_/¯""", + "string" --> """¯\_(ツ)_/¯""", "number" -> 1.79e308, - "bool" -> true, - "false" -> false, - "null" -> JsonNull, - "unit" -> ((): Unit), - "some" -> "str" + "bool" ~> true, + "false" -> false, + "null" -> JsonNull, + "unit" -> ((): Unit), + "some" -> "str" ) val sampleArray = @@ -54,7 +54,7 @@ class JsonSpec println(parsed == sampleObject) println(parsed.array.to[Seq[JsonValue]].get == sampleArray.values) - 5.toSomeJson[JsonNumber] shouldEqual JsonNumber(5d) + (5.toSomeJson: JsonNumber) shouldEqual JsonNumber(5d) 5.toJson shouldEqual Some(JsonNumber(5d)) } @@ -242,7 +242,7 @@ class JsonSpec } yield Image(w, h, title, thumb, url, animated, ids) ) - implicit val toJson: ToSomeJson[Image] = a => + implicit val toJson: ToSomeJson[Image] = ToJson(a => obj( "Width" -> a.Width, "Height" -> a.Height, @@ -252,6 +252,7 @@ class JsonSpec "Animated" -> a.Animated, "IDs" -> (if (a.IDs.isEmpty) None else a.IDs) ) + ) } object Precision extends Enumeration { @@ -261,8 +262,8 @@ class JsonSpec implicit val precisionFromJson: FromJson[Precision.Value] = FromJson.fromSome(_.to[String].flatMap(p => Try(Precision.withName(p)))) - implicit val precisionToJson: ToSomeJson[Precision.Value] = a => - JsonString(a.toString) + implicit val precisionToJson: ToSomeJson[Precision.Value] = + ToJson(a => JsonString(a.toString)) case class Address( precision: Precision.Value, @@ -278,7 +279,7 @@ class JsonSpec object Address { implicit val fromJson: FromJson[Address] = FromJson.auto[Address] - implicit val toJson: ToSomeJson[Address] = a => + implicit val toJson: ToSomeJson[Address] = ToJson(a => obj( "precision" -> a.precision, "Latitude" -> a.Latitude, @@ -289,6 +290,7 @@ class JsonSpec "Zip" -> a.Zip, "Country" -> a.Country ) + ) } val exampleObject = @@ -511,7 +513,6 @@ class JsonSpec head :++ tail shouldEqual arr(1, 2, 3, 4, 5, 6) } - "JsonBlob" should "encode and decode to base64" in { val bytes = new Array[Byte](16) Random.nextBytes(bytes) diff --git a/ninny/test/src-2/io/github/kag0/ninny/example/Userguide.scala b/ninny/test/src-2/io/github/kag0/ninny/example/Userguide.scala index cdcf352..fbacab8 100644 --- a/ninny/test/src-2/io/github/kag0/ninny/example/Userguide.scala +++ b/ninny/test/src-2/io/github/kag0/ninny/example/Userguide.scala @@ -117,11 +117,12 @@ case class Address(street: String, zip: String) implement ToSomeJson instead of ToJson if your object always produces some kind of JSON. this is a common case. */ -implicit val addressToJson: ToSomeJson[Address] = a => - obj( +implicit val addressToJson: ToSomeJson[Address] = ToJson( + a => obj( "street" -> a.street, "zip" -> a.zip ) +) implicit val addressFromJson: FromJson[Address] = { case None => Failure(new NoSuchElementException()) @@ -132,14 +133,15 @@ implicit val addressFromJson: FromJson[Address] = { } yield Address(first, last) } -implicit val personToJson: ToSomeJson[Person] = p => - obj( +implicit val personToJson: ToSomeJson[Person] = ToJson( + p => obj( "firstName" -> p.firstName, "lastName" -> p.lastName, "address" -> p.address, "kids" -> p.kids, "age" -> p.age ) +) implicit val personFromJson = FromJson.fromSome[Person](json => for { diff --git a/ninny/test/src-2/io/github/kag0/ninny/userguide/DomainTo.scala b/ninny/test/src-2/io/github/kag0/ninny/userguide/DomainTo.scala index 2837daa..e69e4ca 100644 --- a/ninny/test/src-2/io/github/kag0/ninny/userguide/DomainTo.scala +++ b/ninny/test/src-2/io/github/kag0/ninny/userguide/DomainTo.scala @@ -23,22 +23,24 @@ object Address { always produces some kind of JSON. this is a common case. */ - implicit val toJson: ToSomeJson[Address] = a => - obj( + implicit val toJson: ToSomeJson[Address] = ToSomeJson( + a => obj( "street" -> a.street, "zip" -> a.zip ) + ) } object Person { - implicit val toJson: ToSomeJson[Person] = p => - obj( + implicit val toJson: ToSomeJson[Person] = ToSomeJson( + p => obj( "firstName" -> p.firstName, "lastName" -> p.lastName, "address" -> p.address, "kids" -> p.kids, "age" -> p.age ) + ) } Person( diff --git a/ninny/test/src-2/io/github/kag0/ninny/userguide/ForProductN.scala b/ninny/test/src-2/io/github/kag0/ninny/userguide/ForProductN.scala index 3aa012e..82c639f 100644 --- a/ninny/test/src-2/io/github/kag0/ninny/userguide/ForProductN.scala +++ b/ninny/test/src-2/io/github/kag0/ninny/userguide/ForProductN.scala @@ -12,7 +12,7 @@ val fromJson: FromJson[Address] = implicit val toJson: ToSomeJson[Address] = - ToJson.forProduct2("street", "zip_code")(Address.unapply(_).get) + ToJson.forProduct2("street", "zip_code")(Address.unapply(_:Address).get) val toAndFromJson: ToAndFromJson[Address] = ToAndFromJson.forProduct2("street", "zip_code")(